redis应用问题解决
缓存穿透
什么是缓存穿透?
可以参考下图,当客户端发送读的请求过来时,会先访问缓存中的数据,如果不存在则直接去访问MySQL服务器中的数据。这时候如果MySQL服务器中并不存在他请求对应的信息,请求就会反反复复一直访问MySQL服务器,黑客利用此漏洞进行攻击可能压垮数据库。

解决方案
-
方案一:缓存空值
如果MySQL服务器中不存在相对应的数据,可以将对应的key的value值设置为空,当请求再次访问时可以直接去缓存中读取空值
-
方案二:布隆过滤器
在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。
缓存击穿
什么是缓存击穿?
比如微博的热点事件,大家都在同一时间点访问这个请求,就可能造成服务器崩溃的情况,就好像子弹打在墙上一样,始终往一个地方大,迟早打穿这堵墙
解决方案
缓存雪崩
什么时候缓存雪崩?
举个栗子,在双十一的时候,非常多的用户访问非常多的请求,我们虽然提前做了缓存,但在一定时间后这些缓存同时失效,那就会有一大批的请求直接访问MySQL数据库,给服务器带来巨大的压力
解决方案
分布式锁
业务需求
我们上述说到的解决缓存击穿的方案,可以使用分布式锁,让请求一个一个访问。随着时代的发展,现在我们使用redis已经不是单单的一台服务器了, 我们会建一个集群,但是锁这个东西他是不能横跨服务器的,这种情况redis也提供了解决方案
解决方案
-
使用setnx命令
我们都知道setnx命令如果这个key不存在的话可以对其value进行设置,但key存在时是不允许设置的,我们就可以利用这一点,将此key作为锁。
-
设置过期时间
就好像有一个人去上厕所,外面排着长队,结果这个人上着上着突然睡着了,但是外面的人就无法使用。所以我们要将锁设置一定的过期时间,如果长时间没有完成就要自动释放锁
-
具体命令
set k2 v2 nx ex 10
Java中使用
@GetMapping("/testLock")
public void testLock(){
Boolean isLock = redisTemplate.opsForValue().setIfAbsent("kkk", "vvv", 10, TimeUnit.SECONDS);
if (isLock){
Object value = redisTemplate.opsForValue().get("num");
if (StringUtils.isEmpty(value)){
return;
}
int i = Integer.parseInt(value + "");
redisTemplate.opsForValue().set("num",i+1);
redisTemplate.delete("kkk");
}else {
try {
Thread.sleep(100);
testLock();
}catch (Exception e){
e.printStackTrace();
}
}
}
设置num的值为0,使用ab工具进行多次访问,如果锁有效的话,1000个请求num的值应该为1000
ab -n 1000 -c 100 http://192.168.0.10:8080/redisTest/testLock
127.0.0.1:6379> get num
"1000"
问题解决
其实上述案例中还是有那么一些些问题的,需要我们来梳理一下
问题一
如下图所示,A请求在执行操作的过程中宕机了,但是key的过期时间已到,B请求获取到了锁并加上了锁,B在执行操作的过程中,A相应过来了,手动释放了锁,这时其他的请求又会一起挤进来,可能会出现两个请求操作一个数据的情况
解决方案:使用UUID作为value值,防止误删除

@GetMapping("/testLock")
public void testLock(){
String uuid = UUID.randomUUID().toString();
Boolean isLock = redisTemplate.opsForValue().setIfAbsent("kkk", "uuid", 10, TimeUnit.SECONDS);
if (isLock){
Object value = redisTemplate.opsForValue().get("num");
if (StringUtils.isEmpty(value)){
return;
}
int i = Integer.parseInt(value + "");
redisTemplate.opsForValue().set("num",i+1);
String keyForUuid = (String) redisTemplate.opsForValue().get("kkk");
if (uuid.equals(keyForUuid)){
redisTemplate.delete("kkk");
}
}else {
try {
Thread.sleep(100);
testLock();
}catch (Exception e){
e.printStackTrace();
}
}
}
问题二
如下入、图所示,A在释放锁的过程中,先判断了uuid是相同的,正准备删除时,锁的过期时间到了,自动删除后,B获取到了锁并加上了锁,A再删除了这个锁
解决方案:使用LUA脚本保证删除的原子性

@GetMapping("/testLock")
public void testLock(){
String uuid = UUID.randomUUID().toString();
Boolean isLock = redisTemplate.opsForValue().setIfAbsent("kkk", "uuid", 10, TimeUnit.SECONDS);
if (isLock){
Object value = redisTemplate.opsForValue().get("num");
if (StringUtils.isEmpty(value)){
return;
}
int i = Integer.parseInt(value + "");
redisTemplate.opsForValue().set("num",i+1);
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
redisTemplate.execute(redisScript, Arrays.asList("kkk"), uuid);
}else {
try {
Thread.sleep(100);
testLock();
}catch (Exception e){
e.printStackTrace();
}
}
}