- 👏作者简介:大家好,我是爱敲代码的小黄,阿里淘天Java开发工程师,CSDN博客专家
- 📕系列专栏:Spring源码、Netty源码、Kafka源码、JUC源码、dubbo源码系列
- 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
- 🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
- 📝联系方式:smallyellow521,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀
文章目录
- 复制
- 领导者与追随者
- 同步复制与异步复制
- 如何设置新从库
- 处理节点宕机
- 从库失效:追赶恢复
- 主库失效:故障切换
- 复制日志的实现
- 基于语句的复制
- 逻辑日志复制(基于行)
- 基于触发器的复制
- 复制延迟问题
- 读已之写
- 单调读
- 一致前缀读
- 多主复制
- 运维多个数据中心
- 需要离线操作的客户端
- 协同编辑
- 处理写入冲突
- 同步与异步冲突检测
- 避免冲突
- 收敛至一致的状态
- 多主复制拓扑
- 无主复制
- 节点故障
- 读修复和反熵
- 总结
与可能出错的东西比,“不可能”出错的东西最显著的特点就是:一旦真的出错,通常就彻底玩完了。
—— 道格拉斯・亚当斯(1992)
复制
简介:通过网络连接的多台机器上保留相同数据的副本。
原因:降低延时、可用性、吞吐量
- 数据存放地与用户接近,减少网络请求延时
- 避免单机故障,提升可用性
- 机器数量可伸缩,提升读取吞吐量
困难:
- 如果复制的数据不随时间而改变,复制将变的非常简单
- 但复制的过程中,数据经常是变更的
解决:
在分布式场景下,一般使用三种变更复制算法:
- 单领导者
- 多领导者
- 无领导者
领导者与追随者
副本:存储数据库拷贝的每个节点
问题:当存在多个副本时,如何确保所有数据都落在了所有的副本上?
解决方案:基于领导者的复制
原理:
- 我们将其中一个副本指定为
领导者(主库)
- 当客户端想要写入数据时,必须将请求发送给
领导者(主库)
- 它将数据写入到本地存储
- 当客户端想要写入数据时,必须将请求发送给
- 其他副本成为追随者(只读副本)
- 每当领导者将新数据写入本地存储时,它也会将数据变更发送给所有的追随者,称为复制日志。
- 每个追随者从领导者拉取日志,并更新本地数据库副本,按照领导者相同的处理顺序来进行写入
- 当客户想要从数据库查询数据
- 领导者和任一追随者都可以查询
- 只有领导者可以写入数据
同步复制与异步复制
- Follower 1:同步复制
- 好处:保持与主库强一致的最新数据
- 坏处:从库挂掉,主库也无法写入数据
- Follower 2:异步复制
- 好处:从库挂掉,主库也能写入数据
- 坏处:无法保持与主库强一致的最新数据
目前业界常用方式:半同步
- 在数据库中开启同步复制,其中一个从库是同步的,其余所有的从库均是异步的
- 如果该从库不可用,则将另外一个异步从库改为同步复制
通常情况下,基于领导者的复制都配置成完全异步,一旦主库失效不可恢复,复制给从库的数据也将丢失。
即使已经向客户端确定成功,写入也不能保证是持久的。
当然也有优点:即使所有的从库都落后了,主库也可以正常写入。
如何设置新从库
原因:
- 增加从库副本数量
- 替换失败的节点
过程:
- 在某个时刻获取主库的一致性快照
- 将快照复制到新的从库节点
- 从库链接主库并拉取快照后的所有数据变更
- 等从库处理完快照之后的数据变更,从库就赶上了主库
复制过程可参考:Redis
和 MySQL
处理节点宕机
系统中的任何节点都可能宕机,即使个别节点生效,也能保持整个系统的运行并尽可能控制节点停机带来的影响
从库失效:追赶恢复
在本地磁盘中,从库记录从主库收到的数据变更。
如果从库崩溃并重新启动或者网络中断等原因,从本地日志中获取最后一个事务
连接到主库,请求在从库断开期间发生的所有数据变更,当解决完这些变更之后,就赶上了主库,正常接受数据变更流。
主库失效:故障切换
故障切换:将其中一个从库提升为新的主库,重新配置客户端,将他们的写操作发送给新的主库,其他从库需要开始拉取来自新主库的数据变更。
过程:
- 确定主库失效(心跳检测)
- 选择一个新的主库(选举机制)
- 重新配置系统以启动新的主库
问题:
-
异步复制,新主库的数据落后老主库。解决方式:丢掉老主库中未复制的写入
-
数据库与外部存储协调,丢弃写入内容极其危险
例如在 GitHub 的一场事故中,一个过时的 MySQL 从库被提升为主库
数据库使用自增 ID 作为主键,因为新主库的计数器落后于老主库的计数器,所以新主库重新分配了一些已经被老主库分配掉的 ID 作为主键
这些主键也在 Redis 中使用,主键重用使得 MySQL 和 Redis 中的数据产生不一致,最后导致一些私有数据泄漏到错误的用户手中。
-
脑裂,两个节点都误认为自己是主库
-
超时时间的配置,如何正确的配置主库失效的超时时间
复制日志的实现
基于语句的复制
主库记录每个写入请求并将该语句发送给从库
问题:
- 任何调用 非确定下函数 的语句,在每个副本生成不同的值。
- NOW():获取当前时间
- RAND():获取一个随机数
- 语句必须按照顺序执行,避免并发问题
- UPDATE … WHERE <某些条件>,必须现有某些条件数据,再进行 UPDATE
### 传输预写式日志(WAL)
存储引擎通常会将写操作追加到日志中
mysql 通过 redo、undo 日志实现 WAL。
redo log 称为重做日志,每当有操作时,在数据变更之前将操作写入 redo log,这样当发生掉电之类的情况时系统可以在重启后继续操作。
undo log 称为撤销日志,当一些变更执行到一半无法完成时,可以根据撤销日志恢复到变更之间的状态。
mysql 中用 redo log 来在系统 Crash 重启之类的情况时修复数据(事务的持久性),而 undo log 来保证事务的原子性。
缺点:由于日志记录的非常底层(WAL包含哪些磁盘块中的哪些字节发生了变化),当从库和主库运行不同版本时,会出现数据解析问题。
逻辑日志复制(基于行)
复制和存储引擎使用不同的日志格式,将复制日志从存储引擎的内部实现中解耦出来,这种复制日志被称为逻辑日志。
例如:MySQL
的 Binlog
- 插入行:日志包含所有列的新值
- 删除行:日志包含主键
- 更新行:日志包含主键及更新列的新值
优点:逻辑日志和存储引擎内部实现解耦,系统可以做到兼容。从而使主库和从库可以运行不同版本的数据库软件。
基于触发器的复制
将数据更改发生时的自定义代码记录在数据库系统中,使用外部程序读取该表,进行响应的复制。
复制延迟问题
当前存在一个主库,多个从库,在数据复制的过程中,如果从库落后于主库,我们会看到过时的信息。
对主库和从库执行相同的查询,得到不同的结果,等后续一段时间后,从库追上主库保持一致,被称为 最终一致性。
读已之写
用户在界面提交一些数据,将其写入到主库,主库异步复制给从库,从而从库追赶上主库。
但如果用户在提交完数据后,立即查询(从库),会发现自己提交的数据库丢失不见
这种情况下,我们需要 写后读一致性,也称为 读己之写一致性,我们需要保证:如果用户重新加载页面,他们总会看到他们自己提交的任何更新
解决方法:
- 对于用户可能修改的内容,总是从主库获取
- 用户个人资料只能本人编辑,而不能其他人编辑
- 从主库读取自己的档案,其他用户档案去从库读取
- 跟踪上次更新的时间,在数据更新的一分钟内,从主库读取
- 监控从库的复制延迟,滞后主库超过一分钟的从库不接受查询请求
单调读
当前从库 2
落后从库 1
如果用户从不同从库读取
- 先读取的从库 1,拿到了最新数据
- 后续读取从库 2,拿到了旧数据
从用户体验来看,时间看上去好像回退了,所以我们需要单调的读取。
单调读 可以保证这种异常不会发生,其程度比 强一致性 弱,比 最终一致性 更强。
实现方式:每个用户总是从一个副本中进行读取(不同客户可以从不同副本读取)。可以基于用户 ID Hash
来选择副本,而不是随机选择读取的数据库副本。
一致前缀读
如果我们有两个因果关系的数据:
Mr. Poons
Mrs. Cake,你能看到多远的未来?
Mrs. Cake
通常约十秒钟,Mr. Poons.
假如:
- Cake 说的话是一个延迟较低的从库
- Poons 说的话是一个延迟较高的从库
当第三个人在读取数据时,会出现这种情况:
Mrs. Cake
通常约十秒钟,Mr. Poons.
Mr. Poons
Mrs. Cake,你能看到多远的未来?
如果某些分区的复制速度慢于其他分区,那么观察者可能会在看到问题之前先看到答案。
需要保证一致前缀读:如果一系列写入按照某个顺序进行,那么任何人读取这些写入时,也看以同样的顺序读取。
解决方案:确保任何因果相关的写入都写入到相同的分区
多主复制
如果数据库被分区,每个分区有一个主库。
多领导配置:处理写入的每个节点都必须将该数据变更转发给其他节点
在这种情况下,每个主库同时是其他主库的从库。
### 多主复制的应用场景
运维多个数据中心
多主配置中在每个数据中心都有主库,每个数据中心内使用常规的主从复制;
在数据中心之间,每个数据中心的主库会将自身变更同步到其他主库。
单主和多主对比:
- 性能
- 单主:每个写入操作必须穿过互联网,进入主库所在的数据中心,网络延时较大
- 多主:每个写入操作在本地数据中心进行处理,与其他数据中心异步复制,网络延时较小
- 容忍数据中心停机
- 单主:主库所在的数据中心发生故障,切换另一从库成为主库
- 多主:数据中心可以独立于其他数据中心继续运行
多主复制的缺点:多主复制在数据库属于改装的功能,常常存在微妙的配置缺陷。因此,多主复制被认为是危险的领域,应尽可能避免。
需要离线操作的客户端
多主复制的另一种适用场景是:应用程序在断网之后仍然需要继续工作。比如:日历应用
在这种情况下,每个设备都有一个充当主库的本地数据库,在所有的设备的日志副本之间同步。
协同编辑
实时协作编辑应用程序允许多个人同时编辑文档
当一个用户编辑文档时,所做的更改将立即应用到其本地副本并异步复制到服务器。
如果不发生编辑冲突,则应用程序必须对文档加锁,为了加速协作,尽可能将加锁的单位设置的非常小。
处理写入冲突
多主复制最大的问题:写入冲突
假如两个人同时更改一个页面
- 用户 1 将页面标题从 A 更改为 B
- 用户 2 将页面标题从 A 更改为 C
当异步复制时,就会出现冲突。
同步与异步冲突检测
如果在单主数据库中,两个操作是串行的,不会发生冲突
但多主数据库中,两个分别写入不同的主库,后续异步复制,必然出现冲突问题
避免冲突
处理冲突的最简单的策略就是避免它们:如果应用程序可以确保特定记录的所有写入都通过同一个主库,那么冲突就不会发生。
例如:一个用户编辑自己数据的应用程序,确保来自特定用户的请求始终路由到同一数据中心并使用该数据中心的主库进行读写。
收敛至一致的状态
我们上述的例子中,在多个主库的情况下,我们的写入顺序是不确定的
数据库必须以一种收敛的方式解决冲突,所有副本必须在变更复制完成时收敛到一个相同的最终值。
解决方案:
- 给每个写入分配唯一ID(时间戳、长随机数、UUID),挑选最高 ID 的写入作为胜利者
- 给每个副本分配唯一ID,ID更高的写入具有更高的优先级
- 将这些值链接在一起,比如:B/C
- 将冲突显式的暴露出来,交于用户决定,比如:GIT冲突
多主复制拓扑
复制拓扑用来描述写入操作从一个节点传播到另一个节点的通信路径。
无主复制
客户端直接写入几个副本中,另一种情况,由一个 协调者 代表客户端写入。
节点故障
无主配置中,发生节点故障,不需要故障转移
如果有三个副本,两个副本写入成功,一个副本写入失败
当我们不可用的副本重新上线,存的是落后的数据
解决方案:当一个客户端从数据库中读取数据时,它不仅仅把它的请求发送到一个副本,而是将读请求将被并行地发送到多个节点,通过版本号来确定哪个值是最近更新的。
读修复和反熵
复制方案应确保最终将所有数据复制到每个副本。在一个不可用的节点重新联机之后,它如何赶上它错过的写入?
- 读修复:当客户端并行读取多个节点时,检测落后节点的回应,并将最新值写会落后节点。适用于频繁读取的值
- 反熵过程:数据存储具有后台进程,进程不断查找副本之间的数据差并进行相关的复制追齐。
总结
鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。
其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。
如果你也对 后端架构 和 中间件源码 有兴趣,欢迎添加博主微信:smallyellow521,一起学习,一起成长
我是爱敲代码的小黄,阿里巴巴淘天集团Java开发工程师,双非二本,培训班出身
通过两年努力,成功拿下阿里、百度、美团、滴滴等大厂,想通过自己的事迹告诉大家,努力是会有收获的!
双非本两年经验,我是如何拿下阿里、百度、美团、滴滴、快手、拼多多等大厂offer的?
我们下期再见。
从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。