本文前半段主要参考和节选沈剑大佬的公众号的下面三篇文章,完整参考见文末

敢说你没遇到过,主从数据库不一致?

DB主从一致性架构优化4种方法

mysql并行复制降低主从同步延时的思路与启示

其他文章参考见文末:Mysql复制方式(半同步复制,并行复制,多源复制)

问:常见的数据库集群架构如何?

一主多从,主从同步,读写分离。

主从不一致解决方案 && 如何降低主从延迟_数据库

如上图:

(1)一个主库提供写服务;

(2)多个从库提供读服务,可以增加从库提升读性能;

(3)主从之间同步数据;

画外音:任何方案不要忘了本心,加从库的本心,是提升读性能。(主从架构是为了读写分离,再次架构上继续添加从库是为了提高读性能)

问:为什么会出现不一致?

主从同步有时延,这个时延期间读从库,可能读到不一致的数据。

主从不一致解决方案 && 如何降低主从延迟_同步复制_02

如上图:

(1)服务发起了一个写请求;

(2)服务又发起了一个读请求,此时同步未完成,读到一个不一致的脏数据;

(3)数据库主从同步最后才完成;

画外音:任何数据冗余,必将引发一致性问题。比如消息队列的分区,著名的 cap 理论等。

问:如何避免这种主从延时导致的不一致?

常见的方法有这么几种。

方案一:忽略。

任何脱离业务的架构设计都是耍流氓,绝大部分业务,例如:百度搜索,淘宝订单,QQ消息,58帖子都允许短时间不一致。

画外音:如果业务能接受,最推崇此法。

如果业务能够接受,别把系统架构搞得太复杂。

方案二:放弃主从架构,强制读主。

主从不一致解决方案 && 如何降低主从延迟_数据_03

如上图:

(1)不采用读写主从架构,只使用一个高可用主库提供数据库服务;

(2)读和写都落到主库上;

(3)采用缓存来提升系统读性能 存储;

这是很常见的微服务架构,可以避免数据库主从一致性问题。

方案三:半同步复制

修改主库后至少同步修改完成一个从库,才返回请求,缓存每次都从那个同步更新的从库中读数据。

方案优点:利用数据库原生功能,比较简单

方案缺点:主库的写请求时延会增长,吞吐量会降低

注意点:

(1)主库和从库都要启用半同步复制才会进行半同步复制功能,否则主库会还原为默认的异步复制。

(2)当主库等待超时时,也会还原为默认的异步复制。当至少有一个从库赶上时,主库会恢复到半同步复制。

参考:Mysql复制方式(半同步复制,并行复制,多源复制)

方案四:cache key 控制选择性读主。

方案二“放弃主从架构,强制读主”过于粗暴,毕竟主从架构里,只有写请求会导致主从不一致,而写请求是占比比较小的,并且每次主从不一致的持续时间很短,如果因为少量的主从不一致而放弃主从架构,未免有点心疼。

那有没有可能采用折中方案,保留主从架构,但是发生主从不一致时读主库,平时读从库呢?

这样的话就需要能辨识什么时候可能发生主从不一致,很明显是发生了写操作时,那么当发生写操作时,我们可以利用一个缓存 key 标记那些不容许主从不一致,也就是必须读主的数据,发生了更新,且设置缓存 key 的超时时间,超时时间设置为“主从同步时延”;

主从不一致解决方案 && 如何降低主从延迟_数据_04

如上图,当写请求发生时:

(1)写主库;

(2)将哪个库,哪个表,哪个主键三个信息拼装一个key设置到cache里,这条记录的超时时间,设置为“主从同步时延”;

画外音:key的格式为“db:table:PK”,假设主从延时为1s,这个key的cache超时时间也为1s。

 

主从不一致解决方案 && 如何降低主从延迟_数据库_05

如上图,当读请求发生且缓存没有命中时:

这时要读哪个库,哪个表,哪个主键的数据呢,也将这三个信息拼装一个key,到cache里去查询,如果,

(1)cache里有这个key,说明1s内刚发生过写请求,数据库主从同步可能还没有完成,此时就应该去主库查询;

(2)cache里没有这个key,说明最近没有发生过写请求,此时就可以去从库查询;

以此,保证读到的一定不是不一致的脏数据。

方案五:数据库中间件控制选择性读主

