应用Docker Compose搭建MySQL主从复制架构

环境筹备

docker 装置MySQL数据库

docker pull mysql

运行MySQL容器

docker run --name mysql mysql -e MYSQL_ROOT_PASSWORD=123456

应用命令将MySQL配置文件my.cnf 复制出主机上

docker cp mysql:/var/lib/mysql/ D:/docker/mysql_cluster/my.cnf

拿到my.cnf原配置文件,加以革新就能够实现数据库主从同步了

配置文件

创立文件夹

在主机创立mysql_cluster 文件夹

mysql_cluster  master/    my.cnf    mysql/ slave/    my.cnf    mysql/  docker-compose.yml   

将从容器内复制进去my.cnf别离放入 master、slave 下

文件配置

设置 master my.cnf

# 上面配置为主节点设置 #开启二进制日志log_bin=mysql-bin    #为以后节点设置一个全局惟一的ID号server_id=95       # 不须要同步数据库binlog-ignore-db = mysqlbinlog_cache_size = 1M# 二级制主动删除的天数,默认为0,表白没有主动删除,启动时和二级制日志循环可能删除工夫expire_logs_days = 7log_bin_trust_function_creators = 1binlog_format=mixed# MySQL 8.x,须要如下配置default_authentication_plugin=mysql_native_passwordcharacter-set-server=utf8mb4collation-server=utf8mb4_unicode_ci

配置 slave my.cnf

server_id = 102log-bin = mysql-binrelay_log = relicas-mysql-relay-bin log-slave-updates = 1binlog-ignore-db = mysqllog_bin_trust_function_creators = 1binlog_format=mixedread_only = 1# MySQL 8.x,须要如下配置default_authentication_plugin=mysql_native_passwordcharacter-set-server=utf8mb4collation-server=utf8mb4_unicode_ci

docker-compose.yml 配置

version: '3.8'services:  mysql-master:    container_name: mysql-master    hostname: mysql-master        image: mysql    restart: always    ports:      - "3306:3306"     volumes:      - D:/docker/mysql_cluster/master/my.cnf:/etc/mysql/my.cnf      - D:/docker/mysql_cluster/master/mysql:/var/lib/mysql    environment:      MYSQL_ROOT_PASSWORD: 123456      TZ: Asia/Shanghai    command: [        '--character-set-server=utf8mb4',        '--collation-server=utf8mb4_general_ci',        '--max_connections=3000'    ]    networks: ## 引入内部事后定义的网段      myweb:        ipv4_address: 192.168.102.120   #设置ip地址        mysql-slave:    container_name: mysql-slave    hostname: mysql-slave        image: mysql    restart: always    ports:      - "3307:3306"     volumes:      - D:/docker/mysql_cluster/slave/my.cnf:/etc/mysql/my.cnf      - D:/docker/mysql_cluster/slave/mysql:/var/lib/mysql    environment:      MYSQL_ROOT_PASSWORD: 123456      TZ: Asia/Shanghai    command: [        '--character-set-server=utf8mb4',        '--collation-server=utf8mb4_general_ci',        '--max_connections=3000'    ]    networks:      myweb:        ipv4_address: 192.168.102.121   #设置ip地址networks:  myweb:    driver: bridge    ipam:       config:        # 自定义 网段         - subnet: "192.168.102.0/24"

还用一点值得注意的,如果宿主机上的mysql 文件夹不是空的,配置中MySQL root 明码不失效的。有次我将一个正在运行中/var/lib/mysql copy到宿主机上,应用逻辑卷映射到容器中去,导致MySQL始终登录不下来。

设置主从同步

运行容器

docker-compose up -d

查看运行状况

docke ps -a

呈现上面状态,表明两个节点都运行胜利了

CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS                                                  NAMESbf3f254d75b2   mysql     "docker-entrypoint.s…"   5 seconds ago   Up 4 seconds   33060/tcp, 0.0.0.0:3307->3306/tcp, :::3307->3306/tcp   mysql-slave691e10949d3f   mysql     "docker-entrypoint.s…"   5 seconds ago   Up 4 seconds   0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp   mysql-master

登录master节点

