Redis缓存雪崩、击穿、穿透技术解析及解决方案

在使用 Redis 缓存时,经常会遇到一些异常问题。

概括来说有 4 个方面:

  • 缓存中的数据和数据库中的不一致;
  • 缓存雪崩;
  • 缓存击穿;
  • 缓存穿透。

关于第一个问题【缓存中的数据和数据库中的不一致】,在之前的文章中,已经深入分析过了,可以参考: 深入解析缓存模式下的数据一致性问题

今天重点看下后面三个问题。

缓存雪崩

缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。

产生原因

产生缓存雪崩的原因有两种:

  • 缓存系统本身不可用,导致大量请求直接回源到数据库;
  • 应用设计层面大量的 Key 在同一时间过期,导致大量的数据回源。

针对第一个原因,可以通过主从节点的方式构建 Redis缓存高可用集群,如果主节点故障宕机了,从节点还可以切换成为主节点,继续提供缓存服务,避免了由于缓存实例宕机而导致的缓存雪崩问题。

重点看下第二种原因,在缓存数据同时过期的情况下,对系统性能的影响。

假如我们有一个查询城市数据的需求。

  • 在系统初始化时,将 1000 条城市数据放入 Redis 缓存中,过期时间为 30s;
  • Redis 中数据过期后,从数据库中获取数据,然后写入缓存,每查询一次数据库,计数器加 1;
  • 程序启动的同时,启动一个定时任务线程每隔一秒输出计数器的值,并把计数器归零。
package com.redis.demo.controller;@Slf4j
@RestController
public class RedisController {@Autowiredprivate RedisUtil redisUtil;private AtomicInteger atomicInteger = new AtomicInteger(); // 全局 QPS 计数器@PostConstructpublic void init() {//初始化1000个城市数据到Redis,所有缓存数据有效期30秒IntStream.rangeClosed(1, 1000).forEach(i -> redisUtil.set("city" + i, getCityFromDb(i), 30));//每秒一次,输出数据库访问的QPS,同时把计数器置0Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {log.info("DB QPS : {}", atomicInteger.getAndSet(0));}, 0, 1, TimeUnit.SECONDS);}@GetMapping("/city")public String city() {//随机查询一个城市int id = ThreadLocalRandom.current().nextInt(1000) + 1;String key = "city" + id;String data = redisUtil.get(key);if (data == null) {//回源到数据库查询data = getCityFromDb(id);if (!StringUtils.isEmpty(data))//缓存30秒过期redisUtil.set(key, data, 30);}return data;}/*** 从数据库中查询数据* @param cityId 城市ID* @return*/private String getCityFromDb(int cityId) {try {TimeUnit.MICROSECONDS.sleep(100); //模拟数据库查询操作} catch (InterruptedException e) {e.printStackTrace();}//模拟查询数据库,查一次计数器加一atomicInteger.incrementAndGet();return "citydata" + System.currentTimeMillis();}
}

使用 wrk 工具,设置 4 线程 4 连接压测 city 接口:

wrk -c4 -t4 -d100s  http://localhost:8080/city

缓存数据 30s 后过期,回源的数据库 QPS 最高达到了635.

2024-11-14 14:48:21.525  INFO 11546 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 307
2024-11-14 14:48:22.525  INFO 11546 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 635
2024-11-14 14:48:23.525  INFO 11546 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 74

解决方案

由于缓存 Key 同时大规模过期导致数据库压力激增的解决方式有两种:

随机化过期时间

随机化过期时间,不要让大量的 Key 在同一时间过期。

在系统初始化时,将过期时间设置为 30s + 10s 以内的随机值,这样的话,Key就会被分散到 30~40s 之间过期了。

