redis-读写一致性
Mysql如何与Redis保持同步
类比计算机组成原理中的cache和内存,redis也就是mysql的缓存,那么保持读写一致性也是十分重要的,我们在修改数据库的同时,缓存中也要对应更新。
- 读操作:缓存命中就直接返回,缓存未命中就查找数据库,然后更新缓存
- 写操作:延迟双删
先删除缓存,再更新数据库
- 正常情况
首先线程1删除缓存,然后更新数据库,此时没有任何线程来打扰,完成操作以后线程2再来查找缓存,这个时候由于线程1已经删除了缓存,那就会查找数据库找到刚刚更新的数据,最后写入缓存。这个过程不会出现读写一致性问题
- 特殊情况
其实下面先写后删除的特殊情况也适用于本例中,首先线程1查询到了一个过期key,去数据库找,暂时保存,但是没写进redis。此时另一个进程是写进程,先删除了这个key,随后更新,最后才轮到线程1,写回刚刚的旧数据到redis,同样会有数据不一致
我们假设线程1删除缓存之后,由于线程是并发的,线程2来查询,此时由于还没有更新数据库,找到的还是原来的数据,随后放回缓存,这个时候才轮到线程1,更新数据库,但是此时redis还是用的以前的数据,数据库的是刚刚更新的,就出现了读写一致性问题
先更新数据库,再更新缓存
那如果我们先更新数据库,在更新缓存是不是正确的呢?答案是还是会有读写一致性问题
- 正常情况
线程2先更新数据库,然后删除,跟上面的一样,只要两个操作是原子性的就不会有问题,但是只要是线程是并发的,那就肯定不一致
- 特殊情况
如果在不在这个刚刚过期的时间节点,那会怎么样呢?其实如果不是刚刚要过期,那就直接会拿走缓存中的数据,也不会删除缓存,那写进程后续先改再写redis也不会出现读写一致性问题,个人觉得核心的问题在于读进程的机制,没查到或者过期的缓存就会主动去数据库里调用,会扰乱正常的写进程导致不一致
如果是命中的读进程,那就根本不会扰乱写进程,只是脏数据的问题,redis和mysql最终都是一致的。而这里讨论的刚好过期,才会导致读进程写redis,才会有不一致性的问题发生)
线程1先去查询缓存,这个时候刚好key过期,就要去数据库找,在数据库找到的是旧数据,先保存下来,但是此时线程2更新了数据库,然后删除,最后才是线程1写回旧数据到redis,出现不一致。
解决方法
- 分布式锁:在更新缓存前先加个分布式锁,保证同一时间只运行一个请求更新缓存,就会不会产生并发问题了,当然引入了锁后,对于写入的性能就会带来影响。
- 延迟双删:针对「先删除缓存,再更新数据库」方案在「读 + 写」并发请求而造成缓存不一致的解决办法是「延迟双删」。
- 为什么要双删?因为刚刚在[先删除缓存,再更新数据库]方案中会出现读写一致问题,这个时候只要再去删除一次缓存就可以了,下一个来的读请求会发现不存在对应的key,然后从数据库找,最终达到一致性
- 为什么要延迟?主要是为了确保请求 A 在睡眠的时候,请求 B 能够在这这一段时间完成「从数据库读取数据,再把缺失的缓存写入缓存」的操作,然后请求 A 睡眠完,再删除缓存。
所以,请求 A 的睡眠时间就需要大于请求 B 「从数据库读取数据 + 写入缓存」的时间。因为这个延时时间不好控制,在极端情况下还是会出现读写不一致的现象。
- 为什么要延迟?主要是为了确保请求 A 在睡眠的时候,请求 B 能够在这这一段时间完成「从数据库读取数据,再把缺失的缓存写入缓存」的操作,然后请求 A 睡眠完,再删除缓存。
- 使用「先更新数据库,再删除缓存」方案:其实这种方案的特殊情况很难遇见,因为缓存的写入通常要远远快于数据库的写入,所以在实际中很难出现请求 B 已经更新了数据库并且删除了缓存,请求 A 才更新完缓存的情况。
而一旦请求 A 早于请求 B 删除缓存之前更新了缓存,那么接下来的请求就会因为缓存不命中而从数据库中重新读取数据,所以不会出现这种不一致的情况。(意思是即使B先写了数据库,A再写旧的值,因为数据库涉及io(扯远点就是io中断)阻塞进程B直到时间片到了,进程会交替执行而io搁一边,可能当执行到A写完了旧值,更新才刚刚结束,这个时候B再执行删除,就可以保证一致性)
在不那么要求强一致性的场景
有的时候可以容忍一瞬间的脏数据,但是要保持最终一致性的场景,可以使用以下两种方法:
- 基于消息队列(例如kafka):写入数据库后,发一个消息给mq通知redis更新缓存,这种方式的可靠性主要取决于mq,肯定是有一定的延时的,但是最终会保证一致
- 基于阿里巴巴的Canal中间件:「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。
Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。