【学习笔记】多副本数据怎么保证一致性?

前几天和朋友聊到这个缓存数据库数据一致性问题,发现这方面确实还有欠缺,因此特地整理了各种情况的数据一致性的解决方案

多副本数据一致性问题

数据一致性基本就是多副本问题的数据一致性

数据库与缓存的数据一致性问题

更新缓存的策略有哪些?

旁路缓存策略

就是数据库的数据为准,缓存的数据按需加载

  • 读策略 读取的数据命中缓存直接返回;没命中则读取并写入缓存,返回数据
  • 写策略 更新数据库的数据再删除缓存中的数据;主流做法就是先更新后删除,因为缓存写入通常快于数据库写入,因此即使是后来的线程先写数据到缓存中,但真正更新数据库的那个线程后来才更新完,这就可以删除掉这个缓存了
  • 高缓存命中率的实现可以:
    • 更新缓存前加分布式锁
    • 缓存的过期时间较短
  • 写多的场景会频繁的对缓存数据操作,对缓存命中率由影响
读穿/写穿策略

该策略是应用程序只与缓存打交道;缓存是中间桥梁;更新数据库的操作由缓存自己代理

  • 读穿策略 查询缓存中数据是否存在,不存在则由缓存组件负责去数据库查询,并写入缓存
  • 写穿策略 数据更新时,先查询数据是否已经在缓存中存在,存在则更新缓存的数据,并同步更新到数据库;如果缓存不存在数据,只更新数据库

操作系统中的维持内存与缓存一致性的最简单的方式也是会先判断是否在缓存中,在的话会先更新缓存;最后更新内存;不在就直接更新内存

写回策略

更新数据时,只更新缓存;同时将缓存数据设置为脏,不更新数据库;数据库的更新通过批量异步的方式更新;这个数据不是强一致性会有数据丢失的风险;
因为缓存是放在内存中的;内存的数据掉电就有可能消失;

操作系统中也有写回策略,发生写操作时,新的数据仅仅被写入 Cache Block 里,只有当修改过的 Cache Block「被替换」(替换算法要驱逐更新过的块)时才需要写到内存

如何实现缓存与数据库的强一致性?

复制是同步的,强一致性意味着要满足所有数据备份在同一时刻都是相同的值,
可以采取写穿策略:
同步写穿策略:写数据库后也同步写Redis缓存,缓存和数据库的数据保持一致,此时不允许任何读线程过来,因此需要加锁,可以加读写锁,写锁与读锁、写锁全互斥

如何实现弱一致性/最终一致性?

双写模式

就是数据库与缓存都需要写,但是可能会存在由于卡顿等原因导致后面的写缓存在最前面;前面的写缓存在后面导致了数据不一致,缓存过期,数据稳定后又能读到最新的正确数据

先更新数据库再更新缓存; 最先写缓存的线程最后才写缓存,最后写数据库的线程最后写数据库:会出现缓存和数据库不一致的情况×
先更新缓存再更新数据库; 最先写数据库的线程最后才写数据库,最后写缓存的线程最后写缓存:会出现缓存和数据库不一致的情况×

失效模式

更新数据库前删除缓存
可能会存在读线程先于写SQL的线程,读到了数据库中旧的值,并同时更新缓存;最后等缓存过期,才会获得最新的正确数据达成最终一致性

在读写并发的时候,还是会出现缓存和数据库不一致

因此呢,可以采用延时双删策略:
这个策略悲观地认为一定会有其他线程过来捣蛋,因此删两次,
流程:先进来的线程先删除缓存(1删),然后去更新数据库,
之后线程暂停几秒钟(根据具体业务时间来定),其他线程就可以去数据库读数据,并且写入缓存,后来这个线程醒来,删掉缓存(2删);
下次再有线程过来发现没有缓存,就回去数据库读数据,保证了读到的数据以及缓存的数据一定是最新的

更新数据库后删除缓存(主流做法)
其实,如果说读数据库的线程在前;并且都此时更新完数据库并删除缓存,读数据库的线程刚读完,就会去将旧的数据写回缓存,此时还是会有缓存数据库不一致的问题

但由于缓存的写入速度快于数据库的写入速度,基本都是缓存写好了之后又去删除缓存,这样下一次请求过来就是直接去数据库查
以上情况适合读多写少的场景,因为写操作频繁,意味着缓存数据就会经常被清理,这样就会影响缓存的命中率

但可能会出现的问题:
缓存删除失败或者请求再次访问缓存,并且命中读到的是旧值的问题

这里的关键是更新数据库和删除缓存两个操作都要操作成功

