首先: 由于缓存的使用,导致数据不一致性的问题是不能绝对避免的,解决方案也只是尽可能地去降低其出现的频率,如果想保证绝对一致性的话,是不推荐使用缓存的
缓存更新策略
业务需求:
低一致性需求: 数据变化频率低的类型,比如商铺分类等,就采用内存淘汰机制进行更新,或设置TTL过期时间
高一致性需求: 主动更新,并设置TTL,以超时剔除做为兜底方案
主动更新方案
- 更新数据库时,同时去更新缓存
- 只对缓存操作,异步记录更新操作,在达到一定数量或时间的阈值后,合并操作发送给数据库进行更新,有丢失数据的风险
- 集成缓存和数据库服务为一个服务,对外开放接口,服务内部进行一致性处理
一般采用第一种方案,因为实现起来比较容易,并且风险不高,较稳定
第一种方案详解
首先有三个问题:
-
数据更新时,是更新缓存还是删除缓存?
- 如果采用更新缓存,一个数据频繁更新,但是读的频率却不是很高,这时候会导致有很多无效操作
- 由于很多缓存的数据是根据数据库的数据进行二次操作计算和筛选出来的,所以频繁更新缓存也浪费系统资源
- 所以,一般采取更新数据库时,删除缓存的操作
- 问题: 如果采用删除缓存操作,对于一个热点数据的更新,可能会导致同一时间大量请求打到数据库,出现缓存击穿的问题,需要另外解决
-
如何保证缓存和数据库的操作是原子性的?
- 单体系统: 将缓存操作和数据库操作直接放在一个事务里面就行
- 分布式系统: 采用分布式事务方案,TCC或柔性事务补偿等,详细方案看分布式事务
-
先操作缓存还是数据库?
-
1. 先更新缓存再更新数据库
-
2. 先删缓存,再更新数据库
-
3. 先更新数据库再更新缓存
-
4. 先更新数据库再删除缓存
-
由于前面说过,采取删除缓存的操作是更优的,并且如果采用1方案,由于写数据库时有加锁、超时等机制所以失败概率比其他都高,如果失败了就会导致数据不一致缓存中为脏数据,且数据库也没有更新,后续如果别的业务通过数据库计算,计算的数据反而是旧数据,会导致比较严重的后果,所以直接排除1、3方案,讨论其他两个方案
1.先删缓存,再更新数据库:
不一致案例: 高并发时,数据值原先Data = X
A删除缓存 --> 写数据库新数据 Data = Y(卡顿)
B查看缓存发现没有 --> 去数据库拿旧数据 --> 同时更新缓存 Data = X
A更新完毕,此时缓存Data = X,数据库Data = Y
解决方案——延迟双删:
A更新完数据库以后,为了防止这段时间有人更新了缓存,需要再删一次,但是不能马上删,因为这时候可能B还没有写完缓存,所以需要sleep一会,具体睡多久看具体业务,只要大于读数据库+写缓存的时间就行
2.先更新数据库,再删缓存:
如果删除缓存失败解决方案:
- 删除缓存任务写入消息队列
- 缺点:存在一定延迟,有消息丢失问题、耦合度高
- 消息队列去监听MySQL的Binlog日志进行缓存删除
- 解决了业务代码的侵入性,服务解耦等
不一致案例
满足条件:
-
此时正在更新数据库
-
更新的同时缓存过期
-
过期时刚好有一个请求进来查询,且网络波动,导致其更新缓存的速度比别人更新数据库+删除缓存的速度还慢
1.缓存过期B去查询到旧数据Data = X,然后由于网络波动还没更新缓存
2.A更新数据库Data = Y,删除缓存
3.B写入缓存Data = X,此时数据库Data = Y,出现不一致
这个问题出现的概率很低,已经很大程度解决了一致性问题,由于使用缓存本身就不能保证绝对一致性,只是为了加快速度,如果需要强一致性,就不要使用缓存
总结:
低一致性需求: 使用Redis自带的内存淘汰机制,或者直接设置TTL过期时间
高一致性需求:
- 读操作
- 缓存命中就直接返回
- 缓存没有命中,去数据库查询后,写入缓存设置超时时间做为兜底
- 写操作
-
需要保证数据库和缓存更新的原子性,单体系统直接写在一个事务中,分布式系统采用合适的分布式事务方案
-
更新数据采用删除缓存的策略:
- 先删缓存再更新数据库配合延迟双删使用
- 先更新数据库再删缓存配合消息队列使用
其实总的来说先更新数据库再删除缓存出现并发问题的概率比较小,推荐使用这个方案
-