先问一个问题你有没有在某个大促节点蹲点抢过券零点一过按钮一点转圈、报错、再点最后券没了——这个过程的背后是一套要在几秒钟内扛住十万级 QPS 的系统。优惠券系统看起来只是发券核销两个动作但它同时踩中了高并发、分布式一致性、防超发、幂等、限流、削峰这一串大坑。这篇文章我就从零开始把搭建一套10万级 QPS 大流量、高并发优惠券系统的完整思路、技术选型、核心代码和踩坑实录都盘一遍。不管你是刚接触高并发不久的中级开发还是正准备在公司里做类似中台系统的负责人这篇东西都能给你一个可以直接对标参考的底稿。先说清楚我这里讨论的优惠券系统指的是电商/外卖/出行这类场景下的通用营销券中台核心流程包括创建活动、发券、查券、核销、对账。流量压力集中在两个地方一是活动开启那一刻的集中领券二是核销高峰期的券状态校验。这两条链路如果设计不好轻则领券超发导致资损重则把数据库打死拖垮整个订单服务。所以本文的重点会放在库存扣减与防超发、缓存架构、异步削峰、限流降级这四个硬骨头上附带完整的代码示例和压测数据。下面直接开始。1. 需求拆解与技术选型先想明白难点在哪1.1 拆解业务链路领券、核销、对账分别考验什么很多人一上来就谈技术选型这是顺序搞反了。做高并发系统第一步永远是把业务链路拆到底找出真正的压力点和一致性要求。优惠券系统的核心链路可以拆成四条发券链路用户点击领取→校验活动规则时间、用户等级、限领数量→校验库存→扣减库存→记录用户券→异步通知。这条链路的压力特征是瞬时峰值极高活动一开百万用户同时进来QPS 曲线直接拉满。查券链路用户进我的卡券页、下单页勾选可用券、收银台展示优惠明细都会查券。这条链路是典型的读多写少QPS 通常比发券高一个数量级但压力可以通过缓存解决。核销链路下单时锁定券、支付成功后核销、退款时回退券。这条链路最考验数据一致性因为核销动作往往不在优惠券系统内部而是在订单系统里通过远程调用或 MQ 触发。对账链路晚间跑批核对发出多少张、核销多少张、退款回退多少张涉及和财务系统的数据对齐要求的是最终一致性和完整的流水记录。把这四条链路摆出来技术难点就清晰了。发券链路的难点是瞬时高并发下的防超发查券链路的难点是缓存结构与缓存一致性核销链路的难点是分布式环境下的状态流转对账链路则要求前面的每一步都留下可靠痕迹。后面所有方案设计都围绕这四个难点展开。1.2 技术栈选型为什么是 Redis MQ MySQL而不是别的先看一下我最终采用的方案再解释为什么这么选。层级选型核心职责接入层Nginx 网关黑白名单、基础限流、路由转发本地缓存Caffeine热点活动信息和券模板的毫秒级读取分布式缓存Redis Cluster库存预扣、用户领券记录、活动配置缓存异步削峰消息队列RocketMQ发券结果落库、核销通知、对账流水数据库MySQL分库分表券实例最终存储、对账数据限流组件Sentinel 等接口限流、热点参数限流、系统自适应限流这套选型是高并发场景的经典答案不是拍脑袋定的。逐个说理由为什么缓存用 Redis Cluster 而不是单机 Redis10万 QPS 的读流量单机 Redis 勉强能顶但写流量一上来单机 CPU 和带宽都会成瓶颈。Cluster 模式可以横向扩容把 key 分散到多台机器上。更重要的是库存扣减需要使用Lua 脚本保证原子性而 Redis 的 Lua 执行是单线程串行的天然适合做库存这类强一致操作这是 MySQL 行锁方案给不了的性能优势。为什么引入消息队列发券链路里扣减库存和写库落账其实可以解耦。库存扣减必须在请求线程内同步完成因为它直接决定用户能不能领到券但生成券实例记录发送核销通知这些操作完全可以异步等 MQ 慢慢消费。这样做的直接好处是数据库的写入压力被摊平了而不是集中在活动开启的那几秒里。为什么数据库最终选了 MySQL 分库分表券实例的量级非常大——一次大型活动发几千万张券很正常一年的累计量轻松上亿。单表肯定扛不住按用户 ID 或券模板 ID 做水平拆分是必经之路。选 MySQL 而不是 NoSQL是因为券数据有强事务和复杂查询需求比如对账系统要按时间状态聚合查询关系型数据库在这一块依然不可替代。本地缓存 Caffeine 是不是多余不多余。活动配置、券模板信息这类数据特点是读多、写极少、允许短暂不一致把它们放在应用本地内存里一次查询耗时可以压到微秒级几乎不占网络开销。我见过不少团队忽略这一层结果 Redis 集群 QPS 高得吓人实际上 70% 的查询是重复读同一份模板配置。本地缓存这几个热点 key能为 Redis 分担大量压力。注意本地缓存一定要控制好 key 数量和过期时间并且要监听配置变更主动失效否则会出现活动配置改了用户端还显示旧规则的问题。2. 整体架构与缓存设计流量怎么分层扛住2.1 从接入到存储每一层分别挡掉什么流量10万级 QPS 的系统绝不是一台应用服务器硬扛。流量必须一层一层被削掉越靠近用户处理要越快越轻。完整链路是这样的用户请求先到 NginxNginx 做 IP 黑白名单和最简单的连接数限制然后进入网关层。网关层做接口级限流、用户身份识别、风控拦截过了网关的请求才到达优惠券系统的应用服务。应用服务先查本地缓存Caffeine查不到再查 RedisRedis 也没有才回源到 MySQL。从流量的角度看每一层都在过滤请求Nginx 挡掉恶意流量和连接风暴网关挡掉超速请求本地缓存挡掉重复查询Redis 挡掉绝大部分数据库读压力。真正打到 MySQL 的只有那些缓存 miss 的读请求和异步写入请求。这里有一个关键思路** 10万 QPS 指的是入口流量而不是数据库流量。** 如果设计合理MySQL 每秒处理的请求应该只有几千甚至几百。衡量架构好不好的标准就是看最终打到数据库的流量被削掉了几个数量级。2.2 三级缓存方案本地、分布式、数据库各自管什么我把缓存设计拆成三级每一级管不同类型的数据互不干扰。一级本地缓存Caffeine存放活动配置、券模板信息、限领规则。这类数据的特点是量小、读极频繁、变更极少。我把过期时间设为 5 分钟同时监听配置中心的变更事件收到变更后立即失效对应 key。二级Redis Cluster存放库存余量、用户已领券记录、发放流水号。这几个数据是并发写的主力也是整个系统防超发的核心所在后面会专门讲。三级MySQL存放券实例最终数据、对账流水。应用层只在 cache miss 时回源而且回源会加一个防止缓存击穿的互斥锁。这里要专门说一个高频踩坑点缓存击穿。活动开启时如果活动配置刚好在那一秒过期大量请求同时发现缓存 miss就会同时打到数据库——这就是击穿。我的处理方式是在回源逻辑里加锁同一个 key 只允许一个线程回源其他线程自旋等待。示例代码如下public CouponTemplate getTemplateFromCache(Long templateId) { String key coupon:template: templateId; // 先查本地缓存 Object localVal localCache.getIfPresent(key); if (localVal ! null) { return (CouponTemplate) localVal; } // 查 Redis String json redis.get(key); if (json ! null) { CouponTemplate template JSON.parseObject(json, CouponTemplate.class); localCache.put(key, template); return template; } // 防止击穿分布式锁只允许一个请求回源 String lockKey lock:template: templateId; boolean locked redis.set(lockKey, 1, NX, EX, 3); if (locked) { try { CouponTemplate template loadFromDb(templateId); redis.set(key, JSON.toJSONString(template), 300); localCache.put(key, template); return template; } finally { redis.delete(lockKey); } } else { // 没抢到锁就短暂等待然后重新走缓存 Thread.sleep(50); return getTemplateFromCache(templateId); } }这个方法实测下来能把数据库的读压力压掉 90% 以上。记住一个原则缓存 miss 回源必须加锁否则高并发下数据库必死。不过要注意上面的代码里自旋等待用的是线程 sleep在高并发下容易浪费线程资源。更优的做法是使用 CompletableFuture 或内部缓存框架自带的单个 key 回源机制效果类似但线程利用率更高。2.3 热点 Key 识别与处理避免单个 Redis 节点被打挂10万级 QPS 下还有一个非常隐蔽的坑——热点 Key。某个爆款活动可能 80% 的请求都集中在同一个券模板 ID 上。Redis Cluster 会把这个 key 路由到某个固定节点于是所有流量都打在同一台机器上导致该节点 CPU 飙升、带宽打满而其他节点空闲。热点 Key 的常规解法有两个方向。一是本地缓存提前挡流量既然热点 key 的读流量巨大就把它主动放进本地缓存用 JVM 内存扛这部分流量。二是给 key 加随机后缀做复制比如把一个热点 key 变成hot_0到hot_9十个 key分散到不同节点读取时随机读其中一个。写操作则需要把数据同时写到十个副本逻辑复杂一些但这是对读热点最有效的兜底。我做过的做法是在应用启动时加一个热点发现定时任务统计最近 5 分钟内 Redis 各 key 的访问次数超过阈值的 key 自动加入本地缓存热点名单动态调整本地缓存过期时间。这个机制上线之后单个 Redis 节点的带宽峰值下降了约 40%。3. 库存扣减与防超发秒级并发下怎么保证一张不多发3.1 三种库存方案对比数据库乐观锁为什么不够用防超发是整个优惠券系统最核心的一致性难题。先看三种主流方案再告诉你我为什么最终选 Lua。方案一数据库乐观锁。在 MySQL 里执行update coupon_stock set stock stock - 1 where stock 0。这个写法本身没问题但问题是数据库行锁的性能上限摆在那里单表每秒能承受的 UPDATE 操作通常只有几千到一万多。10万 QPS 的领券请求全部走这条路数据库直接被打爆。而且库存操作会被锁阻塞延迟飙升用户体验会很差。方案二Redis 原子自减。用DECR命令扣库存如果返回值小于 0 就回补。问题在于扣减成功之后还需要判断用户是否已领过券这是两个操作需要额外的 Lua 脚本才能保证原子性。如果单纯用DECR做扣减会出现库存扣了但用户重复领取的问题。方案三Redis Lua 脚本把库存判断、扣减、记录用户放在一个脚本里原子执行。Redis 执行 Lua 脚本是单线程串行的意味着同一个脚本在同一时刻只有一个实例在执行从根本上消灭了并发竞争。这是目前业界做秒杀类库存扣减的主流方案。结论很明确上 Redis Lua 脚本把库存校验、扣减、限量判断三步合并为一次原子操作。3.2 核心 Lua 脚本代码扣减、限领、记录一次完成下面这段 Lua 脚本是我经过多次迭代后沉淀下来的核心代码-- 入参KEYS[1] 库存keyKEYS[2] 用户领取记录keyARGV[1] 用户IDARGV[2] 限领数量 -- 1. 判断库存 local stock tonumber(redis.call(GET, KEYS[1]) or 0) if stock 0 then return -1 end -- 2. 判断用户是否已领 (SISMEMBER 用户已领集合) local hasClaimed redis.call(SISMEMBER, KEYS[2], ARGV[1]) if hasClaimed 1 then return -2 -- 用户已领过直接返回 end -- 3. 判断是否达到个人限领数量 local claimedCount tonumber(redis.call(SCARD, KEYS[2]) or 0) local limitCount tonumber(ARGV[2] or 1) if claimedCount limitCount then return -3 end -- 4. 扣库存 记录用户 redis.call(DECR, KEYS[1]) redis.call(SADD, KEYS[2], ARGV[1]) -- 5. 设置库存key过期时间防止key永不失效 redis.call(EXPIRE, KEYS[1], 86400) redis.call(EXPIRE, KEYS[2], 86400) return 1这段脚本的关键在于库存扣减和用户领取记录是在同一个原子操作里完成的因此不会出现库存扣了但记录没写或记录写了但库存没扣的中间状态。调用方Java 侧的代码也要注意细节public Result claimCoupon(Long userId, Long templateId, int limitCount) { String stockKey coupon:stock: templateId; String userKey coupon:user: templateId; // Lua 脚本通过 Jedis 或 Lettuce 执行 Long result redis.eval( STOCK_LUA_SCRIPT, Arrays.asList(stockKey, userKey), Arrays.asList(String.valueOf(userId), String.valueOf(limitCount)) ); if (result 1) { // 异步发送 MQ让券实例落库、记录流水 sendClaimEvent(userId, templateId); return Result.success(); } else if (result -1) { return Result.error(库存不足); } else if (result -2) { return Result.error(您已领取过该券); } else { return Result.error(已达到领取上限); } }这里有个关键技巧Lua 脚本不必每次调用都重复传整个脚本内容。Redis 支持SCRIPT LOAD预先加载脚本拿到一个 SHA 签名后续用EVALSHA执行可以减少网络传输开销。高并发场景下这个优化能省下不少带宽。我实测过同样的一万次调用用 EVALSHA 比直接 EVAL 延迟低约 15%。注意Lua 脚本里的 KEYS 和 ARGV 必须严格区分开。动态的 key 名比如用户 ID必须放在 KEYS 里由客户端传入绝不能在脚本内部拼接字符串来拼 key否则 Redis Cluster 模式下会因为 key 不在同一个 slot 上而报错。上面脚本里 KEYS[2] 的构造会在 Java 侧完成确保两个 key 都带相同的 hash tag使它们被路由到同一个节点。3.3 幂等设计重复点击和重试机制怎么防防超发不能仅仅依赖 Lua 脚本还要防止重复请求。用户在活动页疯狂点击领取按钮或者上游系统因超时自动重试都可能导致同一用户同一活动产生多个领券请求。Lua 脚本里用 Set 判断用户是否已领取能挡住大部分重复但挡不住同一请求被重试的情况——比如请求在网络上延迟客户端超时重发服务端实际上已经处理成功了。解决方案是引入请求幂等键。每次领券请求在进入系统时生成一个全局唯一的 requestId可以用 UUID也可以用雪花算法在处理之前先写一个幂等键到 Redispublic Result claimWithIdempotent(String requestId, Long userId, Long templateId, int limitCount) { String idemKey coupon:idempotent: requestId; // 尝试写入幂等键只有第一次能成功 boolean first redis.set(idemKey, 1, NX, EX, 600); if (!first) { // 已经有相同 requestId 处理过了直接返回处理中或成功 return Result.success(重复请求幂等拦截); } // 继续走 Lua 扣库存逻辑 return claimCoupon(userId, templateId, limitCount); }这样处理之后不管是用户手动狂点按钮还是 RPC 框架的重试机制同一笔请求只会被真正处理一次。代价仅仅是在 Redis 里多存了一个 10 分钟的短命 key非常划算。幂等是防止资损的最后一道防线宁可多写几行代码也不能漏掉这道判断。4. 限流与削峰流量太大时先保可用性再保准确性4.1 三层限流配置用户级、接口级、总量级分别怎么设即使缓存和 Lua 脚本能扛住大部分流量也必须在系统入口做限流。限流的意义不在于拒绝用户而在于牺牲一部分请求保住整个系统的可用性。如果系统被冲垮了所有人都会领不到券——那才是真正的灾难。我一般做三层限流第一层用户维度限流。同一个用户在单位时间内最多领取多少次或者一分钟内最多访问多少次接口。这层主要防黄牛脚本可以用 Sentinel 的热点参数限流实现根据用户 ID 做维度限制。比如设置 QPS 为 1一个用户一秒只能领一次点再快也没用。第二层接口维度限流。对整个领券接口设置总 QPS 阈值比如 10万 QPS。这层防的是流量整体超过系统承载能力。使用 Sentinel 的集群流控或网关层的限流规则都可以实现。第三层总量维度限流。在 Redis 里维护一个全局计数器总领取次数达到活动设定的总库存后直接拒绝所有领券请求。这个判断可以在 Lua 脚本里完成返回已抢光。三层限流的配置规则示例# Sentinel 规则配置示例 { resources: [ { resource: coupon:claim, grade: 1, # 1- QPS 模式 count: 100000, # 总 QPS 上限 strategy: 0 # 直接拒绝 }, { resource: coupon:claim:userId, grade: 1, count: 1, # 单用户 QPS 上限 strategy: 0, controlBehavior: 0, # 直接拒绝 parseParameter: [userId] } ] }有同学可能会问限流的阈值设多少合适我的经验是先按压测得到的系统最大承载值乘以 0.8 来设置保留 20% 的余量。比如压测显示单集群最大能扛 12万 QPS那么线上就限 10万。留这 20% 的余量是为了应对流量毛刺和垃圾回收造成的抖动避免限流器自己也成为瓶颈。4.2 基于 MQ 的削峰落库数据库写入为什么要异步化限流之后真正进入业务逻辑的流量会稳定在一个系统能承受的范围内。但接下来的问题是即使只有 10万 QPS 进入了业务逻辑数据库也扛不住这么高频的插入。假设领券成功后要给用户生成一张券实例记录一秒 10万条 INSERT任何 MySQL 单库都顶不住。解决思路是削峰填谷把同步写入变成异步写入。领券成功后应用层只向 MQ 里扔一条消息然后就返回用户领取成功。消费端每秒从 MQ 拉一批数据批量写入数据库。这里有一个非常重要的认知用户感知的成功和数据库里的落库不必是同时发生的。只要消息不丢券最终一定会计入用户账号。对用户来说领券动作的响应时间反而更快了。实际消费端的伪代码Component public class CouponEventConsumer { Autowired private CouponInstanceMapper couponInstanceMapper; RocketMQMessageListener(topic COUPON_CLAIM_TOPIC, consumerGroup coupon-claim-group) public void onMessage(ListMessage messages) { // 攒够一定数量或一定时间后批量落库 ListCouponInstanceDO batch convert(messages); couponInstanceMapper.batchInsert(batch); // 一次插几百条 // 确认消息已消费 } }批量插入的优化效果非常明显。一条一条插入每条大概 1-2 毫秒一个线程一秒最多插几百条改成批量插入后一次插入 500 条可能只需要 20-30 毫秒单线程每秒可以处理一万五千条以上。如果开 10 个消费者线程每秒就能写入 15万条数据库完全来得及。这里有个常被忽略的细节批量插入一定要控制单批的大小和总量。插太多单条 SQL 的网络包太大MySQL 的 undo log 和 binlog 压力也会剧增。我一般把单批控制在 200-500 条同时用多线程分片消费每个线程独立一个数据库连接避免一把大锁堵死。5. 压测数据与性能调优从每秒一万到十万我做了什么5.1 压测环境与工具没有真实数据就没法做调优说这么多设计很多人关心10万 QPS 到底是怎么测出来的。这里分享一下我的压测方法和环境配置。压测工具我用的是开源的压测平台配合脚本生成并发请求。模拟环境规格如下6 台应用服务器8 核 16G、3 台 Redis 节点16G 内存、3 台 MySQL 节点一主两从。压测场景是模拟 500 万用户同时抢 100 万张券。关键压测参数并发线程总数5000模拟高并发用户数压测时长10分钟需超过活动瞬间峰值时长才能暴露慢 SQL 和连接池问题请求分布领券请求 80%查券请求 20%核心关注指标QPS、响应时间 TP99、错误率、Redis CPU、MySQL 线程数这里要提个醒压测不能只看平均响应时间一定要看 TP99 和错误率。平均响应时间 50 毫秒看着不错但 TP99 是 800 毫秒、错误率 5%那就是用户体验灾难。我在调优过程中最关注的就是这两个指标。5.2 三轮调优的对比数据每一步优化带来了什么我把调优过程分成三轮每一轮解决一个层面的问题数据变化很有代表性。指标第一轮原始第二轮加缓存Lua第三轮加MQ调参入口 QPS3万8万10万TP99 响应时间480ms210ms90ms数据库 QPS2.8万6000800错误率8%1.5%0.2%Redis CPU 峰值45%60%55%第一轮的写法是最朴素的直连数据库方案吞吐量很快见顶数据库连接池被占满大量请求排队超时。第二轮的改动主要是引入 Caffeine 本地缓存、Redis 缓存和 Lua 脚本扣库存数据库流量立刻降了一个数量级。第三轮的改动重心是异步落库和参数调优——调整线程池大小、批量写入系数、连接池上限之后系统吞吐量突破了 10万 QPS而且 TP99 稳定在 100 毫秒以内。这个数据说明一个道理高并发系统的瓶颈往往不在单点算力而在流量分层是否合理。数据库被保护住了系统就活下来了数据库被打爆性能再好的应用服务器也白搭。5.3 上线前的最后检查连接池、GC、线程池调参清单除了架构层面的优化上线前还必须过一遍基础参数。这些参数看似不起眼但在高并发下任何一个配置失误都会引发雪崩。我把自己的检查清单列在这里数据库连接池最大连接数不能太小否则高并发下线程会排队等连接。我的配置是initialSize50, maxActive300连接池满时快速失败而不是无限等待。HTTP 线程池Tomcat 或 Vert.x 的线程数要大于理论上限并发默认 200 在 10万 QPS 下远远不够我调整到 500同时配合工作队列避免线程直接打满。Redis 连接池Jedis 连接池 maxTotal 设到 500 以上空闲检测打开。Redis 连接数过少在高并发下会大量出现JedisException: Could not get a resource from pool或者是连接建立风暴。JVM 参数堆内存至少给到 8G使用 G1 收集器-XX:MaxGCPauseMillis100控制 GC 停顿。我在压测中发现 G1 的停顿时间远低于 CMS对 TP99 的改善非常明显。还有一个很重要的经验是连接池大小的反向直觉连接池不是越大越好。数据库连接数过多会导致 MySQL 内部上下文切换开销变大反而拉低整体吞吐。我的经验值是应用线程数的一半到三分之一就够了。在第三轮优化里我把数据库连接池从 500 降到 200吞吐量不降反升数据库负载也显著下降。这一点新人在调优时最容易踩坑。6. 线上事故与排查经验真实场景里踩过的坑6.1 缓存穿透导致数据库被打满第一件真实的教训就是缓存穿透。当时有一个查询券详情的接口传入一个根本不存在的券模板 ID。每个请求都直接绕过缓存查数据库数据库根本扛不住。排查时发现 Redis 的命中率从 95% 掉到了 60%但查看代码逻辑发现缓存 miss 之后直接查库完全没有兜底。修复方法是空值缓存当数据库查不到数据时在 Redis 里也写入一个空值并设置 60 秒过期时间。这样后续请求在过期前都会命中空缓存不会穿透到数据库。同时加布隆过滤器挡掉明显不存在的 key。从这次事故里学到的教训是任何查询接口都必须假设缓存可能 miss提前设计好 miss 后的行为。6.2 网络超时导致的重试风暴第二件事发生在一次大促前。领券网关调下游会员服务时由于下游服务抖动导致大量超时网关开启了自动重试结果一次简单的领券请求在下游抖动期间被放大了 5 倍形成重试风暴直接把会员服务打挂了。这个问题的根因不在会员服务而在调用方的重试策略。现在的做法是只对幂等且非致命错误的重试设置整体次数上限比如最多重试 1 次同时开启快速失败超时时间从 1 秒降低到 300 毫秒宁可这次失败也不给系统添乱。另外配合了熔断器下游持续错误时主动熔断不再发起调用让下游有时间恢复。高并发系统里最怕的不是某一台机器挂了而是故障被重试这个动作无限放大。6.3 消息积压导致用户领券状态不一致还有一次事故源于消费端消费速度下降。某个时间段内活动运营为了冲量临时加大了发券数量消息生产者速度飙升但消费端的批量插入逻辑没有跟着扩容导致 MQ 积压了几十万条消息。用户在页面看到领取成功但券实际到账延迟了十几分钟。排查思路是先看消费端日志发现批量插入偶尔报主键冲突再看数据库发现批量插入的批次里因为包含重复的 requestId导致 insert 失败。修复方式是消费端做批量插入时忽略冲突同时在消费逻辑里增加动态扩缩容机制当积压数量超过阈值时自动增加消费者线程。这次事故之后我还给消费端加了一个监控页面实时展示待消费数量和消费延迟已经积压就立刻报警。消息队列不是万能保险箱消费速度上不去积压只是时间问题。6.4 常见问题速查表问题现象可能原因排查方法解决方案数据库连接池耗尽慢 SQL 占用连接、缓存穿透查看数据库慢日志、连接池监控加缓存、优化 SQL、限制并发Redis CPU 飙升热点 key 集中请求用redis-cli --hotkeys或监控命令查看本地缓存热点 key、多 key 复制分散领券提示库存不足但页面还有数量Redis 库存和 DB 库存不同步检查 Lua 脚本和回补逻辑定期比对库存、设置库存预热任务消息消费延迟大消费者线程不够、批量插入慢查看 MQ 积压数、消费端日志扩容消费者、优化批量插入活动刚开始就报已抢光库存 key 不存在或预热失败检查定时预热任务活动开始前强制预热 Redis 库存这张表是我在多个项目里反复沉淀下来的遇到同类问题可以直接对照。有几次排查我甚至直接靠现象解决方案两列就能定位问题省了不少时间。7. 写在最后关于高并发系统设计我给你三句实话项目做完了压测达标了也有几件事是我想对你说的。第一句系统不可能一步到位调优是一个测量-定位-修改的循环。10万 QPS 不是设计出来的是压测压出来的。每次压测都会暴露出一个新的瓶颈可能是连接池、可能是缓存命中率、可能是 GC 停顿。不要指望画一张完美的架构图就能上线真实的高并发系统是在一次次的故障和压测中被打磨出来的。第二句稳定性永远优于新功能。高并发场景下宁可少发券也不能多发券。少发一张用户可以理解补发通道可以做多发一张资损对账就是事故。在代码层面防超发、幂等、限流这三件事优先级最高。先保证不出事再谈用户体验。第三句监控是最后一道防线报警比优化更重要。我见过太多团队埋头调优参数却忘了加监控。等线上出问题时连从哪里开始看都不知道。至少要把 Redis 命中率、数据库连接池使用率、MQ 积压量、TP99 这四个指标做到可视化配合告警。任何一次线上事故从监控发现问题到定位问题隔的时间越短损失就越小。这套系统运行了几个月之后我又陆续加了几个锦上添花的能力比如活动灰度发布、券模板预热的自动化、分地区限流等。但核心骨架始终没有变还是那三板斧Redis Lua 保一致性、本地缓存挡热点、MQ 削峰填谷。如果你也在设计类似的高并发系统不妨从这三个支撑点开始先跑通主线再逐步完善。踩过几个坑之后你会发现高并发这回事没有那么多玄学就是把每一步都做扎实。
阅读完成 · 觉得有帮助?