因此可以:
采用重试机制:
引入消息队列, 将 删除缓存这个操作 要操作的数据加入到消息队列中,如果删除缓存失败,可以从消息队列中重新读取数据,然后再一次删除缓存, 如果删除缓存超过一定次数还没成功,就会向业务层发送报错信息;如果成功,就要把数据从消息队列中移除,避免之后还重试
订阅binlog
更新数据库数据时,并更新成功,binlog就会更新,此时就可以通过订阅binlog日志,拿到具体要操作的数据,进行缓存的删除

MySQL 一共有三种日志:
UndoLog,用于事务回滚和多版本并发控制,记录更新前的数据 更新操作执行前更新UndoLog
RedoLog,用于掉电等故障恢复,记录事务完成后的更新后的值 更新记录时,会更新RedoLog
Binlog,用于主从复制和数据备份记录所有包括表结构和数据的修改操作 更新语句结束,会更新Binlog

MySQL主库与从库的一致性问题

主从同步:
主库将变更写入Binlog日志,
从库连接到主库有一个IO进程来复制binlog并写入到一个中继log中
读取中继log,实现主从数据一致性

保证一致性的复制模型

同步复制 主库提交事务的线程等所有从库都复制成功并响应了,才会返回客户端成功的结果
异步复制 主库提交事务线程不会等待binlog同步到各个从库,就返回客户端响应成功过的结果
半同步复制 主库提交事务线程,只要一部分从库复制成功并行营,就返回客户端响应成功的结果

保证一致性的数据库中间件

所有读写都走数据库中间件,一般写请求路由到主库,读请求路由到从库
如果在主从复制期间有读请求,就会将其路由到主库去读

保证一致性的缓存

如果有key 要更新,就先将其记录在缓存中,然后修改数据库
读的时候,先去数据库查看,有没有key-value键值对,如果有,说明刚发生写操作,则路由到主库,如果没有则路由到从库

Redis主库与从库的数据一致性

Redis 主从模式是读写分离的,主库写为主,从库都为主;当主库数据发生变化时,会异步同步数据到从库中
具体流程:服务器向主服务器发送同步命令,收到同步命令后,执行bgsave命令(不会阻塞主线程)生成RDB文件,并在缓冲区记录从当前开始执行的写命令;执行完后将生成的RDB发给从库,再将缓冲区的写命令发给从库,从库都执行完写命令后就都一致了

为什么要使用RDB快照,而不是AOF?
因为RDB文件内容是经压缩过的二进制数据,文件小读取数据时会比AOF执行效率高,因为AOF时记录写命令这样就会降低数据同步的速度

一般采取一级一级的主从模式来分担同步的压力,这样下一级的从库就与上一级的从库构成主从关系,只要这两个从库之间进行同步,真正的主库只需要与它下一级的从库进行同步,就不会忙于生成很多个RDB文件来同步了

如果出现主从库无法通信,当从库再次连接就只需要进行增量复制,将断连期间的命令同步给从库即可,(主要是在一个环形缓冲区中,有两条记录,一条是主库写到的位置,一条是从库读到的位置, 只要将这个位置之间相差的操作进行增量复制就可以了)
环形缓冲区会出现从库还没读取读取就被主库新写的操作覆盖导致主从数据不一致,因此需要让缓冲区大一点,避免主从同步比较慢而导致的数据不一致的问题

缓冲空间的计算公式是:缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小。在实际应用中,考虑到可能存在一些突发的请求压力,我们通常需要把这个缓冲空间扩大一倍,即 repl_backlog_size = 缓冲空间大小 * 2,这也就是 repl_backlog_size 的最终值。
https://www.cnblogs.com/huangjuncong/p/14973636.html

分布式事务里多个系统数据的一致性

为啥会有分布式事务?

因为涉及到分布式系统,会出现消息丢失,消息乱序,网络异常等问题,
分布式事务一般是要满足CAP定理:
C :一致性定理, 分布式系统中,所有数据副本同一时刻都是同样的值
A:可用性,一部分节点故障后,这个系统是否还能继续工作
P:分区容错性 可能会存在A服务器在广东,B服务器在北京,这俩服务器之间无法通信
一般P无法避免,因此总是求AP/CP

分布式事务解决数据一致性

2PC 模式:二阶段提交

准备阶段:协调者发送准备命令,所有涉及了这个事务的系统,除了提交数据库事务以外的所有工作,都在这个阶段完成 然后给事务协调者返回一个准备成功
提交阶段 都准备妥当了,就各个系统各自提交自己的事务,并返回给协调者提交成功的响应,协调者返回给客户端提交完成

