从一个主从延迟问题开始回顾主从复制原理,并思考主从延迟造成的原因和解决方案。当然,作为底层开发,最后还是只能快准狠的通过一个简单粗暴的等待方案进行应对。
事情的起因
事情要从我写下这样的代码开始
java
// 获取当前数据库中未使用的数据转为正在使用的状态
int updateUsing = fateDataDao.update(FateDataStatusEnum.UNUSED.getCode(),FateDataStatusEnum.USING.getCode());
log.info("update UNUSED to USING:{}",updateUsing);
// 获取正在使用状态的数据
List<FateData> fateDataList = fateService.queryStatus(FateDataStatusEnum.USING.getCode());
log.info("queryStatus USING:{}",fateDataList.size());
这部分逻辑清晰简单明了,把UNUSED 状态的数据更新为USING 状态,然后查询取出USING状态的数据。
按照一个正常的逻辑来说updateUsing
的数量和fateDataList.size()
的数量应该一样,但是,他不正常。
我在测试环境小数据量测试时,这段代码逻辑完全无误。但是上了灰度环境进行大量数据的测试就出现了这样的问题。
此时,我带着疑惑和不解,将目光投向百度。
首先我认为问题可能好似,MYSQL更新返回的是查询到的行数,而不是受影响的行数。
但是负责MYSQL的同事和我说MYSQL已经配置了返回受影响行数,并告诉我应该是主从延迟问题,没办法解决,看看业务能不能改下吧。
这时,我才反应过来当时粗略了解的主从延迟问题,我已经忘的差不多了。
什么是主从复制?
要了解主从延迟,首先就要知道什么是主从复制。
MySQL的主从复制(Master-Slave Replication)是一种数据库复制技术,用于解决数据备份、读写分离、负载均衡以及故障恢复等问题。
主从复制的基本原理是将一个数据库实例(主服务器)的数据复制到另一个或多个数据库实例(从服务器),使得从服务器的数据与主服务器保持同步。
主从复制的基本工作流程:
- 从服务器连接到主服务器,生成两个线程,一个I/O线程,一个SQL线程
- 主服务器记录所有的数据更改(INSERT、UPDATE、DELETE),同时会生成一个 log dump 线程,用来给从库 i/o线程传binlog。
- 主服务器会生成一个 log dump 线程将binlog写到relay log(中继日志) 文件中
- 从服务器的SQL 线程会读取relay log文件中的日志,并解析成具体操作,来实现主从的操作一致,而最终数据一致;
流程图如下:
主从复制解决的问题
要用到主从复制的原因主要是为了高可用、高并发:
- 数据备份和恢复: 从服务器可以用作主服务器的备份,当主服务器发生故障时,可以快速切换到从服务器进行恢复。
- 读写分离: 主服务器负责写操作,而从服务器可以用于处理读操作,从而分担主服务器的负载,提高系统性能。
- 负载均衡: 多个从服务器可以平均分担读请求,实现负载均衡,提高系统的可伸缩性。
- 高可用性: 当主服务器故障时,可以快速切换到一个从服务器,保证系统的高可用性。
主从复制带来的问题
主从复制也会造成一些衍生出的问题:
- 数据一致性: 主从复制是异步的,存在一定的延迟,因此在进行读写分离时,需要注意可能出现的数据一致性问题。
- 写操作集中: 所有写操作都集中在主服务器上,可能导致主服务器的负载较高。
- 配置和维护: 操作复杂,需要正确配置主从服务器,以及定期进行监控和维护,确保系统正常运行。
本次遇到的bug,主要就是数据一致性方面的问题了。
主从延迟的原因
主库使用单线程顺序写入binlog,效率很高。然而从库的SQL Thread线程需要对主库的日志进行随机IO来重新执行DML和DDL,效率较低,难以跟上主库日志写入速度,因此产生了主从延迟。
另外,从库SQL Thread也是单线程,当主库并发较高时,产生大量DML,超过了从库单线程能处理的速度,或者从库中有大查询语句产生锁等待,也会导致从库执行延迟,无法跟上主库的进度。
主从延迟的解决方案
从主从延迟的原因,我们定位出主要是主库的高并发和从库的SQL Thread效率低造成了这样的问题。
所以,在不增加机器的情况下的解决方案就是控制主库的并发 或者提升从库的SQL Thread处理效率,例如MySQL 5.6 版本后,提供的一种多线程的方式。
简单粗暴的解决方案
当然,对于我们公司的底层开发来说,这种层次的设计需要更高层面的人来推动,而且也需要更长的时间才能处理。
所以这里贴出我自己的解决方案。Thread.sleep
时间请自行控制。
java
// 获取当前数据库中未使用的数据转为正在使用的状态
int updateUsing = fateDataDao.update(FateDataStatusEnum.UNUSED.getCode(),FateDataStatusEnum.USING.getCode());
LOGGER.info("update UNUSED to USING:{}",updateUsing);
// 获取正在使用状态的数据
List<FateData> fateDataList = fateService.queryStatus(FateDataStatusEnum.USING.getCode());
LOGGER.info("queryStatus USING:{}",fateDataList.size());
// 如果数据相等,直接略过。
if(updateUsing != fateDataList.size()){
// updateUsing为0,但fateDataList不为空的情况。任务失败,未更新
if (updateUsing == 0){
Cat.logEvent("updateTmpData","jobFailed:"+"updateUsing:"+updateUsing+"--fateDataList:"+fateDataList.size());
transaction.setStatus(Transaction.SUCCESS);
return response;
}
// 数据数目不相等,等待三秒相等再继续
boolean equalFlag = false;
while(!equalFlag){
// 等待主从延迟
Thread.sleep(3000);
fateDataList = fateService.queryStatus(FateDataStatusEnum.USING.getCode());
Cat.logEvent("updateTmpData","equalFailed:"+"updateUsing:"+updateUsing+"--fateDataList:"+fateDataList.size());
LOGGER.info("queryStatus USING:{}",fateDataList.size());
equalFlag = true;
}
// 数据数目不相等,则需要数据不为空再继续
while(fateDataList.isEmpty()){
// 等待主从延迟
Thread.sleep(1000);
fateDataList = fateService.queryStatus(FateDataStatusEnum.USING.getCode());
Cat.logEvent("updateTmpData","queryFailed:"+"updateUsing:"+updateUsing+"--fateDataList:"+fateDataList.size());
LOGGER.info("queryStatus USING:{}",fateDataList.size());
}
}
参考文章:
MySQL主从同步详解与配置 - 知乎 (zhihu.com)
从一个主从延迟问题,学习Mysql主从复制原理 - 掘金 (juejin.cn)