您的位置:首页 > 数据库 > Redis

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

2017-05-05 15:56 225 查看

在使用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倒是可以实现高效的去重,但并未在根本上解决重复更新的问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息