前言
缓存就是数据交换的缓冲区,是存贮数据的临时地方,一般读写性能较高,作用:降低后端负载、提高读写效率,降低响应时间。本文主要是记录Redis更新策略、缓存穿透、缓存雪崩、缓存击穿,下面进入正题。
更新策略
原理
缓存更新策略一般分为以下三种
对高一致性需求的场景,主动更新缓存时,采用先操作数据库,再删除缓存
原因:如果每次操作数据库都更新缓存,如这期间,没有查询操作,会导致很多的无效写操作,浪费性能,最优方式就是删除缓存,而不是更新缓存。至于为什么需要先操作数据库,再删除缓存,分析见下图:
先删除缓存,再操作数据库:线程1,删除完缓存后,更新数据库(删除缓存很快,更新数据库教慢),在更新数据库的过程中,线程2进行查询,查询为缓存未命中,查询数据库,此时线程1的更新数据还未完成,线程2查到的数据库数据为旧值,查到旧值后,线程2将数据写如缓存,此时线程1更新完成,导致数据库是新值,缓存是旧值。
先操作数据库,再删除缓存:线程1查询,缓存未命中,查询数据库,此时线程2,更新数据库,再删除缓存,因线程1还未写入缓存,所以此时删除无效,线程1,会把查到的旧值更新到缓存中,数据库是新增,导致数据不一致,但是,写入缓存的时间非常快,更新数据库的时间比较慢,这种情况几乎不会发生。
代码实现
查询
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Result queryShopById(Long id) {
String key = RedisConstants.CACHE_SHOP_KEY + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断值是否存在
if (StrUtil.isNotBlank(shopJson)){
// 3.存在直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// 4.不存在根据id查数据库
Shop byId = getById(id);
// 5.不存在返回错误
if (byId== null){
return Result.fail("店铺不存在!");
}
// 6.存在写入redis
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(byId),RedisConstants.CACHE_SHOP_TTL,TimeUnit.MINUTES);
// 7.返回
return Result.ok(byId);
}
更新
// @Transactional 只适用于单体系统采用将缓存与数据库操作放在一个事务
// 如分布式系统,利用TCC等分布式事务方案
@Override
@Transactional
public Result updateShop(Shop shop) {
Long id = shop.getId();
if (id == null) {
return Result.fail("店铺id不能为空");
}
//更新数据库
updateById(shop);
//删除缓存
stringRedisTemplate.delete(RedisConstants.CACHE_SHOP_KEY + id);
return Result.ok();
}
缓存穿透
代码实现
public Result queryShopById(Long id) {
String key = RedisConstants.CACHE_SHOP_KEY + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断值是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
// ①.《解决缓存穿透》 判断命中是的是否为空值,因2中的isNotBlank判断shopJson为空字符串的时候
//返回的也是false,不会进2中的判断条件,此时shopJson有两种情况,一种是空字符串,一种是null,空字符串
//是为解决缓存击穿设置的,不能放行查数据库,故直接返回报错。
if (shopJson != null) {
return Result.fail("店铺信息不存在!");
}
// 4.不存在根据id查数据库
Shop byId = getById(id);
// 5.不存在返回错误
if (byId == null) {
// ②.《解决缓存穿透》,将空值写入redis,过期时间设置2分钟
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在!");
}
// 6.存在写入redis,过期时间设置30分钟
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(byId), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7.返回
return Result.ok(byId);
}
缓存雪崩
缓存击穿
**互斥锁方式:**适应于数据一致性要求较高的场景,通过加锁的方式,来解决热点key失效,大量请求都查询数据库,重建设缓存造成数据库压力。
**逻辑过期方式:**适应于性能要求较高的场景,通过逻辑过期,但实际redis中一直都有数据,永久存在,每次通过逻辑过期时间来判断重建缓存。
代码实现(互斥锁)
/**
* 获取锁
* @param key
* @return
*/
private boolean trylock(String key) {
//setIfAbsent方法和命令行的setnx命令是一样的,并发执行setIfAbsent,只有第一个线程能成功,其余线程都会失败
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
// 此处使用hutool包的工具,防止直接返回flag,造成拆箱时空指针问题。
return BooleanUtil.isTrue(flag);
}
/**
* 释放锁
* @param key
* @return
*/
private void unlock(String key) {
stringRedisTemplate.delete(key);
}
/**
* 击穿(互斥锁实现方式)
* @param id
* @return
*/
public Shop queryWithMutex(Long id) {
String key = RedisConstants.CACHE_SHOP_KEY + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断值是否存在
if (StrUtil.isNotBlank(shopJson)) {
// 3.存在直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
// ①.《解决缓存穿透》 判断命中是的是否为空值,因2中的isNotBlank判断shopJson为空字符串的时候
//返回的也是false,不会进2中的判断条件,此时shopJson有两种情况,一种是空字符串,一种是null,空字符串
//是为解决缓存击穿设置的,不能放行查数据库,故直接返回报错。
if (shopJson != null) {
return null;
}
// 4.《解决缓存击穿》实现缓存重建
// 4.1 《解决缓存击穿》获取互斥锁
String lockkey = RedisConstants.LOCK_SHOP_KEY + id;
Shop byId = null;
try {
boolean islock = trylock(lockkey);
// 4.2《解决缓存击穿》判断是否获取成功
if (!islock) {
// 4.3《解决缓存击穿》失败,则休眠重试
Thread.sleep(50);
return queryWithMutex(id);
}
// 4.4 成功,不存在根据id查数据库
byId = getById(id);
// 5.不存在返回错误
if (byId == null) {
// ②.《解决缓存穿透》,将空值写入redis,过期时间设置2分钟
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 6.存在写入redis,过期时间设置30分钟
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(byId), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 7.《解决缓存击穿》释放互斥锁
unlock(lockkey);
}
// 8.返回
return byId;
}
@Override
public Result queryShopById(Long id) {
Shop shop = queryWithMutex(id);
if (shop == null) {
Result.fail("店铺信息不存在!");
}
return Result.ok(shop);
}
代码实现(逻辑过期)
定义通用对象,增加逻辑过期时间
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class RedisData {
private LocalDateTime expireTime;
private Object data;
}
大致流程图
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* 重建缓存,存储逻辑过期时间的方法
* @param id
* @param expireSeconds
*/
private void saveShopToRedis(Long id, Long expireSeconds) {
// 1.查询数据库数据
Shop shop = getById(id);
// 2.封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
// 3.获取当前时间并且增加expireSeconds
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 4.写入redis
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}
/**
* 创建线程池
*/
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
/**
* 击穿(逻辑过期实现方式)
* @param id
* @return
*/
public Shop queryWithLogiCalExpire(Long id) {
String key = RedisConstants.CACHE_SHOP_KEY + id;
// 1.从redis查询商铺缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断值是否存在
if (StrUtil.isBlank(shopJson)) {
//3.为空直接返回null
return null;
}
// 4.命中把json反序列化成对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 5.判断是否过期,过期时间是否在当前时间之后
if (expireTime.isAfter(LocalDateTime.now())) {
// 5.1 未过期返回店铺信息
return shop;
}
// 5.2 过期,需要重建缓存
// 6.1获取锁
String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
// 此处获取锁方式和互斥锁实现方式一致
boolean isLock = trylock(lockKey);
// 6.2 判断获取锁是否成功
if (isLock) {
// 7.开启一个独立的线程来执行重建
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// 7.1 重建缓存
this.saveShopToRedis(id, 30L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
//7.2 释放锁, 此处释放锁方式和互斥锁实现方式一致
unlock(lockKey);
}
});
}
//先返回旧的数据
return shop;
}
@Override
public Result queryShopById(Long id) {
// 互斥锁实现方式
//Shop shop = queryWithMutex(id);
// 逻辑过期实现方式
Shop shop = queryWithLogiCalExpire(id);
if (shop == null) {
Result.fail("店铺信息不存在!");
}
return Result.ok(shop);
}