缓存穿透全方位防护:空值缓存、布隆过滤器与限流降级
2026/9/9 22:35:28 网站建设 项目流程

缓存穿透是后端开发中最经典也最容易引发事故的问题之一。它的特征很直接:请求一直在查询一个不存在的数据,缓存永远无法命中,所有查询压力全部落到数据库上。平时流量不高看不出问题,一旦遇到恶意刷接口、爬虫扫描,或者大量自然请求命中不存在的 key,数据库连接池会迅速被打满,接口开始超时,最终影响所有依赖这个数据库的业务。

要防住这种问题,核心思想可以理解为“给数据库请几个不同岗位的保安”:入口参数校验负责拦掉明显非法的请求,布隆过滤器负责判断 key 是否可能存在,缓存空值负责把“查不到”的结果也存下来,限流降级则负责在极端流量下保住数据库的最后底线。这四层手段可以单独使用,也可以组合使用,大多数生产环境通常采用两到三层的组合方案。

这篇内容会从故障模型开始,把缓存穿透的成因、演进路径讲清楚,然后逐层讲解四类防护方案的实现思路和 Java 代码示例,最后对比选型、说明性能观察方法,并给出一份常见问题排查清单。适合正在做后端开发、准备数据库面试,或者想加固现有缓存体系的人阅读。文中的代码基于 Spring Boot + Redis 的常见写法,也可以迁移到 Go、Python 或者其他 Redis 客户端实现中。

先说明一个适用范围问题:缓存穿透和底层数据库选型没有直接关系。无论你用的是 MySQL、PostgreSQL,还是达梦数据库、Oracle,只要业务逻辑是“缓存未命中就去查库”,穿透风险就存在。下面所有方案都适用于这类关系型数据库场景。

1. 核心能力速览:一套完整的缓存穿透防护方案

能力项说明
解决问题大量不存在的数据请求绕过缓存,直接打到数据库
典型后果数据库连接池打满、接口超时、服务雪崩
防护层次参数校验、布隆过滤器、缓存空值、限流降级、多级缓存兜底
依赖组件Redis、应用本地内存(Caffeine)、数据库连接池
适配数据库MySQL、PostgreSQL、达梦数据库、Oracle、Doris 等
实现语言Java 示例,可迁移到 Go / Python / C++
对业务侵入性低,主要在查询入口和数据写入层做改动
上线前检查项缓存命中率、数据库 QPS、内存占用、误判率

这套方案不是某个开源框架,而是可落地的工程实践组合。每一层有各自的作用,也有各自的成本。下面先从“什么是缓存穿透”开始,把问题模型建立起来。

2. 缓存穿透、缓存击穿、缓存雪崩:三个关键概念别搞混

很多文章把这几个问题混在一起讲,但实际它们是三种不同的故障,需要不同的防护手段。

2.1 缓存穿透

缓存穿透指查询一个数据库和缓存中都不存在的数据。例如用户表里没有userId = 9527这条记录,但请求不断携带这个 id 访问接口。由于缓存中没有数据,每次请求都会穿透缓存去查询数据库,而数据库也查不到结果,因此不会把结果回写缓存,下一次相同请求仍然打到数据库。

穿透的本质是:请求对应的 key 是真实不存在的,导致各个缓存层完全没有命中机会

2.2 缓存击穿

缓存击穿指某个热点 key 在缓存过期的瞬间,大量并发请求同时发现缓存没命中,于是同时去数据库查询。和穿透的区别在于:击穿的对象是存在但恰好过期的 key,穿透的对象是根本不存在的 key。

2.3 缓存雪崩

缓存雪崩指大量 key 在同一时间段集中过期,或者缓存节点宕机,导致大量请求在短时间内涌入数据库。它更像一种大规模、系统级的故障,解决思路通常是过期时间加随机值、缓存高可用、限流熔断等。

2.4 快速对比

问题针对数据触发条件核心防护
缓存穿透不存在的 key持续请求不存在的数据缓存空值、布隆过滤器、参数校验
缓存击穿单个热点 key热点 key 恰好过期互斥锁、逻辑过期、永不过期
缓存雪崩大量 key集中过期或缓存宕机过期时间随机化、缓存集群、限流

本文重点讲缓存穿透。但实际项目中,击穿和雪崩的防护手段也需要一并考虑,因为它们经常同时出现。

