Redis常见问题的解决方案(缓存穿透/缓存击穿/缓存雪崩/数据库缓存数据不一致)

Redis解决缓存数据库不一致的方案

  • 用 先 操作数据库操作缓存 的策略来实现缓存数据库数据一致
  • 具体做法是 更新数据库数据然后删除缓存

虽然还是会有线程安全问题 比如 假设此时缓存刚好失效了 线程1 查询缓存失败 从数据库读取了旧数据 还没写入缓存的时候 被调度到 线程2执行

线程2 执行更新操作将数据库的数据进行更新 同时删除缓存 由于此时缓存本身就不存在等于说提前执行了删除操作

线程2操作完了以后执行线程1 线程1将读到的旧数据写入到缓存 此时九出现了缓存不一致

这种情况是很少出现的 所以说可以忽略不记

但是为了处理这种情况 我们将缓存设置超时时间,超时以后自动删除然后重写缓存数据

Java 复制代码
    public Result update(Shop shop) {
        Long id = shop.getId();
        //1.更新数据库
        if (id == null){
            return Result.fail("店名不能为空");
        }
        //2.删除缓存
        updateById(shop);
        stringRedisTemplate.delete("cache:shop"+id);
        return Result.ok();
    }

Redis解决缓存穿透的方案

缓存击穿是指客户端大量请求数据库和缓存中不存在值,导致数据库的压力飙升

  • 缓存空对象

    • 当我们客户端访问不存在的数据时,先请求Redis,但是此时Redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如Redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到Redis中去,这样,下次用户过来访问这个不存在的数据,那么在Redis中也能找到这个数据就不会进入到缓存了
    • 代码实现
    Java 复制代码
    public Shop queryWithPassThrough(Long id){
    	String shopCache = stringRedisTemplate.opsForValue().get("cache:shop" + id);
    	
    	//查询到了shopCache但是里面不是空值
    	if(StrUtil.isNotBlank(shopCache)){
    		return JSONUtil.toBean(shopCache,Shop.class);
    	}
    	//查询到了shopCache但是里面是空值
    	if(shopCache != null){
    		return null;
    	}
    	
    	//从数据库中查询
    	Shop shop = getById(id);
    	
        //没有查询到那么就将空值设置到缓存中 防止缓存穿透
        if(shop == null){
            stringRedisTemplate.opsForValue().set("cache:shop"+id,"",30L, TimeUnit.MINUTES);
            return null;
        }
    
        //从数据库中查询数据 查询到了数据就将他设置到缓存中 并且返回
        stringRedisTemplate.opsForValue().set("cache:shop"+id,JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
    
    	return shop;
    
    }

Redis解决缓存雪崩的方案

  • 缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
  • 解决方案:
    *
    1. 设置随机过期时间:为缓存键设置随机的过期时间,避免大量键同时过期的情况发生,减少缓存雪崩的概率。
      1. 实现缓存预热:在系统启动或缓存失效前,提前加载热门数据到缓存中,避免在关键时刻大量请求直接访问后端服务。

Redis解决缓存击穿的方案

  • 缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

  • 解决方案:

    加互斥锁或分布式锁:在访问热点数据时,可以引入互斥锁或分布式锁,保证只有一个线程去访问后端服务或数据库,其他线程等待结果。当第一个线 程获取到数据后,其他线程可以直接从缓存获取,避免多个线程同时访问后端服务,减轻压力。

    • 使用互斥锁的代码
    Java 复制代码
    public boolean tryLock(String key){
    	//加上超时时间避免等待过久
    	boolean flg = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);
    	return flg;
    }
    
    public void unLock(String key){
    	stringRedisTemplate.delete(key);
    }
    
    public Shop queryWithPassThrough(Long id){
    	String shopCache = stringRedisTemplate.opsForValue().get("cache:shop" + id);
    	
    	//查询到了shopCache但是里面不是空值
    	if(StrUtil.isNotBlank(shopCache)){
    		return JSONUtil.toBean(shopCache,Shop.class);
    	}
    	//查询到了shopCache但是里面是空值
    	if(shopCache != null){
    		return null;
    	}
    	
    	String key = "lock:shop:" + id;
    
    	//从数据库中查询				
    	Shop shop = getById(id);
    
    	try{
    
    		if(tryLock(key)){
    
    			//没有查询到那么就将空值设置到缓存中 防止缓存穿透
    			if(shop == null){
    				stringRedisTemplate.opsForValue().set("cache:shop"+id,"",30L, TimeUnit.MINUTES);
    				return null;
    			}
    			
    			//从数据库中查询数据 查询到了数据就将他设置到缓存中 并且返回
    			stringRedisTemplate.opsForValue().set("cache:shop"+id,JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
    			
    		}else{
    			Thread.sleep(50);
    			//递归调用
    			return queryWithPassThrough(id);
    		}
    
    	}catch(InterruptedException e){
    		throw new RuntimeException(e);
    	}finally{
    		unlock(key);
    	}
    
    	return shop;
    
    }

​ 这里说明一下加锁的逻辑 我们调用了

boolean flg = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);