如果有了数据库中间件,所有的数据库请求都走中间件。

(1)所有的读写都走数据库中间件,通常情况下,写请求路由到主库,读请求路由到从库

(2)记录所有路由到写库的key,在经验主从同步时间窗口内(假设是500ms),如果有读请求访问中间件,此时有可能从库还是旧数据,就把这个key上的读请求路由到主库

(3)经验主从同步时间过完后,对应key的读请求继续路由到从库

方案优点:能保证绝对一致

方案缺点:数据库中间件的成本比较高

总结

数据库主库和从库不一致,常见有这么几种优化方案:

(1)业务可以接受,系统不优化;

(2)放弃主从架构,强制读主,高可用主库,用缓存提高读性能;

(3)半同步复制,至少同步完成一个从库才算完成写操作,主从一致性要求特别高的数据从该从库读取数据。

(4)cache key 控制选择性读主。在cache 里记录哪些记录发生了写请求,来路由读主还是读从;

(5)数据库中间件控制选择性读主。利用数据库中间件来代理所有请求,由中间件来控制写请求发生后的读请求路由到从库还是主库。

注:部署db一主多从的时候,可以对外提供两个接口: 强一致读(从主库读),弱一致读(从从库读)。 由调用方根据应用场景判断该调哪个接口。一般来说,需要强一致数据的场景很少的,主库的读写压力应该不大。

如何降低主从延迟

主从延迟来自两个方面:从库进行 binlog 复制,从库日志回放。从库复制 binlog 这个主要影响是网络带宽和网络稳定性,只能提高带宽来解决,没有什么更好的方式,所以这里更多讨论从库回放日志阶段导致的主从延迟。

从库一般是单线程重放 sql 日志,所以如果主库写并发较大,可能导致从库单线程重放 sql 压力较大,主从复制延迟较高。

所以优化思路就有2个,降低主库写并发和多线程重放 sql 日志,提高重放效率。

水平分库(降低主库写并发)

水平分库后单个分库的数据量降低,单个分库的写并发降低,从而降低从库重放 sql 的排队时延,因而降低延迟。

从库并行复制(实际上应该叫并发复制)(多线程重放 sql 日志,提高重放效率)

数据库之所以以默认选择用单线程回放 sql 日志,就是希望重放 sql 日志的顺序跟主库事务提交的顺序保持一致,从而保证主从数据的一致性,如果重放顺序遭到破坏,有可能导致从库数据发生错误。

那能不能对 relay log 中的日志进行分类,有前后依赖关系的日志串行执行,没有前后依赖关系的并发执行呢?答案是有的。

所谓并行复制,指的是从库开启多个 SQL 线程,并发读取和重放 relay log 中的日志,加快重放效率,提高重放效率,降低主从延迟。

数据库级别库级别的并行复制

很明显不同数据库之间的写操作是没有没有任何依赖的,所以数据库之间的日志重放是可以安全并发执行的。

MySQL 5.5 版本不支持并行复制。MySQL 5.6版本开始支持库级别的并行复制,就是如果同个数据库应用中有多个数据库,可以通过配置,开启库级别的日志并发重放,加快重放速度。

但是目前生产环境中大部分都是在单个数据库应用创建单个库,所以数据库级别的并行复制基本上没什么用。

组级别的并行复制

MySQL 5.7版本对并行复制进一步改进,细化到了单库内部,按照依赖关系对主库的事务进行分组,不同分组的事务没有依赖关系,可以安全并发复制,组内的事务日志则串行复制。

问题:

问:如果采用选择性读主,那么假设发生写库拥堵,或者网络抖动主导致从延迟突然增加,超过设置的经验值,怎么办

主从同步延时一般是相对固定的,偶尔会有抖动(概率较小),可以根据实际稳定情况,适当加长主从同步延时经验值。

问:数据库中间件需要有哪些功能

代理所有读写请求,解析sql,路由到对应库,

问:如果数据库中间件服务选择性读主,写操作的 key 存在哪,中间件服务本地吗?还是 redis 中?如果是在本地,那怎么保证下次读不会请求到其他的中间件服务器?

我也很好奇,看样子是需要存在 redis 的全局缓冲中,这样可以全局可见,如果 key 存在的话,读请求无论请求哪个中间件服务都能请求到 key,但是这样是不是多了一次网络 IO,降低性能,而且这中实现好像跟方案 4 没啥区别了。

