“数据一致”一般指的是:缓存中有数据,缓存的数据值 = 数据库中的值。一致性又分为几种程度:
只读缓存:新增数据时,直接写入数据库;更新(修改/删除)数据时,先删除缓存。 后续,访问这些增删改的数据时,会发生缓存缺失,进而查询数据库,更新缓存。
读取数据流程:
更新数据流程:
在更新数据的流程中会有个时序问题:更新数据库与删除缓存的顺序,这里会发生数据不一致的问题
先更新数据库再删除缓存:
如果第二步执行失败则会导致, 缓存中还是旧值, 数据不一致
反之如果是先删除缓存, 再更新数据库, 就第二步更新数据库失败了, 只是缓存被删除了, 读操作回源的时候还是会把数据库中的值load回缓存, 数据还是一致的
解决方案;消息队列 + 异步重试
两个线程同时做更新操作, 由于网络问题可能发生如下时序:
时序 | 线程A | 线程B |
---|---|---|
T1 | 删除数据X的缓存 | |
T2 | 读取X,缓存MISS | |
T3 | 从数据库Load X 的值到缓存 | |
T4 | 更新数据库中X的值 |
或者:
时序 | 线程A | 线程B |
---|---|---|
T1 | 删除数据X的缓存 | |
T2 | 读取X,缓存MISS | |
T3 | 更新数据库中X的值 | |
T4 | 从数据库Load X 的值到缓存 |
这种情况下会导致缓存中是旧值(线程B Load 进去的值)而数据库中是新值
解决方案: 设置缓存过期时间 + 延时双删, 时序如下:
时序 | 线程A | 线程C | 线程D |
---|---|---|---|
T5 | Sleep(N) | 读取到缓存旧值 | |
T6 | 删除缓存数据 | ||
T7 | 更新数据库中X的值 | 缓存miss, load数据库值到缓存 |
时序 | 线程A | 线程C | 线程D |
---|---|---|---|
T1 | 更新主库X=1 | ||
T2 | 删除缓存 | ||
T3 | 缓存miss,查询从库得到从库旧值(X=0) | ||
T4 | 旧值写入缓存 | ||
T5 | 此刻从库同步完成 |
解决方案:
(1) 针对读写分离的场景, 可以采取延迟消息删除缓存(这个延迟的时间要根据项目情况控制下), 另外也要控制主从延迟的时间
(2) 针对第一种情况, 其实只是小段时间的不一致, 一般业务可以接受这种情况, 保证最终一致即可。如果业务一定要强一致, 可以采取: a. 更新数据加写锁 b. 查询数据加读锁
(3) 采取数据库binlog来淘汰缓存, 这种方法成本较高
优先使用“先更新数据库再删除缓存”的执行时序,原因主要有两个:
读写缓存:增删改在缓存中进行,并采取相应的回写策略,同步数据到数据库中
执行顺序性 | 潜在问题 | 结果 | 解决策略 |
---|---|---|---|
先更新缓存,后更新数据库 | 更新缓存成功,更新数据库失败 | 数据库中为旧值 | 消息队列+重试 |
先更新数据库,后更新缓存 | 更新数据库成功,更新缓存失败 | 请求命中缓存,读取缓存旧值 | 消息队列+重试机制;订阅Binlog日志 |
解决方案: 保存请求对缓存的读取记录,延时消息比较,发现不一致后,做业务补偿
1.线程A和线程B同时更新同一条数据 2.更新数据库的顺序是先A后B 3.更新缓存时顺序是先B后A 这种场景下会导致缓存更新覆盖,缓存中其实是旧值(A的值, 应该是后更新数据库中B的值)
解决方案: 对于写请求,需要配合分布式锁使用。写请求进来时,针对同一个资源的修改操作,先加分布式锁,保证同一时间只有一个线程去更新数据库和缓存;没有拿到锁的线程把操作放入到队列中,延时处理。用这种方式保证多个线程操作同一资源的顺序性,以此保证一致性。