浅谈缓存数据库双写一致性
作者:互联网
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 删除缓存还是更新缓存?
- 线程A先发起一个写操作,第一步先更新数据库
- 线程B再发起一个写操作,第二步更新了数据库
- 由于网络等原因,线程B先更新了缓存
- 线程A更新缓存。
这时候,缓存保存的是A的数据(老数据),数据库保存的是B的数据(新数据),数据不一致了,脏数据出现啦。如果是删除缓存取代更新缓存则不会出现这个脏数据问题。
更新缓存相对于删除缓存,还有两点劣势:
- 如果你写入的缓存值,是经过复杂计算才得到的话。更新缓存频率高的话,就浪费性能啦。
- 在写数据库场景多,读数据场景少的情况下,数据很多时候还没被读取到,又被更新了,这也浪费了性能呢(实际上,写多的场景,用缓存也不是很划算了)
2.2 先删除缓存还是先更新数据库?
我们在操作缓存的时候,到底应该先删除缓存还是先更新数据库呢?我们先来看个例子:
- 缓存已经失效
- 线程A查询数据库,得一个旧值
- 线程B将新值写入数据库
- 然后线程B删除缓存
- 线程A将查到的旧值写入缓存
一般来说读比写快很多,第二步和第五步之间插入写动作概率不大。
2.3 先删除缓存,再更新数据库
- 线程A先发起一个写操作,先删除了缓存
- 线程B再发起一个读操作,发现缓存不存在
- 线程B查询旧值后放入缓存
- 线程A更新了数据库
缓存和数据库的数据不一致了。缓存保存的是老数据,数据库保存的是新数据。因此,Cache-Aside
缓存模式,选择了先操作数据库而不是先操作缓存。
解决方法:
- 先删除缓存,再更新数据库,休眠一会再删缓存(双删,第二次删可异步)
这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。 为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。
2.3.1 删除缓存重试机制
不管是延时双删还是Cache-Aside的先操作数据库再删除缓存,如果第二步的删除缓存失败呢,删除失败会导致脏数据哦~
删除失败就多删除几次呀,保证删除缓存成功呀~ 所以可以引入删除缓存重试机制
- 写请求更新数据库
- 缓存因为某些原因,删除失败
- 把删除失败的key放到消息队列
- 消费消息队列的消息,获取要删除的key
- 重试删除缓存操作
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