什么是缓存
缓存就是数据交换的缓冲区(称作Cache),是存贮数据的临时地方,一般读写性能较高。
缓存的作用:
缓存的成本:
添加Redis缓存
商铺查询缓存代码实现
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Object queryById(Long id) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
//4.不存在,返回id查询数据库
Shop shop = getById(id);
//5.不存在,返回错误
if (shop==null){
return Result.fail("店铺不存在! ");
}
//6.存在,写入redis
String jsonShop = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,jsonShop);
//7.返回
return Result.ok(shop);
}
店铺类型查询添加缓存:
@Override
public List<ShopType> getTypeList() {
//先查询redis
String shopType = stringRedisTemplate.opsForValue().get(CACHE_TYPE_KEY);
//如果有数据就返回
if (shopType!=null){
return JSONUtil.toList(shopType,ShopType.class);
}
//如果没有数据就查询数据库
List<ShopType> shopTypes = this.list();
String toJsonStr = JSONUtil.toJsonStr(shopTypes);
//将查询的数据写入到redis中
stringRedisTemplate.opsForValue().set(CACHE_TYPE_KEY,toJsonStr);
return shopTypes;
}
缓存更新策略
业务场景:
主动更新策略:
第一种方案需要考虑的问题:
线程1删除缓存之后要更新数据库,同时线程2查询缓存未命中,然后查询数据库,写入缓存。
在这个过程中由于更新数据过程慢而查询的速度快,线程2查出的缓存依然是旧缓存。
从而造成数据库与缓存数据不一致问题。
上图发生线程安全问题的条件:
当一个线程更新数据库时删除缓存时,另一个线程进行查询操作(发生概率大)
线程1查询缓存未命中,查询数据库之后要写入缓存时,有一个线程2插入进来更新数据库,由于缓存本来就没有所以删除缓存也没有。当线程2执行完之后,线程1将查询的旧数据写入缓存中(小概率)
上图发生线程安全问题的条件:
2个线程并行执行。
缓存刚好失效。
并且线程2要在线程1写缓存这个微秒中完成。
综上:
缓存更新策略的最佳实践方案:
给查询商铺的缓存添加超时剔除和主动更新的策略
修改ShopController中的业务逻辑,满足下面的需求:
实现商铺缓存与数据库的双写一致性问题(即在更新数据库时,使缓存更新)
代码实现
@Transactional
public Result update(Shop shop) {
Long id =shop.getId();
if (id ==null){
return Result.fail("修改失败");
}
//1.更新数据库
this.updateById(shop);
//2.删除缓存
stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
return Result.ok("修改成功");
}
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
常见的解决方案有两种:
缓存空对象
布隆过滤器
优点:内存占用较小,没有多余key
缺点:
解决商铺查询的缓存穿透问题:
代码改变ShopServiceImpl
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
//判断命中的是否是空值
if (shopJson!=null){
return Result.fail("店铺不存在! ");
}
//4.不存在,返回id查询数据库
Shop shop = getById(id);
//5.不存在,返回错误
if (shop==null){
//解决缓存穿透,将空值写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("店铺不存在! ");
}
//6.存在,写入redis
String jsonShop = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,jsonShop,CACHE_SHOP_TTL, TimeUnit.MINUTES);
//7.返回
return Result.ok(shop);
总结:
缓存穿透产生的原因是什么?
缓存穿透的解决方案有哪些?
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务比较复杂的key突然失效,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
基于互斥锁解决缓存击穿问题
需求:修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题
代码实现
//缓存击穿互斥锁实现
public Shop queryWithMutex(Long id) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
return JSONUtil.toBean(shopJson, Shop.class);
}
//判断命中的是否是空值
if (shopJson != null) {
return null;
}
//4.实现缓存重建
//4.1获取互斥锁
Shop shop;
try {
boolean tryLocal = tryLocal(LOCK_SHOP_KEY + id);
//4.2判断是否获取成功
if (!tryLocal) {
//4.3失败,则休眠并重试
Thread.sleep(50);
return queryWithMutex(id);
}
//注意:获取锁成功应该再次检测redis缓存是否存在,做两次检查,如果存在则无需重建缓存
String shopJson2 = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson2)) {
//3.存在,直接返回
return JSONUtil.toBean(shopJson2, Shop.class);
}
//4.4成功,根据id查询数据库
shop = getById(id);
/* //模拟重建的延迟
Thread.sleep(200);*/
//5.不存在,返回错误
if (shop == null) {
return null;
}
//6.存在,写入redis
String jsonShop = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonShop, CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//7.释放互斥锁
unLock(LOCK_SHOP_KEY + id);
}
//8.返回
return shop;
}
基于逻辑过期方式解决缓存击穿问题
需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题
代码实现
//缓存击穿的逻辑过期代码实现
public Shop queryWithLogicalExpire(Long id) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isBlank(shopJson)) {
//3.不存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData<Shop> redisData = JSON.parseObject(shopJson,new TypeReference<RedisData<Shop>>(){});
Shop shop=redisData.getData();
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1未过期,直接返回店铺信息
return shop;
}
//5.2已过期,需要缓存重建
//6.缓存重建
//6.1判断是否获取锁成功
if (tryLocal(LOCK_SHOP_KEY+id)) {
//6.2成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//重建缓存
this.saveShopToRedis(id,20L);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unLock(LOCK_SHOP_KEY+id);
}
});
return shop;
}
//6.3返回过期的商铺信息
return null;
}
public void saveShopToRedis(Long id,Long expireSeconds) throws InterruptedException {
//1.查询店铺数据
Shop shop=getById(id);
Thread.sleep(200);
//2.封装逻辑过期时间
RedisData<Shop> redisData = new RedisData<>();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//3.写入Redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}
缓存工具封装:
基于StringRedisTemplate封装一个缓存工具类,满足下列需求:
方法1:将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
方法2:将任意Java对象序列化为json并存储在string类型的key中,并且key设置逻辑过期时间,用于处理缓存击穿问题
方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题
方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题
代码实现
@Slf4j
@Component
public class CacheClient {
private StringRedisTemplate stringRedisTemplate;
public CacheClient(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间
* @param key
* @param value
* @param time
* @param unit
*/
public void set(String key, Object value, Long time, TimeUnit unit){
stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(value),time,unit);
}
/**
*将任意Java对象序列化为json并存储在string类型的key中,并且key设置逻辑过期时间,用于处理缓存击穿问题
* @param key
* @param value
* @param time
* @param unit
*/
public void setWithLogicalExpire(String key,Object value,Long time,TimeUnit unit){
//设置逻辑过期
RedisData<Object> redisData =new RedisData<>();
redisData.setData(value);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
//写入redis
stringRedisTemplate.opsForValue().set(key,JSON.toJSONString(redisData));
}
/**
*
* @param keyPrefix key的前缀
* @param id
* @param type 具体的类型
* @param 泛型 返回值
* @param 泛型 id
* @return
*/
//实现缓存穿透的工具
public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type,
Function<ID,R> dbFallback,Long time,TimeUnit unit) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(keyPrefix + id);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
return JSON.parseObject(shopJson, type);
}
//判断命中的是否是空值
if (shopJson != null) {
return null;
}
//4.不存在,返回id查询数据库
R shop = dbFallback.apply(id);
//5.不存在,返回错误
if (shop == null) {
//解决缓存穿透,将空值写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6.存在,写入redis
set(CACHE_SHOP_KEY + id,shop,time,unit);
//7.返回
return shop;
}
//定义一把锁
private boolean tryLocal(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "Lock", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放锁
private void unLock(String key) {
Boolean flag = stringRedisTemplate.delete(key);
}
//自定义线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
//实现缓存击穿的逻辑过期工具
public <R,ID> R queryWithLogicalExpire(String keyPrefix,ID id,Class<R> type,
Function<ID,R> dbFallBack,Long time,TimeUnit unit) {
//1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isBlank(shopJson)) {
//3.不存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData<R> redisData = JSON.parseObject(shopJson,new TypeReference<RedisData<R>>(){});
R shop= redisData.getData();
LocalDateTime expireTime = redisData.getExpireTime();
//5.判断是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.1未过期,直接返回店铺信息
return shop;
}
//5.2已过期,需要缓存重建
//6.缓存重建
//6.1判断是否获取锁成功
if (tryLocal(LOCK_SHOP_KEY+id)) {
//1.从redis查询商铺缓存
String shopJson2 = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
//2.判断是否存在
if (StrUtil.isBlank(shopJson2)) {
//3.不存在,直接返回
return null;
}
//4.命中,需要先把json反序列化为对象
RedisData<R> redisData2 = JSON.parseObject(shopJson,new TypeReference<RedisData<R>>(){});
R shop2= redisData.getData();
LocalDateTime expireTime2 = redisData2.getExpireTime();
//5.判断是否过期
if (expireTime2.isAfter(LocalDateTime.now())){
//5.1未过期,直接返回店铺信息
return shop2;
}
//6.2成功,开启独立线程,实现缓存重建
CACHE_REBUILD_EXECUTOR.submit(()->{
try {
//重建缓存
R r1=dbFallBack.apply(id);
Thread.sleep(200);
//写入redis
setWithLogicalExpire(keyPrefix+id,r1,time,unit);
} catch (Exception e) {
throw new RuntimeException(e);
}finally {
//释放锁
unLock(LOCK_SHOP_KEY+id);
}
});
}
//6.3返回过期的商铺信息
return shop;
}
}