缓存:如何处理DB缓存一致性

缓存一致性的原则,介绍强一致性策略和最终一致性策略

首要逻辑

首先,写操作的核心首先目标是:确认的是数据应该先写入一致性最高的数据源,即:数据库;最优先保证数据已经正确持久化。

其次,再考虑解决读操作的瓶颈问题:

  1. 读操作的频率远远大于写操作,意味着数据不会发生频繁变动;
  2. 数据库往往是绝大多数系统的瓶颈,这种变动少的频繁读取的数据,可以考虑使用缓存降低数据库压力;

综上,原则是:保证数据优先落库,其次使用缓存降低数据库压力。

缓存一致性原则

  1. 只要读写不是原子的,就无法完全保证数据一致性;要强一致性就得加锁;
  2. 顺序原则:一定不能先删缓存;一定优先保证数据源是最新的数据(先写数据源);目的是降低数据不一致概率;
  3. 不是并发特别高,直接使用强一致性即可,复杂度低;

强一致性策略

更新缓存时加锁,牺牲并发性能,保证数据一致性;(简单粗暴)

  • 可以采用Redis的分布式锁,降低锁的粒度,如:以用户id作为锁,不同用户间不产生竞争;
  • 可以使用读写锁;读读共享;

尽量一致性的策略

读写不是原子的,无法完全保证数据一致;

1. 先刷库再刷缓存(❌)

并发场景下,不加锁无法保证缓存的值为最新的;

  1. A线程刷库
  2. B线程刷库(最新值)
  3. B线程刷缓存
  4. A线程刷缓存(脏数据)

2. 先刷缓存再刷库(❌)

这种方式更不可取,并发下会导致数据源(DB)的数据为脏数据;

  1. A线程刷缓存
  2. B线程刷缓存(最新值)
  3. B线程刷库
  4. A线程刷库(DB脏数据,相当于丢数据了)

3. 先删缓存刷库(❌)

不可取,这样造成的脏数据,只能等到下一个写操作或缓存失效,才能移除脏数据;

  1. A线程删缓存
  2. B线程读缓存,读不到,从数据库读取到旧值
  3. A线程刷库(最新值)
  4. 缓存中存放脏数据;

4. 刷库删缓存Cache Aside(✅)

  1. 写操作只负责更新数据源;每次更新,删除缓存;
  2. 读操作直接读缓存;
  3. 读操作读不到缓存,则查数据源,直接返回,不处理缓存;

可能发生不一致性的场景:

触发条件:

  • 前提:缓存已经失效,可能是正好过期,也可能是连续两次写;
  • 同时有读操作、写操作,并且查库在写库之前,查到了脏数据;然后写操作刷库;
  • 写入缓存在删缓存之后,写入了脏数据;

这种持续的不一致,只能等到缓存失效、下次写请求删掉,否则一直会不一致;

5. 延迟双删(✅)

针对Cache Aside的不一致问题,原因是:写入新数据之后,没能成功的把缓存的旧数据删掉

延迟双删:延迟一定时间,再次触发删除缓存;降低数据不一致的可能性;但是延迟期间的数据不一致,仍然存在;

延迟策略:一般延迟1-3s即可;

  • 定时任务:写线程执行完业务逻辑,删除缓存,返回之前开启一个异步线程,睡眠一段时间后再次删除缓存;
  • 延迟队列:要借助中间件或者本地队列,

为什么不可以先删缓存,再刷库

首先,写操作的核心首先目标是:确认的是数据应该先写入一致性最高的数据源,即:数据库;最优先保证数据已经正确持久化。

其次,再解决读操作的瓶颈问题:读操作的频率远远大于写操作,意味着数据不会发生频繁变动,因此需要缓存,不需要每次读取数据库;

先删缓存,后写数据源,在写入完成前,任意的并发读线程又将脏数据更新到缓存,则缓存和数据源将持续不一致;并且直到下次缓存失效或再次删除才会更新

本身缓存使用场景是读多写少,所以这种情况的概率很大;

而先刷库,再删缓存,同样存在这种情况,但是脏数据仅存在刷库和删缓存之间的这段时间,删掉之后就一致了,可以容忍;