资讯动态

Redis缓存优化实战:5个技巧有效降低数据库压力

发布时间:2026/9/11 14:17:38 来源:尧图企业网站定制
数据库压力大这种问题我见得太多了。很多团队聊到性能优化第一反应就是上Redis可真上了之后慢查询该有还是有连接数该爆还是爆CPU该飘还是飘。有一回我帮一个项目看问题登录服务器查了一下Redis的INFO命中率只有35%。什么意思三分之二的请求根本没用到缓存全打在数据库上了。问题不在Redis本身而在于缓存这层设计压根没想清楚。这篇文章不绕弯子直接讲我在生产环境里用Redis给数据库减负的5个核心技巧。每个技巧都包含三个部分为什么这么做、具体怎么落地、以及我踩过的坑。不管你现在是刚把Redis引进来还是已经用了一阵子但性能还是不理想这5个点都能直接拿来用。1. 数据库压力的本质命中率才是关键变量1.1 为什么快10倍是可能的三个数量级的延迟对比先说个很多人忽略的基础事实数据库查询慢不是SQL写得烂就是访问路径实在太长了。我经常用一个延迟对比来给团队讲清楚这件事访问目标典型耗时量级本地内存如Caffeine0.01 ~ 0.1ms纳秒~微秒Redis内网、内存中命中0.5 ~ 2ms微秒~毫秒MySQL单次查询含网络、连接池、SQL解析5 ~ 50ms毫秒磁盘随机IO10 ~ 100ms毫秒~十毫秒从数据库到Redis是几十倍的差距从Redis再到本地内存又是几十倍。缓存的价值本质就是把高延迟的访问替换成低延迟的访问把大部分请求挡在数据库前面。我用一个实际算例来说明白快10倍是怎么来的。假设你有个接口数据库平均查询耗时20msRedis命中平均1ms100%请求走数据库平均RT 20ms命中率90%走Redis10%穿透到DB平均RT 0.9 × 1 0.1 × 20 2.9ms接口响应时间从20ms降到2.9ms这就是接近7倍的提升。如果再把热点数据用本地缓存接住让90%请求走本地内存约0.05ms、5%走Redis、5%走DB平均RT 0.9 × 0.05 0.05 × 1 0.05 × 20 ≈ 1.1ms。换算过来就是18倍。所以快10倍不是口号是数量级的胜利。1.2 命中率算一笔账同样的QPS数据库背的压力差四倍命中率的公式很简单命中率 缓存命中的请求数 / 总请求数。在Redis里可以通过INFO stats拿到两个关键计数keyspace_hits缓存命中的次数keyspace_misses缓存未命中的次数命中率就是 keyspace_hits / (keyspace_hits keyspace_misses)。我为什么总盯着这个数字因为它是缓存价值的直接度量。假设系统总QPS是20000你原本缓存命中率是80%那数据库承受的是4000 QPS如果把命中率提到95%穿透到数据库的就只有1000 QPS。数据库压力直接降了四倍连接池也不容易打满慢查询自然就少了。所以判断一个系统需不需要做缓存治理先别看代码看命中率。如果命中率长期低于70%说明要么缓存的数据选错了要么过期策略有问题要么压根就没做好防穿透。接下来的技巧就是围绕把命中率提上去展开的。2. 技巧一动笔之前先把缓存什么、过期多久、怎么更新定清楚2.1 判断数据该不该缓存三个特征就够了Redis不是数据库的影子什么东西都往里面塞只会浪费内存、增加一致性风险。我判断一个数据能不能缓存就看三个特征读多写少读的频次远大于写。商品详情、配置项、用户资料、分类树都是典型代表。反过来如果一条数据每秒被写10次读只有1次缓存它纯属找罪受。数据量可控且热点集中数据可能有几十万条但大部分访问打在同一小批数据上。比如一个电商平台的商品有500万但当天有90%流量都集中在2000个爆款上这种分布最适合缓存。能容忍短时不一致缓存和数据库之间允许存在几秒甚至更久的差异。比如商品名称多一个字少一个字用户刷新一下就能看到最新值但库存扣减、余额变动这种强一致场景就不能简单套缓存。实操建议你把数据库慢查询日志拉出来按执行次数从高到低排序看前20条SQL查的数据是否满足上面三个特征。如果满足优先优化这几个点比无差别缓存一大片数据有效得多。2.2 过期时间千万别用同一个值按业务分三个档我见过不少项目所有缓存key过期时间统一设成30分钟图省事。但这样做的隐患很大一旦这批缓存同时失效数据库瞬间要扛住原本十倍的查询流量这就是后面会讲到的缓存雪崩的典型前兆。我习惯把过期时间分成三档管理核心基础数据如商品标题、图片、类目信息可以缓存30分钟到1小时加上一个60~300秒的随机偏移避免同一秒集体过期。实时性要求中等的数据如库存数量、价格过期时间压到5~15秒宁可多穿透几次也别给用户展示过期的价格。变化极低频的数据如配置项、城市列表可以缓存到24小时甚至更长但要留意发布变更时手动清理。代码里推荐这样设置// 基础缓存30分钟 随机偏移防止雪崩 long ttl 1800 RandomUtil.randomLong(60, 300); stringRedisTemplate.opsForValue() .set(product:detail: productId, jsonStr, Duration.ofSeconds(ttl));核心思想是过期时间本身就是业务策略的一部分不是随意填的数字。每个key的TTL都应该能说清楚为什么是这么久。2.3 更新数据为什么永远是删缓存而不是写缓存缓存更新策略上最常见的一个错误是更新数据库之后顺手把缓存也更新成新值。看起来合理但在并发场景下很容易产生脏数据。举个例子两个线程同时更新同一条库存线程A把库存改成90线程B把库存改成80。如果它们各自先写数据库、再写缓存执行顺序可能变成A写缓存90、A写数据库90、B写数据库80、B写缓存80最终缓存和数据库都是80没问题。但换个顺序呢A写数据库90、B写数据库80、B写缓存80、A写缓存90——缓存是90数据库是80数据就永久不一致了。业界标准的做法叫Cache Aside Pattern旁路缓存模式核心原则是更新数据库之后删除缓存而不是更新缓存等到下次读请求来了再回填新的值。Transactional public void updateProduct(Product product) { // 1. 先更新数据库 productMapper.updateById(product); // 2. 删除缓存让下次读请求重建缓存 stringRedisTemplate.delete(product:detail: product.getId()); }这样即使在并发写的情况下缓存里最多是旧值一旦被删除后重新加载就能拿到最新数据。逻辑简单也更容易保证最终一致。2.4 删缓存失败怎么办从重试队列到binlog订阅上面这个delete操作在真实环境里不是百分之百成功的。网络抖动、Redis实例GC暂停、连接池耗尽都可能让删除失败。一旦删除失败缓存里就会一直是旧数据直到过期。初级方案是延迟双删先删缓存、再更新DB、过几百毫秒再删一次缓存。但我个人的经验是这个方案在严苛并发下依旧有窗口期而且多一次删除操作就多一次失败可能。它适合快速止血不适合当长期方案。更可靠的做法是把删除操作放进一个具备重试能力的链路里应用更新数据库后发送一条缓存失效消息到MQ专门的消费者收到消息后执行缓存删除删除失败就按MQ重试机制来重试多次仍失败则告警。对于中大型团队还可以更进一步通过Canal订阅MySQL的binlog把数据库的每次变更都解析出来由消费端统一删除对应缓存。这样应用代码里连手动删除缓存的动作都省了业务方改完数据库缓存一致性由独立链路保证。代价是多引入一套组件适合缓存key多、团队分工细的情况。3. 技巧二数据结构别乱用Hash、ZSet这些场景省一半内存3.1 String存JSON vs Hash存字段内存和时间差在哪很多项目从头到尾只用一个操作opsForValue().set(key, jsonString)。不管什么对象序列化成JSON往Redis里一放完事。这种写法不是不能用但有两个痛点第一个痛点是更新字段的成本高。用户昵称变了你得先把整个JSON取出来反序列化成对象改字段再序列化再写回去。如果这个对象还很长浪费的时间和带宽就很可观。第二个痛点是内存占用大。String类型存整个JSONRedis需要为一个key存一份完整的字符串如果对象有20个字段每个字段都要高频更新这个方案就更不划算了。换成Hash结构就轻量多了HSET user:10001 name 张三 age 30 level VIP5 HINCRBY user:10001 score 10 HGET user:10001 name字段级更新用HINCRBY直接改一个值不需要动整个对象。Redis对字段少的Hash有专门的紧凑编码ziplist/listpack内存占用比String存JSON低不少。不过Hash也不是万能的。字段结构不稳定、经常增删字段的对象用Hash会让代码变得很别扭这种情况用String存JSON反而更灵活。对象字段多且固定更新频繁用Hash对象结构简单、整体读多用String也不差。3.2 ZSet不只是排行榜还能做延时任务ZSet有序集合最经典的场景是排行榜每个成员带一个scoreRedis按score排序天然支持实时排名。# 商品销量榜对商品销量累加 ZINCRBY product_sales_rank 10 product:1001 # 取TOP10 ZREVRANGE product_sales_rank 0 9 WITHSCORES我做过一个运营后台的销量榜原来实现是每个小时跑一次SQL按SUM(销量) GROUP BY商品再同步数据到一张表。数据量一大SQL执行要两三秒页面打开就卡。改成ZSet之后每次订单成交就ZINCRBY一次排行榜实时可见查询耗时从秒级降到毫秒级数据库的统计SQL也删掉了。ZSet还有一个容易被忽略的用法做延时任务。把score当作未来要执行的时间戳启动一个定时任务用ZRANGEBYSCORE拉取当前时间之前的成员这些就是要执行的任务。很多消息队列的延时能力底层就是这个套路。适合实现下单30分钟未支付自动关闭这类业务。要提醒一点score是double类型如果拿它存雪花ID这类超大整数会损失精度。需要大整数做排序时应该另想办法不要硬塞进score。3.3 Set、HyperLogLog、Bitmap怎么选除了String和Hash另外三个结构也经常被低估。Set适合去重和集合运算。抽奖活动的用户参与名单用SADD把userId塞进去活动结束SADD返回0就说明已经参与过去重逻辑一行命令搞定。两个用户的好友交集SINTER直接算出来比在数据库里做JOIN快得多。HyperLogLog适合超大基数的UV统计。一个日活千万的APP如果用Set存每个用户的ID内存占用非常大换成HyperLogLog无论多少个用户固定占用12KB左右误差率约0.81%。做每天的UV趋势图完全够用PFADD today_uv user:10001 user:10002 PFCOUNT today_uvBitmap适合状态位类数据。用户签到记录一年365天就是365个bit一个用户一年的签到数据不到50字节。在线状态、是否推送过这类布尔量数据用Bitmap最省。选型建议一句话追求精确的集合运算用Set超大基数的数量统计用HyperLogLog布尔状态位用Bitmap。3.4 一张表带走数据类型选型速查数据类型适用场景典型命令String简单KV、计数、JSON整体缓存SET、GET、INCRHash对象缓存、字段级更新HSET、HGET、HINCRBYList消息队列、最新列表LPUSH、RPOP、LRANGESet去重、共同好友、抽奖SADD、SINTER、SPOPZSet排行榜、延时任务、热门列表ZADD、ZREVRANGE、ZRANGEBYSCOREBitmap签到、在线状态SETBIT、GETBIT、BITCOUNTHyperLogLogUV统计、基数估算PFADD、PFCOUNT判断自己有没有用对数据类型有个很简单的检查点翻代码里RedisTemplate调用如果从头到尾只有opsForValue()那大概率可以优化。每多懂一种结构你的缓存方案就多一种解法。4. 技巧三穿透、击穿、雪崩三道防线一个都不能少4.1 缓存穿透空值缓存和布隆过滤器怎么用缓存穿透是指请求的数据在数据库里压根不存在导致每次请求都绕过缓存直接打到数据库。单个请求还好如果被攻击者盯上用一堆不存在的商品ID、用户ID高频刷接口数据库瞬间被打爆。第一道防线是空值缓存。查询数据库发现结果为空时也把这个key写进缓存值设成一个特殊标记比如空字符串或null过期时间给个3~5分钟就够了。后续同样的请求在TTL内会命中这个空值标记直接返回不再打数据库。Object value redis.get(key); if (value ! null) { if (NULL.equals(value.toString())) { return null; } return value; } Object dbResult dao.query(key); if (dbResult null) { // 空值也缓存但TTL要短 redis.set(key, NULL, Duration.ofMinutes(5)); return null; } redis.set(key, JSON.toJSONString(dbResult), Duration.ofMinutes(30)); return dbResult;第二道防线是布隆过滤器。它的原理很简单一个位数组加多个哈希函数把一个key映射到几个bit位置全部置为1就认为可能存在。判断一个key不存在布隆过滤器给出的答案是确定的不存在判断存在时则可能误判。用在缓存穿透场景恰好合适——我们恰恰是想拦住肯定不存在的数据。Redis官方模块里可以直接用BF.RESERVE product_bf 0.001 1000000 BF.ADD product_bf 100001 BF.EXISTS product_bf 100001布隆过滤器最大的价值在于面对批量注入式攻击空值缓存可能被大量不同的key撑爆内存而布隆过滤器内存占用可控、判断速度快。两者配合使用应用层先做基础参数校验ID格式、取值范围再查布隆过滤器判断key是否可能存在最后才是查缓存和数据库。4.2 缓存击穿互斥锁重建和逻辑过期哪个适合你缓存击穿和穿透是两回事。穿透是数据不存在击穿是数据存在但缓存正好失效同一时刻大量请求发现缓存为空蜂拥而至去查数据库。区别就在那一瞬间。解决思路有两个流派。第一个流派互斥锁重建。当缓存没命中时先尝试获取一把分布式锁只有拿到锁的线程真正去查询数据库并回填缓存其他线程短暂等待后重试读缓存。String value redis.get(key); if (value null) { String lockKey lock: key; boolean locked stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(3)); if (locked) { try { value dao.query(key); redis.set(key, JSON.toJSONString(value), Duration.ofMinutes(30)); } finally { redis.delete(lockKey); } } else { // 拿不到锁短暂等待后重读缓存 Thread.sleep(50); return redis.get(key); } }这里要注意锁的过期时间必须大于查询数据库所需的最长时间否则线程还没回填完锁就释放了其他线程还是会闯进数据库。生产环境里我习惯给锁设置5~10秒同时把数据库查询做了超时控制。第二个流派逻辑过期。这是大促场景下我比较推荐的做法。不设置真正的Redis过期时间而是在value里带上一个逻辑过期时间戳// 缓存值结构data 逻辑过期时间 CacheValue cv new CacheValue(data, System.currentTimeMillis() 30000); redis.set(key, JSON.toJSONString(cv)); // 读取时 String value redis.get(key); CacheValue cv JSON.parseObject(value, CacheValue.class); if (cv.getExpireTime() System.currentTimeMillis()) { // 逻辑过期先返回旧数据尝试获取锁去刷新 boolean locked tryLock(key); if (locked) { // 开启异步线程刷新缓存 asynRefresh(key); } return cv.getData(); // 返回旧数据 } return cv.getData();这套方案的核心思路是过期了不是直接穿透到数据库而是先让请求拿到旧数据由后台线程去刷新缓存。用户体验上没有延迟数据库也不会被瞬时流量打穿。代价是缓存中可能出现几秒级的旧数据对一致性要求高的场景需要权衡。4.3 缓存雪崩加随机值只是最基础的一步雪崩是指大量key在同一时段失效或者Redis实例意外宕机导致大量请求直接打到数据库上。最基础的防御手段就是第2章反复强调的过期时间加随机偏移。但这只是第一步。真正要防住雪崩还得靠下面三层多级缓存即使Redis层大规模失效本地缓存还能接住一部分流量这是下一章的重点内容。Redis高可用主从加哨兵或者直接上Redis Cluster避免单点宕机后整体不可用。数据库侧限流降级给数据库查询接口配置限流当流量超过阈值时直接返回降级结果宁可部分请求失败也不能把数据库打挂。我见过一个比较典型的雪崩案例某个活动策划凌晨0点把所有统计key的过期时间设为0点整结果0点一到几千个key集体失效数据库QPS瞬间翻了5倍多个核心接口超时告警。后来把所有统计类key的过期时间打散底层的保护手段也补上了才算消停。雪崩的防御重点不在单个方案而在层级——每一层都兜住一点整体才不会崩。4.4 一次完整排查命中率骤降时我是怎么一步步定位的学了一堆方案最怕的是线上出问题不知道从哪查。我整理了一套排查链路按照这个顺序基本能快速缩小范围症状可能原因优先检查项命中率突然下降DB QPS飙升缓存击穿或雪崩最近过期的key是否集中在同一时间点是否有单一热点key命中率长期偏低DB查询量稳定偏高缓存设计问题或穿透是否存在大量不存在的数据被查询缓存key是否覆盖了核心查询大量key同时消失缓存雪崩Redis是否重启过期时间是否集中maxmemory驱逐策略是否误杀实际排查时先打开Redis监控看keyspace_hits和keyspace_misses的曲线。如果命中率是从一个高点瞬间跌落那大概率是热点key失效触发击穿或者是集中过期触发雪崩。这时候用SCAN命令扫一遍key的TTL分布看看是不是集中在同一时间段。如果命中率一直是平的、长期很低那问题更可能是穿透或被缓存的数据本身选错了。有一次我就是顺着这条链路查出来运营在后台手动清了一批活动统计缓存结果这批缓存刚好在同一个SQL里更新更新完又同时写入同一批TTL第二天同一时刻集体过期。看起来像雪崩实际是人为集中过期。从那以后我定了个规矩后台手动操作缓存也要走统一的TTL生成逻辑不能自己造随机值。5. 技巧四Caffeine本地缓存Redis用两级缓存扛住热点洪峰5.1 为什么有Redis还不够还要加Caffeine正常情况Redis已经能把数据库护住了但有一个场景Redis自己也会扛不住超热点数据。比如大促期间某个爆款商品的详情页每秒可能有几十万次访问即便Redis查询只要1ms单分片的网络带宽和处理能力也会被打满。另外一个问题是网络开销。即使Redis在内网一次查询也要经过网络协议栈、序列化和反序列化无论如何都比本地内存慢一个量级。对于首页推荐、排行榜这类数据不太变、读量极大的接口本地缓存是性价比很高的选择。Caffeine就是目前Java生态里用得比较多的本地缓存库。它的淘汰算法W-TinyLFU在读写性能和命中率之间取得了一个不错的平衡比手动用HashMap做缓存靠谱得多。但要说清楚本地缓存不是所有场景都适合。它的数据是分散在每个应用实例里的一致性更难保证而且它占的是应用进程的内存不能无限塞。我的取舍标准是只在明确的超热点数据上用并且本地数据量控制在几万条以内。5.2 Caffeine配置解读maximumSize、expireAfterWrite、refreshAfterWriteCaffeine的配置看着简单但有几个参数的含义容易被误解。我给出一个常用配置并逐行解释CacheString, Object localCache Caffeine.newBuilder() // 本地最多保留10000条 .maximumSize(10_000) // 写入后30秒过期 .expireAfterWrite(Duration.ofSeconds(30)) // 写入后10秒访问时触发异步刷新 .refreshAfterWrite(Duration.ofSeconds(10)) .build();maximumSize是本地缓存的上限超过之后会按照W-TinyLFU策略淘汰低频数据。expireAfterWrite是写入后多久过期这是硬过期到期后再次访问会重新加载数据。refreshAfterWrite是软刷新它不会让访问线程阻塞等DB而是在访问时触发异步刷新让访问线程先拿到旧值。这种先给旧值后台更新的方式对热点数据非常友好。这里有一个坑单独配置refreshAfterWrite是不生效的必须同时配置一个expireAfterWrite或者expireAfterAccess因为Caffeine需要判断某个key是否到了refresh时间而time source依赖过期机制。我见过同事只配了refreshAfterWrite结果一直不刷新查了半天才发现是这个原因。5.3 多级缓存的查询与回填流程两级缓存的查询流程其实很朴素本地缓存优先没命中再去RedisRedis也没有才查数据库查完之后逐级回填。public Object getProductDetail(Long productId) { String key product:detail: productId; // 1. 本地缓存 Object local localCache.getIfPresent(key); if (local ! null) { return local; } // 2. Redis缓存 String json stringRedisTemplate.opsForValue().get(key); if (json ! null) { localCache.put(key, json); return JSON.parse(json); } // 3. 数据库 Product product productDao.query(productId); String jsonStr JSON.toJSONString(product); // 4. 回填两级缓存注意TTL的随机偏移 long ttl 1800 RandomUtil.randomLong(60, 300); stringRedisTemplate.opsForValue().set(key, jsonStr, Duration.ofSeconds(ttl)); localCache.put(key, jsonStr); return product; }这里有一个细节值得注意不是所有查询到的数据都值得塞进本地缓存。如果一个key只被访问了一次把它塞进本地缓存反而浪费内存、挤占热点数据的空间。我常用的办法是给本地缓存加一层计数逻辑数据在Redis里被访问到第二、第三次时才把它提升到本地缓存。这个逻辑听起来复杂但实现起来就是一个简单的原子计数。5.4 一致性问题Pub/Sub广播和版本号方案多级缓存最大的敌人是一致的性。Redis删除缓存后本地缓存里可能还留着旧值短时间内用户看到的就是脏数据。我的推荐方案是过期兜底广播失效。本地缓存TTL设一个合理的上限比如30秒即使广播消息丢失最多也就是多展示30秒旧数据。同时当业务执行数据库更新并删除Redis缓存后通过Redis的Pub/Sub广播一条失效消息// 发布失效消息 redisTemplate.convertAndSend(cache:invalid, key); // 消费者监听 public void onMessage(String key) { localCache.invalidate(key); }每个应用实例订阅同一个频道收到消息后删除本地缓存的对应key。这套机制实现简单实时性也够。唯一要注意的是Redis Pub/Sub不持久化消息如果某个实例在消息广播期间宕机它恢复后本地缓存里的旧数据只能等TTL自然过期。所以我说要过期兜底两者结合才可靠。如果团队对一致性的要求更苛刻还可以用版本号方案DB更新后给数据版本号1Redis和本地缓存都存版本号每次查询时对比版本号决定是否回源。但这个方案对代码侵入性更强一般业务场景用不上。5.5 实测效果热点接口RT从20ms降到2ms用这个方案处理过一次爆款商品详情的优化。优化前商品详情接口平均RT在20ms左右大促高峰时SQL查询耗时甚至到100ms。上线两级缓存后90%以上的热点流量被本地缓存接住平均RT降到2ms左右数据库QPS从峰值3000降到500以下Redis的带宽压力也明显缓解。这套架构不是银弹但凡是数据变不太快、读量巨大、允许秒级旧数据的接口都能复用同一套模式。你先把热度最高的几个接口改造掉收益立竿见影。6. 技巧五序列化、连接池、热点Key最后一公里的性能调优6.1 序列化方式真的会影响性能JDK、JSON、Kryo实测思路这个点容易被忽略但对系统性能的影响很实在。Spring Boot的RedisTemplate默认使用JdkSerializationRedisSerializer它的序列化结果里包含大量类结构信息同一个对象存进去体积可能是JSON的2到3倍。体积大意味着网络传输慢、内存占用高、反序列化耗时高——三重损失。拿一个典型的用户对象举例10个字段左右不同序列化方式的差异在常见压测中大致是这个量级序列化方式可读性体积序列化反序列化耗时适用场景JDK默认差最大高不推荐除非存量系统JSON好中中推荐通用性强Kryo / Protobuf差小低性能敏感、对可读性要求低切换序列化方式很简单在配置类里指定RedisTemplate的valueSerializer即可Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); GenericJackson2JsonRedisSerializer serializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(RedisSerializer.string()); template.setValueSerializer(serializer); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; }这里有个坑要提醒切换序列化方式前确认线上已有的缓存key都过期或可以被清理否则新代码读老数据会反序列化失败。上线前可以先发一版只读兼容的代码等旧key自然过期后再切换更稳妥。6.2 连接池参数别抄网上的要按QPS算Redis客户端的连接方式直接影响性能和资源占用。Lettuce和Jedis是Java里最常用的两个客户端差异在于Lettuce线程安全底层用Netty多路复用一个连接可以并发处理多个请求Jedis实例非线程安全通常依赖连接池管理。Spring Boot 2.x默认用Lettuce一般情况下不用换。连接池参数maxTotal、maxIdle、minIdle要结合自身QPS和RT来估算。一个简单的估算思路连接池大小约等于目标并发QPS × 单次请求平均耗时秒。比如预期Redis并发QPS是10000平均RT为1ms那需要的连接数大约是10000 × 0.001 10个。但实际还要加上Redis自身处理能力、网络波动等因素留出余量。我的实践配置通常从下面这个起点开始压测再按结果调整spring: data: redis: host: 10.0.0.8 port: 6379 lettuce: pool: max-active: 50 max-idle: 20 min-idle: 5 max-wait: 3000ms最典型的线上问题是连接泄漏连接数缓慢上涨几天后超过max-active新请求全部阻塞等待。排查思路是看连接是借出去没归还还是业务线程异常退出导致归还失败。一般配合代码审查和连接池监控能定位到。6.3 热点Key发现、多副本拆分、本地兜底三板斧热点Key和缓存击穿不一样击穿说的是热点key失效瞬间而热点Key问题是这个key本身的访问量已经超过了单分片Redis能承受的上限。典型症状是Redis集群某个分片的网卡打满其他分片很空闲。发现热点Key有几种方式redis-cli --hotkeys需要把maxmemory-policy设为LFU类策略才能统计访问频次redis-cli monitor采样一段时间统计出现频率最高的key客户端埋点在代码里对get操作做频次统计上报到监控平台解决三板斧按优先级排序第一板斧是本地缓存兜底。热点流量先在应用本地内存直接返回只有未命中才回源Redis这是我最常用的手段和第五章的多级缓存是同一个思路。第二板斧是多副本拆分。把同一个key复制N份加上随机后缀比如product:1001拆成product:1001:0到product:1001:9一共10份。写入时每份都写读取时随机选一份。这样原来打在一个分片上的流量就分散到了多个分片上。代价是内存放大N倍所以只对明确的超热点key用。第三板斧是读写分离或Cluster扩容。把热点key的读操作引流到只读副本上减少主分片压力。但这是架构层面的调整代价较大适合长期热点且流量持续上涨的场景。6.4 大Value是隐形杀手怎么发现怎么拆大Value指的是单个key的value过大比如一个String超过1MB或者一个Hash里有几万个字段。大Value的危害比很多人想象得严重网络传输慢一个2MB的key读一次就要传输2MB高并发下直接打满网卡阻塞Redis删除或压缩一个大key时Redis单线程执行会有明显卡顿影响同一实例上的其他key序列化开销大每次读写都要对大对象做序列化反序列化CPU和耗时都上去了。发现大Value常规用两条命令# 扫描整个实例的大key线上建议低峰期运行 redis-cli --bigkeys # 针对单个key计算大小 DEBUG OBJECT product:1001拆解的思路有两个方向。如果是一个大String存了整个对象按业务维度拆成多个小key。比如商品详情原来是一个大JSON可以拆成商品基本信息、图片列表、描述、参数等几个小key各自设置合理的TTL。在线读取时按需获取而不是一次性拉全量。如果是一个大Hash按哈希取模分成多个桶user:10001拆成user:0:1001、user:1:1001这样的形式具体分桶数看数据量和字段数。拆完之后查询时算一下哈希值定位到对应分片即可。还有一个细节线上要删除大key时尽量用UNLINK而不是DEL。DEL会阻塞Redis处理其他命令UNLINK是异步删除能避免卡顿。7. 缓存上线后我建议你盯紧这几个指标7.1 每天必看的三个指标命中率、淘汰数、慢查询很多团队缓存上线就完事了从来不看监控直到线上出故障才来救火。我个人的习惯是缓存系统的日常巡检至少要看三个指标第一个是命中率。还是那句话命中率是缓存价值的直接体现。建议设置告警命中率低于某个阈值比如70%就提醒排查。你需要知道一个基线比如业务稳定期的命中率是92%某天掉到80%那一定发生了什么。第二个是evicted_keys淘汰key数。这个指标飙升说明Redis内存压力大正在通过淘汰策略清理key清理掉的key下次访问又得回源数据库导致命中率和数据库负载双击。第三个是慢查询。用SLOWLOG GET查看Redis自身执行缓慢的命令比如大key的读写、复杂集合操作。如果Redis本身慢了再好的缓存策略也白搭。这三个指标建议直接接入Grafana监控面板一屏看全。出现问题的时候有曲线和没曲线排查效率差很多。7.2 新上缓存最容易踩的三个坑最后分享三个我在上线新缓存时经常提醒团队的坑。第一个坑是忘记配置maxmemory和淘汰策略。Redis默认不限制内存会导致系统内存耗尽。建议按业务场景显式配置maxmemory并选择合适的淘汰策略比如对缓存类数据用allkeys-lru对需要精确控制保留哪些key的场景用volatile-ttl。不配置maxmemory等于给系统埋了一颗定时炸弹。第二个坑是缓存key没有统一管理。命名混乱、前后端口径不一致排查问题时根本不知道哪个key对应哪个业务。建议建立一份缓存key清单统一前缀规范比如业务域:功能:实体:ID的格式同时记录每个key的数据结构、TTL、更新触发方式和业务负责人。第三个坑是忽略了缓存清单文档的维护。很多项目缓存key散落在各处100个key可能分布在几十个类里每次改动都要全局搜代码。我的做法是提供一个集中管理类定义所有缓存key的构造函数、TTL常量、序列化方式让缓存设计像数据库表一样有规可查。我自己在把上面这些方案落地到多个项目之后最大的体会是缓存不是加在代码里就结束的动作而是一个需要持续治理的过程。命中率掉了要能发现过期策略要跟着业务变化调整大Key和热点Key要定期扫描。每次排查性能问题我都先看命中率再看慢查询最后看大Key这套顺序帮我解决了不少线上疑难杂症。所以别急着加机器加只读库先把Redis这层缓存真正用好系统自然就快了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价