文章目录
- 1.先更新缓存,再更新DB
- 2.先更新DB,再更新缓存
- 3.先删除缓存,后更新DB
- 4.先更新DB,后删除缓存 (推荐)
前言:
只要使用到缓存,无论是本地内存做缓存还是使用 redis 做缓存,那么就会存在数据同步的问题。
例: 先读缓存数据,缓存数据有,则立即返回结果;如果没有数据,则从数据库读数据,并且把读到的数据同步到缓存里,提供下次读请求返回数据。
这样能有效减轻数据库压力,但是如果修改删除数据,因为缓存无法感知到数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题,那该如何解决呢?
有好几种解决方案:
- 先更新缓存,再更新数据库
- 先更新数据库,再更新缓存
- 先删除缓存,后更新数据库
- 先更新数据库,后删除缓存
1.先更新缓存,再更新DB
这个方案我们一般不考虑。原因是更新缓存成功,更新数据库出现异常了,导致缓存数据与数据库数据完全不一致,而且很难察觉,因为缓存中的数据一直都存在。
2.先更新DB,再更新缓存
这个方案也我们一般不考虑,原因跟第一个一样,数据库更新成功了,缓存更新失败,同样会出现数据不一致问题。
3.先删除缓存,后更新DB
该方案也会出问题,具体出现的原因如下。
此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)
请求 A 会先删除 Redis 中的数据,然后去数据库进行更新操作;
此时请求 B 看到 Redis 中的数据时空的,会去数据库中查询该值,补录到 Redis 中;
但是此时请求 A 并没有更新成功,或者事务还未提交,请求B去数据库查询得到旧值;
那么这时候就会产生数据库和 Redis 数据不一致的问题。因此一般不建议使用这种方式。
如何解决呢?其实最简单的解决办法就是延时双删的策略。
(1)先淘汰缓存
(2)再写数据库
(3)休眠1秒,再次淘汰缓存
这么做,可以将1秒内所造成的缓存脏数据,再次删除。
那么,这个1秒怎么确定的,具体该休眠多久呢?
针对上面的情形,自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
但是上述的保证事务提交完以后再进行删除缓存还有一个问题, 例如我们生产环境 Mysql 是读写分离的架构,那么其实主从同步之间也会有时间差。
此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)
请求 A 更新操作,删除了 Redis,
请求主库进行更新操作,主库与从库进行同步数据的操作,
请 B 查询操作,发现 Redis 中没有数据,
去从库中拿去数据,此时同步数据还未完成,拿到的数据是旧数据。
此时的解决办法有两个:
1、还是使用双删延时策略。只是,睡眠时间需要修改为在主从同步的延时时间基础上,加几百ms。
2、就是如果是对 Redis 进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。
继续深入,采用这种同步淘汰策略,吞吐量降低怎么办?
那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。
先删除缓存值再更新数据库有可能导致请求因缓存缺失而访问数据库,给数据库带来压力; 以及 业务应用中读取数据库和写缓存的时间有时不好估算,导致延迟双删中的sleep时间不好设置。
4.先更新DB,后删除缓存 (推荐)
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。更新的时候,先更新数据库,然后再删除缓存。
这种情况不存在并发问题么?
依然存在。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
如果发生上述情况,确实是会发生脏数据。然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
一定要解决怎么办?如何解决上述并发问题?
首先,给缓存设有效时间是一种方案。
其次,采用异步延时删除策略。
但是,更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功怎么办?这个问题,在删除缓存类的方案都是存在的,那么此时再读取缓存的时候每次都是错误的数据了。
此时解决方案有两个,一是就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下:
1、请求 A 先对数据库进行更新操作
2、在对 Redis 进行删除操作的时候发现报错,删除失败
3、此时将Redis 的 key 作为消息体发送到消息队列中
4、系统接收到消息队列发送的消息后
5、再次对 Redis 进行删除操作
但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。
说到底就是通过数据库的binlog来异步淘汰key,利用工具(canal)将binlog日志采集发送到MQ中,然后通过ACK机制确认处理删除缓存。
先更新DB,后删除缓存,这种方式,被称为Cache Aside Pattern,属于缓存更新的设计模式之一。
不管是哪种缓存策略都无法保证数据库与缓存的一致性.因此业务层要对缓存的数据有一定的容忍,缓存中的数据可能是过期的或者是无效的数据。缓存一致性如果追求强一致,要么串行化,要么使用分布式读写锁等,性能比较差。