只要在业务中使用缓存,就必然会面对缓存和数据库之间的一致性保证问题了,这也是 Redis 缓存应用中的必答题,如果某些业务场景数据不一致,就会导致严重的错误,比如某个商品库存信息在 Redis 中和数据库中不一致,这就会导致用户下单操作出现严重错误,这个是在业务上无法接受的,这篇文章来学习一下 Redis 缓存和数据库不一致。
1.笔记图
2.数据一致性是啥意思?
不符合下面这两种情况就属于缓存和数据库的数据不一致:
- 缓存中有数据,缓存的数据值需要和数据库中的值相同
- 缓存中没有数据,数据库中的值必须是最新值
3.写回策略
- 同步直写:写请求发给缓存的同时,也会发给后端数据库进行处理,等到缓存和数据库都写完数据,才给客户端返回,同步直写策略优先保证数据可靠性,增加了缓存的响应延迟
- 异步写回:优先提供快速响应,所有写请求都先在缓存中处理,等到这些增改的数据要被从缓存中淘汰出来时,缓存将它们写回后端数据库,使用这种策略时,如果数据还没有写回数据库,就发生了故障,数据库就没有最新的数据了
4.Redis缓存类型
- 只读缓存:所有的数据写请求,会直接发往后端的数据库,如果 Redis 已经缓存了相应的数据,应用需要把这些缓存的数据删除,当应用再次读取这些数据时,会发生缓存缺失,应用会把这些数据从数据库中读出来,并写到缓存中
- 读写缓存:
- 读写缓存除了读请求会发送到缓存,写请求也会发送到缓存
- 在使用读写缓存时,最新的数据在 Redis 中,一旦出现掉电或宕机,内存中的数据可能就会丢失
5.数据不一致情况
- 如果有数据需要删改时,假设先删除缓存数据成功了,再删改数据库数据失败了,再次访问数据时,缓存中没有数据,就会读到数据库中的旧数据
- 假设先删改数据库数据成功了,再删除缓存数据失败了,数据库中的值是新值,缓存中的值是旧值,其他并发请求会访问到缓存中的旧值
- 更新数据库和删除缓存值的过程中,无论这两个操作的执行顺序谁先谁后,只要有一个操作失败了,就会导致客户端读取到旧值
- 即使删改数据库和删除缓存这两个操作执行时都没有失败,当有大量并发请求时,应用还是有可能读到不一致的数据
6.缓存和数据库数据操作原子性
- 要想保证缓存和数据库中的数据一致,就要采用同步直写策略,需要同时更新缓存和数据库
- 如果发生删改操作,应用既要更新数据库,也要在缓存中删除数据。两个操作如果无法保证原子性(要么都完成,要么都没完成),就会出现数据不一致问题了
- 同步直写策略要在业务应用中使用事务机制,来保证缓存和数据库的更新具有原子性
- 缓存和数据者要么一起更新,要么都不更新,返回错误信息,进行重试
7.解决数据不一致问题
- 重试机制:
- 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(如Kafka),当没有成功删除缓存值或者是更新数据库值时,从消息队列中重新读取这些值,再次进行删除或更新
- 如重试超过一定次数没有成功,就需要向业务层发送报错信息
- 情况一:先删除缓存,再更新数据库
- 问题描述:
- 假设线程 A 删除缓存值后,还没有来得及更新数据库(比如说有网络延迟),
线程 B 就开始读取数据了,线程 B 会发现缓存缺失,就只能去数据库读取 - 线程 B 读取到了旧值
- 线程 B 是在缓存缺失的情况下读取的数据库,它还会把旧值写入缓存,这可能会导致其他线程从缓存中读到旧值
- 解决办法:延迟双删
- 在线程 A 更新完数据库值以后,可以让它 sleep 一小段时间,再进行一次缓存删除操作
- 加上 sleep 的这段时间,就是为了让线程 B 能够先从数据库读取数据,再把缺失的数据写入缓存,线程 A 再进行删除
- 线程 A sleep 的时间,就需要大于线程 B 读取数据再写入缓存的时间,这个 sleep 时间需要根据业务统计下线程读数据和写缓存的操作时间,以此为基础来进行估算
- 伪代码:
redis.delKey(X) db.update(X) Thread.sleep(N) redis.delKey(X)
- 情况二:先删除缓存,再更新数据库
- 问题描述:如线程 A 删除数据库中的值,没来得及删除缓存值,线程 B 就开始读取数据了,线程 B 查询缓存时,发现缓存命中,会读取旧值
- 解决办法:
- 删除缓存值或更新数据库失败而导致数据不一致,你可以使用重试机制确保删除或更新操作成功
- 在删除缓存值、更新数据库的这两步操作中,有其他线程的并发读操作,导致其他线程读取到旧值,应对方案是延迟双删