查询商铺
@Service
public class ShopServiceImpl
extends ServiceImpl implements IShopService {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryById(Long id) {
String key = RedisConstants.CACHE_SHOP_KEY + id;//查商铺用的key
//1、从redis中查询商铺
String shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)) {
//2、 存在 返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//3、不存在 从mysql查询商铺
Shop shop = getById(id);
//4、不存在 报错
if(shop==null){
return Result.fail("店铺不存在");
}
//5、 mysql中存在 在redis中写数据并返回
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
return Result.ok(shop);
}
}
二、缓存更新策略
(1)主要有以下三种
内存淘汰 | 超时剔除 | 主动更新 | |
说明 | 利用Redis的内存淘汰机制,内存不足自动淘汰部分数据,下次查询时更新缓存 | 给缓存数据添加TTL时间,到期自动删除缓存,下次查询更新缓存 | 编写业务逻辑,在修改数据库的同时,更新缓存 |
一致性 | 差 | 一般 | 好 |
维护成本 | 无 | 低 | 高 |
(2)业务场景 :
低一致性需求:内存淘汰。如店铺类型的查询缓存。
高一致性需求:主动更新,并以超时剔除作为兜底。如店铺详情查询缓存
(3)主动更新的三种模式
Cache Aside Pattern:缓存调用者在更新数据库的同时更新缓存(较多使用)
Read/Write Through Pattern:缓存和数据库整合成一个服务,由服务来维护一致性。调用者无需关心一致性问题。
Write Behind Caching Pattern:调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致。
(4)Cache Aside的一些策略
●删除旧缓存而不是更新缓存
●保证数据库和缓存同时成功或失败的方法:对于单体架构,将二者置于同一事物下;对于分布式架构,利用TCC等分布式事务方案
●操作数据库和删除缓存的先后
两种策略的对比。由于redis的读写速度相较于数据库的读写速度是更快的,所以第二种异常发生的概率比第一种更低。因此第二种方案是最佳选择:先操作数据库,再删缓存。
三、修改店铺——缓存更新策略实现
基于二中的CacheAside策略,对ShopService进行改造同时编写修改店铺业务:
(1)读:根据id查店铺时,如果缓存未命中,则查询数据库,查到结果写入缓存,并设置超时剔除
(2)写:根据id修改店铺时,先修改数据库,再删除缓存
查询店铺
//5、 若mysql中存在 在redis中写数据(同时设置超时剔除)并返回
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
修改店铺
//修改店铺
@Override
@Transactional
public Result update(Shop shop) {
//根据id修改数据库
updateById(shop);
//根据token删除缓存
Long id = shop.getId();
if(id==null){
return Result.fail("店铺id不能为空");
}
stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
return Result.ok();
}
四、缓存穿透
基于缓存null值的策略对店铺查询的代码进行改造
//查店铺
@Override
public Result queryById(Long id) {
String key = RedisConstants.CACHE_SHOP_KEY + id;//查商铺用的key
//1、从redis中查询商铺
String shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)) {
//2、 存在 返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}//仅当不为空字符串 不为null时返回true
//判断是否命中空值
if(shopJson!=null){
return Result.fail("店铺信息不存在");
}
//3、不存在 从mysql查询商铺
Shop shop = getById(id);
//4、不存在 报错
if(shop==null){
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
}
//5、 mysql中存在 在redis中写数据并返回
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
除此以外,解决缓存穿透的方法还有:
增强id的复杂度、做好数据的基础格式校验、加强用户权限校验、做好热点参数限流等。
五、缓存雪崩
(1)缓存雪崩指的是在同一时段大量的缓存key同时失效或redis服务宕机,导致大量请求到达数据库,产生巨大压力。
(2)解决方案:
●给不同key添加随机的TTL
●利用redis集群提高服务的可用性
●给缓存业务添加降级限流策略
●给业务添加多级缓存(nginx、redis、jvm、本地等)
六、缓存击穿
(1)也称作热点key问题,指一个被高并发访问的并且缓存重建业务较复杂的key突然失效,无数的请求访问会在瞬间给数据库带来冲击。
(2)解决方案:
●互斥锁
●逻辑过期
解决方案 |
优点 | 缺点 |
互斥锁 | ●没有额外内存消耗 ●一致性 ●实现简单 |
●线程需要等待,性能低 ●死锁风险 |
逻辑过期 | ●线程无需等待,性能较好 | ●不保证一致性 ●额外内存消耗 ●实现复杂 |
(3)基于互斥锁解决缓存击穿的查询商铺业务
互斥锁的实现:可以利用redis 的SETNX创建锁。它的特点是,只有当一个key不存在时才去创建。那么SETNX操作就是获取锁的操作,当第一个线程set了一个锁之后,其他线程将无法继续set,直到这个线程del(delete)这个锁,即释放锁。
//查店铺
@Override
public Result queryById(Long id) {
//解决缓存穿透
//queryWithPassThrough(id);
//解决缓存击穿
Shop shop = queryWithMutex(id);
if(shop!=null){
return Result.ok(shop);
}
return Result.fail("店铺信息不存在");
}
//解决缓存击穿的代码(互斥锁)
private Shop queryWithMutex(Long id){
String key = RedisConstants.CACHE_SHOP_KEY + id;//查商铺用的key
//1、从redis中查询商铺
String shopJson = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson)) {
//2、 存在 返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}//仅当不为空字符串 不为null时返回true
//判断是否命中空值
if(shopJson!=null){
/*return Result.fail("店铺信息不存在");*/
return null;
}
//3、不存在 获取互斥锁 拿到再到数据库查询
String lockKey="lock:shop:"+id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);//true 拿到锁
//判断是否获取锁
if(!isLock){
//否 休眠 从redis查缓存(递归)
Thread.sleep(50);
queryWithMutex(id);
}
//再次查redis 存在则无需重建缓存
String shopJson1 = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isNotBlank(shopJson1)) {
return JSONUtil.toBean(shopJson1, Shop.class);
}
//是 根据id查数据库 重建缓存 模拟重建延时 释放锁
shop = getById(id);
Thread.sleep(200);
//4、不存在 报错
if(shop==null){
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
}
//5、 mysql中存在 在redis中写数据并返回
stringRedisTemplate.opsForValue()
.set(key,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
unlock(lockKey);
}
return shop;
}
重启服务,删除redis中已存在的商铺缓存,使用jmeter工具来模拟高并发场景,测试代码
模拟5秒发出1000次请求,查看后台,只有一次请求到达了数据库、在redis中亦有商铺缓存,代码改造成功。