数据库
首页 > 数据库> > 浅谈缓存数据库双写一致性

浅谈缓存数据库双写一致性

作者:互联网

1. 事务完善双写一致性

17boot-cache引入了@CacheMeta来标注缓存,但是spring的已经有现成的缓存,为什么要新写一个注解去做这个事呢,在cf中提出了一个场景,@CacheMeta就是为了解决这个问题:

1 thread-1 根据id删除表中数据
2 thread-1 根据id清除缓存
3 thread-2 根据id从缓存中查找数据
4 thread-2 缓存中找不到,查询数据库,拿到的是老数据,并加载到缓存
5 thread-1 事务提交

在这个情况下,thread-1拿到就是thread-2放入的旧数据

那如何解决这个问题,他们给出了以下方法

1 thread-1 根据id删除表中数据
2 thread-1 标记该id需要删除 // 只是标记,还未真正发起对缓存的操作
3 thread-2 从缓存中查找数据,找到缓存,返回
4 thread-1 事务提交
5 thread-1 调用spring事务成功回调方法,正式清除该id对应的缓存

CacheMeta的解决方法是事务未结束时对缓存数据进行标记删除,只有当事务返回成功之后才会正式清除对应的缓存。其中标记删除可以进行批量操作,减少交互。

其实这个问题是经典的并发下的问题:如何保障数据库缓存双写一致性

2.数据库缓存双写一致性解决方案

2.1 删除缓存还是更新缓存?

  1. 线程A先发起一个写操作,第一步先更新数据库
  2. 线程B再发起一个写操作,第二步更新了数据库
  3. 由于网络等原因,线程B先更新了缓存
  4. 线程A更新缓存。

这时候,缓存保存的是A的数据(老数据),数据库保存的是B的数据(新数据),数据不一致了,脏数据出现啦。如果是删除缓存取代更新缓存则不会出现这个脏数据问题。

更新缓存相对于删除缓存,还有两点劣势:

2.2 先删除缓存还是先更新数据库?

我们在操作缓存的时候,到底应该先删除缓存还是先更新数据库呢?我们先来看个例子:

  1. 缓存已经失效
  2. 线程A查询数据库,得一个旧值
  3. 线程B将新值写入数据库
  4. 然后线程B删除缓存
  5. 线程A将查到的旧值写入缓存

一般来说读比写快很多,第二步和第五步之间插入写动作概率不大。

2.3 先删除缓存,再更新数据库

  1. 线程A先发起一个写操作,先删除了缓存
  2. 线程B再发起一个读操作,发现缓存不存在
  3. 线程B查询旧值后放入缓存
  4. 线程A更新了数据库

缓存和数据库的数据不一致了。缓存保存的是老数据,数据库保存的是新数据。因此,Cache-Aside缓存模式,选择了先操作数据库而不是先操作缓存。

解决方法:

这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。 为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。

2.3.1 删除缓存重试机制

不管是延时双删还是Cache-Aside的先操作数据库再删除缓存,如果第二步的删除缓存失败呢,删除失败会导致脏数据哦~

删除失败就多删除几次呀,保证删除缓存成功呀~ 所以可以引入删除缓存重试机制

image.png

  1. 写请求更新数据库
  2. 缓存因为某些原因,删除失败
  3. 把删除失败的key放到消息队列
  4. 消费消息队列的消息,获取要删除的key
  5. 重试删除缓存操作

2.4 17boot-cache解决了什么

对于2.3的情况,第一步A的事务没有结束,所以@CacheMeta并没有删除缓存,所以第二步B去读还是能读到缓存,避免了将缓存更新成旧缓存的问题。

但是还是无法避免2.2的情况,其实先操作db还是cache,都会有各自的问题,根本原因是cache和db的更新不是一个原子操作,因此总会有不一致的问题。想要彻底解决这种问题必须将cache和db的更新操作归在一个事务之下(例如使用一些分布式事务,或者强一致性的分布式协议),可能反而得不偿失。

在RedisCacheManager中设置transactionAware也能实现

在这里插入图片描述

加入template.setEnableTransactionSupport(true);即可打开redis事务

标签:缓存,浅谈,删除,数据库,更新,thread,线程,双写
来源: https://blog.csdn.net/sinat_29774479/article/details/120703317