目录
缓存简单介绍
Redis缓存的大致实现图
缓存更新策略
三种方案
三个问题
先操作缓存 还是 先操作数据库?
先删除缓存再操作数据库
正常情况:
异常情况:
先操作数据库,再删除缓存
正常情况:
异常情况:
总结
缓存的三个问题
缓存穿透
缓存雪崩
缓存击穿
解决:当数据库中的数据进行了更改,Redis缓存并不知晓,在缓存中查询到仍然是旧数据
示例:
缓存与数据库事先存入的初始值都为10
线程1删除缓存,然后更新数据库 v = 20;
线程2查询缓存未命中,而后查询数据库 v,并写入缓存
当线程1删除缓存后,由于更新数据库的耗时较长,线程2插入,执行查询缓存,未命中查询数据库v = 10,并写入缓存;线程1而后更新数据库 v = 20,这导致缓存中数据变成了旧数据,缓存中的数据和数据库中的数据不一致
线程2更新数据库 v= 20,再删除缓存;
线程1查询缓存未命中,查询数据库 v = 20,并写入缓存
线程1查询的时候,缓存恰好失效,查询缓存未命中,而后查询数据库,被线程2插入,执行更新数据库,删除缓存操作,线程1再写入缓存,这就导致线程1写入缓存的数据为旧数据
方案二更好,方案二发生的情况要求 有线程查询的时候,缓存恰好失效;发生的概率相较于方案一的异常状况更加小
解决方法:(1)缓存空对象:当缓存未命中,数据库也未命中时,缓存null,而后每次查询
(2)布隆过滤:
修改流程,缓存空对象
实现
public Shop queryWithPassThrough(Long id) {
String key=CACHE_SHOP_KEY+id;
String JsonShop = stringRedisTemplate.opsForValue().get(key);
if(StrUtil.isNotBlank(JsonShop)){//null,"","\n"都属于false
Shop shop = JSONUtil.toBean(JsonShop, Shop.class);
return shop;
}
//判断命中的是否为空值
if(JsonShop!=null){
return null;
}
Shop shop = getById(id);
if(shop==null){
//缓存空对象
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
return shop;
}
利用互斥锁与逻辑过期(旧的key仍存在)解决缓存击穿
互斥锁与逻辑过期的优缺点
简单实现互斥锁:利用Redis中的SETNX命令实现互斥锁的效果,释放锁就相当于删除key(一般设置有效期)
获取锁与释放锁
(不直接return flag的原因:防止在拆箱装箱过程中出现空指针,所以使用hutool中的工具类)
private boolean trylock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key){
stringRedisTemplate.delete(key);
}
具体实现
public Shop queryWithMuteX(Long id) {
String key=CACHE_SHOP_KEY+id;
// redis查看缓存
String JsonShop = stringRedisTemplate.opsForValue().get(key);
if(StrUtil.isNotBlank(JsonShop)){//null,"","\n"都属于false
Shop shop = JSONUtil.toBean(JsonShop, Shop.class);
return shop;
}
//判断命中的是否为空值
if(JsonShop!=null){
return null;
}
//实现缓存重建
//1.尝试获取互斥锁
String lockKey="lock:shop"+id;
Shop shop = null;
try {
boolean isLock = trylock(lockKey);
//2.判断是否获取成功
if(!isLock){
//失败,休眠并重试
Thread.sleep(50);
return queryWithMuteX(id);
}
//成功,根据id查询数据库
shop = getById(id);
//模拟重建的延时
Thread.sleep(200);
if(shop==null){
//缓存空对象
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//3.释放互斥锁
unlock(lockKey);
}
return shop;
}
不需对原来的shop进行字段的修改,重新写一个类,使其进行继承或设置
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
声明线程池
链接:java并发编程:Executor、Executors、ExecutorService
//线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
实现:
@Override
public Shop queryWithLogicalExpire(Long id) {
String key=CACHE_SHOP_KEY+id;
// redis查看缓存
String JsonShop = stringRedisTemplate.opsForValue().get(key);
if(StrUtil.isBlank(JsonShop)){
//不存在直接返回null
return null;
}
//命中,需要json反序列化为对象
RedisData redisData = JSONUtil.toBean(JsonShop, RedisData.class);
// Object data = redisData.getData(); //本质是JSONObject
JSONObject data = (JSONObject) redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
//未过期,直接返回店铺信息
return shop;
}
//过期,需要缓存重建
//1.尝试获取互斥锁
String lockKey=LOCK_SHOP_KEY+id;
boolean islock = trylock(lockKey);
//2.判断是否成功
if(islock){
//成功开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//重建缓存
this.saveShop2Redis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//释放锁
unlock(lockKey);
}
});
}
//返回过期的商铺信息
return shop;
}
优化:对上述查询缓存进行封装
实现:(采用泛型与函数式编程)
@Slf4j
@Component
public class CacheClient {
//线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
@Resource
private StringRedisTemplate stringRedisTemplate;
private boolean trylock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unlock(String key){
stringRedisTemplate.delete(key);
}
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
}
public void setWithLogicalExpire(String key,Object value,Long time,TimeUnit unit){
//设置逻辑过期
RedisData redisData=new RedisData();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//写入Redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
public R queryWithPassThrough(
String keyPrefix, ID id, Class type, Function dbFallback, Long time, TimeUnit unit) {
String key=keyPrefix+id;
String Json = stringRedisTemplate.opsForValue().get(key);
if(StrUtil.isNotBlank(Json)){//null,"","\n"都属于false
return JSONUtil.toBean(Json, type);
}
//判断命中的是否为空值
if(Json!=null){
return null;
}
R r = dbFallback.apply(id);
if(r==null){
//缓存空对象
stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
this.set(key,r,time,unit);
return r;
}
public R queryWithLogicalExpire(String keyPrefix,ID id,Class type,Function dbFallback,Long time, TimeUnit unit) {
String key=keyPrefix+id;
// redis查看缓存
String JsonShop = stringRedisTemplate.opsForValue().get(key);
if(StrUtil.isBlank(JsonShop)){
//不存在直接返回null
return null;
}
//命中,需要json反序列化为对象
RedisData redisData = JSONUtil.toBean(JsonShop, RedisData.class);
// Object data = redisData.getData(); //本质是JSONObject
JSONObject data = (JSONObject) redisData.getData();
R r = JSONUtil.toBean(data, type);
LocalDateTime expireTime = redisData.getExpireTime();
//判断是否过期
if(expireTime.isAfter(LocalDateTime.now())){
//未过期,直接返回店铺信息
return r;
}
//过期,需要缓存重建
//1.尝试获取互斥锁
String lockKey=LOCK_SHOP_KEY+id;
boolean islock = trylock(lockKey);
//2.判断是否成功
if(islock){
//成功开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//查询数据库
R r1 = dbFallback.apply(id);
//重建缓存
this.setWithLogicalExpire(key,r1,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//释放锁
unlock(lockKey);
}
});
}
//返回过期的商铺信息
return r;
}
}
与之前的差异
@Override
@Transactional
public Result queryById(Long id) {
//缓存穿透
// Shop shop = queryWithPassThrough(id);
// Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, id2 -> getById(id2), CACHE_SHOP_TTL, TimeUnit.MINUTES);
//互斥锁解决缓存击穿
// Shop shop=queryWithMuteX(id);
//利用逻辑过期解决缓存击穿
// Shop shop=queryWithLogicalExpire(id);
Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
if(shop==null){
return Result.fail("店铺不存在!");
}
return Result.ok(shop);
}