什么是缓存
缓存就是数据交换的缓冲区,是存数据的临时地方,一般读写性能较高
添加Redis缓存
ShopServiceImpl
@Override
public Result queryById(Long id) {
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
Gson gson = new Gson();
if (StrUtil.isNotBlank(shopJson)) {
Shop shop = gson.fromJson(shopJson, Shop.class);
return Result.ok(shop);
}
Shop shop = this.getById(id);
if (shop == null) {
return Result.fail("店铺不存在!");
}
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,gson.toJson(shop));
return Result.ok(shop);
}
- 作业
ShopTypeServiceImpl
@Override
public Result queryTypeListByRedis() {
List<String> list = stringRedisTemplate.opsForList().range(CACHE_SHOP_LIST_KEY, 0, stringRedisTemplate.opsForList().size(CACHE_SHOP_LIST_KEY));
Gson gson = new Gson();
if (list != null && !list.isEmpty()){
List<ShopType> shopTypeList = list.stream().map((json) ->{
return gson.fromJson(json,ShopType.class);
}).collect(Collectors.toList());
return Result.ok(shopTypeList);
}
List<ShopType> shopTypeList = this.query().orderByDesc("sort").list();
List<String> shopJsonList = shopTypeList.stream().map(gson::toJson).collect(Collectors.toList());
stringRedisTemplate.opsForList().leftPushAll(CACHE_SHOP_LIST_KEY,shopJsonList);
return Result.ok(shopTypeList);
}
缓存更新策略
缓存更新策略的最佳实践方案:
1.低一致性需求:使用Redis自带的内存淘汰机制
2.高一致性需求:主动更新,并以超时剔除兜底
- 读操作:
- 缓存命中则直接返回
- 缓存未命中查数据库,并写入缓存,设定超时时间
- 写操作
- 先写数据库,然后再删除缓存
- 确保数据库与缓存操作的原子性
给查询店铺的缓存添加超时剔除和主动更新的策略
- 超时剔除
ShopServiceImpl
@Override
public Result queryById(Long id) {
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
Gson gson = new Gson();
if (StrUtil.isNotBlank(shopJson)) {
Shop shop = gson.fromJson(shopJson, Shop.class);
return Result.ok(shop);
}
Shop shop = this.getById(id);
if (shop == null) {
return Result.fail("店铺不存在!");
}
// 给缓存的数据添加过期时间就完成了超时剔除操作
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,gson.toJson(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
- 主动更新
@Override
// 同步操作需要加上事务注解,要么全部成功,要么全部失败
@Transactional
public Result update(Shop shop) {
// 先判断id的合法性
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空!");
}
// 先更新数据库
updateById(shop);
// 再删除redis的缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+shop.getId());
return Result.ok();
}
缓存穿透
- 是什么?
指客户端请求的数据即不在redis,也不在数据库,这样缓存永远不会生效。这些请求都会被打到数据库。
- 怎么解决?
- 缓存空对象
- 优点:简单粗暴
- 缺点:额外的内存消耗,可能造成短期不一致
- 布隆过滤
- 优点:内存占用少,没有多余的key
- 缺点:存在误判,实现复杂
解决商铺查询缓存穿透问题:
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;
}
缓存雪崩
在同一时刻缓存大量失效或者redis宕机,导致大量请求到达数据库,带来压力
- 解决方案
- 给不同的key设置不同的ttl
- 利用redis集群提高服务的高可用性
- 就缓存业务实现降级限流策略
- 给业务添加多级缓存
缓存击穿
也叫热点key问题,被一个高并发并且缓存重建业务较为复杂的key失效了,无数的请求会在瞬间给数据库带来巨大的冲击。
- 解决方案:
- 互斥锁
- 逻辑过期
基于互斥锁解决缓存击穿问题:
使用redis的setnx操作
// 通过逻辑过期解决缓存击穿
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;
}
// 获取锁
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
// 互斥锁
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
标签:shop,缓存,return,商户,stringRedisTemplate,查询,key,id
From: https://blog.51cto.com/u_16123065/6529676