在今天的课程设计的都是缓存相关的知识,想要加深理解,记忆八股可以查看我的另一篇博客,详细记载着黑马的java八股-redis篇黑马java八股篇之Redis篇-优快云博客
给店铺类型查询业务添加缓存
一开始店铺查询是需要使用mp的查询数据库实现的,由于这个列表基本上不会更改,所以很适合使用缓存
在Redis中使用String方法存储缓存以及查询
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryTypeLits() {
//查询商铺
String shopTypeList = stringRedisTemplate.opsForValue().get(CACHE_SHOP_TYPE);
//获取商铺列表
if(StringUtils.isNotBlank(shopTypeList)){
List<ShopType> list = JSONUtil.toList(shopTypeList, ShopType.class);
return Result.ok(list);
}
//如果是空的直接查找数据库
List<ShopType> shopTList = query().orderByAsc("sort").list();
//如果数据不存在需要返回错误结果
if(shopTList==null){
return Result.fail("商家列表不存在!");
}
stringRedisTemplate.opsForValue().set(CACHE_SHOP_TYPE, JSONUtil.toJsonStr(shopTList));
return Result.ok(shopTList);
}
使用List实现如下:具体需要使用ArrayList将所有查询到的一个一个json转换为对象
@Slf4j
@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryShopType() {
// 1.从 Redis 中查询商铺缓存
List<String> shopTypeJsonList = stringRedisTemplate.opsForList().range(CACHE_SHOP_TYPE_LIST_KEY, 0, -1);
// 2.判断 Redis 中是否有该缓存
if (shopTypeJsonList != null && !shopTypeJsonList.isEmpty()) {
// 2.1.若 Redis 中存在该缓存,则直接返回
ArrayList<ShopType> typeList = new ArrayList<>();
for (String str : shopTypeJsonList) {
typeList.add(JSONUtil.toBean(str, ShopType.class));
}
//也可以用下面的stream流的方式,其实大差不差,都是遍历,每个数据都转换类型后再操作
// List typeList = shopTypeJsonList.stream().map((shopTypeJson)->{
// ShopType shopType = JSONUtil.toBean(shopTypeJson, ShopType.class);
// return shopType;
// }).collect(Collectors.toList());
return Result.ok(typeList);
}
// 2.2.Redis 中若不存在该数据,则从数据库中查询
List<ShopType> typeList = query().orderByAsc("sort").list();
// 3.判断数据库中是否存在
if (typeList == null || typeList.isEmpty()) {
// 3.1.数据库中也不存在,则返回 false
return Result.fail("分类不存在!");
}
// 3.2数据库中存在,则将查询到的信息存入 Redis
for (ShopType shopType : typeList) {
stringRedisTemplate.opsForList().rightPushAll(CACHE_SHOP_TYPE_LIST_KEY, JSONUtil.toJsonStr(shopType));
}
//下面是stream流的方式
// List shopTypeJson = typeList.stream().map((shopType)-> {
// String jsonStr = JSONUtil.toJsonStr(shopType);
// return jsonStr;
// }).collect(Collectors.toList());
// stringRedisTemplate.opsForList().rightPushAll(CACHE_SHOP_TYPE_LIST_KEY,shopTypeJson);
// 3.3返回
return Result.ok(typeList);
}
}
如果是使用Zset的话唯一的区别就是把opsForList变成opsForZSet。
缓存更新策略
而主动更新中又区分有三个
操作缓存和数据库时有三个问题需要考虑
删除缓存还是更新缓存?
- 更新缓存:每次更新数据库都更新缓存,无效写操作较多(发生可能更大,不采用)
- 删除缓存:更新数据库时让缓存失效,查询时再更新缓存(发生的概率更小)
如何保证缓存与数据库的操作的同时成功或失败?
- 单体系统,将缓存与数据库操作放在一个事务
- 分布式系统,利用TCC等分布式事务方案
先操作缓存还是先操作数据库?
先操作数据库,再删除缓存
先删除缓存,再操作数据库
给查询的店铺的缓存添加超时剔除和主动更新的策略
新增的功能是设置超时时间以满足超时剔除的要求
修改店铺是先修改数据库再删除缓存.
首先就是修改查询逻辑,在存储缓存的同时还要让缓存设置超时时间,用超时剔除保底
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
//有先写入在返回
return Result.ok(shop);
随后就是在更新的时候,采取先更新数据库,再删除缓存(这样发生不一致问题的几率更加小(要先查询看看店铺id是否存在,否则会发生空指针问题)
@Override
public Result update(Shop shop) {
if(shop.getId()==null){
return Result.fail("店铺id不存在");
}
//首先就是直接1更新咯,随后就是删除咯
updateById(shop);
//删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY + shop.getId());
return Result.ok(shop);
}
在查询一次之后再查询发现并没有查询数据库,而是缓存直接返回
缓存穿透!
缓存穿透理论知识
这里可以看我博客中的详细黑马redis八股汇总 ,更详细更易理解
缓存穿透实际解决
穿透的风险在哪?-》在缓存未命中中再查询数据库发现数据库也查不到。
-》改成数据库不存在的话将空值写入redis
-》而且再命中之后还要判断缓存中存储的值是不是空值(不是则返回,是则直接结束)
这里采取的措施是使用存储null值
@Override
public Result queryById(Long id) {
//通过缓存查询
String shopString = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
if(StringUtils.isNotBlank(shopString)){
//如果有直接返回
Shop shop = JSONUtil.toBean(shopString, Shop.class);
//如果是空值(null)的话,isNotBlank不会放行
return Result.ok(shop);
}
if(shopString!=null){
return Result.fail("店铺不存在");
}
//首先查询,如果没有就查询数据库
Shop shop=getById(id);
//查询数据库
//没有返回false
if(shop==null){
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("不存在该商店");
}
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
//有先写入在返回
return Result.ok(shop);
}
前两种是被动措施,后面的是主动的措施
封装工具类,其实实际上就是只需要根据shop的代码改造即可,首先就是
- 需要传入查询数据库的回调函数,因为不知道要查询的是哪个类的数据库
- 需要传入id类型,不知道id是不是Long类型还是其他类型,以及要返回的类的类型
防止缓存穿透的查询函数
public <R,ID> R queryWithPassThrough(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(json)) {
// 3.存在,直接返回
return JSONUtil.toBean(json, type);
}
// 判断命中的是否是空值
if (json != null) {
// 返回一个错误信息
return null;
}
// 4.不存在,根据id查询数据库
R r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
return r;
}
防止缓存击穿-》逻辑过期查询函数
public <R, ID> R queryWithLogicalExpire(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String json = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isBlank(json)) {
// 3.存在,直接返回
return null;
}
// 4.命中,需要先把json反序列化为对象
RedisData redisData = JSONUtil.toBean(json, RedisData.class);
R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期
if(expireTime.isAfter(LocalDateTime.now())) {
// 5.1.未过期,直接返回店铺信息
return r;
}
// 5.2.已过期,需要缓存重建
// 6.缓存重建
// 6.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2.判断是否获取锁成功
if (isLock){
// 6.3.成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 查询数据库
R newR = dbFallback.apply(id);
// 重建缓存
this.setWithLogicalExpire(key, newR, time, unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
// 释放锁
unlock(lockKey);
}
});
}
// 6.4.返回过期的商铺信息
return r;
}
防止缓存击穿-》互斥锁查询函数
public <R, ID> R queryWithMutex(
String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
String key = keyPrefix + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在,直接返回
return JSONUtil.toBean(shopJson, type);
}
// 判断命中的是否是空值
if (shopJson != null) {
// 返回一个错误信息
return null;
}
// 4.实现缓存重建
// 4.1.获取互斥锁
String lockKey = LOCK_SHOP_KEY + id;
R r = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2.判断是否获取成功
if (!isLock) {
// 4.3.获取锁失败,休眠并重试
Thread.sleep(50);
return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
}
// 4.4.获取锁成功,根据id查询数据库
r = dbFallback.apply(id);
// 5.不存在,返回错误
if (r == null) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.set(key, r, time, unit);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 7.释放锁
unlock(lockKey);
}
// 8.返回
return r;
}