如准备阶段有一方准备失败,协调者互让所有系统都回滚
如提交阶段执行的是回滚,如果失败了会不断重试直到所有参与者都回滚成功,不然第一阶段准备成功的参与者会一直阻塞
如提交阶段是提交事务,如有一个事务提交失败,会不断重试,一直到提交成功,重试超过一定次数,人工该介入处理;
可能会存在协调者出问题,
如果在发送准备命令后挂了, 事务无法执行,一些参与者处于事务资源锁定状态,还会因锁定了公共资源而阻塞系统其他操作
如果发送回滚事务命令前挂了,会存在第一阶段准备成功的参与者都阻塞;之后挂了,则很大概率会回滚成功;
如果发送提交事务命令之前挂了 ,所有资源都会阻塞;如果之后挂了,大概率会提交成功释放资源

解决方案:选举得到新的协调者,但每个参与者的状态只有自己和交流过的协调者直到,因此新协调者是不知道挂了的参与者什么情况,所以可以采用日志记录协调者发过的请求 新协调者就可通过日志来做事了

可以看出,是强一致性的设计,因为要么全部系统同时成功,要么同时失败
但事务执行过程会阻塞服务端的线程和数据库,因此并发场景性能不高

柔性事务-事务补偿性方案

柔性事务是满足最终一致性的,它允许一段时间内,不同节点的数据不一致,但最终必须一致
核心思想:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作
该方案一共是三阶段:
try阶段,预留和锁定资源
Confirm阶段,执行业务
Cancel阶段,业务执行错误,执行的业务取消
有一个事务管理者记录全局事务状态并提交或回滚事务(类似于上面的协调者)

根据特定的场景和业务逻辑设计响应的操作,撤销和确认操作

柔性事务-最大努力通知型

就是不保证数据一定能通知成功,但会提供一个可查询的操作接口来进行核对
比如结合MQ,
给MQ的服务器发送消息后,发送方开始执行事务
事务的结果向服务器发送提交或回滚命令
如果Broker 没收到操作请求,broker可以通过查询接口得知事务是否执行成功,然后执行相应的命令
比如数据库新增一张表–消息表,执行业务时把业务执行和将消息放入消息表中的操作放入同一事务中,
后台定时读取这个消息表,筛选还未成功的消息进行重试,重试需要保证操作时幂等的,且重试超过一定次数就人工介入;

对于结合了MQ的消息也是,可以将每个消息都做好日志记录,定期扫描数据库,将发送失败的消息重新发送

补充:

分布式锁:

更新缓存前加一个分布式锁/锁;
比如:

Redisson支持分布式锁:

使用时需要创建一个Redissonclient 对象;
RLock lock = redisson.getLock("name") 一定要设置名字,对应特定的一把锁,不然就会导致乱删乱加的情况,只有锁的持有者才能解锁,不然就会报异常:非法监视器状态异常;这个锁是可重入的;
有一个看门狗机制,是干嘛的?给锁自动续期的,一般会达到1/3的看门狗时间即10s会进行自动续期,继续续期到30s,
主要就是给锁设置了一个locktime,然后启动了一个守护线程,这个守护线程会重新设置这个locktime
具体流程就是:先判断锁的持有者是否改变,没有,守护线程在合适的时间内重新上锁;如持有锁的线程处理完业务,守护线程也会被销毁;可以通过Config.lockwatchdogtimeout 来指定检查锁的超时时间
支持公平锁:redisson.getFairLock("mylock") 保证按照请求的顺序来依次分配锁给对应的线程;

支持多重锁:允许将Lock对象分组将它们作为单个锁处理;每个RLock对象都属于不同的 Redisson 实例

RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");

RLock multiLock = anyRedisson.getMultiLock(lock1, lock2, lock3);
multiLock.lock();

支持读写锁:

RReadwriteLockRx rwlock = redisson.getReadWriteLock("XXX")
rwlock.readLock();
rwlock.writeLock();

支持信号量:类似于Semaphore但是是分布式版本

 RSemaphore semaphore = redisson.getSemaphore("XXX")
 semaohore.acquire(10);//可以获得10个permit
 semaphore.release();
 tryacquire//非阻塞等待

支持CountDownLatch对象

RCountDownLatch latch = redisson.getCountDownLatch("myCountDownLatch");
latch.countDown();

支持自旋锁SpinLock

RLock lock = redisson.getSpinLock("myLock");

但加锁会出现只允许一个线程更新缓存,会对写入性能有一些影响;
也可以给缓存加一个较短的过期时间;

参考:
https://www.cnblogs.com/zoujiejun96/articles/14489288.html
https://zq99299.github.io/note-book/back-end-storage/01/05.html#%E6%80%9D%E8%80%83%E9%A2%98
https://zhuanlan.zhihu.com/p/183753774
https://www.cnblogs.com/huangjuncong/p/14973636.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

54V

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值