添加缓存
查询数据库之前先查询缓存,缓存命中就返回数据,未命中,再查询数据库,并将查询到的数据存入缓存
缓存更新流程
@Transactional
public Result updateShop(Shop shop) {
Long id = shop.getId();
// 判断id合法性
if (id == null) {
return Result.fail("店铺id不能为空");
}
// 1.更新数据库数据
updateById(shop);
// 2.删除缓存数据
stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + shop.getId());
return Result.ok();
}
缓存穿透流程
private Result selectThrough(Long id) {
String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
// 1.先查询缓存
Map<Object, Object> shopMap = stringRedisTemplate.opsForHash().entries(shopKey);
// 2.缓存是否命中
if(MapUtil.isNotEmpty(shopMap) && shopMap.containsKey("id")){
// 3.命中,并且数据不为"",直接返回
Shop shop = BeanUtil.fillBeanWithMap(shopMap, new Shop(), false);
return Result.ok(shop);
}
// 3.1命中,但是数据为""
if(MapUtil.isNotEmpty(shopMap) && !shopMap.containsKey(id)){
// 返回错误信息,
return Result.fail("店铺信息不存在");
}
// 4.缓存未命中,再查询数据库
Shop shop = getById(id);
// 5.数据库是否命中
if (shop == null) {
// 6.未命中,存入缓存""值,设置ttl,返回错误信息
HashMap<Object, Object> map = MapUtil.newHashMap();
map.put("","");
stringRedisTemplate.opsForHash()
.putAll(shopKey, map);
stringRedisTemplate.expire(shopKey,RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺信息不存在");
}
// 6.命中,存入缓存,设置ttl,并返回数据
// 将shop转换为map
Map<String, Object> map = BeanUtil.beanToMap(shop, new HashMap<>(),
new CopyOptions()
.setIgnoreNullValue(true)
.setFieldValueEditor((field, value) ->{
/* setFieldValueEditor优先级要高于ignoreNullValue导致前者首先被触发,
因此出现空指针问题。
所以需要在setFieldValueEditor中手动判空。
*/ if(value != null) {
return value.toString();
}
return null;
})
);
stringRedisTemplate.opsForHash()
.putAll(shopKey, map);
stringRedisTemplate.expire(shopKey,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
缓存雪崩
private Result selectWithMutex(Long id) {
String shopKey = RedisConstants.CACHE_SHOP_KEY + id;
// 1.先查询缓存
Map<Object, Object> shopMap = stringRedisTemplate.opsForHash().entries(shopKey);
// 2.缓存是否命中
if(MapUtil.isNotEmpty(shopMap) && shopMap.containsKey("id")){
// 3.命中,并且数据不为"",直接返回
Shop shop = BeanUtil.fillBeanWithMap(shopMap, new Shop(), false);
return Result.ok(shop);
}
// 3.1命中,但是数据为""
if(MapUtil.isNotEmpty(shopMap) && !shopMap.containsKey(id)){
// 返回错误信息,
return Result.fail("店铺信息不存在");
}
Shop shop = null;
String lockKey = LOCK_SHOP_KEY + id;
try {
// 4.缓存未命中,尝试获取锁
boolean isLock = tryLock(lockKey);
if (!isLock){
// 4.1.没有获取到锁,就休眠一段时间
Thread.sleep(100);
// 4.2再次读取缓存
return selectWithMutex(id);
}
// 5.获取到锁,就从数据库查询数据
shop = getById(id);
// 5.1数据库是否命中
if (shop == null) {
// 6.未命中,存入缓存""值,设置ttl
HashMap<Object, Object> map = MapUtil.newHashMap();
map.put("","");
stringRedisTemplate.opsForHash()
.putAll(shopKey, map);
stringRedisTemplate.expire(shopKey,RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺信息不存在");
}
// 6.命中,存入缓存,设置ttl
// 将shop转换为map
Map<String, Object> map = BeanUtil.beanToMap(shop, new HashMap<>(),
new CopyOptions()
.setIgnoreNullValue(true)
.setFieldValueEditor((field, value) ->{
/* setFieldValueEditor优先级要高于ignoreNullValue导致前者首先被触发,
因此出现空指针问题。
所以需要在setFieldValueEditor中手动判空。
*/ if(value != null) {
return value.toString();
}
return null;
})
);
stringRedisTemplate.opsForHash()
.putAll(shopKey, map);
stringRedisTemplate.expire(shopKey,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 7.释放锁
unLock(lockKey);
}
return Result.ok(shop);
}
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForHash().putIfAbsent(key, "1", "1");
stringRedisTemplate.expire(key,RedisConstants.LOCK_SHOP_TTL,TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unLock(String key){
stringRedisTemplate.delete(key);
}
缓存击穿-互斥锁
缓存击穿-逻辑过期
@Component
@Data
public class CacheService {
private final StringRedisTemplate stringRedisTemplate;
/**
* 任意对象序列化为json
*/
public <R,I> void setStr(String keyPrefix, I id, R r, Long expire, TimeUnit unit){
stringRedisTemplate.opsForValue().set(keyPrefix + id,JSONUtil.toJsonStr(r),expire,unit);
}
// json反序列化
public <R,I> R getStr(String keyPrefix, I id, Class<R> type){
String json = stringRedisTemplate.opsForValue().get(keyPrefix + id);
return JSONUtil.toBean(json, type);
}
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,I> R selectWithThrough(String keyPrefix, I id, Class<R> type,
Function<I,R> dbFallBack,
Long expire,
TimeUnit unit
) {
String key = keyPrefix + id;
// 1.先查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.缓存是否命中
if(StrUtil.isNotBlank(json)){
// 3.命中,并且数据不为"",直接返回
return BeanUtil.toBean(json,type);
}
// 3.1命中,但是数据为""
if(json != null){
// 返回错误信息,
return null;
}
// 4.缓存未命中,再查询数据库
R r = dbFallBack.apply(id);
// 5.数据库是否命中
if (r == null) {
// 6.未命中,存入缓存""值,设置ttl,返回错误信息
stringRedisTemplate.opsForValue().set(key,"",expire,unit);
return null;
}
// 6.命中,存入缓存,设置ttl,并返回数据
// 将shop转换为map
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(r));
return r;
}
private static final String LOCK_SHOP_KEY = "lock:shop:";
// 缓存击穿-互斥锁
public <R,I> R selectWithMutex(
String keyPrefix, I id, Class<R> type,
Function<I,R> dbFallBack,
Long expire,
TimeUnit Unit) {
String key = keyPrefix + id;
// 1.先查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.缓存是否命中
if(StrUtil.isNotBlank(json)){
// 3.命中,并且数据不为"",直接返回
R r = JSONUtil.toBean(json, type);
return r;
}
// 3.1命中,但是数据为""
if(json != null){
// 返回错误信息,
return null;
}
R r = null;
String lockKey = LOCK_SHOP_KEY + id;
try {
// 4.缓存未命中,尝试获取锁
boolean isLock = tryLock(lockKey);
if (!isLock){
// 4.1.没有获取到锁,就休眠一段时间
Thread.sleep(100);
// 4.2再次读取缓存
return selectWithMutex(keyPrefix,id,type,dbFallBack,expire,Unit);
}
// 5.获取到锁,就从数据库查询数据
r = dbFallBack.apply(id);
Thread.sleep(100);
// 5.1数据库是否命中
if (r == null) {
// 6.未命中,存入缓存""值,设置ttl
stringRedisTemplate.opsForValue().set(key,"",expire,Unit);
return null;
}
// 6.命中,存入缓存,设置ttl
// 将shop转换为map
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(r),expire,Unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 7.释放锁
unLock(lockKey);
}
return r;
}
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForHash().putIfAbsent(key, "1", "1");
stringRedisTemplate.expire(key,RedisConstants.LOCK_SHOP_TTL,TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
private void unLock(String key){
stringRedisTemplate.delete(key);
}
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(1);
// 缓存击穿-逻辑过期
public <R,I> R selectWithLogicExpire(
String keyPrefix, I id, Class<R> type,
Function<I,R> dbFallBack,
Long expire,
TimeUnit Unit) {
String key = keyPrefix + id;
// 1.先查询缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.缓存是否命中
if(StrUtil.isBlank(json)){
// 2.1未命中,直接返回null
return null;
}
// 2.2命中
// 3.判断key是否过期
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
if(expireTime.isAfter(LocalDateTime.now())){
// 3.1没有过期直接返回数据
return r;
}
// 3.2过期就尝试获取锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 4.判断是否获取到锁
if (isLock){
// 5.获取到锁,就开启一个新的线程来从数据库查询数据
CACHE_REBUILD_EXECUTOR.submit(() ->{
try {
// 6.查询数据库
R newR = dbFallBack.apply(id);
// 7.重建缓存
setWithLogicalExpire(key,newR,20L,TimeUnit.SECONDS);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 8.释放锁
unLock(lockKey);
}
});
}
// 4.2没有获取到锁,返回过期数据
return r;
}
}
public Result select(Long id) {
//return selectThrough(id);
//return selectWithMutex(id);
// 解决缓存穿透
//Shop shop = cacheService
//.selectWithThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 互斥锁解决缓存击穿
//Shop shop = cacheService
//.selectWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 逻辑过期解决缓存击穿
Shop shop = cacheService
.selectWithLogicExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);
if (shop == null) {
return Result.fail("店铺不存在!");
}
// 7.返回
return Result.ok(shop);
}
总结
在将数据存入缓存的时候,我选择的hash,那么就需要将符合的对象转换为对应的数据才能存入缓存中
几个易错点
// 6.命中,存入缓存,设置ttl,并返回数据
// 将shop转换为map
Map<String, Object> map = BeanUtil.beanToMap(shop, new HashMap<>(),
new CopyOptions()
.setIgnoreNullValue(true)
.setFieldValueEditor((field, value) ->{
/* setFieldValueEditor优先级要高于ignoreNullValue导致前者首先被触发,
因此出现空指针问题。
所以需要在setFieldValueEditor中手动判空。
*/ if(value != null) {
return value.toString();
}
return null;
})
);