【黑马点评】商户查询缓存
缓存
缓存:数据交换的缓冲区,存储数据的临时地方,读写性能高。
- 优点:降低后端负载、提高读写效率
- 缺点:缓存往往需要保证数据的一致性,就需要代码去维护缓存的一致性
缓存更新策略
业务场景:
低一致性需求:使用内存淘汰机制
高一致性需求:主动更新,以超时剔除为兜底方案
主动更新
- 自己写代码,在更新数据库的同时更新缓存
- 缓存与数据库整合为一个服务,由服务来维护一致性,调用者调用该服务,无需关心缓存的一致性问题
- 调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致性
缓存穿透解决方案:缓存空对象
/**
* 缓存穿透:缓存空对象
* @param id
* @return
*/
public Shop queryWithPassThrough(Long id) {
// 从redis中查缓存
String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
if(StrUtil.isNotBlank(shopJson)) {
// 存在 - 直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
// 判断命中的是否是空值
Assert.isTrue(shopJson == null , "店铺不存在");
// 不存在 - 操作数据库 - 写入redis
Shop shop = getById(id);
if(shop == null) {
stringRedisTemplate.opsForValue().set(shopKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
throw new RuntimeException("店铺不存在");
}
stringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return shop;
}
缓存击穿解决方案:互斥锁(setnx)
一个线程在请求redis的数据时,先尝试去获取锁;只有获取到锁的线程才能去执行相应的操作,如果获取不到锁,只能阻塞等待。
为了防止服务出现故障时导致锁无法释放,所以一般设置锁的时候会加一个过期时间。
/**
* 获取锁
* @param key
* @return
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); // 10秒锁
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
* @param key
*/
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
/**
* 缓存击穿(互斥锁setnx) + 缓存穿透
* @param id
* @return
*/
public Shop queryWithMutex(Long id) {
// 从redis中查缓存
String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(shopKey);
if(StrUtil.isNotBlank(shopJson)) { // 只有是有字符串的时候才会是true
// 存在 - 直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
// 判断命中的是否是空值
Assert.isTrue(shopJson == null , "店铺不存在");
// 实现缓存重建
Shop shop = null;
// 获取互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
try {
boolean isLock = tryLock(lockKey);
// 获取失败 - 休眠、重试
if(!isLock) {
Thread.sleep(50);
queryWithMutex(id); // 递归、再次调用
}
// 获取成功 - 根据id查询数据库
// 不存在 - 操作数据库 - 写入redis
Thread.sleep(200);
shop = getById(id);
if(shop == null) {
stringRedisTemplate.opsForValue().set(shopKey, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES); // 店铺不存在 - 设置空值
throw new RuntimeException("店铺不存在");
}
stringRedisTemplate.opsForValue().set(shopKey, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey); // 释放锁
}
return shop;
}
缓存击穿解决方案:互斥锁 + 逻辑过期
/**
* 缓存击穿(互斥锁setnx、逻辑过期)
* @param id
* @return
*/
public Shop queryWithLogicalExpire(Long id) {
// 从redis中查缓存
String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
String redisDataJson = stringRedisTemplate.opsForValue().get(shopKey);
/*if(StrUtil.isBlank(redisDataJson)) { // 不存在
return null;
}*/
RedisData<Shop> redisData = JSONUtil.toBean(redisDataJson, new TypeReference<RedisData<Shop>>() {}, false);
// 未过期、直接返回
if(redisData != null && redisData.getExpireTime().isAfter(LocalDateTime.now())) {
return redisData.getData();
}
// 实现缓存重建
Shop shop = redisData.getData();
// 获取互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 获取失败 - 返回旧数据
if(!isLock) {
return shop;
}
// 获取成功 - 开启独立线程 - 实现缓存重建 - 暂时返回旧的店铺信息
CACHE_REBUILD_EXECUTOR.submit(()->{ // 开启独立线程
try {
saveShopToRedis(id, 30L); // 实现缓存重建
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey); // 释放锁
}
});
return shop;
}
这里封装RedisData对象的时候也有个技巧,为了不破坏原本Shop的类型,所以决定使用组合的方式,让Shop称为RedisData的成员:
@Data @Accessors(chain = true) public class RedisData<T> { private LocalDateTime expireTime; private T data; // 这样可以避免对原来的数据做修改 }
缓存工具封装
@Slf4j
@Component
public class CacheClient {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 设置过期时间
* @param key
* @param value
* @param expireTime
* @param timeUnit
* @param <T>
*/
public <T> void set(String key, T value, Long expireTime, TimeUnit timeUnit) {
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), expireTime, timeUnit);
}
/**
* 设置逻辑过期时间
* @param key
* @param data
* @param logicalExpireTime
* @param timeUnit
* @param <T>
*/
public <T> void setWithLogicalExpire(String key, T data, Long logicalExpireTime, TimeUnit timeUnit) {
RedisData<T> redisData = new RedisData<T>().setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(logicalExpireTime))).setData(data);
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}
/**
* 缓存穿透解决方案封装:缓存空对象
* 调用:cacheClient.queryWithPassThrough(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
* @param keyPrefix
* @param id
* @param type 返回的数据类型
* @param dbFallback:Function<ID, R> dbFallback函数式编程——Function<参数, 返回值>
* @param expireTime
* @param timeUnit
* @return
* @param <R>
* @param <ID>
*/
public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long expireTime, TimeUnit timeUnit) {
String key = keyPrefix + id;
// 从redis中查缓存
String json = stringRedisTemplate.opsForValue().get(key);
if(StrUtil.isNotBlank(json)) {
// 存在 - 直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
Assert.isTrue(json == null , "店铺不存在");
// 不存在 - 操作数据库 - 写入redis
R r = dbFallback.apply(id);
if(r == null) {
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
throw new RuntimeException("店铺不存在");
}
set(key, r, expireTime, timeUnit);
return r;
}
/**
* 获取锁
* @param key
* @return
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS); // 10秒锁
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
* @param key
*/
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 缓存击穿(互斥锁setnx、逻辑过期)
* 调用:cacheClient.queryWithLogicalExpire(RedisConstants.CACHE_SHOP_KEY, id, Shop.class, this::getById, RedisConstants.CACHE_SHOP_TTL, TimeUnit.SECONDS);
* @param keyPrefix
* @param id
* @param type
* @param dbFallback
* @param expireTime
* @param timeUnit
* @return
* @param <R>
* @param <ID>
*/
public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long expireTime, TimeUnit timeUnit) {
// 从redis中查缓存
String key = keyPrefix + id;
String redisDataJson = stringRedisTemplate.opsForValue().get(key);
RedisData<R> redisData = JSONUtil.toBean(redisDataJson, new TypeReference<RedisData<R>>() {}, false);
// 未过期、直接返回
R rOld = JSONUtil.toBean((JSONObject) redisData.getData(), type);
if(redisData != null && redisData.getExpireTime().isAfter(LocalDateTime.now())) {
return rOld;
}
// 实现缓存重建
// 获取互斥锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 获取失败 - 返回旧数据
if(!isLock) {
return rOld;
}
// 获取成功 - 开启独立线程 - 实现缓存重建 - 暂时返回旧的店铺信息
CACHE_REBUILD_EXECUTOR.submit(()->{ // 开启独立线程
try {
// 重建缓存 - 查询数据库
R rNew = dbFallback.apply(id);
this.setWithLogicalExpire(key, rNew, expireTime, timeUnit);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(lockKey); // 释放锁
}
});
return rOld;
}
}