从一个主从延迟问题开始回顾主从复制原理,并思考主从延迟造成的原因和解决方案。当然,作为底层开发,最后还是只能快准狠的通过一个简单粗暴的等待方案进行应对。
事情的起因
事情要从我写下这样的代码开始
// 获取当前数据库中未使用的数据转为正在使用的状态
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更新返回的是查询到的行数,而不是受影响的行数。
Mybatis使用<update>标签怎么返回影响行数_mybatis返回影响行数-CSDN博客
但是负责MYSQL的同事和我说MYSQL已经配置了返回受影响行数,并告诉我应该是主从延迟问题,没办法解决,看看业务能不能改下吧。
这时,我才反应过来当时粗略了解的主从延迟问题,我已经忘的差不多了。
什么是主从复制?
要了解主从延迟,首先就要知道什么是主从复制。
MySQL的主从复制(Master-Slave Replication)是一种数据库复制技术,用于解决数据备份、读写分离、负载均衡以及故障恢复等问题。
主从复制的基本原理是将一个数据库实例(主服务器)的数据复制到另一个或多个数据库实例(从服务器),使得从服务器的数据与主服务器保持同步。
主从复制的基本工作流程:
流程图如下:
graph LR
subgraph 主服务器
主库-->日志记录
日志记录-->|生成|日志转储线程[Log Dump]
end
subgraph 从服务器
从库-->I/O线程[I/O Thread]
I/O线程-->SQL线程[SQL Thread]
end
日志转储线程-->|写binlog|中继日志[Relay Log]
中继日志-->|读|I/O线程
主从复制解决的问题
要用到主从复制的原因主要是为了高可用、高并发:
主从复制带来的问题
主从复制也会造成一些衍生出的问题:
本次遇到的bug,主要就是数据一致性方面的问题了。
主从延迟的原因
主库使用单线程顺序写入binlog,效率很高。然而从库的SQL Thread线程需要对主库的日志进行随机IO来重新执行DML和DDL,效率较低,难以跟上主库日志写入速度,因此产生了主从延迟。
另外,从库SQL Thread也是单线程,当主库并发较高时,产生大量DML,超过了从库单线程能处理的速度,或者从库中有大查询语句产生锁等待,也会导致从库执行延迟,无法跟上主库的进度。
主从延迟的解决方案
从主从延迟的原因,我们定位出主要是主库的高并发和从库的SQL Thread效率低造成了这样的问题。
所以,在不增加机器的情况下的解决方案就是控制主库的并发或者提升从库的SQL Thread处理效率,例如MySQL 5.6 版本后,提供的一种多线程的方式。
简单粗暴的解决方案
当然,对于我们公司的底层开发来说,这种层次的设计需要更高层面的人来推动,而且也需要更长的时间才能处理。
所以这里贴出我自己的解决方案。Thread.sleep
时间请自行控制。
// 获取当前数据库中未使用的数据转为正在使用的状态
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)
架构师必备:MySQL主从延迟解决办法 - 掘金 (juejin.cn)
(二十四)全解MySQL之主从篇:死磕主从复制中数据同步原理与优化 - 掘金 (juejin.cn)