mysql集群

docker 搭建mysql主从

1.先建立数据存放目录(/Users/wangyanyang/docker_mysql_info)
image.png

  1. ####master
  2. [mysqld]
  3. server_id = 1
  4. log-bin= mysql-bin
  5. read-only=0
  6. replicate-ignore-db=mysql
  7. replicate-ignore-db=sys
  8. replicate-ignore-db=information_schema
  9. replicate-ignore-db=performance_schema
  10. !includedir /etc/mysql/conf.d/
  11. !includedir /etc/mysql/mysql.conf.d/
  12. ####slave
  13. [mysqld]
  14. server_id = 2
  15. log-bin= mysql-bin
  16. read-only=1
  17. replicate-ignore-db=mysql
  18. replicate-ignore-db=sys
  19. replicate-ignore-db=information_schema
  20. replicate-ignore-db=performance_schema
  21. !includedir /etc/mysql/conf.d/
  22. !includedir /etc/mysql/mysql.conf.d

2.构建两个mysql容器

  1. master
  2. docker run -d -e MYSQL_ROOT_PASSWORD=root --name mastermysql --restart always
  3. -v /Users/wangyanyang/docker_mysql_info/master/data:/var/lib/mysql
  4. -v /Users/wangyanyang/docker_mysql_info/master/conf:/etc/mysql/conf.d
  5. -p 3307:3306 mysql
  6. slave
  7. docker run -d -e MYSQL_ROOT_PASSWORD=root --name slavemysql1 --restart always
  8. -v /Users/wangyanyang/docker_mysql_info/slave1/data:/var/lib/mysql
  9. -v /Users/wangyanyang/docker_mysql_info/slave1/conf:/etc/mysql/conf.d
  10. -p 3308:3306 mysql

3.配置master

进入容器
docker exec -it ea159e960099 /bin/bash
登录数据库
mysql -u root -p

创建用户, identified with 'mysql_native_password' 表示使用该用户的密码加密方式。
create user 'reader'@'%' identified with 'mysql_native_password' by 'reader';
赋予用户相应的权限.
grant all privileges on *.* to 'reader'@'%';
刷新 MySql 系统权限。
flush privileges;

查看状态,记住File、Position的值.
show master status;

image.png
4.配置slave

//设置主库链接
CHANGE MASTER TO MASTER_HOST='10.12.254.39',    -- 主库的 IP 地址
Master_Port=3307,    -- 主库端口
MASTER_USER='reader',    --指定用户名
MASTER_PASSWORD='reader',    -- 密码
MASTER_LOG_FILE='mysql-bin.000003',    -- 从库需要从主库上备份的文件名称
MASTER_LOG_POS=0;    -- 从库开始备份的位置,此时设置为 0,也就是从初始位置开始备份

//启动从库同步
start slave;

image.png
image.png
5.测试主从同步即可。
详见: https://learnku.com/articles/35374

主从复制原理

image.png
master数据写⼊,更新binlog
master创建⼀个dump线程向slave推送binlog
slave连接到master的时候,会创建⼀个IO线程接收binlog,并记录到relay log中继⽇志中
slave再开启⼀个sql线程读取relay log事件并在slave执⾏,完成同步
slave记录⾃⼰的binglog

主从延迟问题

原因

随机重放: 从库中sql线程读取从relay.log日志中读取数据, 并写入磁盘过程是随机的, 效率不是太高, 会存在主从延迟的问题。
主库并发高: 某一时刻,大量写请求打到主库上,意味着要不断对 binlog 进行写入,此时从库中的 SQL 线程就会应接不暇,自然会产生主从延迟。
锁等待: 对于 SQL单线程来说,当遇到阻塞时就会一直等待,直到执行成功才会继续进行。如果某一时刻从库因为查询产生了锁等待的情况,此时只有当前的操作执行完成后才会进行下面的操作,产生了主从延迟的情况。

解决方案

并行复制: sql线程读取relay.log日志并写入磁盘, 采用多线程的方式进行. (mysql5.6之后提供)
image.png
还可以通过控制主库并发, 强制读主库来解决延迟。

读写分离(mybatis插件简单实现)

1.定义多数据源(一主一从)
2.实现AbstractRoutingDataSource来动态的使用数据源
3.实现mybatis plugin来动态的选择数据源

引入多数据源

image.png

实现AbstractRoutingDataSource

spring提供了AbstractRoutingDataSource, 提供了动态选择数据源的功能。替换原有的单一数据源后,即可实现读写分离


