Mysql读写分离

什么是主从复制、读写分离?

MySQL的主从复制和读写分离是一起出现的,目的是为了提升数据库的并发性能。

MySQL 主从复制是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。

MySQL 的读写分离是指如果有三台MySQL,一台mater只负责写操作,两台salve只负责读操作。

主从复制、读写分离就是为了数据库能支持更大的并发。

随着业务量的扩展、如果是单机部署的MySQL,会导致I/O频率过高。采用主从复制、读写分离可以提高数据库的可用性。

主从复制

主从复制的原理

  • 1、当Master节点进行insert、update、delete操作时,会按顺序写入到binlog中。

  • 2、salve从库连接master主库,Master有多少个slave就会创建多少个binlog dump线程。

  • 3、当Master节点的binlog发生变化时,binlog dump 线程会通知所有的salve节点,并将相应的binlog内容推送给slave节点。

  • 4、I/O线程接收到 binlog 内容后,将内容写入到本地的 relay-log。

  • 5、SQL线程读取I/O线程写入的relay-log,并且根据 relay-log 的内容对从数据库做对应的操作。

如何实现主从复制

目前有两台主机,一台是Master(192.168.10.214),一台是Slave(192.168.10.227)。

master配置

使用命令行进入mysql:

mysql -u root -p

创建从库连接主库的账号并授权:

1
2
3
4
# 192.168.10.227是slave从机的IP
GRANT REPLICATION SLAVE ON *.* to 'slave_user'@'192.168.10.227' identified by 'testpass';
# 刷新权限
FLUSH PRIVILEGES;

在/etc/my.cnf配置文件中添加以下配置:

1
2
3
4
5
6
7
8
# 开启binlog
log-bin=mysql-bin
server-id=1
# 需要同步的数据库,如果不配置则同步全部数据库
binlog-do-db=test_db
# binlog日志保留的天数,清除超过10天的日志
# 防止日志文件过大,导致磁盘空间不足
expire-logs-days=10

配置完成后重启数据库:

service mysql restart

可以通过命令show master status\G;查看当前binlog日志的信息。

1
2
3
4
5
6
*************************** 1. row ***************************
File: mysql-bin.000005
Position: 80770
Binlog_Do_DB: test_db
Binlog_Ignore_DB:
Executed_Gtid_Set:

Slave配置

在/etc/my.cnf配置文件中添加以下配置:

server-id=2

登录mysql:

mysql -u root -p

运行sql:

1
2
3
4
5
6
7
CHANGE MASTER TO
MASTER_HOST='192.168.10.214', # 主机IP
MASTER_USER='slave_user',
MASTER_PASSWORD='testpass',
MASTER_LOG_FILE='mysql-bin.000005',
MASTER_LOG_POS=68737, # 这里是show master status;中的Position
master_port=3306;

然后启动slave:

1
2
# 启动slave服务
start slave;

启动完之后查看一下结果:

1
show slave status\G;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 192.168.10.214
Master_User: root
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql-bin.000005
Read_Master_Log_Pos: 71914
Relay_Log_File: mysql-relay-bin.000002
Relay_Log_Pos: 3469
Relay_Master_Log_File: mysql-bin.000005
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: 71914
Relay_Log_Space: 3778
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: No
Master_SSL_CA_File:
Master_SSL_CA_Path:
Master_SSL_Cert:
Master_SSL_Cipher:
Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 2
Master_SSL_Crl:
Master_SSL_Crlpath:
Using_Gtid: No
Gtid_IO_Pos:
Replicate_Do_Domain_Ids:
Replicate_Ignore_Domain_Ids:
Parallel_Mode: conservative
SQL_Delay: 0
SQL_Remaining_Delay: NULL
Slave_SQL_Running_State: Slave has read all relay log; waiting for the sl
ave I/O thread to update it
Slave_DDL_Groups: 0
Slave_Non_Transactional_Groups: 0
Slave_Transactional_Groups: 0

如果Slave_IO_RunningSlave_SQL_Running就是正常了。

这时可以测试下新增表或者插入数据,如果从库正常同步那就OK了。

读写分离

实现了主从复制后,我们开始实现读写分离。

目前使用的架构是:

