拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Redis实战解析:缓存、分布式锁与防穿透击穿

1. 项目概览一次梳理 Redis 生产环境的四大核心玩法做后端服务这几年Redis 几乎成了技术栈里的“标配水电气”。不管是高并发秒杀、商品详情缓存、用户会话维护还是分布式场景下的数据一致性控制Redis 都扮演着关键角色。但说实话很多开发把 Redis 用得比较浅——只停留在“缓存一下数据库查询结果”这个层面对分布式锁、过期策略、防穿透击穿这些进阶能力一知半解实际碰到线上问题才发现踩了一堆坑。这篇内容我想把 Redis 实战中最重要的四个方向完整梳理一遍缓存使用、分布式锁、过期策略、防穿透防击穿。这四个方向基本覆盖了面试高频考点也覆盖了生产环境中最常见的问题场景。适合把 Redis 用好、用对的后端开发者阅读也适合准备 Redis 面试题的同学作为系统性参考。我会结合真实的业务场景来拆解比如商品详情的缓存怎么设计、订单支付时分布式锁怎么加、热点 key 失效时怎么避免把数据库打垮。每个部分都会给出可落地的代码示例和配置参数并把我实际踩过的一些坑一并写出来。2. 缓存设计从数据类型到序列化每一步都有讲究2.1 Redis 五种数据类型怎么选别一个 String 走天下实际开发中很多人习惯把所有缓存都塞进 String 类型用 JSON 序列化搞定一切。这种做法在小项目里没什么问题但一旦数据访问模式复杂起来String 类型在很多场景下并不是最优解。Redis 的 String、Hash、List、Set、ZSet 五种基础类型对应着不同的数据访问需求。比如商品详情页如果每次都要读取商品的多个字段标题、价格、库存、图片用 Hash 类型就比 String 更合适。Hash 可以做到字段级别的读写比如只更新价格字段不需要把整个 JSON 拿回来再反序列化再写回去还能省下不少内存开销。List 适合消息队列、最新消息列表这类场景。生产环境里我见过不少团队用 List 的 LPUSH BRPOP 组合实现简单的任务队列虽然功能上不如专业 MQ 那么完整但胜在轻量、无需额外部署。Set 则适合做去重、共同好友、标签系统用 SADD / SISMEMBER 就能做 O(1) 级判断。ZSet 就更常用了排行榜、按分数排序的延迟队列都用得上ZADD 加 ZREVRANGE 就能轻松实现 Top N 排名。关于选型我有一个经验不要只盯着数据类型本身要看你未来要执行什么样的操作。如果只是按键取值String 就行如果涉及字段级读写选 Hash如果需要范围查询和排序ZSet 是唯一选择如果需要集合运算交集、并集、差集Set 是天然适配的。这些选型逻辑面试官最喜欢问实际项目中也是区分代码质量的关键点。2.2 序列化方案对比JSON 够用但要注意这些坑Redis 的键值本质上都是二进制安全的字符串所以存入任何对象都需要经过序列化。常见的方案有 JDK 原生序列化、Jackson / Fastjson JSON 序列化、Protobuf、Kryo 等。JDK 原生序列化最大的问题是数据体积大、可读性差而且序列化后的内容夹杂 Java 类信息一旦类结构调整老数据可能反序列化失败。JSON 系列化则友好得多键值可读调试方便跨语言也兼容。我在实际项目中默认推荐 Jackson 或 Fastjson2配置好 JavaTimeModule 处理 LocalDateTime 就基本够用。使用 JSON 序列化有个容易踩的坑类型信息丢失。比如你把一个 Product 对象序列化成 JSON 存入 Redis取出来反序列化时如果直接转成 Product.class一般情况下没问题但如果缓存的是一个 List 或者一个多态对象反序列化就容易翻车。解决方式是存入时带上类型信息或者在代码里显式指定反序列化目标类型。Protobuf 和 Kryo 的优势在于序列化后体积小、性能高适合对网络带宽和序列化性能有较高要求的场景。但代价是引入了额外依赖且调试不便——Redis Desktop Manager 里看二进制内容基本是乱码。我的建议是大部分业务场景用 JSON 就好只有当缓存数据量极大比如几千万条或者序列化耗时就成为瓶颈时才考虑升级到 Protobuf 这类方案。2.3 缓存 Key 设计规范别让线上出问题缓存 Key 的设计直接决定了缓存的可管理性和排查效率。一个规范的做法是使用“业务域:实体类型:标识符”的分层结构。比如商品信息 Key 可以是 product:info:10001订单锁 Key 可以是 lock:order:20231001这样即使不同业务共用同一个 Redis 实例也不会出现 Key 冲突在 Redis 可视化客户端里按前缀筛选也方便。除了命名规范还要注意 Key 的长度和内容。Redis 的 Key 越长占用的内存越多查询匹配的效率也越低虽然 Redis 6.0 对长 Key 做了优化但还是建议控制长度。另外不要把用户输入的明文内容直接作为 Key一是不安全二是容易产生超长 Key。合理做法是用哈希后的值或者 ID 加业务前缀。还有一个很多人忽视的问题客户端前缀没区分环境。有时候开发环境、测试环境、生产环境共用一套 Redis 的 Key 前缀就会出现在本地把生产数据覆盖掉的重大事故。建议不同环境使用不同的 Key 前缀或者在创建 Redis 连接时直接使用不同的 DB index。两者结合使用更稳妥。3. 过期策略Redis 是怎么清理过期 Key 的如何调参最合理3.1 惰性删除、定期删除和内存淘汰一次讲清楚Redis 的过期删除策略是面试高频考点也是理解缓存治理的基础。Redis 并不是在 Key 过期的那一刻就立即把它从内存中删掉而是采用“惰性删除 定期删除”的组合方案。惰性删除指的是当客户端请求某个 Key 时Redis 会检查这个 Key 是否已经过期如果过期了就删除并返回空。这种机制的好处是 CPU 开销极低——不访问就不删。坏处是如果这个 Key 一直不被访问它就一直占着内存不释放形成“过期数据堆积”。定期删除则是 Redis 每隔一段时间默认每秒 10 次随机抽取一批设置了过期时间的 Key 进行检查如果过期了就删除。这里要注意“随机抽取”四个字——这意味着 Redis 并不会扫描全部 Key而是抽样检查。抽样的数量和频率可以通过hz参数调整但调得太高会额外消耗 CPU。除了这两种主动删除机制Redis 还提供了 8 种内存淘汰策略在maxmemory达到上限后触发。常用的有noeviction不淘汰直接报错、allkeys-lru对所有 Key 按 LRU 淘汰、volatile-lru仅对设置了过期时间的 Key 按 LRU 淘汰、allkeys-random等。生产环境中我比较常用allkeys-lru因为它既考虑了数据热度又能保证缓存不会因为一个热点 Key 占满内存而崩溃。需要注意的是volatile-lru只针对有过期时间的 Key 生效如果业务里大部分 Key 都没设置过期时间那么达到 maxmemory 时这个策略基本就等于 noeviction容易报 OOM 错误。这一点非常容易被人忽略。3.2 过期时间设置的业务考量给缓存设置过期时间不是随便选一个值。设置得太短会导致缓存命中率低、数据库压力大设置得太长容易读到脏数据而且异常恢复时数据长时间不更新。我常用的做法是给不同业务数据设置差异化的过期时间实时性要求高的数据比如库存数量过期时间设置为 30 秒到 1 分钟相对稳定的数据比如商品详情描述设置为 30 分钟到 2 小时几乎不变的数据比如配置项则可以设置 24 小时甚至更长。同时为了避免大量 Key 在同一时刻集中过期造成缓存雪崩我会在设置过期时间时加入一个随机扰动。比如设定过期时间为 1 小时那么实际过期时间就是 3600 random(0, 300) 秒。这个细节在应对瞬时大流量场景时非常关键。“随机过期时间”是缓存治理里性价比最高的一招代码量只需一行收益却是显著的——它能避免因为定时任务或某批数据集中写入导致大量缓存同时失效从而把压力一次性压到数据库。3.3 针对热点 Key 的逻辑过期方案生产中我遇到过不少热点 Key比如秒杀活动中的商品库存、微博热搜榜单。这类 Key 的访问量非常大如果采用物理过期过期后的一瞬间可能涌入大量请求同时去查询数据库数据库很容易被击穿。针对这个场景一个行之有效的方案是“逻辑过期”——也就是说Redis 中的 Key 本身不设置过期时间但在 Value 里额外存储一个逻辑过期的时间戳。当读取到这个 Key 时先判断逻辑过期时间是否已到如果还没到直接返回缓存值如果已经到了则获取分布式锁在锁内执行查询数据库并重建缓存的操作其他请求则直接返回旧缓存值即使逻辑上已经过期。这种方案可以完全避免热点 Key 过期时数据库被击穿的问题但代价是返回的数据在极端情况下可能不是最新值。所以它更适合数据实时性要求不高的场景。顺带提一句逻辑过期方案经常和互斥锁方案放在一起对比这两个都是缓存击穿问题的经典解决方案后面防击穿部分我会展开讲。4. 缓存穿透、击穿、雪崩三兄弟的成因与解决方案4.1 缓存穿透不只是加个 null 判断那么简单先区分三个概念。缓存穿透指的是查询一个根本不存在的数据缓存和数据库都没有这个数据导致请求直接打到数据库层。恶意攻击者通过构造大量不存在的 ID 来发起请求可以在很短时间内把数据库压垮。最经典的解决方式有两个缓存空值和布隆过滤器。缓存空值的做法是当数据库查询结果为空时仍然把这个 Key对应一个空值或标记写入 Redis并设置一个较短的过期时间比如 3~5 分钟。这样后续相同请求就会直接命中缓存而不是穿透到数据库。注意空值的过期时间不能太长否则当数据真正写入后缓存仍然返回空影响正常业务。布隆过滤器的思路则是拦截不存在的 Key。在缓存之前再加一层布隆过滤器将所有合法数据的 ID 预先存入布隆过滤器。查询时先判断 ID 是否可能存在于过滤器中如果不存在就直接返回空根本不会查询 Redis 和数据库。布隆过滤器允许一定的误判率判断存在但实际不存在但不会漏判判断不存在就一定不存在所以它能挡住绝大多数的无效请求。使用布隆过滤器要注意两个问题一是初始化时需要提前把存量数据的 ID 加载进去这个过程如果数据量大可能需要分批异步完成二是布隆过滤器不支持删除单个元素标准实现下如果业务上有删除 ID 的需求需要定期重建过滤器或者改用 Counting Bloom Filter。4.2 缓存击穿热点 Key 失效的瞬间怎么接住缓存击穿指的是某个热点 Key 在过期的瞬间大量并发请求同时发现缓存不命中于是全部穿透到数据库导致数据库负载突增甚至被打挂。解决缓存击穿业界最常用的两种方案是互斥锁和逻辑过期。互斥锁方案的思路是当缓存不命中时并不让所有请求都去查询数据库而是先尝试获取一个分布式锁。拿到锁的请求负责查询数据库并重建缓存拿不到锁的请求则短暂等待一段时间比如 50ms后重试读取缓存。这样最终只有一个请求打到数据库其他请求等待缓存重建完成后直接从缓存读取。互斥锁方案有一个需要特别注意的问题如果第一个请求重建缓存的过程中失败了那么后续所有请求都会在等待锁上浪费时间甚至可能导致大范围超时。所以在使用互斥锁时必须设置锁的超时时间并在捕获异常后将锁释放或者让锁自动过期同时考虑对重建缓存操作加一个简单的重试逻辑。逻辑过期方案我在 3.3 已经提到过就不再重复。两者的取舍在于互斥锁能保证缓存数据的一致性数据始终是最新的但请求等待时间会增加逻辑过期能保证请求响应速度快但数据可能短暂不一致。在一致性要求高的支付、订单场景用互斥锁在并发要求极高但一致性要求低的资讯、榜单场景用逻辑过期这个选择逻辑在面试中也很加分。4.3 缓存雪崩第一批 Key 集体失效的连锁反应缓存雪崩指的是大量的缓存 Key 在同一时间失效导致所有请求同时打到数据库层数据库瞬间承受不住压力进而可能引发一系列连锁反应比如数据库连接池被打满、应用线程阻塞、整个服务不可用。雪崩和击穿的区别在于击穿是单个热点 Key 失效导致的问题雪崩是大量 Key 同时失效导致的问题。两者的解决方案侧重点也不同。防雪崩的思路有三层第一层在业务层面错开缓存过期时间。前面 3.2 提到过的“过期时间加随机扰动”就是最基础的做法把原有的统一过期时间拆散让 Key 的失效时间在时间轴上均匀分布数据库的请求压力就不会在某一瞬间集中到达。第二层构建多级缓存。在 Redis 之上再加一层本地缓存比如 Caffeine 或 Guava Cache当 Redis 发生大规模失效时本地缓存还能抗住一部分流量。多级缓存的引入能显著提升系统的整体韧性但需要解决本地缓存和 Redis 之间的数据一致性问题通常的做法是给本地缓存设置较短的过期时间比如 30~60 秒。第三层依赖熔断、限流和降级机制。当数据库压力过大时通过 Sentinel 或 Hystrix 将非核心链路的请求直接拒绝或返回兜底数据保证核心功能的可用性。比如商品详情页在不依赖价格和库存时也可以返回基础的商品名称和图片而不是直接报错。雪崩的防护是一个体系工程没有一招制敌的银弹。生产环境中我会把“缓存过期时间随机化”作为默认策略写入代码规范同时要求所有查询数据库的入口都加上熔断限流保护这样即使缓存真的失效了数据库也能在可控负载下撑到缓存重建完成。5. 分布式锁从基础实现到生产级方案的完整演进5.1 SETNX EXPIRE 的演进史一步步解决原子性、死锁问题分布式锁是分布式系统中的经典话题面试时基本必考。分布式锁的核心要求是在分布式环境下多个进程同时竞争同一个资源时保证只有一个进程可以拿到锁其他进程等待或失败。用 Redis 实现分布式锁经历了多个版本的迭代。最初一版是用 SETNX 加 EXPIRE 两条命令// 第一版先设置锁再设置过期时间 Boolean success redisTemplate.opsForValue().setIfAbsent(lock:order:10001, 1); if (success) { redisTemplate.expire(lock:order:10001, 30, TimeUnit.SECONDS); // 执行业务逻辑 }这个版本有一个致命问题如果 SETNX 成功之后、EXPIRE 执行之前服务器突然宕机或者网络异常那么锁就没有过期时间其他请求永远拿不到锁形成死锁。于是有了第二版用一条命令原子性地同时设置值和过期时间。Redis 2.6.12 版本之后提供的 SET 命令扩展参数可以完美解决这个问题// 第二版SETNX 和 EXPIRE 合并为一条原子命令 Boolean success redisTemplate.opsForValue().setIfAbsent(lock:order:10001, requestId, 30, TimeUnit.SECONDS);这里一定要加requestId可以是一个 UUID 或线程标识因为释放锁的时候需要校验“这个锁是不是我自己加的”避免误删其他线程持有的锁。这个 uuid 参数在面试和实际工作中都是必考点。5.2 释放锁的陷阱先判断再删除必须保证原子第二个容易踩坑的地方是释放锁的操作。很多开发会写成这样// 错误示范 String value redisTemplate.opsForValue().get(lock:order:10001); if (requestId.equals(value)) { redisTemplate.delete(lock:order:10001); // 不是原子操作 }这段代码的问题是“先判断再删除”两个操作不是原子的。如果当前线程判断完锁是自己的之后锁正好过期了另一个线程重新加锁成功此时当前线程再执行 DELETE就会删掉别人的锁锁的互斥性就被破坏了。正确做法是使用 Lua 脚本将“判断 删除”合并为一个原子操作// 正确释放锁使用 Lua 脚本保证原子性 String releaseLockScript if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; DefaultRedisScriptLong releaseScript new DefaultRedisScript(releaseLockScript, Long.class); Long result redisTemplate.execute(releaseScript, Collections.singletonList(lock:order:10001), requestId);Lua 脚本在 Redis 中是原子执行的所以判断和删除之间不会插入其他客户端操作。这是生产环境分布式锁释放的标准写法。5.3 看门狗机制与 Redisson 的正确姿势前两节的写法虽然能正常运行但还有几个问题悬而未决锁的过期时间设置多长合适如果业务执行时间超过了锁的过期时间怎么办锁内的业务代码可能因为各种原因执行很久一旦超过过期时间锁就自动释放了其他线程就开始争抢同一资源还是会出现并发问题。这里就涉及 Redisson 库提供的看门狗机制。Redisson 实现 RLock 时默认设置了 30 秒的锁过期时间但有一个后台线程看门狗会每隔 10 秒自动将锁的过期时间“续期”到 30 秒。只要锁持有者还在执行业务逻辑且没有显式释放锁锁就不会过期。这就解决了业务执行时间超过锁过期时间的问题。使用 Redisson 的分布式锁代码非常简洁// Redisson 分布式锁的正确用法 RLock lock redissonClient.getLock(lock:order:10001); boolean locked false; try { // waitTime获取锁的等待时间leaseTime锁的持有时间-1 表示启用看门狗自动续期 locked lock.tryLock(10, -1, TimeUnit.SECONDS); if (!locked) { throw new RuntimeException(系统繁忙请稍后再试); } // 在这里执行订单创建、支付扣款等核心业务逻辑 orderService.createOrder(orderDTO); } finally { // finally 中释放锁确保任何情况下都会放锁 if (locked) { lock.unlock(); } }这里有一个细节需要注意tryLock的第一个参数waitTime表示等待获取锁的最长时间。如果设置为 10 秒意味着当前线程愿意在锁被占用时等待 10 秒超过 10 秒仍未获取到锁就返回失败。这样可以避免大量线程在同一把锁上无限等待导致的线程堆积。leaseTime设置为 -1 时启用看门狗需要引入RedissonClient依赖并在配置类中注册。如果业务方希望锁内代码执行时间不能超过某个上限也可以显式设置 leaseTime比如 30 秒。这样即使看门狗没来得及续期锁也会在 30 秒后自动释放避免了锁长时间被一个异常线程持有导致其他线程一直拿不到锁的问题。5.4 可重入锁与主从一致性问题RedLock 又是怎么回事分布式锁还有一个高频考点是可重入性。如果同一个线程已经持有锁在锁内又尝试获取同一把锁普通实现会因为没有可重入机制而死锁。Redisson 的可重入锁实现类似于 Java 的 ReentrantLock内部维护了一个计数器同一线程可以多次加锁每次加锁计数器加 1释放一次计数器减 1计数器归零才真正释放锁。更深一层的问题是主从架构下的锁安全问题。默认的 Redis 主从部署中主节点成功写入锁数据并返回成功后如果主节点在同步到从节点之前故障了那么从节点晋升为主节点后锁数据就丢失了其他线程就可以重复加锁。这就是分布式锁在主从模式下可能存在的“睡眠窗口”。Martin Kleppmann 曾公开质疑过基于 Redis 的分布式锁的安全性而 Redisson 作者则给出了 RedLock 方案作为回应。RedLock 的思路是在多个互相独立的 Redis 节点上依次尝试加锁当且仅当在超过一半的节点上成功加锁且总耗时小于锁的过期时间时才认为加锁成功。这样即使少数节点故障锁依然不会被错误放开。RedLock 在生产中是否值得使用业界有争议。我的经验是如果业务对锁的绝对安全性要求极高比如涉及资金操作可以考虑 RedLock 或改用 ZooKeeper / etcd 这类提供强一致性的分布式协调组件如果业务对偶尔的并发重入容忍度可以接受大部分互联网业务属于此类那么单机 Redis Redisson 看门狗就已经能覆盖绝大多数场景了。不要为了追求理论的绝对安全而牺牲系统的复杂度和性能这一点在实际项目中真的很重要。6. 缓存一致性数据库和 Redis 怎么保证不打架6.1 Cache Aside Pattern先更新数据库还是先删缓存缓存一致性问题可以在任何有缓存和数据库共存的系统中出现。业务一侧更新数据库一侧更新缓存如果操作有先后就很容易出现短暂的脏数据。业界最经典的缓存策略是Cache Aside Pattern读路径和写路径分开设计读路径先查缓存命中就返回不命中则查询数据库将结果写入缓存并返回。写路径先更新数据库然后删除缓存。下次读取时由于缓存被删除就会重新从数据库加载最新数据。为什么不更新缓存而是删除缓存原因有二一是为了减少缓存更新的频率如果业务对同一个 Key 频繁执行写操作每次写都更新缓存不但浪费 Redis 性能还会因为并发写顺序不一致导致缓存数据和数据库不一致二是延迟更新缓存可能引入并发问题——两个线程都在写缓存后写的可能是旧数据。删除缓存则简单得多要么没有缓存要么从数据库重新加载永远是最新数据。6.2 先更新数据库再删除缓存还是反过来先更新数据库再删除缓存Cache Aside 的标准顺序和先删除缓存再更新数据库这两种顺序业界主流推荐前者。原因在于先删除缓存再更新数据库如果在“删除缓存”和“更新数据库”之间存在并发请求很可能会发生以下情况线程 A 删除缓存线程 B 读到缓存为空去查询数据库拿到的还是旧数据写回缓存然后线程 A 才更新数据库。最终缓存里保存的是旧数据而且只有在缓存过期后才能被纠正。而先更新数据库再删除缓存虽然也存在窗口期比如缓存被删之前有请求读到旧缓存但这个窗口的持续时间是“数据库更新完成到缓存删除完成”之间的毫秒级间隔实际上很短。缓存删除失败导致的脏数据可以通过设置较短的过期时间来兜底。需要考虑的是删除缓存失败的情况。如果更新数据库成功后删除缓存时 Redis 刚好故障缓存依然是旧值。这时候就需要重试机制可以把待删除的 Key 放入消息队列或本地延迟队列由后台任务异步重试删除直到删除成功。6.3 延迟双删和订阅 binlog 的进阶方案如果业务对一致性要求更高可以在 Cache Aside 的基础上做“延迟双删”先更新数据库然后删除缓存等待几百毫秒再次删除缓存。为什么要等几百毫秒再删一次就是为了解决 6.2 中提到的那种并发场景——线程 A 更新数据库后删除缓存但线程 B 在此之前已经把旧数据写回缓存。等待几百毫秒后再次删除可以从容地把这种问题覆盖掉。延迟时间的选择需要谨慎一般控制在 300~500ms 之间太短可能覆盖不了慢请求太长会影响写入链路性能。另一个更彻底的方案是订阅数据库的 binlog比如使用 Canal当数据库发生数据变更时异步地删除或刷新对应的缓存。这个方案的好处是业务代码不需要做额外处理消除了缓存操作和数据库操作之间的一致性隐患也更便于集中管理缓存失效逻辑。缺点是需要额外部署 Canal 服务增加了运维复杂度。延时双删和 Canal 方案生产环境里我都实际落地过。如果你们团队有成熟的 MQ 基础设施Canal MQ 的方案确实省心——你只需要监听 binlog 事件路由到对应的缓存删除逻辑即可。如果项目比较简单延迟双删完全够用千万别为了追求架构上的“完美”而过度设计。7. 高频踩坑点与生产环境避坑指南7.1 大 Key 问题一次删除操作把 Redis 搞卡缓存和锁之外还有一个非常隐蔽但破坏力极大的问题大 Key。当一个 Key 的 Value 值特别大比如一个 List 里有几百万条数据或者一个 String 存储了几 MB 的 JSON 字符串对这条 Key 的读写可能直接导致 Redis 服务阻塞。为什么因为 Redis 是单线程模型如果某个命令的执行时间过长它后面排队的其他请求都会变慢。对一个大 Key 执行删除、集合运算或者序列化操作可能会让 Redis 整体卡顿几百毫秒甚至几秒这在线上是不可接受的。处理方案一是避免大 Key对数据进行拆分——比如把一个大 List 拆成多个小 List二是对大 Key 的删除操作使用异步方式Redis 4.0 支持UNLINK命令异步释放内存三是通过redis-cli --bigkeys命令定期扫描 Redis 实例中的大 Key提前治理。生产环境里建议在监控大盘中配置大 Key 告警一旦发现及时处理。7.2 连接池参数调优别让连接成为瓶颈Redis 客户端使用连接池来复用连接。连接池配置不当在高并发下也会成为系统瓶颈。Jedis 的配置模板大致如下jedis: pool: max-total: 50 # 最大连接数 max-idle: 20 # 最大空闲连接数 min-idle: 5 # 最小空闲连接数 max-wait: 3000 # 获取连接的最大等待时间毫秒配置 max-total 时要考虑业务的峰值 QPS 和单个请求对 Redis 的调用次数。比如 QPS 为 2000每次请求平均调用 Redis 3 次那么需要 6000 次/秒的 Redis 操作如果平均每次操作耗时 1ms理论上需要 6 个连接就可以满足。但实际上由于网络延迟、GC、连接池获取竞争等原因一般会按理论值的 5~10 倍来设置。max-wait 也要重点关注。如果连接池耗尽获取连接的线程会等待 max-wait 的时间超过后抛出异常。设置过短会导致高并发下大量请求直接失败设置过长会拖慢整个服务的响应时间。我通常设置为 2000~3000ms同时结合线程池和熔断机制保证系统在极端情况下不会因为 Redis 连接池耗尽而雪崩。7.3 Redis 的持久化与可靠性AOF 和 RDB 该怎么选缓存场景下Redis 的持久化策略往往是被忽视的。很多团队部署 Redis 时直接用默认配置对 RDB 快照和 AOF 日志都采用默认频率结果在故障恢复时发现缓存数据大量丢失或者恢复时间过长。在纯缓存场景下Redis 本身并不对数据可靠性负责丢失部分数据可以通过回源数据库来弥补。但考虑到 Redis 中可能存储了分布式锁等关键状态持久化的合理性依然很重要。我的建议是开启 AOF 日志并设置为appendfsync everysec每秒刷盘一次这样即使发生宕机最多丢失 1 秒内的写操作恢复时间也可以接受。纯 RDB 模式则更适合对数据丢失容忍度很高的场景。在生产环境里如果条件允许Redis 最好以主从 哨兵方式部署保证主节点挂了以后能自动切换。使用 Docker 部署 Redis 主从节点时要注意数据卷的挂载和持久化参数的传递不能因为容器重启就丢失配置和数据。Redis 的主从复制在高并发读多写少的场景下非常有用能有效分担单节点的读压力但要注意主从复制的延迟——在主从延迟较大的情况下刚写入主节点的数据可能无法在从节点上立刻读到。如果业务对数据的读取有时序要求比如刚写入就要立刻读到需要采用读写都走主节点或者等待主从同步确认后返回。8. 写在最后的实战心得这篇文章比较长我把 Redis 在缓存、分布式锁、过期策略、防穿透击穿这四个方向上的核心要点都过了一遍。实际操作中我最大的感受是Redis 用得好不好关键不在于你用得多花哨而在于你踩过的坑够不够多以及你是否养成了先考虑边界情况、再写业务逻辑的习惯。就拿缓存穿透来说很多项目上线前没人想到要去防攻击者伪造 ID 的攻击直到数据库监控告警打到自己脸上才回来加班补方案。再拿分布式锁来说很多人栽在“忘记加过期时间”和“锁释放不是原子操作”这两个最常见的坑上原理明明很简单但就是容易忽略。如果让我给几个最核心的建议第一所有缓存 Key 都要设置过期时间并加上随机扰动第二分布式锁一定要用 Redisson 或者等价的具备看门狗机制的库不要自己用手写 SETNX 拼业务代码第三防穿透优先做缓存空值必要场景再加布隆过滤器第四防击穿在热点数据上优先考虑逻辑过期方案逻辑简单而且对请求延迟影响最小。最后分享一个小技巧在排查线上问题时可以通过redis-cli --stat实时观察 Redis 的操作数和内存变化再配合SLOWLOG GET 20查看慢命令很多疑难问题都能快速定位。比如我之前排查过一个“Redis 偶尔卡顿几十毫秒”的问题就是通过 SLOWLOG 发现是一条SORT命令在超大集合上执行导致的把集合改用 ZSet 存储并建立索引后问题就消失了。多熟悉这些排查工具线上遇到问题时你的底气会足很多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门