缓存双写一致性
- 对于读:先读缓存,缓存没有,再读ku,回写缓存,这种没啥说的。
- 对于写:先写库,再删除缓存,本文主要基于这种来探讨一下这样处理有什么问题?
如下图所示:
图中表示大量请求同时涌入,读数据和写数据请求同时执行,下面我们基于这幅图来剖析为何先写库,再删除缓存会有问题?
大前提:缓存正好失效
正常情况: 有一个读数据和一个写数据请求同一时刻过来,读请求发现缓存失效,去读数据库(旧数据),然后回写缓存(旧数据),此时写请求也更新好了数据库,再把缓存删掉,这样的话即使之前读请求把旧值回写缓存也没事。
问题的关键在于: 步骤3和步骤5谁先执行,在正常情况下,3都是先于5执行的,因为读库一定比写库快。可是高并发下就是会有不正常的情况,比如读数据接口和写数据接口是在两个不同的jvm中,并且读数据请求在步骤3的时候发生了FULL GC, 延迟了回写缓存的时间,导致缓存旧数据。
解决方案:
前提:一定设置缓存过期时间,如1小时
- 定时刷库:每一分钟取出数据库数据重新放到redis,需要额外线程池消耗,可以优化成当更新数据的时候把id封成延迟消息发送给MQ,由MQ去查库再回写缓存
- 延迟双删:更新数据的时候删除缓存后在开启一个定时器15s后再删除一次缓存,也可以把id封成延迟消息发送给MQ,由MQ去删除缓存,说到家了方案二和方案一很像。
- 分布式锁:在读写接口加分布式锁,让读写串行化。分布式锁
- 分布式读写锁:因为大多数情况下,都是读请求,读读无并发还是可以提高不少相应速度的。仓库(代码在com.lry.basic.redis.readWriteLock)
- 分布式乐观锁:
@Autowired
RedisCache redisCache;
String goodsKey = "goodsId:1";
String lockKey = "lock:" + goodsKey;
String goodsKeyVersion = goodsKey+":version";
@RequestMapping("updateByCas")
public void updateByCas(){
//先更新数据库
try {
//模拟更新,耗时500ms
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//在删除缓存
redisCache.deleteObject(goodsKey);
updateVersion();
}
private Integer updateVersion(){
//版本字段
Integer version = redisCache.getCacheObject(goodsKeyVersion);
if(version==null){
version = 1;
}else {
version++;
}
redisCache.setCacheObject(goodsKeyVersion,version);
return version;
}
private boolean canReWrite(int cur){
Integer version = redisCache.getCacheObject(goodsKeyVersion);
if(null==version)
return true;
if(cur>=version)
return true;
return false;
}
@RequestMapping("getByCas")
public void getByCas(){
//先从缓存拿
Object obj = redisCache.getCacheObject(goodsKey);
//缓存没有再从数据库拿
if(null==obj){
Integer version = updateVersion();
try {
//模拟读库200ms,读的是旧的数据
Thread.sleep(200);
obj = "goods";
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
//模拟GC 1000ms
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(canReWrite(version)){
//最后回写缓存
redisCache.setCacheObject(goodsKey,obj);
}
}
}