设置slave 连贯master节点

 mysql> grant replication client,replication slave on *.* to 'root'@'192.168.102.120'

保留设置

mysql> flush privileges

获取binlog 文件名和Position

mysql> show master status
+------------------+----------+--------------+------------------+-------------------+| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |+------------------+----------+--------------+------------------+-------------------+| mysql-bin.000003 |      156 |              | mysql            |                   |+------------------+----------+--------------+------------------+-------------------+

登录slave 节点

mysql> reset master;mysql> CHANGE MASTER TO MASTER_HOST='mysql-master',MASTER_USER='root',MASTER_PASSWORD='123456',MASTER_PORT=3306,MASTER_LOG_FILE='mysql-bin.000003',MASTER_LOG_POS=156mysql> start slave

查看同步后果

mysql> show slave status \G;*************************** 1. row ***************************               Slave_IO_State: Waiting for source to send event                  Master_Host: mysql-master                  Master_User: root                  Master_Port: 3306                Connect_Retry: 60              Master_Log_File: mysql-bin.000003          Read_Master_Log_Pos: 156               Relay_Log_File: relicas-mysql-relay-bin.000002                Relay_Log_Pos: 324        Relay_Master_Log_File: mysql-bin.000003             Slave_IO_Running: Yes            Slave_SQL_Running: Yes              Replicate_Do_DB:          Replicate_Ignore_DB:           Replicate_Do_Table:       Replicate_Ignore_Table:      Replicate_Wild_Do_Table:  Replicate_Wild_Ignore_Table:                   Last_Errno: 0                   Last_Error:                 Skip_Counter: 0          Exec_Master_Log_Pos: 156              Relay_Log_Space: 541              Until_Condition: None               Until_Log_File:                Until_Log_Pos: 0        Seconds_Behind_Master: 0  Replicate_Ignore_Server_Ids:             Master_Server_Id: 95                  Master_UUID: 903599fc-0336-11ec-9228-0242adc80678             Master_Info_File: mysql.slave_master_info                    SQL_Delay: 0          SQL_Remaining_Delay: NULL      Slave_SQL_Running_State: Replica has read all relay log; waiting for more updates           Master_Retry_Count: 86400

看见 Slave_IO_Running: Yes Slave_SQL_Running: Yes这两个都是Yes 阐明同步曾经胜利了。

验证同步

连贯master 节点,创立一个数据库,在新数据库下再创立一个新表。再连贯slave 节点能够看见再master 创立数据库下的新表,这样就表明数据曾经实现同步了。

应用springboot 搭建读写拆散

maven pox.xml

 <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>2.5.1</version>        <relativePath/>    </parent>    <dependencies>        <dependency>            <groupId>org.mybatis.spring.boot</groupId>            <artifactId>mybatis-spring-boot-starter</artifactId>            <version>2.2.0</version>        </dependency>        <dependency>            <groupId>com.alibaba</groupId>            <artifactId>druid-spring-boot-starter</artifactId>            <version>1.2.3</version>        </dependency>        <dependency>            <groupId>mysql</groupId>            <artifactId>mysql-connector-java</artifactId>            <scope>runtime</scope>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-aop</artifactId>        </dependency>        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <optional>true</optional>        </dependency>    </dependencies>

次要原来就是利用org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource能够将不同连贯设置到Map 依据key 获取获取dataSource ,重写determineCurrentLookupKey() ,实现对读取数据库时,切换到读库,写操作切换到写库。 上面会贴出全副代码

application.yml 配置

server:  port: 8001spring:  datasource:    druid:      master:        url: jdbc:mysql://127.0.0.1:3306/example?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true        username: root        password: 123456      slave:        url: jdbc:mysql://127.0.0.1:3307/example?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true        username: root        password: 123456

多数据库druid 设置

@Configurationpublic class DataSourceConfig {    @Bean    @ConfigurationProperties(prefix = "spring.datasource.druid.master")    public DataSource master() {        return DruidDataSourceBuilder.create().build();    }    @Bean    @ConfigurationProperties(prefix = "spring.datasource.druid.slave")    public DataSource slave(){        return DruidDataSourceBuilder.create().build();    }    @Bean("dynamicDB")    public DataSourceRouter dynamicDB(@Qualifier("master") DataSource master,                                      @Qualifier("slave") DataSource slave){        DataSourceRouter router = new DataSourceRouter();        Map<Object,Object> map = new HashMap<>(2);        map.put(SLAVE,slave);        map.put(MASTER,master);        router.setTargetDataSources(map);         router.setDefaultTargetDataSource(slave);        return router;    }}

