浏览量:1942 最近编辑于:2024-08-24 10:24:34
# 写在最前面
因为数据库与缓存是不同的组件,操作必须有先后顺序,无法像数据库的事务一样满足ACID的特性,所以就会出现数据在缓存中与在数据库中不一致的问题。
本文所探讨的场景均基于“先读取缓存,缓存读不到再读数据库,并更新缓存值”的Cache Aside Pattern 模式,该模式只适用于读多写少的场景
若对数据库和缓存的一致性要求极高,则不建议使用缓存!
此外,没有最完美的方案,只有最适合的方案!
并发问题,网络、机器抖动等因素不可控!
# 一、新增场景
不存在一致性问题。往数据库里插入一条新数据,这时候请求a过来,缓存里面没有任何数据副本,去数据库里读
# 二、删除场景
## 1.先删缓存再删数据库(不可行)
缺点:
1.并发问题。请求a先删了缓存,然后读请求b过来发现缓存中没有数据,去数据库中取数据,然后更新了缓存值,接着请求a删除了数据库,造成数据不一致
## 2.先删数据库再删缓存
缺点:
1.并发问题。读请求a先读缓存中的数据,发现没有(首次读取),然后去数据库中读;接着请求b删除了数据库,再删除了缓存;最后请求a设置了缓存值,导致脏数据
# 三、更新场景
## 1.先更新缓存后更新数据库
缺点:
1.更新完缓存后,写数据库失败(超时、锁、外键约束等),导致缓存中脏数据
2.并发问题。请求a先写缓存,请求b过来也写了缓存,a由于网络gc等原因卡顿没来得及更新数据库,b先更新了数据库,a结束了卡顿也更新了数据库。b在数据库中的数据被a的旧值覆盖
## 2.先更新数据库后更新缓存
缺点:
1.写数据库成功,更新缓存失败(概率相比更新数据库失败要低)
2.并发问题。请求a先写数据库,请求b过来也写了数据库,a由于网络gc等原因卡顿没来得及更新缓存,b先更新了缓存,a结束了卡顿也更新了缓存。b在缓存中的数据被a的旧值覆盖
方案1和2还有一个共同的问题:即在写多读少的场景下,更新缓存较耗费系统资源,因为缓存数据需要计算得出
## 3.先删缓存再更新数据库
优点:
1.实现简单
2.即使删除缓存成功,更新数据库失败了回滚也没事,因为下次读请求get不到缓存还是会从数据库中取然后set缓存
缺点:
1.并发问题。请求a过来将缓存删除,卡顿了一下;读请求b过来先查缓存发现没数据,再查数据库发现有数据,但是是旧值;b将旧值更新到缓存中;这时请求a结束卡顿将新值写入数据库
2.缓存击穿
### 3.1延时双删
为了避免上述的并发问题,可以更新完数据库后再删一次缓存
第二次删除不是立刻就删,而是隔一段时间
缺点:
1.延迟时间难以确定,间隔时间内数据还是不一致
2.主从同步还是无法绝对保障数据的一致性,还是有一致性问题
### 3.2延时覆盖
null
## 4.先更新数据库再删缓存(Cache Aside Pattern 的主流方案)
优点:确保数据库中的数据是最新的
缺点:
1.还是并发问题。读请求a查询数据库,然后卡顿;请求b更新了数据库,接着b删除了缓存;请求a醒过来重新set了缓存。导致脏数据
2.缓存会删除失败。解决方案:设置过期时间、异步重试、mq、异步监听binlog删除缓存等(非事务)
3.缓存击穿。
### 4.1异步删除+重试
流程:先更新数据库、再发送需要删除缓存的MQ消息去删除;缓存删除失败则通过MQ不断重试,直至删除成功
缺点:
引入组件带来新的问题,要保证组件的高可用等,消息丢失会删除失败
对业务代码有侵入
脏数据时间窗口“较大”,mq延时
多表还是会有并发问题
### 4.2 订阅binlog异步删除
流程:更新数据库,监听数据库binlog变更消息去删除
优点:解耦
缺点:复杂度上升
# 写在最后
以上所有分析,只分析了单节点缓存的一致性问题,如果涉及分布式和多节点,则更为复杂
以上所有分析,只保证了“最终一致性”,无法100%保证实时一致性
总结:没法完全保证强一致,只能保证最终一致性。
若一定要保证强一致性,只能加锁,若涉及分布式,则更复杂
先理清自身业务的特点,再在一致性要求和实现复杂度中做取舍!选择适合的方案!