如果存在本地,通过key路由,把相同 key 路由到同一个中间件服务器,这样好像中间件服务器的作用就有点不实际名归了,没有对外屏蔽掉所有的路由细节。

问:如果查询,某个列表,怎么办?缓存 key 用不上,列表查询就只有强制读主库?

暂时来看好像是的

问:主库的 binlog 是事务提交之前写的,还是事务提交后写的,如果是提交前写的,万一写完 binlog,但是事务提交失败怎么办?事务提交成功的依据是什么?

主库的 binlog 是事务提交之前写的,redo log 先于 binlog

事务提交的依据是修改行记录隐藏字段中的last_transaction_id吗,这个应该是在写 binlog 时就已经写来才对

问:半同步复制,每次完成半同步的从库是固定的一个吗,还是任何一个完成同步就行

如果每次完成半同步复制的从库不一样,还是无法保证马上发生的读请求能读到最新的数据。

只要有一台从写入relay-log,就返回給主库,这种方式一旦读请求落在其他从库,也是不一致的

问:半同步复制的具体过程是怎么样的

主库写binlog,从库复制 binglog 到自己的 relay log 文件,commit

主库在提交后,执行事务提交的线程将一直等待,至少收到一个半同步从库将事件写入其中继日志(relay log)并刷新到磁盘的确认后,才会执行之后的事务提交,然后才响应客户端写操作成功。

问:半同步复制除了提高主从一致性外,还有什么作用

半同步复制保证了事务成功提交后,至少有两份日志记录,一份在主库的binlog上,另一份在至少一个从库的中继日志 relay log 上,这样就进一步保证了数据的持久性,进一步避免来主库宕机数据丢失。

问:主从复制时,IO 线程和 SQL 线程都是单线程的吗

IO 线程是多线程的,SQL 线程是单线程的。如果开了并行复制,那么 sql 线程就是多线程的,具体值由全局变量 slave_parallel_workers 决定。

问:什么时候考虑用主从复制

读操作称为瓶颈,需要提高读并发。或者读库需要做高可用时。

问:主从复制可能有什么问题

在数据量较大并发量较大的场景下,主从延时会比较严重。导致出现主从数据一致性问题,可能会发现,刚写入库的数据结果没查到。(如果不需要立刻读说明对主从一致性要求不高,可以考虑使用主从复制)

主库宕机数据丢失问题。

问:怎么判断现在主从延迟高不高

通过监控 show slave status 命令输出的 Seconds_Behind_Master 参数的值来检测主从延时

  • NULL:表示 io_thread 或是 sql_thread 其中一个发生故障;
  • 0:该值为零,表示主从复制良好;
  • 正值:表示主从已经出现延时,数字越大表示从库延迟越严重。

问:怎么开启并行复制

通过设置 slave_parallel_workers = 4 来开启数据库级别的并行复制

分组的并行复制还需要额外设置 global.slave_parallel_type=‘LOGICAL_CLOCK’

问:分组并发复制,从库怎么判断 log 是否处于同一个组

根据 last_committed 来判断,relay log中 last_committed 相同的事务则认为是同一个事务组,last_committed 不同,则处在不同组。组内的事务通过 sequence_number 来标号。

问:分组并发复制,主库分组的依据是什么

未发生资源竞争,比如写操作不同表、不同行的记录的事务可以归到同一个组,主库上并发执行的写操作事务可以归为一个组,因为理论上在主库上能并发执行,在从库上也应该能并发执行。

具体的实现算法有 2 种:

  • 基于逻辑时钟的并行回放

因为MySQL本身事务具有ACID的特点,所以从主库同步到从库的事务,只要其执行的逻辑时间上有重叠,那么这两个事务就能安全的进行并行回放。

  • 基于writeSet的并行回放

使用一个HashMap保存一定时间内针对某一块数据区域的事务的集合。如果事务在同一组内或者是逻辑时钟有重叠,说明没有冲突,其他情况不能确定是否有冲突。

参考:MySQL Binlog日志与主从复制一文详解

