Redis(七)缓存穿透、缓存击穿、缓存雪崩以及分布式锁

2023-06-25,,

应用问题解决

1 缓存穿透

1.1 访问结构

正常情况下,服务器接收到浏览器发来的web服务请求,会先去访问redis缓存,如果缓存中存在数据则直接返回,否则会去查询数据库里面的数据,然后保存在redis中再进行返回。

1.2 缓存击穿的现象

出现大量web请求,应用服务器压力变大

redis命中率降低:redis中频繁查不到数据

应用服务器一直在查询数据库

1.3 缓存击穿出现的原因

网站被恶意攻击,主要表现为:

redis查询不到数据库

出现大量非正常url访问

查询不到数据并不是真的想要获取到数据,而是希望借此增大redis内存压力进而致使服务器瘫痪。

1.4 缓存击穿解决方案

一个不存在于缓存并且注定也不存在于数据库的数据,由于缓存是不命中的时候被动写的,并且处于容错的考虑,当存储层查不到数据就不会写到缓存里面,这将导致每次在缓存中查不到数据就去存储层查询,失去了缓存的意义。

对空值也进行缓存:如果查询到一个数据的结果为空,那么我们也对这个空值进行缓存,不管这个数据是不是存在,设置空结果的过期时间都会很短,最多不超过五分钟

设置可以访问的名单(白名单):只允许指定的id进行访问,使用redis 的 bitmaps,将id作为偏移量,然后每次访问都需要和bitmaps中的id进行比较,如果访问的id不在bitmaps中,则不允许访问。

布隆过滤器:它底层实现实际上类似于bitmaps,用一个很长的二进制量(位图)和一系列随机哈希函数。布隆过滤器可以检测一个元素是否存在于一个集合中,优点是空间效率和查询时间,缺点是由一定的误识别率和删除困难。

将所有可能存在的数据哈希到一个足够大的bitmaps里面,一个一定不会存在的数据会被这个bitmaps拦截掉,从而避免了底层系统的查询压力。

进行实时监控:当发现Redis的命中率开始降低,排查访问对象和查询的数据,和运维人员配合,进行设置黑名单拦截。

2 缓存击穿

2.1 缓存穿透的现象

数据库访问压力瞬间增大