@Component("dynamicDataSource")
public class DynamicDataSource extends AbstractRoutingDataSource {

    @Resource(name = "normalDataSource")
    private DataSource normalDataSource;

    @Resource(name = "writeDataSource")
    private DataSource writeDataSource;

    @Resource(name = "readDataSource")
    private DataSource readDataSource;

    // 调用数据源时:determineCurrentLookupKey()方法返回了需要使用的数据源对应的key
    @Override
    protected Object determineCurrentLookupKey() {
        DynamicDataSourceGlobal dynamicDataSourceGlobal = DynamicDataSourceHolder.getDataSource();
        if (dynamicDataSourceGlobal == null
                || dynamicDataSourceGlobal == DynamicDataSourceGlobal.NORMAL) {
            return DynamicDataSourceGlobal.NORMAL.name();
        }
        if (dynamicDataSourceGlobal == DynamicDataSourceGlobal.WRITE) {
            return DynamicDataSourceGlobal.WRITE.name();
        }
        return DynamicDataSourceGlobal.READ.name();
    }

    // AbstractRoutingDataSource内部维护了一个Map<Object, Object>的Map 
    // 在初始化过程中,我们将多个数据源加入到这个map 
    @Override
    public void afterPropertiesSet() {
        //将所有数据源放到Map中
        Map<Object, Object> targetDataSources = new HashMap<>(4);
        targetDataSources.put(DynamicDataSourceGlobal.NORMAL.name(), normalDataSource);
        targetDataSources.put(DynamicDataSourceGlobal.WRITE.name(), writeDataSource);
        targetDataSources.put(DynamicDataSourceGlobal.READ.name(), readDataSource);
        //动态数据源
        //设置默认数据源
        setDefaultTargetDataSource(normalDataSource);
        //设置可通过路由key,切换的数据源Map集
        setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }
}

当前线程需要使用的数据源对应的key,是在DynamicDataSourceHolder类中维护
image.png
image.png

实现mybatis plugin

当前线程使用的数据源对应的key, 这个key需要在mybatis plugin根据sql类型来确定。

package com.dmall.spring;

import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;


@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {
                MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DynamicPlugin implements Interceptor {
    private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";
    private static final Map<String, DynamicDataSourceGlobal> cacheMap = new ConcurrentHashMap<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
        if (!synchronizationActive) {
            Object[] objects = invocation.getArgs();
            MappedStatement ms = (MappedStatement) objects[0];
            DynamicDataSourceGlobal dynamicDataSourceGlobal = null;
            if ((dynamicDataSourceGlobal = cacheMap.get(ms.getId())) == null) {
                //读方法
                if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                    //!selectKey 为自增id查询主键(SELECT LAST_INSERT_ID() )方法,使用主库
                    if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
                        dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
                    } else {
                        BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
                        String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
                        if (sql.matches(REGEX)) {
                            dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
                        } else {
                            dynamicDataSourceGlobal = DynamicDataSourceGlobal.READ;
                        }
                    }
                } else {
                    dynamicDataSourceGlobal = DynamicDataSourceGlobal.WRITE;
                }
                System.out.println("设置方法[{ " + ms.getId() + "}] use [{" + dynamicDataSourceGlobal.name() + "}] Strategy, SqlCommandType [{" + ms.getSqlCommandType().name() + "}]..");
                cacheMap.put(ms.getId(), dynamicDataSourceGlobal);
            }
            DynamicDataSourceHolder.putDataSource(dynamicDataSourceGlobal);
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }
    @Override
    public void setProperties(Properties properties) {
        //
    }
}

分库分表

详见 https://www.yuque.com/wangyanyang/qz7fwa/ee92bw

不停机扩容

第⼀阶段: 在线双写,查询⾛⽼库

  1. 建⽴好新的库表结构,数据写⼊久库的同时,也写⼊拆分的新库
    2. 数据迁移,使⽤数据迁移程序,将旧库中的历史数据迁移到新库
    3. 使⽤定时任务,新旧库的数据对⽐,把差异补齐
    image.png

    第⼆阶段: 在线双写,查询⾛新库

  2. 完成了历史数据的同步和校验
    2. 把对数据的读切换到新库
    image.png

第三阶段:旧库下线

  1. 旧库不再写⼊新的数据
    2. 经过⼀段时间,确定旧库没有请求之后,就可以下线⽼库
    image.png