资讯动态

Redis实战:集成、分布式锁与秒杀系统设计

发布时间:2026/10/21 3:12:57 来源:尧图企业网站定制
从标题里的三个关键词就能看出来这套东西基本覆盖了日常后端开发和面试里最常被问到的Redis场景Spring Boot 集成、Node.js 集成、Redisson 分布式锁、秒杀系统。这几个点我都在不同项目里实际趟过水有的坑是上线前压测才暴露的有的是半夜告警才发现连接池炸了。这篇就把我的做法、选型理由和踩坑经历整理一下从客户端选型一直聊到秒杀预案适合准备做 Redis 集成的后端开发也适合想搞清楚分布式锁到底怎么用的同学。1. 客户端选型Lettuce、Jedis 和 node-redis 到底怎么选1.1 Spring Boot 默认用 Lettuce不是没有道理Spring Boot 从 2.x 开始默认把 Lettuce 作为 Redis 客户端很多人只是接受了这个默认值没细想过为什么。Jedis 和 Lettuce 最大的区别在于 IO 模型Jedis 用的是阻塞 IO客户端连接不是线程安全的并发场景下必须靠连接池获取连接用完归还Lettuce 基于 Netty连接实例是线程安全的多个线程可以共享同一个连接底层通过异步方式处理请求。工作方式不同直接影响了高并发下的表现。Jedis 在连接数多的时候每个连接对应一个阻塞 socket线程一多上下文切换开销就上来了。Lettuce 的单连接复用模型天然适合多线程连接资源占用少吞吐量上限更高。Spring Boot 选 Lettuce 作为默认方案本质上是为了让开发者在默认配置下就能撑住更大的并发。但这不代表 Lettuce 完美。Lettuce 的异步模型也带来一个经典问题共享连接的时候如果某个命令执行时间特别长比如KEYS *或者大SMEMBERS会拖累同连接上的其他请求。生产环境里我见过有人用 Lettuce 执行全量 KEYS 导致整个应用 Redis 请求变慢。应对办法不是换客户端而是从源头禁止危险命令并且单独准备一个 Jedis 连接池给特殊操作用。1.2 Node.js 集成 Redis 用什么库Node.js 生态里最常用的是node-redisredis这个 npm 包和ioredis。node-redis是官方维护的4.x 之后 API 改成了 Promise 风格支持连接重试、命令超时、TLS 等日常单机场景完全够用。ioredis是社区库在集群支持、Sentinel 支持、Lua 脚本、Pipeline 和 Pub/Sub 方面都更顺手适合集群部署的业务。简单说我的选择标准单机或者哨兵环境、团队对官方依赖有要求就用node-redis需要 Redis Cluster、大量使用 Lua 脚本、需要监控命令耗时或者做自定义命令插件用ioredis更省事。两个库的 API 风格差异不小项目中尽量避免混用免得排查问题时分不清是哪条链路的命令。一个常见的踩坑点是连接实例的管理。不管用哪个库都不应该每次请求都createClient()必须做成全局单例。Redis 连接本身有握手和鉴权开销频繁建连会在高并发时把 Redis 端的连接数打满还会出现大量ECONNREFUSED。我习惯在模块加载时初始化一个 client导出给路由层和业务层复用。2. Spring Boot 整合 Redis配置、序列化与连接管理2.1 RedisTemplate 的序列化策略决定了很多隐性坑Spring Boot 整合 Redis 本身不复杂加依赖、写配置、注入RedisTemplate就能用。真正容易出问题的是序列化策略。Spring Boot 默认创建的RedisTemplate使用JdkSerializationRedisSerializer往 Redis 里写数据的时候会带上类描述信息存储结果以\xAC\xED开头看起来就是一串乱码。这种序列化方式有两个问题第一可读性极差。你用redis-cli看库里的数据全是二进制排查数据对不对非常痛苦。第二跨语言兼容性差。不同版本 JDK、不同语言的客户端都读不了 JDK 序列化数据。如果系统里还有其他服务技术栈不是 Java这套数据基本是死的。我的做法是统一使用JacksonJSON 序列化。配置一个自定义RedisTemplatekey 用StringRedisSerializervalue 用GenericJackson2JsonRedisSerializer。key 保持字符串是为了让运维和排查更方便value 保留类型信息则是为了反序列化时能还原目标对象。实际配置和网上大多数模板类似但有几个细节很多人注意不到GenericJackson2JsonRedisSerializer默认需要目标类有无参构造函数否则反序列化会报错如果 value 里有泛型嵌套反序列化会丢失泛型信息需要借助TypeReference或封装一层包装对象ObjectMapper需要禁用对一些未知属性的报错否则系统间数据字段变化会直接异常2.2 StringRedisTemplate 和 RedisTemplate 的混用事故我踩过一个很典型的坑某个内部项目里一部分代码用RedisTemplate存数据另一部分代码用StringRedisTemplate读数据。运行一段时间后凡是跨这块读的数据全部为空。原因上文也提到了两个模板默认序列化器不一样RedisTemplate写入的 key 带了特殊前缀StringRedisTemplate当然读不到。修这个问题不难但排查过程花了不少时间。原因是读到方一直认为是缓存过期了反复改过期时间都没用最后用redis-cli --raw看 key 才发现二进制前缀。后来我在团队里定了一条规则纯字符串操作比如计数器、分布式锁、限流全部用StringRedisTemplate对象存取统一用自定义的RedisTemplate且全局只有这一种禁止在代码里 new 出新的模板或者混合注入两种模板操作同一块业务数据这条规则看着简单但能避免很多看似诡异的数据读不到问题。另外缓存工具类的封装里也最好固定模板不要在工具方法里各自引入依赖否则同样的坑会在不同业务里反复出现。2.3 连接池参数不能照抄默认值Spring Boot 2.x 之后虽然默认用 Lettuce但真正的连接池并不是默认启用的。如果只是引入了spring-boot-starter-data-redis没有引入commons-pool2那 Lettuce 用的是单连接直连模式不会有连接池。一旦引入commons-pool2并配置lettuce.pool才会启用连接池。这个差异直接影响了资源消耗峰值很多线上连接数飙升的问题就是这么来的。连接池参数我的经验值单机场景max-active设在 50 左右max-idle设 20min-idle设 5如果应用并发量特别高再配合压测去调整但不能无脑调大。有次我见过某服务把 max-active 配成 500结果 Redis 实例连接数直接破千服务端集群被连接压垮。连接池不是越大越好Redis 处理请求是单线程模型连接多到一定程度只会增加上下文切换和内存占用反而降低整体吞吐。还有一个容易忽略的点连接池的time-between-eviction-run和min-evictable-idle-time默认值在流量波动大的业务里会让连接频繁创建销毁。我一般会把空闲连接检测时间调大减少无谓的资源回收这样做对突发流量更友好。3. Node.js 侧整合实践连接管理、Lua 脚本与事务3.1 node-redis 的生产级连接配置node-redis4.x 里创建客户端的写法比较直观但连接稳定性靠的是reconnectStrategy。默认策略是固定间隔重试实际生产里我更倾向于指数退避并且设置最大重试延迟避免 Redis 短暂故障时客户端疯狂重连把网络打满。const redis require(redis); const client redis.createClient({ url: redis://127.0.0.1:6379, socket: { reconnectStrategy(retries) { if (retries 20) { // 重试次数过多不再继续自动重连抛出错误交给上层处理 return new Error(Redis connection lost too many times); } return Math.min(retries * 200, 3000); }, connectTimeout: 5000, }, }); client.on(error, (err) { // 这里不能直接抛异常必须记录日志否则会触发 unhandled error console.error(Redis client error:, err); }); await client.connect();这里有一个细节node-redis 4.x 如果没有显式connect()命令不会自动执行而是排队等待连接。如果你在服务启动时忘了connect()请求会一直挂着。启动阶段应该检查连接状态加上一个 wait 超时的健康检查避免服务看起来正常、实际 Redis 操作全部不可用。多实例部署时还要注意所有实例共用同一个 Redis 实例才是合理的不能每个 Node 进程都建几百个连接。单个 Node 服务的并发其实靠事件循环不需要靠多连接堆吞吐一条连接对一个 Node 进程来说完全足够。3.2 Lua 脚本实现原子扣减Redis 提供 Lua 脚本是为了把多个操作变成原子的。从 Redis 2.6 开始支持EVAL脚本脚本在服务端执行时不会被其他命令插队所以非常适合做库存扣减这类需要判断再修改的场景。以秒杀预减库存为例我需要保证两点库存存在且大于 0然后完成扣减。这两步如果分成两条命令执行并发下会出现超卖。用 Lua 脚本把判断和扣减放在一个脚本里-- KEYS[1]: 库存 key -- ARGV[1]: 扣减数量 if redis.call(exists, KEYS[1]) 1 then local stock tonumber(redis.call(get, KEYS[1])) if stock tonumber(ARGV[1]) then redis.call(decrby, KEYS[1], ARGV[1]) return 1 end return -1 end return -2脚本返回不同的值代表不同语义1表示扣减成功-1表示库存不足-2表示 key 不存在。业务层根据返回值决定是否继续创建订单。用脚本的好处是逻辑全部收敛在 Redis 端不用考虑多线程并发下先读后写的竞态。在 Node.js 里调用时ioredis直接支持defineCommand可以像调用普通命令一样调用自定义脚本。node-redis就通过sendCommand或eval执行。需要提醒的是脚本内容不要拼接字符串尽量用KEYS和ARGV参数传递避免每次调用重新传脚本内容带来不必要的网络开销。3.3 Pipeline 和 MULTI 别混为一谈Node.js 场景里很多人会把 Pipeline 和事务搞混。Pipeline 的作用是批量发送命令减少网络往返但命令之间并没有原子性保证中间某条命令失败其他命令仍然会执行。MULTI/EXEC 才是事务事务里的命令按顺序执行中间不会被其他客户端的命令插入。需要一个操作都能成功或者都不执行的场景比如一次扣多个库存 key或者同时写用户维度和商品维度数据用 MULTI/EXEC 更合适。而纯粹是攒一批查询再批量返回的场景用 Pipeline 就够。用错了会有一个很明显的体验Pipeline 执行中途出错数据出现一半更新一半没更新的情况排查时很难想到是发起端把事务语义理解错了。Redis 事务还有个限制它不支持回滚EXEC之后某个命令失败并不会撤销前面成功的操作。这也是为什么像库存扣减这类强一致操作我更推荐 Lua 脚本而不是事务。Lua 脚本语义更清晰可以显式控制业务规则出错也能按脚本逻辑返回明确错误码。4. Redisson 分布式锁原理、参数与实战配置4.1 自己写 SETNX 为什么容易翻车网上很多讲分布式锁的帖子会教一个很朴素的写法SETNX lock_key client_id然后EXPIRE lock_key 30。这个思路没错但实现上有细节坑。最直接的问题是设置锁和设置过期时间不是原子的如果SETNX成功之后进程崩溃了锁就永远不会过期。现在可以用SET key value NX EX seconds一条命令解决原子性但那也只是解决了第一步。第二个坑是误删锁。A 线程加了锁业务执行超过预期锁自动过期了B 线程此时也拿到了锁。A 执行完毕后直接DEL lock_key就把 B 的锁删掉了。正确的做法是删除前用 Lua 脚本或事务校验 value 是否还是自己的。这个校验逻辑看似简单但很多人会在高并发场景下把它省略结果就是锁形同虚设。第三个坑是锁过期时间怎么定。如果固定 30 秒业务执行超过 30 秒就失去了锁保护如果设太大持有锁的线程崩溃了其他线程要等很久才能抢到锁。与其自己折腾一套续期逻辑不如直接用 Redisson 这类成熟库。Redisson 把加锁、续期、解锁这几个操作都封装好了内部还会处理值校验比手写靠谱得多。4.2 Redisson 加锁的核心机制Redisson 的分布式锁基于 Redis 的 Hash 和 Lua 脚本实现。加锁时它向 Redis 写入一个 Hashfield 是锁持有者的线程标识value 是重入计数。这样做的好处是可以支持同一个线程重入。比如多个方法内部都要加同一把锁但它们是同一个线程在调用就不会死锁。Redisson 最让我满意的其实是看门狗机制。默认情况下Redisson 加锁后如果没有显式指定leaseTime客户端会启动一个定时任务每隔锁过期时间的三分之一续期一次。也就是说锁不会因为业务执行时间过长而自动过期只要持有锁的客户端还在正常运行锁就会一直存在。这里有一个非常重要的理解看门狗守护的是业务执行的生命周期而不是“锁一定要永续”。如果持有锁的客户端崩溃了看门狗自然停止续期锁会在最长leaseTime后自动释放不会造成永久死锁。解锁操作同样通过 Lua 脚本完成脚本会先校验当前线程是否就是锁持有者是才删除否则不处理。这个过程保证了误删问题的消除。还有一点要说明的是Redisson 的锁不是绝对的强一致锁。在主从模式下如果主节点加锁成功但从节点还没来得及同步主节点就挂了锁会丢失另一个客户端就可能拿到同一把锁。Redis 官方给出的工具是 RedLock但它也有自己的争议和适用边界。在绝大多数内部系统里我通常直接告诉团队业务允许极低概率的重复执行就直接用 Redisson否则再考虑上数据库唯一约束做兜底而不是盲目引入 RedLock。4.3 实战配置和编码姿势引入 Redisson 首先要配置客户端。Spring Boot 项目里可以直接使用RedissonClient但它不会自动注册需要手动创建配置类和 Bean。Configuration public class RedissonConfig { Bean(destroyMethod shutdown) public RedissonClient redissonClient() { Config config new Config(); config.useSingleServer() .setAddress(redis://127.0.0.1:6379) .setDatabase(0) .setConnectionPoolSize(10) .setConnectionMinimumIdleSize(3); return Redisson.create(config); } }实际使用中有几个容易被忽略的细节。第一个是加锁失败的处理默认的tryLock(waitTime, leaseTime, timeUnit)在等待时间内没有拿到锁会返回 false业务上要提前想好拿不到锁怎么办很多人直接抛异常会让一些本可以降级的请求直接进入错误链路。第二个是锁的粒度锁 key 要尽量细。比如给用户下单加锁要加lock:user:{userId}而不是全系统共用一个全局锁否则大量用户的请求会被串行化。第三个是锁内不要做耗时 IO尤其是调用外部接口或大批量查库操作锁内越短越好最大限度缩短持有时间。一个我用着比较舒服的模板是定义泛型业务方法把加锁逻辑和业务逻辑分离public T T runWithLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit, SupplierT action) { RLock lock redissonClient.getLock(lockKey); boolean locked false; try { locked lock.tryLock(waitTime, leaseTime, unit); if (!locked) { throw new BizException(获取锁失败请稍后重试); } return action.get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new BizException(获取锁被中断, e); } finally { if (locked lock.isHeldByCurrentThread()) { lock.unlock(); } } }最后有一个经验Redis 分布式锁只能防君子不能防小人它能防止业务逻辑自身出现并发竞态但防不了同一用户绕过你的后端直接重放请求、防不了下游数据库事务超时导致的数据不一致。锁只是第一道防线业务侧依然要结合幂等、唯一索引、状态机等手段做兜底。5. 秒杀系统完整设计从库存预热到异步下单5.1 秒杀场景的瓶颈到底在哪里秒杀系统的特点是瞬时流量极高、参与用户多、真实库存很少。瓶颈通常不在 Redis而在下游的数据库订单表和库存表。如果每个请求都直接落到数据库扣库存数据库连接和行锁会瞬间被打爆。所以秒杀设计的第一条原则是在离用户更近、能力更强的那一层拦截流量让尽量少的请求真正落库。具体拆开看秒杀要处理几个关键问题瞬时高并发入口需要限流避免无限流量打进来库存准确性超卖是绝对不允许出现的重复下单同一用户多次下单只应成功一次库存回补下单失败或者超时未支付库存要恢复热点数据库存 key 会被超高并发访问Redis 单 key 要扛住大量读这个问题的组合其实很经典单独某一块都不难但组合在一起就需要设计方案来应对了。我的习惯是把秒杀流程拆成四个阶段事前预热、请求拦截、库存扣减、异步落单。5.2 请求入口限流与防刷秒杀的限流要分几个层次做。首先是网关或接入层的全局限流保护后端整体不被打垮这个可以用 Sentinel 控制单机 QPS 或集群总量也可以用 Redis 做计数器做分布式限流。第二个层次是用户维度限流每个用户几秒内只能请求一次这个在单体应用直接上本地缓存标记就行分布式场景才需要 Redis。一个简单的用户维度限流做法是// key: seckill:limit:{userId} // 窗口内第一次请求设置过期时间后续请求自增并判断次数 const key seckill:limit:${userId}; const count await client.incr(key); if (count 1) { await client.expire(key, 5); } if (count 3) { // 拒绝请求 }这只是简单计数器实际能做到的拦截粒度有限。更完善的做法是用 Lua 脚本把“固定窗口计数 滑动窗口滑动 过期时间设置”整合起来保证原子性。还有人用令牌桶算法实现更平滑的流量控制但在秒杀这种瞬时流量本就该被拒绝的场景里我不觉得令牌桶有多必要固定窗口就已经很实用了。接口防刷不能只靠限流。用户是否登录、用户是否在黑名单里、设备指纹、订单频率等都会被组合成风控规则。秒杀系统的门槛设计要保证正常用户可以参与但脚本批量请求能被识别并拦截。这部分我通常配合网关层统一处理业务代码不掺和。5.3 库存预热与原子扣减秒杀开始前把库存提前写入 Redis这一步叫库存预热。预热和真正展示的库存口径尽量一致避免出现页面上显示有库存点进去却提示已售罄的体验问题。预热时还可以在 Redis 里放一个秒杀活动的元数据 key包含活动 ID、商品 ID、限购数量、开始时间和结束时间业务层请求时先校验活动状态避免非活动时间内也去扣库存。库存扣减就是前面提过的 Lua 脚本。不管用的是 Spring Boot 还是 Node.js核心都是同一个脚本判断库存足够再DECRBY。扣减结果直接决定是否走到下一步成功则发送一条异步消息去创建订单失败则直接返回商品已抢完。这里有一个关键点扣减成功后Redis 里的库存已经减少了但订单还没创建如果创建订单环节失败必须回补库存。回补动作可以通过消息队列的重试或定时任务扫描未完成订单来实现。我曾经在一个项目里把落库和库存扣减倒过来做先创建数据库订单再去扣 Redis 库存结果高并发下数据库创建订单的速度远跟不上扣减速度导致大量用户在 Redis 侧已经买成功了数据库里根本查不到订单。这种方案看着合理实际上反向放大了数据库压力最后的结论是必须先做 Redis 预扣才能挡住绝大多数请求。5.4 分布式锁在秒杀里的正确位置很多人一听到秒杀系统的做法就想到用分布式锁把整个下单流程锁起来。这其实是个误区。Redis 库存扣减本身就是原子的加了分布式锁并不会让扣减更安全反而会把并发度降低到接近 1。分布式锁在秒杀里真正有价值的场景是防止重复下单同一用户并发点击多次购买按钮可能导致多条订单创建请求同时进入。这个场景下给用户维度加锁是合理的因为同一个用户的下单请求本来就是串行处理的不会影响其他用户。不过防止重复下单还有更轻量的做法比如数据库订单表加(user_id, activity_id, sku_id)唯一索引重复请求触发唯一索引冲突时回滚并返回已下单。这种兜底方案比纯用锁更可靠。如果项目里已经有了 Redisson那么两者结合用户维度加锁挡住大部分重复请求数据库唯一索引挡住极端并发下的漏网之鱼。分布式锁另一个少为人知的用途是在秒杀活动的库存回补和状态更新上。多个机器同时处理超时未支付订单可能存在同一订单被两个任务同时扫描到给 Redis 回补了两次库存。这种情况用订单维度的分布式锁加标记位配合 Redis 的SETNX状态标记能有效避免重复回补。5.5 异步下单与最终一致性库存扣减成功后会发送一条消息比如丢到 RabbitMQ 或 RocketMQ 里由消费者创建订单、扣减用户余额、做后续业务。异步的目的是把同步接口的 RT 控制在毫秒级用户点击按钮Redis 扣减成功就直接返回“已抢到”后面的订单创建在消息队列里慢慢执行。异步带来的问题是最终一致性。消息发送成功了但消费者处理失败怎么办我习惯的做法是发送消息前先落一条本地消息表状态为“待发送”发送成功后再标记为“已发送”消费者处理成功后再调用回执接口更新状态。如果消费者处理失败消息表里的记录会一直停留在“已发送”由定时任务驱动重新投递。库存回补也要配合这个机制。消费者创建订单失败后要执行一次 Redis 库存回补操作同时把订单记录标记为失败防止定时任务重复处理同一条消息。这里特别要注意回补时的原子性回补前检查订单状态如果订单已经成功创建就不能回补否则库存会多出来。5.6 超时未支付与库存释放秒杀成功的人不一定真的会付款尤其是商品单价低的场景。所以秒杀系统通常会设置一个支付超时时间比如 15 分钟。这个超时机制我一般不做实时扫描而是通过延迟消息处理。下单时往延迟队列里投递一条消息延迟时间等于支付超时时间消息到达后检查订单支付状态如果尚未支付就关闭订单并回补库存。这个方案比定时扫表更实时也更能应对秒杀这类时间敏感型业务。但延迟消息也有自己的问题延迟时间过长比如超过一小时时对消息中间件的压力会很大有些中间件在大量延迟消息时会退化。备选方案是把订单按创建时间分表轮询定时扫出超时订单批量处理。两种方案都行看团队对基础组件的熟悉程度。我在一个项目里先用定时任务后面发现秒杀订单数量大、扫描全表太慢改了延迟消息性能好了不少。6. 常见问题与排查技巧实录6.1 缓存三大问题对秒杀的叠加影响秒杀系统的高并发会对 Redis 造成一种特有的“热点压力”所有用户都在访问同一个商品的库存 key。这个 key 本身就是热 key单台 Redis 实例的 CPU 会集中在一个分片或者一个 slot 上。如果 Redis Cluster 没有把热 key 分散那么这台节点的 CPU 会被打满其他请求也跟着变慢。缓存穿透、击穿、雪崩这些常规问题在秒杀里也会以变体形式出现。比如活动结束后用户疯狂刷新页面如果库存 key 已经被删除而代码里没有加缓存空值保护那么大量请求会穿透 Redis 直达数据库这就是穿透。解决方式是在活动结束后保留一个“售罄”状态的 key而不是直接删除这个 key 里存一个空结果或者固定结果让请求不会走到数据库。热 key 的另一个解法是本地缓存副本。在 JVM 进程内维护一份库存状态的只读备份定期从 Redis 刷新这样大量读请求直接命中本地缓存只有库存变更时才同步 Redis。这个方案对一致性要求不那么高的读场景很有效但秒杀场景的读一致性其实很高本地缓存的一小段时间窗可能造成显示有库存但实际扣不到所以使用前要权衡。6.2 常见问题速查表现象可能原因排查思路Spring Boot 里用 StringRedisTemplate 读不到 RedisTemplate 写入的数据序列化器不一致key 带上二进制前缀用redis-cli --raw查看存储内容统一序列化方案高并发下 Redis 连接数飙升甚至把 Redis 打挂连接池配置过大或客户端没有复用连接检查 Lettuce pool 参数、Node 端 client 是否单例分布式锁偶尔失效两个线程同时进入临界区锁过期时间太短业务没执行完或者主从切换丢锁使用 Redisson 看门狗评估业务耗时必要时加数据库兜底扣库存成功但数据库没有订单Redis 预扣与下单异步链路断裂检查 MQ 投递与消费、消息表状态、回补逻辑Redis 的 CPU 飙高ping 延迟正常但命令很慢存在大 key 或热 key慢查询居多用--bigkeys扫描大 key开启慢日志定位问题命令同一个用户下单请求返回成功但订单出现多条防重逻辑没覆盖并发窗口加用户维度锁加唯一索引检查幂等逻辑6.3 我的一些压测和上线心得每次接 Redis 相关项目我最先做的都不是写业务代码而是确认基础配置和连接管理模式。很多线上问题都出在“能用”和“扛住并发”之间的距离上。一个简单的自测方法压测前先用redis-benchmark打个底确认 Redis 本身的能力边界然后再压自己的服务通过对比偏离程度判断是客户端的问题还是业务代码的问题。秒杀系统的线上验证方面我的习惯是搞一次小规模的预演只开放部分用户流量进到库存扣减环节重点观察 Redis 的CPU、内存、连接数、慢日志和 MQ 的积压情况。秒杀前 10 分钟检查各节点的健康状态活动开始后盯着四个核心指标Redis 的 ops/s、平均 RT、错误率和 MQ 消费积压数。只要这四个指标稳定系统基本不会出大问题。最后再说一个容易被忽略的小点Redis 的过期 key 删除策略是惰性删除加定期删除高并发下大量 key 同时过期会让 Redis 的 CPU 出现一个短暂的尖峰。秒杀场景里如果给所有用户的临时标记位设置了相同的过期时间就会触发这种效应。解决的思路很简单给过期时间加上一个随机偏移让 key 分散过期能显著降低 CPU 尖峰。这些坑我大部分都亲自踩过有的修起来很快有的确实折腾了整晚。但回过头看Redis 本身是稳定可靠的绝大多数故障都出在使用姿势上。配置不复杂复杂的是理解这些配置在极端流量下会怎么表现。希望这篇整理能让你在立项时少走点弯路。

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

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

免费获取报价 →
↑