redis并没有出现大量key过期(大量key过期为缓存雪崩

redis正常运行

2.2 缓存击穿造成的原因

redis中的某个key过期的时候,突然出现了大量对于该key的web服务请求,导致只能去访问数据库,而造成的数据库压力瞬间增大。

2.3 缓存击穿解决方案

可以在某个时间点被超高并发访问的问题,被称作热点数据问题,解决方案主要有:

预先设置热门数据:在redis访问高峰之前,就提前把一些热门数据放入redis中并增大key的时长

实时调整:现场监控热门数据,实时调整key的时长

使用锁:即在查询失败的时候设置一个排它锁,并开启一个线程查询数据库并同步缓存,查询过程中不允许其他线程查询数据库,查询成功后则删除排它锁。

3 缓存雪崩

3.1 缓存雪崩的现象

key对应的数据在数据库中,但是在redis中短时间大量key过期

数据库崩溃

3.2 缓存雪崩造成的原因

key对应的数据在数据库中,但是在redis中短时间大量key过期,导致大量请求请求key的时候就会去直接从后端DB加载数据并回设到缓存,这时候大并发的请求可能会瞬间把后端DB压垮。

缓存击穿和缓存雪崩的区别在于是否出现大量key过期

3.3 解决方案

构建多级缓存:nginx 缓存 + redis 缓存 + 其他缓存 (ehcahe等)

使用锁或者队列:使用锁或者队列能够避免有大量的线程对数据库一次性读写,从而避免失效时大量的并发请求落在底层存储系统上,不适用于高并发地情况。

设置过期标志更新缓存:记录缓存是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际的key缓存。

将缓存过期时间分散开

4 分布式锁

4.1 简介

随着业务发展的需要,原来单体单机部署的系统被演化成为了分布式集群系统,由于分布式系统多线程、多进程,并且分布在不同的系统上,这使得原来单机部署情况下的并发锁策略失效,单纯的JavaAPI并不能提供分布式锁的能力,为了解决这个问题就需要一个跨JVM的互斥机制来控制共享资源的访问。

4.2 分布式锁的主流解决方案

基于数据库实现分布式锁

基于redis

基于zookeeper

4.3 设置锁和过期时间
setnx 对key值添加锁

添加锁之后其他进程不能够进行修改

del 释放锁
expire 设置锁的过期时间

上面存在的问题是锁一直没有释放则导致数据一直无法访问,解决方案是对锁设置过期时间,时间到了会自动释放

set nx ex 同时进行上锁和过期时间设置

用于实现上面两条命令的原子操作

4.4 UUID防止误删
分布式锁的代码实现
@Autowired
StringRedisTemplate redisTemplate; @GetMapping("/test")
public String testHandle() {
// 上锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111", 3, TimeUnit.SECONDS);
if(lock) {
Object value = redisTemplate.opsForValue().get("num");
if(StringUtils.isEmpty(value)) {
return "success";
}
int num = Integer.parseInt(value + "");
redisTemplate.opsForValue().set("num", String.valueOf(++num));
// 释放锁
redisTemplate.delete("lock"); } else {
// 获取锁失败,3s后再次尝试
try {
Thread.sleep(3000);
testHandle();
} catch (InterruptedException e){
e.printStackTrace();
}
}
return "success";
}
ab压力测试
[root@hadoop100 ~]# ab -n 1000 -c 100 http://192.168.1.108:8080/test
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 192.168.1.108 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests Server Software:
Server Hostname: 192.168.1.108
Server Port: 8080 Document Path: /test
Document Length: 7 bytes Concurrency Level: 100
Time taken for tests: 199.311 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 139000 bytes
HTML transferred: 7000 bytes
Requests per second: 5.02 [#/sec] (mean)
Time per request: 19931.077 [ms] (mean)
Time per request: 199.311 [ms] (mean, across all concurrent requests)
Transfer rate: 0.68 [Kbytes/sec] received Connection Times (ms)
min mean[+/-sd] median max
Connect: 1 3 6.4 1 36
Processing: 1 8360 30440.5 2 198574
Waiting: 1 8360 30440.5 2 198574
Total: 2 8363 30445.3 3 198580 Percentage of the requests served within a certain time (ms)
50% 3
66% 5
75% 9
80% 12
90% 3078
95% 72293
98% 141421
99% 171505
100% 198580 (longest request)
存在的问题:可能导致锁的误删

比如上面我们设置了锁的自动过期时间为3s,A上锁执行操作的过程中出现了服务器卡顿导致操作暂停超过了三秒,锁被自动释放了,这时候B抢到了锁,然后上锁进行一系列操作,而此时服务器正常执行了,A又执行了没有执行的释放锁操作,导致实际上是删除了B的锁,解决的方法是每次上锁的时候都给锁设置UUID,然后释放锁的时候检查是不是之前自己上的锁,防止误删。

@Autowired
StringRedisTemplate redisTemplate; @GetMapping("/test")
public String testHandle() {
String uuid = UUID.randomUUID().toString();
// 上锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
if(lock) {
Object value = redisTemplate.opsForValue().get("num");
if(StringUtils.isEmpty(value)) {
return "success";
}
int num = Integer.parseInt(value + "");
redisTemplate.opsForValue().set("num", String.valueOf(++num));
// 防止误删
if(uuid.equals(redisTemplate.opsForValue().get("lock"))) {
// 释放锁
redisTemplate.delete("lock");
}
} else {
// 获取锁失败,3s后再次尝试
try {
Thread.sleep(3000);
testHandle();
} catch (InterruptedException e){
e.printStackTrace();
}
}
return "success";
}
4.5 LUA保证原子性

上面使用uuid之后,仍然会存在误删的风险:线程A在判断uuid相同后,准备删除lock,这时候锁到期自动释放了,B抢到了锁然后设置了uuid,最后A删掉了B的锁。 这其实就是因为uuid判断与删除锁的操作不是原子性造成的,解决方案是使用lua脚本,保证在没有删除完成之前,别人是不能进行操作的。

4.6 分布式锁可用需要满足的条件

互斥性

不会发生死锁:即使有客户端在持有锁期间没有主动解锁,其他客户端也能正常获取锁

不能误删其他客户端的锁

加锁和解锁必须具有原子性

Redis(七)缓存穿透、缓存击穿、缓存雪崩以及分布式锁的相关教程结束。

《Redis(七)缓存穿透、缓存击穿、缓存雪崩以及分布式锁.doc》

下载本文的Word格式文档,以方便收藏与打印。