创立枚举类用作数据库路由Key

public enum  ClusteEnum {    SLAVE,    MASTER;}

重写路由数据库实现依据key 切换数据库

public class DataSourceRouter extends AbstractRoutingDataSource {    @Override    protected Object determineCurrentLookupKey() {        return DataSourceContextHolder.get();    }}

DataSourceContextHolder 提供线程平安形式返回不同连贯切换key

public class DataSourceContextHolder  {    private static ThreadLocal<ClusteEnum> contextHolder = new ThreadLocal<>();    public static void  setEnum(ClusteEnum clusteEnum){        contextHolder.set(clusteEnum);    }    public static  void  remove(){        contextHolder.remove();    }    public static ClusteEnum get(){       return contextHolder.get();    }}

设置了多数据源,须要手上生成SqlSessionFactory,SqlSessionTemplate bean,让Mybatis 失效。

@Configurationpublic class MybatisConfig {    @Resource(name = "dynamicDB")    private DataSource dataSource;    @Bean("sqlSessionFactory")    public SqlSessionFactory sessionFactory() throws Exception {        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();        factoryBean.setDataSource(dataSource);        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml"));        return factoryBean.getObject();    }    @Bean    public PlatformTransactionManager transactionManager(){        return new DataSourceTransactionManager(dataSource);    }    @Bean("sqlSessionTemplate")    public SqlSessionTemplate sqlSessionTemplate( @Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory)  {        return new SqlSessionTemplate(sqlSessionFactory);    }}

自定义注解申明须要切换数据源

@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface DatabaseSelector {    ClusteEnum value() default SLAVE;    boolean clear() default true;}

最初一步,设置AOP 盘绕告诉,动静批改数据库路由Key

@Slf4j@Aspect@Component@Order(1)  //这个注解很要害,如果没有不能胜利切换数据源public class DatabaseAopPointCut {    @Around("@annotation(tk.shenyifeng.study.bean.DatabaseSelector)")    public Object setDynamicDataSource(ProceedingJoinPoint pjp) throws Throwable {        boolean clear = true;        try {            MethodSignature signature = (MethodSignature) pjp.getSignature();            Method method = signature.getMethod();            DatabaseSelector databaseSelector = method.getAnnotation(DatabaseSelector.class);            clear = databaseSelector.clear();            log.info("set cluster db => {}",databaseSelector.value().name());            DataSourceContextHolder.setEnum(databaseSelector.value());            return pjp.proceed();        }finally {            if (clear)                DataSourceContextHolder.remove();        }    }}

b编写一个简略service 验证成绩

@Componentpublic class UserService {    @Autowired    private UserMaper userMaper;    @Transactional(rollbackFor = Exception.class)    @DatabaseSelector(MASTER)    public void insert(UserModel userModel){        userMaper.insert(userModel);    }    public UserModel findOne(int id){        return userMaper.findOne(id);    }    @DatabaseSelector(MASTER)    public void updateOne(UserModel userModel){        userMaper.updateOne(userModel);    }}

编写测试类

@ExtendWith(SpringExtension.class)@SpringBootTest(classes = RunApp.class)@Slf4jpublic class ClusterServiceTest {    @Autowired    private UserService userService;    @Test    public void insertTest(){        UserModel userModel = new UserModel();        userModel.setName("神易风");        userModel.setAge(8848);        userService.insert(userModel);    }    @Test    public void findOne(){        UserModel one = userService.findOne(1);        log.info("user data {}",one);    }    @Test    public void integrationTest(){        UserModel userModel = userService.findOne(1);        userModel.setName("神易风02");        userModel.setAge(18);        userService.updateOne(userModel);    }}

运行insert 插入 后果如下 胜利切换到master 库
再运行 findOne 办法 应用默认连贯slave ,能胜利查问到后果
最初混合应用也是没问题的