我们在进行更新操作的同时,必定要更新缓存、更新数据库。这俩个操作不是原子性的,在一些严苛的情况下面,难免会出现一些差错,导致数据库中与缓存中的数据不一致的情况出现,出现了这种情况,会导致读取脏数据的情况出现。
本文概述
常见的读取线程请求流程
请求缓存,缓存中存在数据直接返回给客户端,缓存中没有数据,接着请求数据库,数据库请求到的数据写入缓存,同时把数据返回给客户端
常见更新操作方案
1:先删缓存、再更新数据库(有隐患)
假设情景: a(更新线程),b(读取线程)
导致的情况: 缓存为旧数据、数据库为新数据
- a:删除缓存
- b:读取缓存,此时缓存为空
- b:读取数据库,拿到旧数据
- b:成功拿取到旧数据后,将旧数据写入缓存
- a:成功删除缓存成功后,将新数据写入数据库
只要出现了这么一次情况,之后读取线程一直是读到的脏数据
伪代码实现
/**
* 更新线程(先删缓存,再更新数据库)
*/
@RequestMapping("/update3")
public String update2(@RequestParam("id") int id) {
try {
userService.delCacheBykey(id);
userService.creatPessimismOrder(id);
//userService.creatOptimisticOrder(id);
} catch (Exception e) {
log.info(e.getMessage());
return "操作失败";
}
return "操作成功";
}
采用延时双删策略(优化)
先删缓存、再更新数据库、指定时间再次删除缓存,这样能确保缓存一定删除成功、且下次读取数据回写给缓存中的数据一定为最新的数据,保证了数据一致性
假设情景:a(更新线程),b(读取线程)
- a:删除缓存
- b:读取缓存,此时缓存为空
- b:读取数据库,拿到旧数据
- b:成功拿取到旧数据后,将旧数据写入缓存
- a:成功删除缓存成功后,将新数据写入数据库
- a:指定时间,再次删除缓存
伪代码实现
/**
* 更新线程(先删缓存,再更新数据库)优化,延时双删
*/
@RequestMapping("/update4")
public String update4(@RequestParam("id") int id) {
try {
//删除缓存
userService.delCacheBykey(id);
//悲观锁更新数据库(查库存数据,更新库存,创建订单)
userService.creatPessimismOrder(id);
//userService.creatOptimisticOrder(id);
//延时删除缓存
poolExecutor.execute(new delayDelCache(id));
} catch (Exception e) {
log.info(e.getMessage());
return "操作失败";
}
return "操作成功";
}
2:先更新数据库,再更新缓存(直接pass)
假设场景: a(更新线程),b(更新线程)
导致的情况: 先更新的数值覆盖后更新的数值
- a:更新数据库
- b:更新数据库
- b:成功更新好数据库后,更新缓存
- a:成功更新好数据库后,更新缓存
出现了网络延迟,明明是b后更新的,但是数据库最终的数据是a线程的值
3: 先更新数据库,再删除缓存(推荐)
假设场景:a(更新线程),b(读取线程)
- (1) b:读取缓存,恰好此时缓存过期了
- (2)b:读取数据库,得到旧值
- (3)a:更新数据库
- (4)a:成功更新好数据库后,删除缓存
- (5)b:成功读取到旧值后,将旧值写入缓存
此时也会出现数据不一致的问题,但是读操作是快于写操作的(不然读写分离干嘛吃的),步骤二耗时<步骤三耗时,耗时短的先执行后面的操作,所以步骤五先于步骤四执行,所以上面这种情况发生的概率很低。
伪代码实现
/**
* 更新线程(先更新数据库,在删缓存)
*/
@RequestMapping("/update1")
public String update(@RequestParam("id") int id) {
try {
userService.creatPessimismOrder(id);
//userService.creatOptimisticOrder(id);
userService.delCacheBykey(id);
} catch (Exception e) {
log.info(e.getMessage());
return "操作失败";
}
return "操作成功";
}
优化:消息中间件,删除重试机制
/**
* 更新线程(先更新数据库,在删缓存)优化,rabbitmq
*/
@RequestMapping("/update2")
public String update3(@RequestParam("id") int id) {
try {
userService.creatPessimismOrder(id);
//userService.creatOptimisticOrder(id);
userService.delCacheBykey(id);
//延时删除缓存
poolExecutor.execute(new delayDelCache(id));
// TODO 通知消息队列,删除缓存
} catch (Exception e) {
log.info(e.getMessage());
return "操作失败";
}
return "操作成功";
}
完整代码链接
https://github.com/zhangzihang3/myRedisUniformityBlog.git
面试专栏
redis和db的不一致咋解决的?
答:采用的延时双删的策略,更新 db 前先删除 redis 中的数据,此时当有请求访问 db 时由于 redis 中没有数据会去请求 db ,并将 db 中的数据写入 redis,但是可能此时的 db 更新数据比较慢,重新写入 redis 中的数据仍然为旧数据(造成 redis 与 db 不一致),因此当 db 更新完成后,延时再次进行删除 redis,这样就解决了 redis 与 db 不一致的问题了。