3. 缓存穿透的典型场景与故障演进

3.1 什么请求会打穿缓存

最常见的场景有三类。

第一类:恶意攻击或扫描。攻击者遍历接口参数,批量请求不存在的 ID、手机号、订单号。这种请求通常是自动化脚本,QPS 可以拉得很高。

第二类:业务数据本身不存在。比如用户在前端发起了一个查询,但后端对应的记录因为未上线、已删除、权限不足等原因查不到。当一个功能刚上线、历史数据还没迁移完成时,这种情况最明显。

第三类:参数格式问题。调用方传入了非法格式的 ID,例如把字符串拼进数值型主键字段,底层查询自然永远查不到结果。

3.2 故障演进路径

只要线上存在上面任意一类场景,故障演进是有固定路径的:

  1. 少量穿透请求到达应用层,缓存未命中,应用去查数据库。
  2. 穿透请求量持续增加,数据库连接池中的连接开始不够用。
  3. 正常业务的数据库查询也需要等待连接,接口响应时间拉长。
  4. 上层服务开始超时重试,重试又带来更多请求,形成恶性循环。
  5. 如果数据库没有做好连接数和 CPU 保护,数据库实例可能直接进入高负载状态,最终导致整条业务链路不可用。

3.3 用数字感受一下压力

假设数据库单机最多能扛 5000 QPS 的简单查询,如果穿透请求打到 2 万 QPS,即使正常业务只需要几百 QPS,数据库也会被这 2 万无效查询压垮。穿透请求的成本极低,但数据库处理一个无效查询的成本和有效查询几乎一样,这才是最棘手的地方。

4. 适用场景与使用边界

4.1 适合用什么场景

  • 基于主键 ID 查询详情的接口,例如用户详情、订单详情、商品详情。
  • 外部开放接口或容易被脚本刷的接口。
  • 缓存命中率偏低的业务,例如数据冷热不均衡的系统。
  • 新系统上线初期,大量 key 尚未被预热,空查询比例较高。

4.2 不适合什么场景

  • 写入频繁且几乎不读的数据,没有必要做缓存防护,做了反而增加一致性问题。
  • 数据库查询本身极快且压力不大的内部接口,加防护层反而浪费资源。
  • key 数量极大且生命周期极短的数据,比如实时日志流,布隆过滤器维护成本太高。

4.3 合规与安全边界

需要特别提醒:在设计防护方案时,涉及用户 ID、手机号、订单号等敏感数据,日志和监控中不要记录完整的业务数据。做恶意请求拦截时,需要基于真实业务规则判断,不能因为某个用户触发多次空查询就直接封禁账号,要在保护系统的同时避免误伤正常用户。生产环境做压测前,必须先申请测试账号和测试链路,避免对线上数据库造成次生故障。

5. 防护方案一:缓存空值,成本最低的一层保安

5.1 核心思路

数据库查不到数据时,不要直接返回 null,而是把一个特殊标记写入缓存,并设置较短的过期时间。这样后续相同请求在 TTL 内直接命中缓存,看到标记后直接返回空结果,不再查数据库。

缓存空值方案适合大多数业务,实现简单,不需要引入额外组件。它的缺点是:每个不存在的 key 都会占据一份 Redis 内存,如果攻击者用随机 ID 不停构造请求,缓存中会出现大量无效 key,反而带来内存压力。因此空值的 TTL 必须比正常数据短很多。

5.2 Java 实现示例