问:有哪些主从复制模型

  • 异步复制:master 把Binlog日志推送给slave,master不需要等到slave是否成功更新数据到Relay log,主库直接提交事务即可。这种模式牺牲了数据一致性。
  • 同步复制:每次用户操作时,必须要保证Master和Slave都执行成功才返回给用户。
  • 半同步复制:不要求Slave执行成功,而是成功接收Master日志就可以通知Master返回。

主从不一致解决方案 && 如何降低主从延迟_数据库_06

问:什么是多源复制

多个不同业务主库都复制到一个从库中。主要应用场景有三个:

  • 跨业务数据库汇总,方便数据分析部门做数据分析
  • 对水平分库的多个分库进行数据汇总,方便后期实现一些数据统计功能。
  • 多个主库数据汇总备份到一个从库,而不是多个从库,减少资源浪费和DBA的维护成本

问:如果数据库有张表,频繁有新数据写入。同时高峰期这表查询请求也多,而且需要查询最新的数据。 我们遇到问题是高峰期查询请求大,导致写数据延迟了,同时也就导致查询出来的不是最新的数据。(因为新数据还在排队等待写入) 像这种写多读多的话,需要怎么优化呢?

写多,先水平分库,读多主从复制。

追求一致性那么需要从降低主从延迟和发生主从延迟怎么读到最新数据来思考,参考本文上面提到的降低延迟和解决主从一致性的方案。

问:什么是 GTID

全局事务标识符(GTID)

事务 id 拼接主库实例 id

1)全局事务标识:global transaction identifiers。

2)GTID是一个事务一一对应,并且全局唯一ID。

3)一个GTID在一个服务器上只执行一次,避免重复执行导致数据混乱或者主从不一致。

4)GTID用来代替传统复制方法,不再使用MASTER_LOG_FILE+MASTER_LOG_POS开启复制。而是使用MASTER_AUTO_POSTION=1的方式开始复制。

5)MySQL-5.6.5开始支持的,MySQL-5.6.10后开始完善。

6)在传统的slave端,binlog是不用开启的,但是在GTID中slave端的binlog是必须开启的,目的是记录执行过的GTID(强制)。

参考:MySQL主从复制,并行复制,半同步复制和组复制

案例

20160607 23:22 server_id 58 XXX GTID last_committed=0 sequence_numer=1
20160607 23:22 server_id 58 XXX GTID last_committed=0 sequence_numer=2
20160607 23:22 server_id 58 XXX GTID last_committed=0 sequence_numer=3
20160607 23:22 server_id 58 XXX GTID last_committed=0 sequence_numer=4

GTID 使用过程

GTID是指全局事务标志,用来标记主从同步的情况。

master提交一个事务时会产生GTID,并且记录在Binlog日志中。从库的IO线程在读取Binlog日志时,会将其储存在自己的Relaylog中,并且将这个值设置到gtid_next中,即下一个要读取的GTID,从库读取这个gtid_next时,会对比自己的Binlog日志中是否有这个GTID:

  • 如果有这个记录,说明这个GTID的事务已经执行过了,可以忽略掉(幂等)。
  • 如果没有这个记录,slave就会执行该GTID事务,并记录到自己的Binlog日志中。

参考:MySQL Binlog日志与主从复制一文详解

问:什么是组复制 MGR

(MRG, MySQL Group Replication)

把主从关系变成分组内的成员关系,通过分布式共识算法,每个成员节点都可以读写。

分布式一致性算法Paxos。由至少3个或更多个节点共同组成一个数据库集群,事务的提交必须经过半数以上节点同意方可提交提供,支持多写模式。

MGR 是 share-nothing 的复制方案,基于分布式paxos协议实现,每个实例都有独立的完整数据副本,集群自动检查节点信息,做数据的同步。同时提供单主模式和多主模式,单主模式在主库宕机后能够自动选主,所有写入都在主节点进行,多主模式支持多节点写入。同时集群提供冗余的容错功能,保证集群大多数节点正常集群就可以正常提供服务。

参考:MySQL Binlog日志与主从复制一文详解

参考&特别致谢

说你没遇到过,主从数据库不一致?

DB主从一致性架构优化4种方法

mysql并行复制降低主从同步延时的思路与启示

https://zhuanlan.zhihu.com/p/373576459


特别感谢沈剑大佬的博文,看完再总结一遍会有很大收获,强烈推荐关注沈剑大佬的公众号“架构师之路”,也感谢另外两篇参考文章的大佬。