缓存一致性的原则,介绍强一致性策略和最终一致性策略
首要逻辑
首先,写操作的核心首先目标是:确认的是数据应该先写入一致性最高的数据源,即:数据库;最优先保证数据已经正确持久化。
其次,再考虑解决读操作的瓶颈问题:
- 读操作的频率远远大于写操作,意味着数据不会发生频繁变动;
- 数据库往往是绝大多数系统的瓶颈,这种变动少的频繁读取的数据,可以考虑使用缓存降低数据库压力;
综上,原则是:保证数据优先落库,其次使用缓存降低数据库压力。
缓存一致性原则
- 只要读写不是原子的,就无法完全保证数据一致性;要强一致性就得加锁;
- 顺序原则:一定不能先删缓存;一定优先保证数据源是最新的数据(先写数据源);目的是降低数据不一致概率;
- 不是并发特别高,直接使用强一致性即可,复杂度低;
强一致性策略
更新缓存时加锁,牺牲并发性能,保证数据一致性;(简单粗暴)
- 可以采用Redis的分布式锁,降低锁的粒度,如:以用户id作为锁,不同用户间不产生竞争;
- 可以使用读写锁;读读共享;
尽量一致性的策略
读写不是原子的,无法完全保证数据一致;1. 先刷库再刷缓存(❌)
并发场景下,不加锁无法保证缓存的值为最新的;
- A线程刷库
- B线程刷库(最新值)
- B线程刷缓存
- A线程刷缓存(脏数据)
2. 先刷缓存再刷库(❌)
这种方式更不可取,并发下会导致数据源(DB)的数据为脏数据;
- A线程刷缓存
- B线程刷缓存(最新值)
- B线程刷库
- A线程刷库(DB脏数据,相当于丢数据了)
3. 先删缓存刷库(❌)
不可取,这样造成的脏数据,只能等到下一个写操作或缓存失效,才能移除脏数据;
- A线程删缓存
- B线程读缓存,读不到,从数据库读取到旧值
- A线程刷库(最新值)
- 缓存中存放脏数据;
4. 刷库删缓存Cache Aside(✅)
- 写操作只负责更新数据源;每次更新,删除缓存;
- 读操作直接读缓存;
- 读操作读不到缓存,则查数据源,直接返回,不处理缓存;
可能发生不一致性的场景:
触发条件:
- 前提:缓存已经失效,可能是正好过期,也可能是连续两次写;
- 同时有读操作、写操作,并且查库在写库之前,查到了脏数据;然后写操作刷库;
- 写入缓存在删缓存之后,写入了脏数据;
这种持续的不一致,只能等到缓存失效、下次写请求删掉,否则一直会不一致;
5. 延迟双删(✅)
针对Cache Aside的不一致问题,原因是:写入新数据之后,没能成功的把缓存的旧数据删掉;
延迟双删:延迟一定时间,再次触发删除缓存;降低数据不一致的可能性;但是延迟期间的数据不一致,仍然存在;
延迟策略:一般延迟1-3s即可;
- 定时任务:写线程执行完业务逻辑,删除缓存,返回之前开启一个异步线程,睡眠一段时间后再次删除缓存;
- 延迟队列:要借助中间件或者本地队列,
为什么不可以先删缓存,再刷库
首先,写操作的核心首先目标是:确认的是数据应该先写入一致性最高的数据源,即:数据库;最优先保证数据已经正确持久化。
其次,再解决读操作的瓶颈问题:读操作的频率远远大于写操作,意味着数据不会发生频繁变动,因此需要缓存,不需要每次读取数据库;
先删缓存,后写数据源,在写入完成前,任意的并发读线程又将脏数据更新到缓存,则缓存和数据源将持续不一致;并且直到下次缓存失效或再次删除才会更新;
本身缓存使用场景是读多写少,所以这种情况的概率很大;
而先刷库,再删缓存,同样存在这种情况,但是脏数据仅存在刷库和删缓存之间的这段时间,删掉之后就一致了,可以容忍;