应用 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 = mysql
binlog_cache_size = 1M
# 二级制主动删除的天数,默认为 0,表白没有主动删除,启动时和二级制日志循环可能删除工夫
expire_logs_days = 7
log_bin_trust_function_creators = 1
binlog_format=mixed
# MySQL 8.x,须要如下配置
default_authentication_plugin=mysql_native_password
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
配置 slave my.cnf
server_id = 102
log-bin = mysql-bin
relay_log = relicas-mysql-relay-bin
log-slave-updates = 1
binlog-ignore-db = mysql
log_bin_trust_function_creators = 1
binlog_format=mixed
read_only = 1
# MySQL 8.x,须要如下配置
default_authentication_plugin=mysql_native_password
character-set-server=utf8mb4
collation-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 NAMES
bf3f254d75b2 mysql "docker-entrypoint.s…" 5 seconds ago Up 4 seconds 33060/tcp, 0.0.0.0:3307->3306/tcp, :::3307->3306/tcp mysql-slave
691e10949d3f 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=156
mysql> 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: 8001
spring:
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 设置
@Configuration
public 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 失效。
@Configuration
public 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 验证成绩
@Component
public 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)
@Slf4j
public 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,能胜利查问到后果
最初混合应用也是没问题的