Redis缓存穿透,雪崩,击穿
什么是缓存
缓存就是数据交换的缓冲区(Cache),是存储数据的临时地方,读写性能较高
缓存的优点:
1.降低数据库的负载(优点片面,这里以传统来解释)
2.提高读写效率,降低响应时间
缓存的成本:(也不能说缺点,成本比较合适)
1.需要解决数据一致性问题
2.代码的维护成本提高
查询时候的缓存模型
例如原先查询商品信息,通过Mybatis-plus直接从数据库查询,那么现在需要改写这个逻辑
开始改写逻辑
这里我们使用RedisTemplate<String,Object> 需要配置一下 如果使用的是StringRedisTemplate则不需要配置 注意如果使用RedisTemplate不加泛型(默认就是Object) 或者RedisTemplate<Object,Object> 也可以不配置,只不过key value看不懂
配置RedisTemplate<String,Object>的序列化方式
/**
* @author hrui
* @date 2025/1/28 18:41
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 创建 RedisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置序列化工具
Jackson2JsonRedisSerializer<Object> j2jrs = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 解决jackson无法反序列化LocalDateTime的问题
om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
om.registerModule(new JavaTimeModule());
om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
j2jrs.setObjectMapper(om);
// key 和 hashKey 采用 string 序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// value 和 hashValue 采用 JSON 序列化
redisTemplate.setValueSerializer(j2jrs);
redisTemplate.setHashValueSerializer(j2jrs);
return redisTemplate;
}
}
为商店添加缓存
第一次查询,缓存里没有,会查数据库,后面都是查缓存
解决时间格式问题问题
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
记得先删除Redis中原先的缓存
缓存更新策略
下面介绍下主动更新策略的三种模式
根据缓存更新策略修改
那么我们在修改和和删除的时候
缓存穿透及解决思路
什么是缓存穿透:
指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会请求数据库
布隆过滤器的简单理解:内部是个byte数组 会提前将数据库中的例如(id)存起来,通过某种算法实现,当布隆过滤器中说存在的时候,有可能不存在(并非100%准确),但是当布隆过滤器说不存在的时候,那么就真实的不存在 ---->也就是说有一定的穿透风险
修改原先的缓存逻辑
缓存雪崩及解决方案
什么是缓存雪崩:
是指在同一时间段内大量的key同时失效或者Redis服务宕机,导致大量请求到达数据库,给数据库带来巨大压力.
最简单常用的:错开TTL过期时间
耍酷的可以用些其他手段
缓存击穿及解决方案
什么是缓存击穿:
就是一个被高并发访问(并且缓存重建业务较为复杂)的key突然失效(到期)了,无数请求瞬间给数据库带来了巨大冲击
所谓的缓存重建业务比较复杂:例如需要多表查询,运算 可能建立起这个缓存业务需要几秒甚至更久
互斥锁和逻辑过期图解:都是为了解决缓存过期之后的高并发问题
互斥锁的不足是当缓存重建过程非常耗时的情况下,等待时间交久(当然有人会说,为什么不直接访问数据库???首先为什么需要缓存,使用缓存往往针对并发量非常高的场景的一种解决方案,当然如果你只是为了解决复杂业务查询而使用缓存,单纯为了加快响应速度,那么完全可以不需要等待,直接查询数据库)
优缺点
所谓出现死锁现象例如一个业务中有多个缓存查询需求,而另外一个业务里也有相应的缓存查询需求,你得到一把锁,后续缓存中其他缓存业务的锁在其他业务中,互相等待(你等着我解锁,我等着你的锁解决,好了,都等着吧,谁也解不了了),出现死锁现象(死锁的出现是有条件的,一般不会)
1.基于互斥锁解决缓存击穿问题
自定义互斥锁:可以参考Redis中的一种方式
setnx name hahaha 这条命令的意思是:当key为name不存在的时候,才会在redis中设置值,如果为name的key已经存在,则不会执行
也就是说只有第一个线程可以去执行这个操作,加锁就是设置这个key,释放锁,就是删除这个key
那么就可以利用这种机制,来自定义一个互斥锁(这也是分布式锁的一个基本原理,当然真正的分布式锁比这个复杂)
但是这里要考虑一点,例如设置这把锁之后,中间程序出了问题,没有来得及删除这把锁,那么会导致这把锁永远存在,那么我们可以考虑设置过期时间(根据具体业务复杂度设置过期时间,一般业务1秒内肯定就可以完成,那么我们可以考虑设置过期时间5秒这样,看具体的),另外保险点,在finally中删除锁
代码
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Override
public Result queryById(Long id) {
//缓存穿透解决方案
//Shop shop = queryWithPassThrough(id);
//使用互斥锁 解决缓存击穿
Shop shop = queryWithMutex(id);
if (shop == null){
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
//缓存击穿解决方案
public Shop queryWithMutex(Long id) {
String cacheKey = "cache:shop:" + id;
// 1. 从缓存中查询
Object cacheData = redisTemplate.opsForValue().get(cacheKey);
// 2. 判断缓存是否存在
if (cacheData != null) {
if ("null".equals(cacheData)) {
return null; // 缓存中存的是空值
}
return (Shop) cacheData; // 直接返回缓存中的 Shop 对象
}
Shop shop=null;
try {
//这里进行缓存击穿后的逻辑(重建缓存逻辑) 步骤是 拿锁 拿锁成功查询数据库 释放锁 拿锁不成功说明已经有线程在重建缓存 等待(并重新调用)
boolean b = tryLock("lock" + id);
if(!b){
//休眠
Thread.sleep(50);
//递归 重试
queryWithMutex(id);
}
//如果拿锁成功就会往下走
// 3. 不存在则查询数据库
shop = getById(id);
// 4. 如果数据库中也没有这个数据,防止缓存穿透,存入 "null" 并设置短期过期时间
if (shop == null) {
redisTemplate.opsForValue().set(cacheKey, "null", 10L, TimeUnit.MINUTES);
return null;
}
// 5. 存在则写入缓存,设置正常过期时间
redisTemplate.opsForValue().set(cacheKey, shop, 30L, TimeUnit.MINUTES);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
unLock("lock" + id);
}
//释放锁
return shop;
}
//缓存穿透解决方案
public Shop queryWithPassThrough(Long id) {
String cacheKey = "cache:shop:" + id;
// 1. 从缓存中查询
Object cacheData = redisTemplate.opsForValue().get(cacheKey);
// 2. 判断缓存是否存在
if (cacheData != null) {
if ("null".equals(cacheData)) {
return null; // 缓存中存的是空值
}
return (Shop) cacheData; // 直接返回缓存中的 Shop 对象
}
// 3. 不存在则查询数据库
Shop shop = getById(id);
// 4. 如果数据库中也没有这个数据,防止缓存穿透,存入 "null" 并设置短期过期时间
if (shop == null) {
redisTemplate.opsForValue().set(cacheKey, "null", 10L, TimeUnit.MINUTES);
return null;
}
// 5. 存在则写入缓存,设置正常过期时间
redisTemplate.opsForValue().set(cacheKey, shop, 30L, TimeUnit.MINUTES);
return shop;
}
//获取锁
private boolean tryLock(String key) {
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
return Boolean.TRUE.equals(flag);
}
//释放锁
private void unLock(String key) {
redisTemplate.delete(key);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result update(Shop shop) {
//1.先修改数据库
updateById(shop);
//2.直接删除缓存
redisTemplate.delete("cache:shop:" + shop.getId());
return Result.ok();
}
public static void main(String[] args) {
}
}
2.基于逻辑过期解决缓存击穿问题
要使用逻辑过期,那么首先需要加个字段来标识逻辑过期字段
可以设计一个实体类
@Data public class RedisData { private LocalDateTime expireTime; private Object data; }
代码
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
//重建缓存的线程池
private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
@Override
public Result queryById(Long id) {
//缓存穿透解决方案
//Shop shop = queryWithPassThrough(id);
//使用互斥锁 解决缓存击穿
//Shop shop = queryWithMutex(id);
//使用逻辑过期解决缓存击穿
Shop shop = queryWithLogicalExpire(id);
if (shop == null){
return Result.fail("店铺不存在");
}
return Result.ok(shop);
}
//缓存击穿解决方案 使用逻辑过期
//主线上找不到就返回null 不需要再判断空值
public Shop queryWithLogicalExpire(Long id) {
String cacheKey = "cache:shop:" + id;
// 1. 从缓存中查询
Object cacheData = redisTemplate.opsForValue().get(cacheKey);
// 2. 判断缓存是否存在
if (cacheData == null) {
return null;
}
//命中,需要判断过期时间
RedisData redisData = (RedisData) cacheData;
Shop shop = (Shop) redisData.getData();
if (redisData.getExpireTime().isAfter(LocalDateTime.now())) {//没有过期
return shop;
}
//过期 重建缓存 获取互斥锁
if (tryLock("lock" + id)) {
//如果成功 开启线程 重建缓存 获取锁成功理论上应该再次检查redis缓存是否过期 做DoubleCheck 如果存在则无需重建缓存
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
//重建缓存
saveShop2Redis(id, 30L);
}catch (Exception e){
throw new RuntimeException(e);
}finally {
//释放锁
unLock("lock" + id);
}
});
}
return shop;
}
//缓存击穿解决方案 使用互斥锁
public Shop queryWithMutex(Long id) {
String cacheKey = "cache:shop:" + id;
// 1. 从缓存中查询
Object cacheData = redisTemplate.opsForValue().get(cacheKey);
// 2. 判断缓存是否存在
if (cacheData != null) {
if ("null".equals(cacheData)) {
return null; // 缓存中存的是空值
}
return (Shop) cacheData; // 直接返回缓存中的 Shop 对象
}
Shop shop=null;
try {
//这里进行缓存击穿后的逻辑(重建缓存逻辑) 步骤是 拿锁 拿锁成功查询数据库 释放锁 拿锁不成功说明已经有线程在重建缓存 等待(并重新调用)
boolean b = tryLock("lock" + id);
if(!b){
//休眠
Thread.sleep(50);
//递归 重试
queryWithMutex(id);
}
//如果拿锁成功就会往下走
// 3. 不存在则查询数据库
shop = getById(id);
// 4. 如果数据库中也没有这个数据,防止缓存穿透,存入 "null" 并设置短期过期时间
if (shop == null) {
redisTemplate.opsForValue().set(cacheKey, "null", 10L, TimeUnit.MINUTES);
return null;
}
// 5. 存在则写入缓存,设置正常过期时间
redisTemplate.opsForValue().set(cacheKey, shop, 30L, TimeUnit.MINUTES);
}catch (InterruptedException e){
e.printStackTrace();
}finally {
unLock("lock" + id);
}
//释放锁
return shop;
}
//缓存穿透解决方案
public Shop queryWithPassThrough(Long id) {
String cacheKey = "cache:shop:" + id;
// 1. 从缓存中查询
Object cacheData = redisTemplate.opsForValue().get(cacheKey);
// 2. 判断缓存是否存在
if (cacheData != null) {
if ("null".equals(cacheData)) {
return null; // 缓存中存的是空值
}
return (Shop) cacheData; // 直接返回缓存中的 Shop 对象
}
// 3. 不存在则查询数据库
Shop shop = getById(id);
// 4. 如果数据库中也没有这个数据,防止缓存穿透,存入 "null" 并设置短期过期时间
if (shop == null) {
redisTemplate.opsForValue().set(cacheKey, "null", 10L, TimeUnit.MINUTES);
return null;
}
// 5. 存在则写入缓存,设置正常过期时间
redisTemplate.opsForValue().set(cacheKey, shop, 30L, TimeUnit.MINUTES);
return shop;
}
//获取锁
private boolean tryLock(String key) {
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
return Boolean.TRUE.equals(flag);
}
//释放锁
private void unLock(String key) {
redisTemplate.delete(key);
}
//逻辑过期解决缓存击穿 这个key是永久有效的用逻辑过期判断 好比缓存预热(启动时候就存进去了)
private void saveShop2Redis(Long id,Long expireSeconds) {
//1.查询店铺数据
Shop shop = getById(id);
//2.封装逻辑过期时间
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//3.写入redis
redisTemplate.opsForValue().set("cache:shop:" + id, shop);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result update(Shop shop) {
//1.先修改数据库
updateById(shop);
//2.直接删除缓存
redisTemplate.delete("cache:shop:" + shop.getId());
return Result.ok();
}
public static void main(String[] args) {
}
}