​ 这一段代码来进行加锁操作 本质上是调用了 Redis 的 SETNX 这条命令 当这个键值对不存在时就创建这个键值对 返回TRUE 反之返回 FALSE

下面是另一种解决方案

​ 对于一些热点数据,可以将其设置为永不过期,或者设置一个较长的过期时间,确保热点数据在缓存中可用,减少因为过期而触发的缓存击穿。

​ 具体做法参考下面这张图

我们设置逻辑过期时间既可以保证热点数据永不过期,同时又可以避免数据库缓存数据不一致的情况

Java 复制代码
	//加上超时时间避免等待过久
	boolean flg = stringRedisTemplate.opsForValue().setIfAbsent(key,"1",10,TimeUnit.SECONDS);
	return flg;
}

public void unLock(String key){
	stringRedisTemplate.delete(key);
}

public void reMakeCache(Long id,Long expireSeconds){
	//数据库查询操作
	Shop shop = selectById(id);

	RedisData redisData = new RedisData();
	
	redisData.setData(shop);
	redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
}

//使用线程池
private static final ExecutorService CACHE_REBUILD_EXCUTOR = Executors.newFixedThreadPool(10);

public Shop queryWithPassThrough(Long id){
	String shopCache = stringRedisTemplate.opsForValue().get("cache:shop" + id);
	
	//查询到了shopCache但是里面不是空值
	if(StrUtil.isNotBlank(shopCache)){
		return JSONUtil.toBean(shopCache,Shop.class);
	}
	//查询到了shopCache但是里面是空值
	if(shopCache != null){
		return null;
	}

	//将redis数据反序列化
	RedisData redisData = JSONUtil.toBean(shopCache,RedisData.class);
	//获取数据
	Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(),Shop.class);
	//获取过期时间
	LocalDataTime expireTime = redisData.getExpireTime();


	//没有过期直接返回
	if(expireTime.isAfter(LocalDateTime.now())){
		return shop;
	}
	
	String key = "lock:shop:" + id;

	if(tryLock(key)){
		try{
			//开启独立线程完成
			CACHE_REBUILD_EXCUTOR.submit(() -> {				
				reMakeCache(id,20L);
			});
		}catch(InterruptedException e){
			throw new RuntimeException(e);
		}finally{
			unlock(key);
		}
	}

	return shop;

}
相关推荐
程序员JerrySUN2 小时前
基于 RAUC 的 Jetson OTA 升级全攻略
java·数据库·redis
还是大剑师兰特3 小时前
Redis面试题及详细答案100道(01-15) --- 基础认知篇
redis·大剑师·redis面试
布朗克1683 小时前
MySQL UNION 操作符详细说明
数据库·mysql·union
IT小辉同学5 小时前
Spring Boot Redis 缓存完全指南
spring boot·redis·缓存
喵桑..6 小时前
视图是什么?有什么用?什么时候用?MySQL中的视图
数据库·mysql
奋进小子8 小时前
达梦DISQL执行SQL和SQL脚本
数据库·sql
EasyCVR8 小时前
视频汇聚系统EasyCVR调用设备录像保活时视频流不连贯问题解决方案
数据库·ubuntu·音视频·云存储·云端录像
YueiL9 小时前
Linux文件系统基石:透彻理解inode及其核心作用
linux·网络·数据库
cyhysr11 小时前
redis8.0.3部署于mac
redis·macos
老华带你飞11 小时前
数码论坛|基于SprinBoot+vue的数码论坛系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·毕设·数码论坛系统