1. 如何保证缓存和 MySQL 的双写一致 ?
什么叫做如何保证缓存和 MySQL 双写一致,这个问题就是指当应用程序执行写(增删改)操作时,如何保证 Redis 和 MySQL 的数据一致性。
当用户发送请求时,程序的执行流程如下:
要保证 Redis 和 MySQL 的数据一致性,从大的角度来说,无非就两种思路:
1.修改
- 先修改数据库,再修改 Redis
- 先修改 Redis,再修改数据库
2.删除
- 先删除 Redis,再操作数据库
- 先操作数据库,再删除 Redis
这四种方式在正常情况下,都可以保证 Redis 和 MySQL 的数据一致性,但是在异常情况下,就只有一种方式才能真正保证 Redis 和 MySQL 的双写一致。
当上述方式,每个方案都是执行了前半部分后,主机掉电了 >>
① 先修改数据库,再修改 Redis(掉电)
这种方式,Redis 中是旧数据,而 MySQL 中是新数据,当应用程序访问 Redis 时,发现有数据,直接就返回给用户了,显然这种方式是不合理的。
② 先修改 Redis,再修改数据库(掉电)
这种方式,Redis 中是新数据,而 MySQL 中是旧数据,当应用程序访问 Redis 时,发现有数据,直接就返回给用户了,此时 Redis 和数据库中的数据也不一致,下一次 Redis 还需要从数据库中同步数据的,显然这种方式也不合理。
③ 先删除 Redis,再操作数据库(掉电) √ √ √
这种方式,不管主机有没有掉电,Redis 始终没数据,始终 都是要从数据库中同步数据的,所以这种方式 Redis 和 MySQL 的数据一定是一致的。
④ 先操作数据库,再删除 Redis(掉电)
这种方式也是行不通的,操作完数据库,主机掉电,此时 Redis 中还是有数据的,而且是旧数据,而 MySQL 中的数据已经更新了,导致双写不一致。
【并发场景下的问题】
虽然先删除 Redis,再操作数据库,这种方式看似解决了 Redis 和 MySQL 中的双写一致问题,但是它的前提是要在单线程的情况下。
在多线程的场景下,由于操作系统的随机调度,可能会出现以下情况:
- 线程 1 执行完删除 Redis,然后时间片用完了,
- 线程 2 执行查询操作,由于线程 1 把 Redis 删除了,所以线程 2 拿到了数据库的旧数据,然后时间片用完了,
- 线程 1 继续执行更新数据库,并把新数据缓存至 Redis,执行结束,
- 线程 2 苏醒,将刚刚查到的数据,缓存一份至 Redis.
此时 Redis 和数据库中的数据又变成不一致了,那该怎办 ?
【解决办法】延时双删
在线程 2 执行完之后后,再删除一次 Redis,以便下次查询还得走数据库,此时拿到的就是最新的数据了。(此时线程 1 的缓存 Redis 就可以不必执行了)
如果在面试中碰到这个问题,回到到这里其实已经差不多了,如果遇到了刁钻一点的面试官,那么我们以上看似美好的推理过程,其实还是存在问题的。
面试官问:延时双删是否彻底解决了 Redis 和 MySQL 双写一致的问题 ?
答案是不一定,它只是最大限度的解决双写一致性问题。因为延时,延长的时间是固定的,而操作系统的调度是随机的,极端情况下,还是可能会存在线程 1 第二次删除操作 处在线程 2 的将旧数据缓存至 Redis这个操作之前。(极端情况下,线程 2 是个饥饿线程)
那这个问题该怎么完美解决 ?
这个问题我也问过一些大佬,他们给出的结论是从理论上来说,目前没有更好的解决方案,但是前面也说了只有极端情况下,连延时双删都解决不了,因为它发生的概率也是比较小的,所以可以忽略不计了,并且使用延时双删几乎可以解决 99.99% 的双写一致问题了。
如果硬要追求完美主义,可以给它设置一个过期时间,等到键值过期了,自然会把数据库中新的数据给保存到 Redis 中;
另外,对于主机掉电,另一半操作没有执行的问题,可以通过加入消息队里欸来解决(保证业务的完整性),消费者再消费任务的时候,即使掉电,下一次还是会继续消费的。