public class CachePenetrationGuard { private final StringRedisTemplate redisTemplate; private final UserMapper userMapper; private static final String EMPTY_FLAG = "EMPTY"; // 正常数据缓存 10 分钟 private static final long NORMAL_TTL_MINUTES = 10; // 空值缓存只放 2 分钟,避免大量空 key 堆积 private static final long EMPTY_TTL_MINUTES = 2; public UserVO getUserById(String userId) { String cacheKey = "user:detail:" + userId; // 第一步:查缓存 String cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { // 命中空值标记,直接返回 null,不查数据库 if (EMPTY_FLAG.equals(cached)) { return null; } return JSON.parseObject(cached, UserVO.class); } // 第二步:缓存未命中,查数据库 UserVO user = userMapper.selectById(userId); if (user != null) { redisTemplate.opsForValue().set( cacheKey, JSON.toJSONString(user), NORMAL_TTL_MINUTES, TimeUnit.MINUTES); } else { redisTemplate.opsForValue().set( cacheKey, EMPTY_FLAG, EMPTY_TTL_MINUTES, TimeUnit.MINUTES); } return user; } }

5.3 注意事项

空值标记要能和正常数据区分开。建议用一个业务中不会出现的字符串,例如EMPTY,或者用一个包装对象保存类型字段,避免误把空值当真实数据解析。

空值 TTL 不能太长。一般设置 1 到 5 分钟。如果数据被创建后用户立刻需要能看到,TTL 过长会导致短时间数据不一致。如果业务对一致性要求较高,可以缩到 30 秒左右。

当数据库写入新数据时,主动删除对应的空值缓存,可以缩短不一致时间。在新增用户、创建订单等写入逻辑里,执行完数据库写入后,直接删除缓存 key,下一轮查询自然读到新值。

5.4 优点与局限

这个方案的优点是实现成本低、立竿见影,不用引入新的中间件。局限是只能拦截“已经出现过一次”的 key,如果攻击者每次都用全新随机 ID,缓存空值就没有命中机会,内存还可能被打满。因此,缓存空值适合配合下面几种方案一起用。

6. 防护方案二:布隆过滤器,入口处先拦一道

6.1 原理解析

布隆过滤器是一个位数组加多个哈希函数构成的数据结构。添加元素时,把元素经过 k 个哈希函数映射到位数组的 k 个位置,并把对应位置置为 1。判断元素是否存在时,如果所有映射位置都是 1,则元素可能存在;如果任意一个位置是 0,则元素一定不存在。

布隆过滤器有两个核心特点:

