2.三种问题相应的解决方案
2.1-缓存穿透解决方案
了解完上述关于缓存穿透的概念之后我们就知道了只要问题就出在数据库无法将不存在的数据存储到Redis中,导致Redis中一直没有该数据,使得关于该数据的访问全部都是直接怼到数据库上,最后导致数据库崩溃.
既然这样,我们就将该数据存储到Redis里面,这样对于该数据的访问就又重新怼到Redis上面了,但是我们要注意这条数据既然不存在,那么我们就将该数据定义为空,并且要 给它设置过期时间,并且这种国旗时间不要设置的太长,20-30秒即可,否则这种无用的数据一致存储在Redis里面,也是浪费.
public PmsSkuInfo selectBySkuId(Integer skuId) { PmsSkuInfo pmsSkuInfo=new PmsSkuInfo(); //连接缓存 Jedis jedis=redisUtil.getJedis(); //查询缓存 String skuKey="sku:"+skuId+":info"; String skuJson=jedis.get(skuKey); //缓存不为空 if(StringUtils.isNotBlank(skuJson)){ //通过fastjson将我们的字符串转化成我们对应的Sku对象 pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class); } else{ //如果缓存没有,查询mysql pmsSkuInfo=selectBySkuIdFromDB(skuId); //mysql查询结果存储到Redis if(pmsSkuInfo!=null){ jedis.set("sku:"+skuId+":info", JSON.toJSONString(pmsSkuInfo)); } else{ //数据库中同样也不存在该数据 //将空值设置给该Key,并且设置30秒的过期时间 jedis.setex("sku:"+skuId+":info",30,JSON.toJSONString("")); } } jedis.close(); return pmsSkuInfo; }
2.2-缓存击穿解决方案
缓存击穿的解决方案就比较复杂了,不像缓存穿透和缓存雪崩一样,只需要设置相应的过期时间或者是将数据存进Redis即可解决.
缓存击穿的解决方案相应的就比较多,主要有两种:
Redis自身的分布式锁实现
通过redisson框架实现
接下来我们分别讲一下两者的实现方式:
Redis自身的分布式锁
缓存击穿的特殊性就在于是一个热点数据突然失效,导致大规模的请求直接怼到数据库上,这其中的重点就是一条热搜数据,大规模的请求在同一时间点怼到数据库.
所以分布式锁的思想就是, 每次向Redis请求数据的时候,都在Redis里面给该条数据上锁,一旦锁设置成功,那么就只有当前的进程可以进入到数据库中进行查询,并且查询完成之后就 将该数据重新存到Redis之中,数据存储成功之后 再释放掉该锁,在此之前其他锁没有设置成功的进程就 只能自旋等待锁被释放为止.
因为第一条请求结束之后,Redis中就已经重新有了该热点数据的缓存,所以之前自旋的进程就可以 直接从Redis中拿到该热点数据,不用再去访问数据库了,这样就极大的降低了数据库的压力.
我们也可以通过下面的思维导图来帮助大家理解:
下面是一个小的Demo:
public PmsSkuInfo selectBySkuId(Integer skuId,String ip) { System.out.println("ip:"+ip+"的机器进入访问,"+"进程名称为:"+Thread.currentThread().getName()); PmsSkuInfo pmsSkuInfo=new PmsSkuInfo(); //连接缓存 Jedis jedis=redisUtil.getJedis(); //查询缓存 String skuKey="sku:"+skuId+":info"; String skuJson=jedis.get(skuKey); //缓存不为空 if(StringUtils.isNotBlank(skuJson)){ System.out.println("ip:"+ip+"的机器进入访问,"+"进程名称为:"+Thread.currentThread().getName()+"已经成功拿到缓存中的数据"); //通过fastjson将我们的字符串转化成我们对应的Sku对象 pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class); } else{ System.out.println("ip:"+ip+"的机器进入访问,"+"进程名称为:"+Thread.currentThread().getName()+"开始申请分布式锁:"+"sku:"+skuId+":lock"); //如果缓存没有,查询mysql //设置分布式锁,避免缓存击穿 String OK=jedis.set("sku:"+skuId+":lock","1","nx","px",10*1000); if(StringUtils.isNotBlank(OK)&&OK.equals("OK")){ System.out.println("ip:"+ip+"的机器进入访问,"+"进程名称为:"+Thread.currentThread().getName()+"已经申请到分布式锁:"+"sku:"+skuId+":lock"+"过期时间为10秒"); pmsSkuInfo=selectBySkuIdFromDB(skuId); try { Thread.sleep(1000*7); } catch (InterruptedException e) { e.printStackTrace(); } //mysql查询结果存储到Redis if(pmsSkuInfo!=null){ //过期时间随机避免缓存雪崩 jedis.setex("sku:"+skuId+":info", (int) (10*Math.random()*10),JSON.toJSONString(pmsSkuInfo)); } else{ //数据库中同样也不存在该数据,也传到Redis中,避免缓存穿透 jedis.setex("sku:"+skuId+":info",30,JSON.toJSONString("")); } System.out.println("ip:"+ip+"的机器进入访问,"+"进程名称为:"+Thread.currentThread().getName()+"使用完毕了,释放了分布式锁:"+"sku:"+skuId+":lock"); //在访问mysql之后,需要将分布式锁释放掉 jedis.del("sku:"+skuId+":lock"); } else{ System.out.println("ip:"+ip+"的机器进入访问,"+"进程名称为:"+Thread.currentThread().getName()+"没有申请到分布式锁:"+"sku:"+skuId+":lock"+"已经开始自旋"); try { //进程休眠几秒之后,开始自旋 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } //开始自旋 return selectBySkuId(skuId,ip); } } jedis.close(); return pmsSkuInfo; }
可以看到这是测试结果:
- redisson框架
引入Redisson框架之后,我们就不用上面使用Redis的分布式锁那么繁琐.直接几行代码就能搞定.
首先我们需要先引入Redisson框架所需要的依赖
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.10.5</version> </dependency>
之后我们就需要配置Redisson框架的配置信息
@Configuration public class RedissonConfig { //读取配置文件中的redis的ip地址.端口号,数据库,密码 @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private String port; @Value("${spring.redis.database}") private int database; @Value("${spring.redis.password}") private String password; @Bean public RedissonClient redissonClient() { Config config = new Config(); //链式编程 config.useSingleServer().setAddress("redis://" + host + ":" + port) .setPassword(password) .setDatabase(database); RedissonClient redisson = Redisson.create(config); return redisson; } }
这样我们就已经将Redisson引入到我们的Spring容器之中了
之后我们便来编写代码进行测试:
@Controller public class RedissonController { @Autowired RedisUtil redisUtil; @Autowired RedissonClient redissonClient; @RequestMapping("/testRedisson") @ResponseBody public String testRedisson(){ Jedis jedis=redisUtil.getJedis(); RLock lock=redissonClient.getLock("lock");//声明锁 //上锁 lock.lock(); try { String v=jedis.get("k"); if(StringUtils.isBlank(v)){ v="1"; } System.out.println("---->"+v); jedis.set("k",(Integer.parseInt(v)+1)+""); jedis.close(); } finally { //解锁 lock.unlock(); } return "success"; } }
为了模拟高并发,我们通过Apache来进行压力测试(后续我会单独出一篇博客讲解压力测试,主要因为篇幅已经很长了)
之后我们再来分别看看三个程序打印的结果:
8071端口:
8072端口:
8073端口:
可以很明显的看到数据没有重复,的确已经实现了安全性.
2.3-缓存雪崩解决方案
了解完上述的缓存雪崩的概念之后,解决办法就比较简单了,既然是因为数据的过期时间都是一样的才导致数据同时失效,那么我们就可以通过 将数据的过期时间设置成随机的 ,这样就会在极大程度上减少大量数据同时过期的情况.
举个例子,可以通过下面的方法来实现:
public PmsSkuInfo selectBySkuId(Integer skuId) { PmsSkuInfo pmsSkuInfo=new PmsSkuInfo(); //连接缓存 Jedis jedis=redisUtil.getJedis(); //查询缓存 String skuKey="sku:"+skuId+":info"; String skuJson=jedis.get(skuKey); //缓存不为空 if(StringUtils.isNotBlank(skuJson)){ //通过fastjson将我们的字符串转化成我们对应的Sku对象 pmsSkuInfo = JSON.parseObject(skuJson, PmsSkuInfo.class); } else{ //如果缓存没有,查询mysql pmsSkuInfo=selectBySkuIdFromDB(skuId); //mysql查询结果存储到Redis if(pmsSkuInfo!=null){ //设置随机过期时间 jedis.setex("sku:"+skuId+":info", (int) (10*Math.random()*10),JSON.toJSONString(pmsSkuInfo)); } else{ //数据库中同样也不存在该数据 jedis.setex("sku:"+skuId+":info",30,JSON.toJSONString("")); } } jedis.close(); return pmsSkuInfo; }