1
2
3
Springboot: 1.5.13.RELEASE
Mybatis : 3.4.5
Druid : 1.1.4

基于Spring的AbstractRoutingDataSource和Mybatis plugin来实现动态切换数据源。

配置

首先我们先配置数据源,建议从库赋予只读权限,因为从库不能更改

1
2
3
4
5
6
7
8
9
datasource.master.driver-class-name=com.mysql.jdbc.Driver
datasource.master.url=jdbc:mysql://192.168.10.214:3306/youbang
datasource.master.username=root
datasource.master.password=

datasource.slave0.driver-class-name=com.mysql.jdbc.Driver
datasource.slave0.url=jdbc:mysql://192.168.10.186:3306/youbang
datasource.slave0.username=root
datasource.slave0.password=

读取配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class DataSourcePropertiesConfig {

@Primary
@Bean("masterDataSourceProperties")
@ConfigurationProperties("datasource.master")
public DataSourceProperties masterDataSourceProperties() {
return new DataSourceProperties();
}

@Bean("slave0DataSourceProperties")
@ConfigurationProperties("datasource.slave0")
public DataSourceProperties slave0DataSourceProperties() {
return new DataSourceProperties();
}
}

实现AbstractRoutingDataSource

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Component
public class CustomRoutingDataSource extends AbstractRoutingDataSource {

@Resource(name = "masterDataSourceProperties")
private DataSourceProperties masterProperties;

@Resource(name = "slave0DataSourceProperties")
private DataSourceProperties slave0Properties;


@Override
public void afterPropertiesSet() {
DataSource masterDataSource =
masterProperties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
DataSource slave0DataSource =
slave0Properties.initializeDataSourceBuilder().type(DruidDataSource.class).build();

setDefaultTargetDataSource(masterDataSource);

Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(DataSourceHolder.MASTER_DATASOURCE, masterDataSource);
dataSourceMap.put(DataSourceHolder.SLAVE0_DATASOURCE, slave0DataSource);
setTargetDataSources(dataSourceMap);

super.afterPropertiesSet();
}

@Override
protected Object determineCurrentLookupKey() {
String key = DataSourceHolder.getDataSource();

if (key == null) {
// default datasource
return DataSourceHolder.MASTER_DATASOURCE;
}

return key;
}

AbstractRoutingDataSource内部维护了一个Map<Object, Object>的Map
初始话时,将master、slave两个数据源加入到这个map
调用数据源时,determineCurrentLookupKey()方法返回了需要使用的数据源对应的key

当前线程需要使用的数据源对应的key,在DataSourceHolder中维护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DataSourceHolder {
public static final String MASTER_DATASOURCE = "master";
public static final String SLAVE0_DATASOURCE = "slave0";

private static final ThreadLocal<String> local = new ThreadLocal<>();


public static void putDataSource(String dataSource) {
local.set(dataSource);
}

public static String getDataSource() {
return local.get();
}

public static void clearDataSource() {
local.remove();
}

}

实现Mybatis Plugins

当调用的Sql时Select类型时,将数据源切换到slave,其他情况数据源使用master;

注意,如果处于事务中,数据源只使用master,保持数据一致性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@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}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,
CacheKey.class, BoundSql.class})})
public class MybatisDataSourceInterceptor implements Interceptor {

@Override
public Object intercept(Invocation invocation) throws Throwable {

boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
if(!synchronizationActive) {
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];

if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
DataSourceHolder.putDataSource(DataSourceHolder.SLAVE0_DATASOURCE);
}
}

return invocation.proceed();
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
}
}

SqlSessionFactoryBean中新增插件:

1
2
3
4
5
6
7
8
9
10
11
12
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setTypeAliasesPackage(MODEL_PACKAGE);

MybatisDataSourceInterceptor mybatisDataSourceInterceptor = new MybatisDataSourceInterceptor();
//添加插件
factory.setPlugins(new Interceptor[]{mybatisDataSourceInterceptor});

return factory.getObject();
}

这里可以验证一下是否写操作走master,读操作走slave,我这里是通过jmeter并发请求来测试。

作者

VGEAREN

发布于

2021-10-19

更新于

2022-01-27

许可协议

评论