  • 判断“不存在”是绝对准确的。
  • 判断“存在”有一定概率误判,也就是把不存在的元素误判为存在。

这个特点正好适合缓存穿透防护:查询前先用布隆过滤器判断 key 是否存在,如果过滤器说“一定不存在”,直接返回空结果;只有过滤器说“可能存在”时,才允许进入后续缓存和数据库查询逻辑。

6.2 应用内布隆过滤器实现

在单机应用或数据量可控的场景,可以使用 Guava 提供的BloomFilter

@Component public class UserBloomFilter { private final BloomFilter<Long> userIdFilter; public UserBloomFilter() { // 预计插入 100 万个用户 ID,误判率设为 1% userIdFilter = BloomFilter.create( Funnels.longFunnel(), 1_000_000L, 0.01); } @PostConstruct public void init() { List<Long> userIds = userMapper.selectAllIds(); userIds.forEach(userIdFilter::put); System.out.println("布隆过滤器初始化完成,加载 ID 数量:" + userIds.size()); } public boolean mightExist(Long userId) { return userIdFilter.mightContain(userId); } }

查询服务在查缓存前先调用mightExist方法:

public UserVO getUserById(Long userId) { if (!userBloomFilter.mightExist(userId)) { // 布隆过滤器判断一定不存在,直接返回 return null; } // 再走缓存和数据库查询 return doQuery(userId); }

这个方案能挡住绝大多数不存在的 key 的请求,因为过滤器对“一定不存在”的判断不会出错。只有那些被误判为存在的 key 会继续向下传递,这部分请求再交给缓存空值方案去拦截。

6.3 分布式场景与 Redis 布隆过滤器

单机 Guava 布隆过滤器在应用重启后会丢失数据,需要重新加载全量 ID。如果应用是多实例部署,每个实例都要维护一份,数据一致性很难保证。这时更推荐使用 Redis 布隆过滤器。

第一种方式是使用 Redis 4.0 提供的BF.ADDBF.EXISTS命令,需要 Redis 加载布隆过滤器模块。第二种方式是使用 Redisson 等客户端提供的分布式布隆过滤器:

RBloomFilter<Long> bloomFilter = redisson.getBloomFilter("user-id-bloom"); // 初始化:预计元素数量 100 万,误判率 1% bloomFilter.tryInit(1_000_000L, 0.01); // 启动时加载已有 ID List<Long> userIds = userMapper.selectAllIds(); userIds.forEach(bloomFilter::add); // 查询时判断 boolean exists = bloomFilter.contains(userId);

Redisson 会把位数组存储在 Redis 中,多个应用实例共享同一个过滤器,重启应用后不需要重新加载全量数据。

6.4 使用布隆过滤器要注意的问题

第一个问题是删除操作。布隆过滤器不支持删除单个元素。如果一个业务 ID 被删除后不再有效,它仍然会被过滤器判断为可能存在,多出的请求会继续往下走。解决办法是定期重建过滤器,或者在关键删除操作后触发全量重建。

第二个问题是初始化成本。全量加载数据库 ID 到布隆过滤器时,如果数据量很大,启动过程会比较慢。可以选择在低峰期做异步初始化,或者用批量管道写入 Redis。

第三个问题是误判率。误判率越低,需要的位数组空间越大。通常设置 0.01 到 0.001 已经足够。数据量增长后,原过滤器容量不足,误判率会上升,需要基于实际数据量重新评估容量。

7. 防护方案三:参数校验、限流与降级

缓存空值和布隆过滤器主要解决“key 不存在”的穿透问题。但有时候请求本身就不该进入查询链路,或者流量已经高到超出系统承受能力,这时需要更前置的拦截手段。

7.1 参数合法性校验

第一层拦截是参数校验。很多穿透请求是参数本身就不合法,比如:

  • 主键 ID 应为正整数,却传入了负数或 0。
  • ID 应为定长字符串,却传入了超长字符串。
  • 用户手机号应为 11 位,却传入了字母组合。

这些请求可以在 Controller 层直接拦截,返回参数错误,完全不需要进入查询逻辑。

@GetMapping("/user/{userId}") public Result<UserVO> getUserById(@PathVariable Long userId) { // 参数校验:ID 必须为正整数,且不超过业务上限 if (userId == null || userId <= 0 || userId > 100_000_000L) { return Result.error("非法用户 ID"); } // 后续缓存和数据库查询 UserVO user = userService.getById(userId); return Result.ok(user); }

参数校验不能只做前后端联动校验,后端服务必须独立校验。尤其是在开放接口场景下,调用方可能完全绕过前端页面,直接发 HTTP 请求。后端校验是最后一道参数防线。

7.2 限流

限流的核心目标是控制“进入查询链路”的请求速率。即使前面几层都没拦住,限流也能保证数据库的 QPS 不会超过安全阈值。

常用的限流方案有三种:

  • 单机限流:使用 Guava RateLimiter 或 Semaphore 限制单实例的并发查询数。
  • 分布式限流:使用 Redis + Lua 脚本实现令牌桶或滑动窗口算法。
  • 网关/Sentinel 限流:在流量进入应用前统一拦截。

以 Sentinel 为例,可以在查询接口上配置 QPS 限流规则:

# Sentinel 限流规则配置示意 # 资源名:/user/{userId} # 阈值:单机 QPS 500 # 流控效果:快速失败

被限流的请求直接返回“系统繁忙,请稍后重试”,不再向下传递。限流是最后的保护伞,它可以牺牲一部分请求,但不让数据库被打垮。

7.3 熔断与降级

当数据库或下游服务的错误率升高时,需要触发熔断。熔断打开后,后续请求在短时间内直接走降级逻辑,不调用数据库。降级策略可以是返回默认数据、返回历史缓存快照,或者直接返回一个可读的错误提示。

在缓存穿透防护中,熔断降级尤其重要。如果数据库已经被异常流量打到高负载,继续让所有请求进入查询链路只会加速崩溃。通过熔断降低对数据库的调用频率,给数据库留出恢复时间。

8. 防护方案四:热点数据多级缓存与兜底

布隆过滤器和缓存空值可以拦截大部分穿透,但面对超高并发时,应用层到 Redis 的连接也是资源。如果同一个热点 key 被大量请求同时命中,Redis 虽然能扛住,应用线程和网络带宽也有压力。更合理的方式是把热点数据再做一层本地缓存。

8.1 本地缓存兜底

应用本地缓存可以使用 Caffeine 或 Guava Cache,放在 Redis 之前。查询流程变成:

  1. 查本地缓存,命中则直接返回。
  2. 本地缓存未命中,查 Redis。
  3. Redis 未命中,查数据库。
  4. 数据库结果回写 Redis,本地缓存也保存一份。

本地缓存放在应用进程内,查询不走网络,性能极好。但由于每个实例各自保存一份,数据一致性最弱。它适合读多写少、一致性要求不高的热点数据,例如商品详情、用户昵称、配置项等。

8.2 数据库连接池保护

防护方案再完善,也不可能做到 100% 拦截所有无效请求。数据库连接池本身的参数配置也需要调整。连接池的maximum-pool-size不宜设置过大,否则数据库线程数被拉高,操作系统上下文切换成本暴涨,整体吞吐反而下降。合理设置等待队列长度,让超出能力的请求快速失败,而不是无限排队等待。

以 HikariCP 为例,常见的合理配置思路是:

spring: datasource: hikari: # 最大连接数,按数据库实例规格估算 maximum-pool-size: 20 # 连接最大空闲时间 idle-timeout: 600000 # 获取连接的超时时间,超过则快速失败 connection-timeout: 2000

这些参数没有固定标准,需要结合数据库规格、业务 QPS 和慢查询情况调整。核心原则是:宁可快速失败,不要让所有请求堆积在连接池等待队列里。

9. 四种方案对比与选型建议

方案实现成本内存/资源成本误判或副作用适用场景
缓存空值最低每个空 key 占一份缓存空 key 过多会占内存空查询量不大、key 可枚举
布隆过滤器中等位数组占用固定内存判断存在有误判率key 全量可加载、删除不频繁
参数校验 + 限流 + 降级中低几乎无额外内存限流可能误伤正常请求开放接口、高并发入口
多级缓存兜底中高增加本地内存占用数据一致性弱热点数据读多写少

实际项目很少只用一个方案。最常见的组合是:

  • 参数校验 + 缓存空值,这是最基本的配置,任何查询类接口都应该做。
  • 数据量可控且 ID 有规律的业务,再叠加布隆过滤器。
  • 高并发且容易被脚本刷的开放接口,再加上限流和熔断降级。
  • 热点数据访问频繁时,再加本地缓存。

从成本角度考虑,建议按顺序逐步加:先做参数校验和缓存空值,观察缓存命中率;如果数据库压力依然高,再加布隆过滤器;如果还存在超预期流量,再上限流。不要一上来就全上,每一层都有维护成本。

10. 部署效果与性能观察

上线防护方案后,不能只看功能正常,还要量化验证效果。

10.1 观察缓存命中率

缓存命中率是判断穿透防护是否有效的核心指标。在 Redis 中可以使用INFO stats命令查看:

keyspace_hits: 98000 keyspace_misses: 2000

命中率 = keyspace_hits / (keyspace_hits + keyspace_misses)。正常情况下,缓存命中率越高,说明数据库查询次数越少。如果一个业务接口的缓存命中率长期低于 80%,就需要检查是否有大量无效 key 在穿透。

在业务层也可以埋点统计:进入查询接口的总请求数、实际查询数据库的次数、命中空值缓存的次数,分别打点上报到监控系统。

10.2 确认数据库压力下降

数据库侧的观察指标包括:

  • 数据库 QPS:压在 MySQL 或达梦数据库、Oracle 上的查询量是否下降。
  • 数据库连接数:通过连接池监控查看活跃连接数是否趋于平稳。
  • 慢查询数量:是否存在大量重复的无效查询语句。
  • CPU 使用率:数据库实例 CPU 是否回到正常水位。

上线前先在测试环境压测,模拟一批不存在的 key 请求,对比加防护前后的数据库 QPS 和接口响应时间。上线后不要立刻做全量切换,可以先放 5% 流量观察指标,确认数据库侧压力明显下降后再逐步放量。

10.3 内存占用变化

缓存空值方案会新增 Redis 内存占用,布隆过滤器的位数组也会占用一定空间。上线前要预估内存变化:

  • 空值缓存按条数估算,每条大约几十字节,乘以 TTL 时间窗内的空 key 数量。
  • 布隆过滤器按公式计算位数组长度,或者直接根据误判率和预估元素数调整tryInit参数。

如果发现 Redis 内存增长过快,优先检查空值 TTL 是否过长,或者布隆过滤器误判率是否设置过严导致位数组过大。

10.4 压测建议

压测时使用 JMeter 或其他压测工具,分别测三组场景:

  • 正常存在的 key 请求,观察基线 QPS 和响应时间。
  • 不存在的随机 key 请求,观察数据库 QPS 是否暴涨。
  • 混合场景,70% 正常 key 加 30% 随机不存在 key,观察开启防护前后的性能差异。

压测环境要与生产环境的数据库规格、网络延迟尽量保持一致,否则压测结果只能作为参考,不能直接推导线上容量。

11. 常见问题与排查方法

问题现象可能原因排查方式解决方案
加了缓存空值后内存涨得很快空 key TTL 太长,随机 key 太多查看 Redis key 数量和内存占用缩短空值 TTL,增加布隆过滤器前置拦截
布隆过滤器拦截后正常数据也查不到ID 未正确加载到过滤器检查启动初始化日志,确认数据量重建过滤器,使用增量加载脚本
布隆过滤器误判率高数据量超过预估容量,位数组太小查看过滤器容量和误判率参数扩容位数组,重新初始化
限流把正常流量也限掉了阈值设置过小,或限流规则作用域过大查看限流日志和监控曲线调大阈值,按接口拆分限流规则
数据库 QPS 仍然很高防护层未覆盖所有查询入口检查是否有其他代码路径绕过缓存直接查库统一走缓存查询入口,禁止散落查询代码
缓存空值导致数据不一致数据更新后空值缓存未删除数据写入时是否执行了删除缓存操作写入逻辑中主动删除旧 key,缩短空值 TTL
重启应用后布隆过滤器失效使用应用内 Guava 过滤器,数据未持久化检查重启后的命中情况改用 Redis 布隆过滤器或启动时全量重建
Redis 连接数过多加了布隆过滤器但 Redis 连接池配置过小查看 Redis 客户端连接数调大连接池,或对热点 key 增加本地缓存

排查时建议从一条完整请求链路开始:请求进入应用后,是否先过参数校验,再过布隆过滤器,再查本地缓存,再查 Redis,最后才到数据库。每一个环节打印一条轻量日志,记录命中状态,就能快速定位是哪一层没有拦住。

12. 最佳实践与合规提醒

12.1 工程实践建议

第一,防护方案要统一封装,不要散落在业务代码里。建议做一个公共方法,比如getWithBloomFilterAndEmptyCache,业务代码只调用一个方法,内部完成布隆过滤器判断、缓存查询、空值写入的逻辑。这样新接入的业务不需要重新理解整套流程,也不容易漏掉某一层防护。

第二,空值缓存和正常缓存的 key 命名要能区分。建议使用不同的前缀,例如empty:user:detail:9527user:detail:9527,方便排查问题时快速识别。

第三,布隆过滤器需要定期维护。业务数据量增长时,原过滤器容量可能不足。建议在数据量增长到预估值的 80% 时,准备重建过滤器。

第四,监控告警要覆盖关键指标。至少要配置以下告警:

  • 缓存命中率低于阈值。
  • 数据库活跃连接数超过连接池上限的 70%。
  • 接口响应时间 P99 超过业务 SLA。
  • Redis 内存占用超过实例容量的 70%。

第五,不要在代码和日志中记录完整用户 ID、手机号等敏感信息。排查问题需要关联请求时,可以使用脱敏后的标识。

12.2 合规与安全使用边界

在设计拦截和限流规则时,要基于业务合理性,不能因为用户触发了空查询就永久拉黑用户。拉黑账号、限制访问等功能涉及用户权益,必须有明确的业务规则和管理流程。

如果系统面向外部开放 API,还需要做好调用方身份认证和配额管理。每个调用方分配独立的 AppKey 和配额,单调用方超量时只对该调用方限流,不影响其他正常调用方。这样即使有恶意调用方在刷不存在的 key,影响范围也能被控制在最小单元。

涉及商业数据、用户个人信息时,缓存中的数据同样需要遵循权限管理和加密要求。存储用户缓存信息时,按业务需要设置合理的 TTL,避免敏感数据长时间滞留缓存层。

13. 总结

缓存穿透是数据库最常见的人为灾害之一,但它的防护手段并不复杂。优先级建议从低到高:先把参数校验做扎实,再上缓存空值,数据量大之后加布隆过滤器,最后用限流熔断兜底。

任何方案都要以数据说话。上线前在测试环境压测三组场景,上线后盯住缓存命中率、数据库 QPS、Redis 内存三个指标。穿透请求拦截得干净,数据库压力自然就下来了。

如果现在项目里还没有任何穿透防护,建议先从缓存空值开始动手,这是成本最低、见效最快的一步。写完代码后,别忘了补上监控指标,否则下次故障来了,你可能都不知道是哪一层被穿透的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询