@PostConstructpublic void init() {//初始化1000个城市数据到Redis,所有缓存数据有效期30~40秒IntStream.rangeClosed(1, 1000).forEach(i -> redisUtil.set("city" + i, getCityFromDb(i), 30 + ThreadLocalRandom.current().nextInt(10)));}

缓存过期后,回源数据库不会集中在同一秒,数据库的 QPS 从 600 多降到了 100 左右。

2024-11-14 15:01:31.556  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 93
2024-11-14 15:01:32.555  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 95
2024-11-14 15:01:33.555  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 107
2024-11-14 15:01:34.555  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 91
2024-11-14 15:01:35.555  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 104
2024-11-14 15:01:36.557  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 120
2024-11-14 15:01:37.555  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 89
2024-11-14 15:01:38.558  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 114
2024-11-14 15:01:39.555  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 99
2024-11-14 15:01:40.558  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 54
2024-11-14 15:01:41.557  INFO 11845 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 13

缓存永不过期

让缓存永不过期,启动一个后台线程 60 秒一次定时把所有数据更新到缓存,而且通过适当的休眠,控制从数据库更新数据的频率,降低数据库压力。

@PostConstructpublic void rightInit2() throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(1);//每隔60秒全量更新一次缓存Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {log.info("定时去更新缓存");IntStream.rangeClosed(1, 1000).forEach(i -> {String data = getCityFromDb(i);// 通过适当的休眠,来控制访问数据库的频率try {TimeUnit.MILLISECONDS.sleep(20);} catch (InterruptedException e) {}if (!StringUtils.isEmpty(data)) {//缓存永不过期,被动更新redisUtil.set("city" + i, data);}});log.info("定时更新缓存完成");//启动程序的时候需要等待首次更新缓存完成countDownLatch.countDown();}, 0, 60, TimeUnit.SECONDS);Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {log.info("DB QPS : {}", atomicInteger.getAndSet(0));}, 0, 1, TimeUnit.SECONDS);countDownLatch.await();}

修改后,虽然缓存整体更新的耗时在 21 秒左右,但数据库的压力会比较稳定:

2024-11-14 15:29:03.620  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:29:04.614  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 42
2024-11-14 15:29:05.618  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 44
2024-11-14 15:29:06.613  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 44
2024-11-14 15:29:07.614  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 42
2024-11-14 15:29:08.615  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 45
2024-11-14 15:29:09.615  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 42
2024-11-14 15:29:10.617  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 45
2024-11-14 15:29:11.617  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 43
2024-11-14 15:29:12.613  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 45
2024-11-14 15:29:13.618  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 44
2024-11-14 15:29:14.613  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 45
2024-11-14 15:29:15.615  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 45
2024-11-14 15:29:16.613  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 44
2024-11-14 15:29:17.614  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 41
2024-11-14 15:29:18.613  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 45
2024-11-14 15:29:19.617  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 45
2024-11-14 15:29:20.616  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 44
2024-11-14 15:29:21.613  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 43
2024-11-14 15:29:22.615  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 43
2024-11-14 15:29:23.618  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 44
2024-11-14 15:29:24.613  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 43
2024-11-14 15:29:25.618  INFO 12381 --- [pool-2-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 43

注意事项

  • 内存是有限的,使用方案二要注意内存问题;
  • 在从数据库查询到数据回写缓存时,要判断数据的合法性。

缓存击穿

缓存击穿是指针对某个访问非常频繁的热点数据的请求,无法在缓存中获取,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,直接影响数据库无法处理其他请求。

产生原因

比如,在程序启动的时候,初始化一个热点数据到 Redis 中,过期时间设置为 5 秒,每隔 1 秒输出一下回源的 QPS:

 @PostConstructpublic void init() {//初始化一个热点数据到Redis中,过期时间设置为5秒redisUtil.set("hotspot", getExpensiveData(), 5);//每隔1秒输出一下回源的QPSExecutors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {log.info("DB QPS : {}", atomicInteger.getAndSet(0));}, 0, 1, TimeUnit.SECONDS);}@GetMapping("/hotspot")public String wrong() {String data = redisUtil.get("hotspot");if (StringUtils.isEmpty(data)) {data = getExpensiveData();//重新加入缓存,过期时间还是5秒redisUtil.set("hotspot", getExpensiveData(), 5);}return data;}private String getExpensiveData() {try {TimeUnit.MILLISECONDS.sleep(20);  //模拟数据库查询操作} catch (InterruptedException e) {e.printStackTrace();}//模拟查询数据库,查一次计数器加一atomicInteger.incrementAndGet();return "hotspotdata" + System.currentTimeMillis();}

可以看到,每隔 5 秒数据库都有 16 的 QPS:

2024-11-14 15:51:04.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:05.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:06.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:07.287  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:08.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 16
2024-11-14 15:51:09.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:10.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:11.289  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:12.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:13.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 16
2024-11-14 15:51:14.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:15.299  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:16.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:17.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:18.287  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 16
2024-11-14 15:51:19.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:20.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 15:51:21.286  INFO 12843 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0

解决方案

热点数据永不过期

对于访问特别频繁的热点数据,不设置过期时间。这样一来,对热点数据的访问请求,都可以在缓存中进行处理,而 Redis 数万级别的高吞吐量可以很好地应对大量的并发请求访问。

互斥锁机制限制回源

当热点数据过期时,使用互斥锁来保证只有一个请求去查询数据库并更新缓存,其他请求等待或直接返回过期数据。

 @GetMapping("/hotspot")public String right() {String data = redisUtil.get("hotspot");if (StringUtils.isEmpty(data)) {//去获取分布式锁Boolean lock = redisUtil.setIfAbset("hotspot_lock", "1", 1, TimeUnit.SECONDS);if (lock) {try {data = redisUtil.get("hotspot");if (StringUtils.isEmpty(data)) {data = getExpensiveData();redisUtil.set("hotspot", data, 5);}} finally {// 释放锁redisUtil.delete("hotspot_lock");}} else {TimeUnit.SECONDS.sleep(1);data = redisUtil.get("hotspot");}}return data;}

通过锁机制可以把回源到数据库的并发限制在 1。

2024-11-14 16:58:49.396  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:58:50.393  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 1
2024-11-14 16:58:51.397  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:58:52.393  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:58:53.393  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:58:54.393  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:58:55.394  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 1
2024-11-14 16:58:56.392  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:58:57.392  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:58:58.392  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:58:59.393  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:59:00.396  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 1
2024-11-14 16:59:01.392  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:59:02.392  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:59:03.392  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:59:04.392  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:59:05.392  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 1
2024-11-14 16:59:06.392  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:59:07.393  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:59:08.392  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:59:09.394  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 16:59:10.392  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 1
2024-11-14 16:59:11.393  INFO 14525 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0

在真实的业务场景下,不一定要这么严格地使用双重检查分布式锁进行全局的并发限制,因为这样虽然可以把数据库回源并发降到最低,但也限制了缓存失效时的并发。

可以考虑的方式是:

  • 方案一,使用进程内的锁进行限制,这样每一个节点都可以以一个并发回源数据库;
  • 方案二,不使用锁进行限制,而是使用类似Semaphore 的工具限制并发数,比如限制为5,这样既限制了回源并发数不至于太大,又能使得一定量的线程可以同时回源。

以信号量为例:

private Semaphore semaphore = new Semaphore(5); // 信号量限制最大并发数@GetMapping("/hotspot")
public String right(){String data = redisUtil.get("hotspot");if (!StringUtils.isEmpty(data)) {return data;}try {// 尝试获取信号量semaphore.acquire(1);try {// 再次检查缓存,防止多个线程同时到达这里data = redisUtil.get("hotspot");if (!StringUtils.isEmpty(data)) {return data;}data = getExpensiveData();redisUtil.set("hotspot", data, 5);} finally {// 释放信号量semaphore.release();}} catch (InterruptedException e) {Thread.currentThread().interrupt();log.info("Failed to acquire semaphore");}return data;
}
2024-11-14 17:25:50.841  INFO 15182 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-11-14 17:25:50.847  INFO 15182 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 6 ms
2024-11-14 17:25:51.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:25:52.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:25:53.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 5
2024-11-14 17:25:54.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:25:55.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:25:56.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:25:57.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:25:58.622  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 5
2024-11-14 17:25:59.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:26:00.620  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:26:01.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:26:02.620  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:26:03.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 5
2024-11-14 17:26:04.621  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:26:05.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:26:06.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:26:07.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0
2024-11-14 17:26:08.618  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 5
2024-11-14 17:26:09.621  INFO 15182 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 0

缓存穿透

缓存穿透是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。

此时,应用也无法从数据库中读取数据再回写缓存,来服务后续请求,这样一来,缓存也就成了“摆设”,如果应用持续有大量请求访问数据,就会同时给缓存和数据库带来巨大压力。

这里需要注意,缓存穿透和缓存击穿的区别:

  • 缓存穿透是指,缓存没有起到压力缓冲的作用;
  • 而缓存击穿是指,缓存失效时瞬时的并发打到数据库。

产生原因

  • 恶意攻击:专门访问数据库中没有的数据;
  • 业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据。

比如这段代码,去访问一个无效的 id 为 0 的数据,每次都会回源数据库。

 @GetMapping("/wrong")public String wrong(@RequestParam("id") int id) {String key = "user" + id;String data = redisUtil.get(key);//无法区分是无效用户还是缓存失效if (StringUtils.isEmpty(data)) {data = getCityFromDb(id);redisUtil.set(key,data,30);}return data;}private String getCityFromDb(int id) {atomicInteger.incrementAndGet();//注意,只有ID介于0(不含)和10000(包含)之间的用户才是有效用户,可以查询到用户信息if (id > 0 && id <= 10000) return "userdata";//否则返回空字符串return "";}

压测后的 QPS:

2024-11-14 17:48:06.497  INFO 15668 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 12
2024-11-14 17:48:07.494  INFO 15668 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 2530
2024-11-14 17:48:08.498  INFO 15668 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 3787
2024-11-14 17:48:09.495  INFO 15668 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 6910
2024-11-14 17:48:10.494  INFO 15668 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 6311
2024-11-14 17:48:11.494  INFO 15668 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 5244
2024-11-14 17:48:12.494  INFO 15668 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 5519
2024-11-14 17:48:13.494  INFO 15668 --- [pool-1-thread-1] c.redis.demo.controller.RedisController  : DB QPS : 6242

如果这种漏洞被恶意利用的话,就会对数据库造成很大的性能压力。

解决方案

设置一个特殊的值

对于不存在的数据,设置一个特殊的 Value 到缓存中,比如当数据库中查出的用户信息为空的时候,设置 NODATA 这样具有特殊含义的字符串到缓存中,这样下次请求缓存的时候还是可以命中缓存,即直接从缓存返回结果,不查询数据库。

 @GetMapping("/right")public String right(@RequestParam("id") int id) {String key = "user" + id;String data = redisUtil.get(key);if (StringUtils.isEmpty(data)) {data = getCityFromDb(id);if (!StringUtils.isEmpty(data)) {redisUtil.set(key,data,30);} else {//设置一个特殊的值redisUtil.set(key,"NODATA",30);}}return data;}

注意:这种方式会把大量无效的数据加入到缓存中,占用空间。

布隆过滤器

布隆过滤器由一个初值都为 0 的 bit 数组和 N 个哈希函数组成,可以用来快速判断某个数据是否存在。

当我们想标记某个数据存在时(例如,数据已被写入数据库),布隆过滤器会通过三个操作完成标记:

  • 首先,使用 N 个哈希函数,分别计算这个数据的哈希值,得到 N 个哈希值。
  • 然后,我们把这 N 个哈希值对 bit 数组的长度取模,得到每个哈希值在数组中的对应位置。
  • 最后,我们把对应位置的 bit 位设置为 1,这就完成了在布隆过滤器中标记数据的操作。

如果数据不存在(例如,数据库里没有写入数据),我们也就没有用布隆过滤器标记过数据,那么,bit 数组对应 bit 位的值仍然为 0。

当需要查询某个数据时,就执行刚刚说的计算过程,先得到这个数据在 bit 数组中对应的 N 个位置。

紧接着,查看 bit 数组中这 N 个位置上的 bit 值。只要这 N 个 bit 值有一个为 0,这就表明布隆过滤器没有对该数据做过标记,所以,查询的数据一定没有在数据库中保存。

在这里插入图片描述

所以,可以把所有可能的值保存在布隆过滤器中,从缓存读取数据前先过滤一次:

  • 如果布隆过滤器认为值不存在,那么值一定是不存在的,无需查询缓存也无需查询数据库;
  • 对于极小概率的误判请求,才会最终让非法 Key 的请求走到缓存或数据库。
private BloomFilter bloomFilter;@PostConstruct
public void init() {//创建布隆过滤器,元素数量10000,期望误判率1%bloomFilter = BloomFilter.create(Funnels.integerFunnel(), 10000, 0.01);//填充布隆过滤器IntStream.rangeClosed(1, 10000).forEach(bloomFilter::put);
}@GetMapping("right2")
public String right2(@RequestParam("id") int id) {String data = "";//通过布隆过滤器先判断if (bloomFilter.mightContain(id)) {String key = "user" + id;//走缓存查询data = redisUtil.get(key);if (StringUtils.isEmpty(data)) {//走数据库查询data = getCityFromDb(id);if (!StringUtils.isEmpty(data)) {redisUtil.set(key,data,30);} else {//设置一个特殊的值redisUtil.set(key,"NODATA",30);}}}return data;
}

前端进行请求检测

对于大量的恶意请求访问不存在的数据,可以在请求入口前端,对业务系统接收到的请求进行合法性检测,把恶意的请求(例如请求参数不合理、请求参数是非法值、请求字段不存在)直接过滤掉,不让它们访问后端缓存和数据库。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/886055.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[C++11] 包装器 : function 与 bind 的原理及使用

文章目录 functionstd::function 的基本语法使用 std::function 包装不同的可调用对象function包装普通成员函数为什么要传入 this 指针参数&#xff1f;传入对象指针与传入对象实例的区别 例题 &#xff1a;150. 逆波兰表达式求值 - ⼒扣&#xff08;LeetCode&#xff09; bin…

企业一站式管理系统odoo的研究——系统搭建

大纲 1. 环境准备1.1 安装操作系统1.2 更新操作系统1.3 配置用户组和用户1.3.1 创建用户组 odoo1.3.2. 创建用户 odoo1.3.3. 设置用户 odoo 的密码1.3.4. 验证用户和组1.3.5. 将用户 odoo 添加到添加sudo组&#xff1a;1.3.6. 切到odoo用户 2. 安装 Odoo1. 安装依赖项目2.2. 安…

今天给在家介绍一篇基于jsp的旅游网站设计与实现

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…

SMA-BP基于黏菌算法优化BP神经网络时间序列预测

项目源码获取方式见文章末尾&#xff01; 600多个深度学习项目资料&#xff0c;快来加入社群一起学习吧。 《------往期经典推荐------》 项目名称 1.【基于CNN-RNN的影像报告生成】 2.【卫星图像道路检测DeepLabV3Plus模型】 3.【GAN模型实现二次元头像生成】 4.【CNN模型实现…

vue3+ts+antd 运行报错 convertLegacyToken is not a function

以上代码报错 在github上看到有将 const v3Token convertLegacyToken(mapToken); 改成 const v3Token convertLegacyToken.default(mapToken);运行时就不报错了 但是到了打包的时候还是报错 但是ctrl点击convertLegacyToken能够正常跳转过去 于是我打印convertLegacyToken 发…

StarRocks Summit Asia 2024 全部议程公布!

随着企业数字化转型深入&#xff0c;云原生架构正成为湖仓部署的新标准。弹性扩展、资源隔离、成本优化&#xff0c;帮助企业在云上获得了更高的灵活性和效率。与此同时&#xff0c;云原生架构也为湖仓与 AI 的深度融合奠定了基础。 在过去一年&#xff0c;湖仓技术与 AI 的结…

HTML之列表学习记录

练习题&#xff1a; 图所示为一个问卷调查网页&#xff0c;请制作出来。要求&#xff1a;大标题用h1标签&#xff1b;小题目用h3标签&#xff1b;前两个问题使用有序列表&#xff1b;最后一个问题使用无序列表。 代码&#xff1a; <!DOCTYPE html> <html> <he…

掌控板micropython编程实现网页实时显示板载光线传感器的值

掌控板micropython编程实现网页实时显示板载光线传感器的值 一、AJAX简介 AJAX&#xff08;Asynchronous JavaScript and XML&#xff09;是一种在无需重新加载整个页面的情况下&#xff0c;能够更新部分网页内容的技术。它允许Web页面与服务器进行异步数据交换&#xff0c;这…

Linux 常用操作指令大揭秘(下)

&#x1f31f;快来参与讨论&#x1f4ac;&#xff0c;点赞&#x1f44d;、收藏⭐、分享&#x1f4e4;&#xff0c;共创活力社区。 &#x1f31f; &#x1f6a9;用通俗易懂且不失专业性的文字&#xff0c;讲解计算机领域那些看似枯燥的知识点&#x1f6a9; 目录 &#x1f4af;…

Spring源码(十二):Spring MVC之Spring Boot

本篇将详细讨论Spring Boot 的启动/加载、处理请求的具体流程。我们先从一个简单的Spring Boot项目日志开始分析&#xff08;这里假设读者已经仔细阅读完了前面的文章&#xff0c;且对Spring源码有一定深度的了解&#xff0c;否则会看得一脸懵逼&#xff09;。 本文为2024重置…

【C语言刷力扣】13.罗马数字转整数

题目&#xff1a; 解题思路: 倒序遍历&#xff0c;若当前字符代表的数字比上一字符代表的数字小&#xff0c;即减去当前字符数字。 时间复杂度&#xff1a; 空间复杂度&#xff1a; int romanToInt(char* s) {int ans 0;int low 0;int num[26];num[I - A] 1;num[V - A]…

【Unity Bug 随记】unity version control 报 xx is not in a workspace.

可能原因是更改了仓库或者项目名称。 解决办法就是重置Unity Version Control&#xff0c;去Hub disconnect 然后重新connect cloud和UVC UVC可能连不上&#xff0c;直接进入项目就行&#xff0c;打开版本管理标签会让你重新连工作区&#xff0c;选择你的仓库和工作区 然后In…

springboot读取modbus数据

1、引入依赖 jlibmodbus <dependency><groupId>com.intelligt.modbus</groupId><artifactId>jlibmodbus</artifactId><version>1.2.9.7</version> </dependency> 2、数据获取 public String processData(String ip) {tr…

LabVIEW中坐标排序与旋转 参见附件snippet程序

LabVIEW中坐标排序与旋转 参见附件snippet程序LabVIEW中坐标排序与旋转 参见附件snippet程序 - 北京瀚文网星科技有限公司 在LabVIEW中处理坐标排序的过程&#xff0c;尤其是按顺时针或逆时针排列坐标点&#xff0c;常见的应用包括处理几何形状、路径规划等任务。下面我将为您…

51单片机应用开发(进阶)---定时器应用(电子时钟)

实现目标 1、巩固定时器的配置流程&#xff1b; 2、掌握按键、数码管与定时器配合使用&#xff1b; 3、功能1&#xff1a;&#xff08;1&#xff09;简单显示时间。显示格式&#xff1a;88-88-88&#xff08;时-分-秒&#xff09; 4、功能2&#xff1a;&#xff08;1&#…

FPGA实现PCIE采集电脑端视频转SFP光口万兆UDP输出,基于XDMA+GTX架构,提供2套工程源码和技术支持

目录 1、前言工程概述免责声明 2、相关方案推荐我已有的PCIE方案10G Ethernet Subsystem实现万兆以太网物理层方案 3、PCIE基础知识扫描4、工程详细设计方案工程设计原理框图电脑端视频PCIE视频采集QT上位机XDMA配置及使用XDMA中断模块FDMA图像缓存UDP视频组包发送UDP协议栈MAC…

使用 unicorn 和 capstone 库来模拟 ARM Thumb 指令的执行(一)

import binascii import unicorn import capstonedef printArm32Regs(mu):for i in range(66,78):print("R%d,value:%x"%(i-66,mu.reg_read(i)))def testhumb():CODE b\x1C\x00\x0A\x46\x1E\x00"""MOV R3, R0 的机器码&#xff1a;0x1C 0x00&#xf…

git重置的四种类型(Git Reset)

git区域概念 1.工作区:IDEA中红色显示文件为工作区中的文件 (还未使用git add命令加入暂存区) 2.暂存区:IDEA中绿色(本次还未提交的新增的文件显示为绿色)或者蓝色(本次修改的之前版本提交的文件但本次还未提交的文件显示为蓝色)显示的文件为暂存区中的文件&#xff08;使用了…

第三十一天|贪心算法| 56. 合并区间,738.单调递增的数字 , 968.监控二叉树

目录 56. 合并区间 方法1&#xff1a;fff 看方法2&#xff1a;fff优化版 方法3&#xff1a; 738.单调递增的数字 968.监控二叉树&#xff08;贪心二叉树&#xff09; 56. 合并区间 判断重叠区间问题&#xff0c;与452和435是一个套路 方法1&#xff1a;fff 看方法2&am…

LeetCode 热题100(八)【二叉树】(3)

目录 8.11二叉树展开为链表&#xff08;中等&#xff09; 8.12从前序与中序遍历序列构造二叉树&#xff08;中等&#xff09; 8.13路径总和III&#xff08;中等&#xff09; 8.14二叉树的最近公共祖先&#xff08;中等&#xff09; 8.15二叉树中的最大路径和&#xff08;困…