Redis在高并发下常见的错误场景-优惠券列表显示

第一种,看看自己是否已经入坑了。

//判断Redis缓存是否有数据
if(!jedis.exists(“testlockListV_1”)){
System.out.println(“多线程情况下多次击穿缓存,直接访问数据库”);
String list=TestClient.getList();//查询数据库
jedis.set(“testlockListV_1”,list);//保存在缓存中
System.out.println(list);
}else
System.out.println(jedis.get(“testlockListV_1”));//缓存有数据直接返回缓存数据

很多同学都是判断是否存在相同的key,如果不存在,就访问数据库,然后把数据放入缓存中,然后返回。

实际多线程运行时,会打印如下

多线程情况下多次击穿缓存,直接访问数据库
多线程情况下多次击穿缓存,直接访问数据库
多线程情况下多次击穿缓存,直接访问数据库
多线程情况下多次击穿缓存,直接访问数据库
多线程情况下多次击穿缓存,直接访问数据库
多线程情况下多次击穿缓存,直接访问数据库

答案很简单,因为多个线程同时执行jedis.exists(“testlockListV_1”)这一段代码时,缓存中确实没有数据。然后都执行查询数据库,放入缓存中。这样请求还是直接通过数据库拿数据

第二种经过改造,我们知道Redis是可以实现分布式锁的jedis.setnx(key,value); 那我们改造一下,通过分布式锁来解决穿透的问题。

改造如下:

Jedis jedis = jedisPool.getResource();
//判断Redis缓存是否有数据
if(!jedis.exists(“testlockListV_2”)){
if(jedis.setnx(“lockkeyV1”, “lockvalues”)==1){//获取锁
System.out.println(“多线程情况下多次击穿缓存,直接访问数据库”);
String list=TestClient.getList();//查询数据库
jedis.set(“testlockListV_2”,list);//保存在缓存中
System.out.println(list);
jedis.del(“lockkeyV1”);//释放锁,便于下次执行正常获取
}
}else
System.out.println(jedis.get(“testlockListV_2“));//缓存有数据直接返回缓存数据

高并发先,打印结果

多线程情况下多次击穿缓存,直接访问数据库
数据库中返回的查询记录
多线程情况下多次击穿缓存,直接访问数据库
数据库中返回的查询记录

结果发现,即使加了锁,我们还是穿透了缓存,直接访问数据库了,虽然请求较少。仔细分析原因是因为,多个线程同时进入后判断是否有相同key,由于第一次访问,redis中是没有相应的key的,但是多个线程又同时去拿锁,此时虽然只有一个线程能获取锁成功,进入查询数据库,然后把数据放入缓存。但是会出现刚放完缓存,释放所。同一时间内有几个线程刚通过if(!jedis.exists(“testlockListV_2”))判断是否有次键值对的判断,然后也重新获取锁,也访问数据库,保存数据到缓存。这样也出现了击穿缓存,直接访问数据库中。

第三种,上面分析完毕前面几种情况以后。

归纳一下修改后的逻辑:

1.查询缓存,如果缓存存在,返回结果

2.缓存不存在,查询数据库

3.争夺分布式锁

4.成功获得锁,再次判断缓存的存在

5.如果缓存仍旧不存在,把查询数据库的结果循环放入缓存

6.释放分布式锁

这种二次判断存在性的机制有一个专门的名字,叫做双重检测

代码如下:

Jedis jedis = jedisPool.getResource();
//判断Redis缓存是否有数据
if(!jedis.exists(“testlockListV_3”)){
if(jedis.setnx(“lockkeyV1”, “lockvalues”)==1){//获取锁
jedis.setex(“lockkeyV1”, 2, “lockvalues”);//设置获取锁的时间,超过时间直接让锁失效。避免死锁
if(!jedis.exists(“testlockListV_3”)){
System.out.println(“多线程情况下多次击穿缓存,直接访问数据库”);
String list=TestClient.getList();//查询数据库
jedis.set(“testlockListV_3”,list);//保存在缓存中
System.out.println(list);
jedis.del(“lockkeyV1”);//释放锁,便于下次执行正常获取
}else
System.out.println(jedis.get(“testlockListV_3”));//缓存有数据直接返回缓存数据

}
}else
System.out.println(jedis.get(“testlockListV_3”));//缓存有数据直接返回缓存数据

多线程下运行结果:

多线程情况下多次击穿缓存,直接访问数据库
数据库中返回的查询记录

非常好已经只访问一次数据库,并且没有穿透缓存。

原理,因为所有的线程下都只能有一个线程获取锁,如果获取锁以后,更新完缓存以后。另外一个缓存进入,也要判断是否已经存在key,没有key才会查询数据库更新。只要执行过获取锁的操作完毕以后,肯定会存入缓存的。所以这种方案是不会有击穿缓存的情况的。

几点补充:

1.文中所使用的分布式锁,其实并不是“正宗”的分布式锁,当线程争夺锁失败的时候,会直接返回查询DB的结果,而不会依靠自旋机制来等锁。

2.为什么优惠券列表的信息要使用List类型来存入缓存,而不是把整个列表存为一个很长的Json字符串?这是由于业务需要,使用List在某些情况下更方便对单个优惠券信息进行修改(LSET指令)。

3.为什么优惠券列表的信息不使用Redis的Set或者Hash数据类型来存储,实现自动去重呢?对于Set类型,去重前需要对比整个字符串是否完全相同,而每一张优惠券是一个较长的Json字符串,对比的效率会比较低。使用Hash倒是可以实现高效的去重,但并未在根本上解决重复更新的问题。

原文链接:https://blog.csdn.net/u013381397/article/details/71213503?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522165277499316781683997394%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=165277499316781683997394&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~times_rank-18-71213503-null-null.nonecase&utm_term=%E4%BC%98%E6%83%A0

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
文明发言,共建和谐米科社区
提交
头像

昵称

取消
昵称表情图片