☰
基于Golang的秒杀系统:Redis+Lua如何实现库存原子扣减?
2026/10/8 16:24:00 网站建设 项目流程

简介:秒杀抢券是典型的高并发业务场景,这份面向Go后端开发者的完整代码示例,以Gin作为Web框架、Redis承担缓存与计数、Lua脚本保障库存扣减原子性,兼顾性能与开发效率。压缩包共58个文件、约4.63MB,包含25个Go源码、4个CSV测试数据、多份YAML/XML配置、Dockerfile、JMeter压测脚本、说明文档和13张图片,覆盖开发、配置、测试与部署环节;CSV数据可导入数据库模拟业务,JMeter脚本用于并发压测。当前已有41人学习/浏览。项目按SecKill-System组织,区分engine、middleware、model、service等模块,提供注册、抢券等并发测试用例,可直接试运行;Redis与MySQL服务封装、JWT鉴权中间件、Docker编排和多环境配置样例,有助于快速启动和压测,并理解Web层、缓存层与持久层的协作方式。对想理解秒杀业务、学习Redis+Lua与Gin集成方式的开发者有直接参考价值。

1. 基于 Golang 的秒杀系统:为什么我拆完这份源码后把 Redis 事务忘得一干二净

凌晨 0 点的秒杀,一台 4C8G 的云主机,一个 10000 库存的商品,压测跑了 20 分钟,库存直接变成负数——这是我亲眼在线上见过的事故。也是我第一次认真拆这份「Golang + Redis + Lua + Gin 高并发秒杀系统」源码包的原因。它要解决的核心就一件事:在请求峰值的几秒钟里,把库存扣减做得又快又不出错。整份源码按「限流 → 判重 → 扣库存 → 异步下单」四条链路组织,覆盖了秒杀从请求进来到订单落库的完整闭环。适合两类人:一类是接了个秒杀需求但没想清楚库存该放 MySQL 还是 Redis 的后端工程师;另一类是刷 redis 面试题刷到「超卖、缓存穿透」但没见过完整实现的人。看完这套代码,你会意识到一个反直觉的结论:Redis 自带的 MULTI/EXEC 事务在秒杀场景里,远不如一段几十行的 Lua 脚本好用。

2. 整体架构与选型:三层防护、库存缓存和这套实现的代码结构

我先直接说结论:秒杀系统的技术选型,九成的决定都发生在「库存放哪」这个问题上。库存放 MySQL,逻辑最简单但扛不住峰值;库存放 Redis,逻辑稍微绕一点但能抗压。这套实现选的是后者,并且用 Lua 把并发竞态压到了最小。下面先看整体链路,再逐个方案对比,最后说源码包应该怎么读。

2.1 秒杀请求的三层防护:限流、判重、扣减

秒杀请求的流量特征是一瞬间几万个请求,但真正能进入业务逻辑的只该有一小部分,剩下全是无效流量。第一层防护就是接口限流。常见做法是令牌桶限流,Gin 中间件里用 Redis 的 INCR + EXPIRE 做一个固定窗口计数:每个用户每秒最多 1 个请求,每个商品每秒最多 200 个请求,超出直接返回「排队中」。固定窗口没有令牌桶平滑,但实现成本低,而且限流在这套架构里只是第一道闸,不需要特别精准。

第二层是用户判重。一人一单是秒杀的硬规则,判重必须和库存扣减放在同一个原子操作里完成,否则两个请求带着同一个 user_id 同时穿过检查,数据库里就会留下重复订单。这套实现里为每场秒杀维护了一个 SET,key 形如bought:{goodsId},成员就是已购买的 user_id,用 SISMEMBER 检查、SADD 写入。

第三层才是库存扣减。库存数量用 Redis 的 String 类型存,单个 key 的值就是剩余量,由 Lua 脚本统一完成「读库存 → 判断是否大于 0 → DECR」。扣减成功之后不立刻写订单,而是把订单任务丢进一个 channel 缓冲队列,由后台 worker 去写 MySQL。这里先记住一个结论:Redis 只负责「承诺」库存,MySQL 负责「兑现」订单。

三层缺一不可。我见过只做库存扣减不做限流的,压测一开 MySQL 直接 CPU 拉满;也见过只做限流不判重的,同一个用户把库存刷了几十单。这三个环节的代码都不复杂,但漏掉任何一层,线上都会以很狼狈的方式翻车。

2.2 数据库行锁、Redis 事务、分布式锁、Lua:四个方案怎么选

把常见的库存扣减方案放在一张表里对比,选型理由一眼就能看清:

方案会不会超卖峰值吞吐实现复杂度主要坑点
MySQL 行锁不会低低行锁串行,连接一打满就瘫痪
Redis MULTI/EXEC 事务会中中事务内不能根据上一步结果做分支
分布式锁 + 二次校验不会中高锁超时、误删锁、锁竞争拖垮性能
Redis + Lua 脚本不会高中脚本需要预热和错误处理

MySQL 行锁方案很多人首选,因为UPDATE goods SET stock = stock - 1 WHERE stock > 0单条语句确实原子,不会超卖。但秒杀这种全部请求命中同一行的场景,行锁要排队,连接池被瞬并发打满只是时间问题。我压过单行更新,1000 并发下平均延迟能直接飙升到秒级。

Redis MULTI/EXEC 事务方案是最典型的误区。MULTI 只是把一串命令按顺序打包执行,之间不会被其它命令打断,但它不支持「先读、再判断、再写」这种条件逻辑。事务开始前要 WATCH,冲突就失败,不冲突就原样执行,根本没有 if 分支。库存检查这种三步逻辑用 MULTI 写出来就是白纸一张——先 GET 再 DECR,两个请求同时 GET 到剩余 1,照样扣成负数。

分布式锁方案是另一个常见选择:SETNX 拿到锁之后读库存判断再扣减。逻辑上对,但锁超时怎么办、锁没释放会不会拖垮系统、每次请求抢锁本身又成了一次热点访问。锁竞争到一定程度,性能和 MySQL 行锁也差不了太多。

Lua 脚本方案是四个方案里我最推荐的:把「读库存 → 判断大于 0 → DECR → 标记用户」整个包进一个脚本,Redis 服务端单线程执行整个脚本,执行期间不可能有别的命令插进来。没有锁、没有事务包装、没有行锁排队,一个脚本从进到出就是原子操作。Redis 里常被问的两大数据类型在这里也恰好对上了:库存用 String,已购用户集合用 SET。选 Redis 做库存载体,不只是性能问题,更是数据结构的天然契合。

2.3 这套源码的代码结构:先读哪几个文件

这类秒杀系统源码拆开之后,核心文件并不算多,难点是阅读顺序。按主流程顺一遍,建立脑子里这条链:连接初始化 → 路由注册 → 接口处理 → 脚本执行 → 异步落库。

文件(常见命名)作用建议阅读顺序
main.go初始化 Redis 连接、创建 Lua 脚本对象、启动 Gin1
router/router.go路由注册,挂载限流、鉴权等中间件2
handler/seckill.goHTTP 层,参数解析与结果包装3
service/stock.go核心业务,执行 Lua 脚本,处理返回码4
lua/seckill.lua库存扣减与判重的原子脚本5

main.go 主要看三件事:Redis client 是不是全局单例、连接池参数有没有设置、Lua 脚本是启动时加载还是每次请求现编。router 看中间件挂载顺序——限流必须在业务 handler 之前。handler 和 service 是分层是否干净的试金石,如果 handler 里直接拼 Redis key,说明这套实现的分层没想清楚。

最值得静下心读的是 lua/seckill.lua,整份源码最关键的一段逻辑全在里面。不要跳着看,要把每个边界条件都过一遍,比如库存 key 不存在时返回什么、用户判重失败时返回什么。第 3 章我把这段脚本完整拆开讲。

3. 核心扣减逻辑:用 Redis+Lua 写出不会超卖的原子操作

这一章是整个源码的命门。库存扣减不是简单的 DECR,它要同时满足三个条件:不够卖时不扣、超卖这种事不发生、同一用户只能扣一次。三个条件放在同一个脚本里,才能从根上消除竞态。

3.1 为什么分布式锁解决不了秒杀,Lua 却能

先看一段最常见的分布式锁写法,很多项目的秒杀代码就是这么写的:

// 不推荐:抢锁 → 读库存 → 判断 → 扣减 lockKey := fmt.Sprintf("lock:goods:%s", goodsId) ok, _ := rdb.SetNX(ctx, lockKey, uid, 5*time.Second).Result() if !ok { return -3 // 排队失败 } defer rdb.Del(ctx, lockKey) stock, _ := rdb.Get(ctx, stockKey).Int() if stock <= 0 { return -1 } rdb.Decr(ctx, stockKey)

这写法看着对,线上会在三处翻车:第一,锁超时 5 秒,业务没处理完锁自动没了,后面新请求带着新锁进来,两笔扣减同时发生;第二,延迟执行Del时锁可能已经过期被别人重新持有,你把别人的锁删了;第三,抢锁失败的请求直接返回,吞吐量被锁竞争拖垮,压测数据很难看。

Lua 方案把这三类问题全部删掉,只剩一个脚本调用。Redis 官方保证:脚本执行期间,其它客户端的任何命令都不会插入,整个脚本天然是一个原子操作。这也是为什么在 Redis 高并发场景里,Lua 是处理「读-判-写」这类复合逻辑的标准解法——比 WATCH/MULTI 更灵活,比分布式锁更轻。

3.2 可抄走的 Lua 脚本:校验、扣减、去重三合一

这份实现的核心脚本,逻辑非常紧凑,值得逐行读:

-- 秒杀核心脚本 lua/seckill.lua -- 返回值:1 成功,-1 库存不足,-2 重复下单 -- KEYS[1] 库存 key,KEYS[2] 已购用户集合 key local stock = tonumber(redis.call("GET", KEYS[1])) if not stock or stock <= 0 then return -1 end local already = redis.call("SISMEMBER", KEYS[2], ARGV[1]) if already == 1 then return -2 end redis.call("DECR", KEYS[1]) redis.call("SADD", KEYS[2], ARGV[1]) redis.call("EXPIRE", KEYS[2], tonumber(ARGV[2])) return 1

逻辑说明:第一步读取库存并转成数字,库存不存在或小于等于 0 直接返回 -1,这一步堵住了超卖;第二步用 SISMEMBER 检查当前用户是否已经在集合里,在就直接返回 -2,这一步实现了用户判重;第三步才执行 DECR 扣减,同时 SADD 把用户加入已购集合,并给集合设置过期时间防内存堆积。

参数说明:KEYS[1]是库存 key,形如stock:1001,用商品 ID 区分每场活动;KEYS[2]是已购集合 key,形如bought:1001;ARGV[1]是用户 ID,来源是鉴权中间件解析出的 uid,不是前端传什么就信什么;ARGV[2]是集合过期时间,秒杀场景建议 86400 秒,活动结束后自动回收内存。

这里有一个容易被忽略的边界行为:如果库存 key 不存在,tonumber(false)得到 nil,脚本会直接返回 -1,也就是「已抢光」。这意味着活动开始前必须把初始库存 SET 进去,否则活动一开始就是抢光状态。我一般会在 main.go 里加一个 initStocks 方法,用 pipeline 批量写入,避免循环逐个 SET。

3.3 在 Gin 里集成:go-redis 的 NewScript 与返回码处理

Go 侧代码用 go-redis 的成熟封装,脚本对象只需要创建一次:

package main import ( "context" "github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" ) var rdb *redis.Client // 秒杀脚本,整个进程只创建一次 var seckillScript = redis.NewScript(` local stock = tonumber(redis.call("GET", KEYS[1])) if not stock or stock <= 0 then return -1 end local already = redis.call("SISMEMBER", KEYS[2], ARGV[1]) if already == 1 then return -2 end redis.call("DECR", KEYS[1]) redis.call("SADD", KEYS[2], ARGV[1]) redis.call("EXPIRE", KEYS[2], tonumber(ARGV[2])) return 1 `) // killStock 执行扣减,返回 1 成功 / -1 无库存 / -2 重复参与 func killStock(ctx context.Context, goodsId, uid string) (int, error) { keys := []string{"stock:" + goodsId, "bought:" + goodsId} vals := []interface{}{uid, 86400} res, err := seckillScript.Run(ctx, rdb, keys, vals...).Int() if err != nil { return 0, err } return res, nil }

逻辑说明:redis.NewScript内部会自动帮你 SCRIPT LOAD 把脚本缓存到 Redis,拿到 SHA1 之后用 EVALSHA 执行;如果 Redis 重启导致脚本缓存丢失,客户端收到 NOSCRIPT 错误会自动回退成 EVAL。这一步直接解决了「脚本没预热就报 NOSCRIPT」的经典故障,比手写 SHA1 管理省心得多。

参数说明:keys 和 vals 分开传是 Redis Lua 通信规范,KEYS 之外的参数一律走 ARGV。有些人习惯把 key 也塞进 ARGV,单机模式下能跑通,但一旦切 Redis Cluster,key 的 hash slot 计算就会出问题,所以这个规范从第一天就该守好。

注意:Redis 执行 Lua 脚本期间其它请求都在排队等待,脚本里不要写大循环或批量查大量 key,执行时间越短越好。秒杀这种场景,脚本逻辑控制在 10 行以内是底线。

错误处理是秒杀最容易含糊的部分。killStock返回 err 时,handler 要统一回 503「系统繁忙」,绝对不要返回成功。因为网络超时场景下,你无法确定脚本到底执行了没有;好在这套脚本对同一用户天然幂等——即使客户端重试,第二次进来会被 SISMEMBER 挡住返回 -2,不会造成超卖,只是用户体验上可能看到「已参与」。这个设计是有意为之的。

4. Gin 接入层与异步下单:接口校验、队列落库与压测参数

Lua 脚本解决的是原子扣减,Gin 层解决的是「请求怎么进来、结果怎么返回、订单怎么落库」。这一章把 HTTP 层到存储层的完整链路串起来。

4.1 路由注册、鉴权中间件与秒杀 handler

Gin 侧的路由和 handler 写法比较常规,但中间件顺序值得讲一下:

func main() { r := gin.Default() // 秒杀路由组:限流在前,鉴权在后 seckillGroup := r.Group("/api/v1") seckillGroup.Use(RateLimit(200)) seckillGroup.Use(AuthRequired()) { seckillGroup.POST("/seckill/:goodsId", SeckillHandler) } r.Run(":8080") } func SeckillHandler(c *gin.Context) { goodsId := c.Param("goodsId") uid := c.GetString("uid") result, err := killStock(c.Request.Context(), goodsId, uid) if err != nil { // 网络超时等不确定状态,不返回成功 c.JSON(503, gin.H{"code": 503, "msg": "系统繁忙,请重试"}) return } switch result { case 1: // 扣减成功,订单异步落库 SubmitOrder(OrderTask{GoodsId: goodsId, UserId: uid}) c.JSON(200, gin.H{"code": 0, "msg": "抢到了,订单处理中"}) case -1: c.JSON(200, gin.H{"code": -1, "msg": "已抢光"}) case -2: c.JSON(200, gin.H{"code": -2, "msg": "您已经参与过"}) } }

逻辑说明:中间件顺序是有讲究的,限流放在鉴权之前,这样未登录的海量请求直接在最外层被拦掉,根本到不了业务逻辑。uid由 AuthRequired 中间件从 token 解析后写进 Gin 的 Context,handler 里只通过c.GetString("uid")拿,不要再查一次用户表。

参数说明::goodsId是路由参数,handler 里用c.Param("goodsId")获取。商品 ID 必须做白名单校验,因为第 5 章会提到,非法的商品 ID 会让缓存穿透打到数据库。返回码设计成三档:0 表示成功,-1 表示抢光,-2 表示重复参与,前端拿到 -1 和 -2 不需要再发请求,直接展示对应文案即可。

4.2 异步下单:channel 缓冲队列把 MySQL 从热路径里摘出去

扣减成功只代表库存被预订了,订单还没产生。如果每个秒杀请求都同步写 MySQL,数据库瞬间就会成为瓶颈。这套实现的做法是把订单任务丢进 channel,由后台 worker 消费:

type OrderTask struct { GoodsId string UserId string Seq string // 订单号,由雪花算法生成 } var orderQueue = make(chan OrderTask, 1024) func init() { for i := 0; i < 8; i++ { go orderWorker(orderQueue) } } func orderWorker(q <-chan OrderTask) { for task := range q { err := createOrder(task) if err != nil { if strings.Contains(err.Error(), "Duplicate entry") { // 同一请求被重复投递,订单已存在,不需要回补 continue } // 数据库临时故障,订单没写进去,把预扣的库存还回去 rdb.Incr(context.Background(), "stock:"+task.GoodsId) continue } } }

逻辑说明:channel 缓冲区大小是 1024,超过这个值 producer 会阻塞,这是一层天然背压,比无界队列安全。worker 数量 8 个,每个 worker 串行写库,MySQL 的写入压力被限制在可控范围内。createOrder失败时的处理是关键:如果是唯一索引冲突,说明订单已经存在,直接跳过;如果是数据库故障,要把预扣的库存用 INCR 还回去,保证用户失败后库存不丢失。

参数说明:channel 方案不持久化,服务重启会丢未落库的订单——小规模秒杀可以接受,生产环境我会把 channel 换成 RabbitMQ 或 Kafka,但「扣减和落库解耦」这个思路是通用的。本条还有一个容易被忽视的点:订单表必须加UNIQUE KEY uk_user_goods (user_id, goods_id)。这个唯一索引是数据库层面的最后兜底,Redis 判重被绕过时,它还能挡住重复订单。

4.3 连接池与超时参数:压测前必须调对的几个数字

秒杀手滑第一件事是调参数,不是改代码。这套实现里 Redis 客户端的初始化参数对压测结果影响极大:

rdb = redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", PoolSize: 200, // 连接池上限 MinIdleConns: 50, // 预热连接数 DialTimeout: 1 * time.Second, ReadTimeout: 1 * time.Second, WriteTimeout: 1 * time.Second, MaxRetries: 0, // 秒杀场景不要自动重试 })

参数说明:PoolSize200 是单机压测的保守值,经验上取预估并发的 1.5 到 2 倍;再往上提升意义不大,单机 Redis 处理能力有限,要更高吞吐就上 Redis Cluster 做水平扩展。MinIdleConns设 50 是为了压测开始时连接已经就绪,避免几百个请求同时触发建连的冷启动尖刺。

MaxRetries设 0 是秒杀场景特有的选择。go-redis 默认在网络错误时自动重试,但秒杀接口对不确定状态的原则是「不重试、返回繁忙、让用户自己决定是否再点」。万一第一次 DECR 已经执行成功只是响应超时,自动重试虽然会被用户的 SISMEMBER 挡住不会超卖,但会造成无谓的 Redis 压力,而且响应时间翻倍。

注意:ReadTimeout 别设成 3 秒 5 秒这种宽值。秒杀接口要求快速释放连接,超时太长会让连接池在 Redis 故障时被全部占住,错误率从这时候开始就压不下来了。这套实现本身不依赖 Redis 特殊版本,Lua 脚本能力从 Redis 2.6 起就有,本地用 Docker 或 Windows 版 Redis 都能直接跑。

5. 避坑与常见问题:五个秒杀线上的真实排查记录

这一章是这几年做秒杀活动攒下来的血泪经验,每一条都是线上真实出现过、有明确复现路径的问题。按「现象 → 原因 → 解决」记录,方便你踩坑时对照排查。

5.1 压测后库存变成负数,数据库订单比初始库存还多

现象:压测跑完,Redis 里库存 key 的值是 -1878,数据库订单数量远超初始库存。

原因:这是最经典的超卖路径。业务代码先 GET 库存判断大于 0,再 DECR 扣减,判断和扣减是两条独立的命令。两个并发请求同时 GET 到剩余 1,都认为可以卖,然后各自 DECR,库存被扣两次。问题本质是「读-判-写」不是一个原子操作。

解决:把 GET 判断和 DECR 扣减以及用户判重全部写进同一个 Lua 脚本里,也就是第 3 章那份脚本。Redis 执行脚本期间不会插入任何其它命令,竞态从根上被消除。从那以后我把所有「先读后写」的 Redis 操作都改成了脚本实现。

5.2 同一个用户在同一场秒杀里抢到两单

现象:活动结束后导订单表,发现 (user_id, goods_id) 组合出现了两条记录。

原因:判重逻辑写在业务代码里而不是 Lua 脚本里。业务代码先把已购列表查出来做 Contains 判断,再调扣减,这两步之间用户的第二次请求又进来了,两次都通过检查。数据库层没有唯一索引,重复记录直接落库。

解决:判重写进 Lua 脚本的 SISMEMBER 分支,这一步已经把并发窗口关死;同时给订单表加UNIQUE KEY uk_user_goods (user_id, goods_id)做兜底。即使 Redis 判重被绕过,数据库也会拒绝插入,订单 worker 收到 Duplicate entry 后直接跳过,不会多扣库存。

5.3 一个不存在的商品 ID 把 MySQL 打爆(缓存穿透)

现象:压测脚本里商品 ID 拼错了一个,比如 1001 写成了 1010,结果 Redis 命中率直接掉到 0,所有请求都穿透到 MySQL,数据库连接数瞬间打满。

原因:这是标准的缓存穿透。商品信息虽然预热到了 Redis,但那个写错的 ID 在 Redis 里没有对应 key,每个请求都去 MySQL 查一次。秒杀场景的请求量是平时的几十倍,穿透效果被放大得极猛。

解决:三个措施一起上。第一,活动开始前把所有合法商品 ID 预热进 Redis,handler 里对不存在的 ID 直接返回「参数错误」,这一步就能挡掉 90% 的穿透流量;第二,对未命中的 key 设置空值缓存,TTL 30 到 60 秒;第三,要更稳就加布隆过滤器。秒杀这种封闭场次,第一招其实就够用了。

5.4 压测报 redis: connection pool exhausted

现象:压测进行到第 10 秒,日志开始大量刷redis: connection pool exhausted,接口错误率直线上升。

原因:两个典型原因。一个是 Redis Client 不是全局单例,每个请求都 New 了一个连接,连接根本复用不了;另一个是 PoolSize 设得太小,压测并发超过连接数上限,新请求只能排队等连接释放。

解决:把 Client 提升为全局变量,main.go 里只初始化一次;按压测并发调整 PoolSize,经验值是并发数的 1.5 到 2 倍;配合 MinIdleConns 预热连接。同时把 ReadTimeout 压到 1 秒,防止慢请求占着连接不放。排查这类问题最快的办法是先看连接池监控指标,而不是先怀疑代码逻辑。

5.5 Lua 脚本报 NOSCRIPT 错误

现象:本地用 Redis Desktop Manager 或 redis-cli 手动执行脚本一切正常,压测一开始就报NOSCRIPT No matching script. Please check EVALSHA。

原因:代码里直接调 EVALSHA,但 Redis 服务端没有对应脚本的缓存。Redis 重启之后 SCRIPT LOAD 缓存的脚本全部丢失,如果客户端只有 EVALSHA 没有回退逻辑,脚本就执行不了。

解决:用 go-redis 的redis.NewScript,它内部做了 SCRIPT LOAD + EVALSHA,收到 NOSCRIPT 会自动回退 EVAL;如果是裸 Redis 命令操作,就在服务启动时统一做一遍 SCRIPT LOAD 预热脚本,并捕获 NOSCRIPT 错误改用 EVAL 重新执行。这个坑在本地环境几乎不会出现,因为本地 Redis 没重启过,上线前务必测试一下 Redis 重启后的行为。

6. 上线前的验证:用 wrk 压测和库存对账把系统跑通

6.1 压测与对账:一组命令验证系统不多卖

压测前先写一个 wrk 的请求脚本:

# post.lua wrk.method = "POST" wrk.headers["Content-Type"] = "application/json" wrk.body = '{"userId": 12345}'

然后启动压测,8 个线程 200 个连接跑 30 秒:

wrk -t8 -c200 -d30s -s post.lua http://127.0.0.1:8080/api/v1/seckill/1001

压测结束后重点关注两类结果:错误率(Errors 列)和 p99 延迟。Redis 正常时,秒杀接口 p99 应该稳定在 10ms 以内;如果错误率不为 0,先查连接池参数而不是改代码。

压测通过只证明性能达标,还要验证数据一致性。对账只需要三条命令:

# Redis 剩余库存 redis-cli get stock:1001 # 订单表实际落库数 mysql -e "select count(*) from orders where goods_id=1001" # 检查有没有重复订单 mysql -e "select goods_id,user_id from orders group by goods_id,user_id having count(*) > 1"

对账公式是:剩余库存加上落库订单数等于初始库存。前两组命令相加对不上,说明扣减或落库有一个环节出错;第三条命令有任何输出,说明判重被绕过。这两步做完,才敢说系统是真的能上线的。

6.2 上线前强制检查的清单

每次上线秒杀前,我都固定走一遍这套检查,五分钟搞定:

  • Lua 脚本在启动时已完成 SCRIPT LOAD 预热,并测试过 Redis 重启后能自动回退 EVAL
  • Redis 里所有商品的库存 key 已 SET 好,key 命名和代码完全一致
  • 订单表 (user_id, goods_id) 唯一索引已建立
  • MaxRetries = 0,ReadTimeout = 1s,连接池参数已按压测配额调好
  • 回补库存逻辑已接好,数据库故障时预扣库存能还回去

这套检查现在已经是肌肉记忆,因为真在半夜上线时翻过一次车:脚本没预热,Redis 重启后秒杀直接全断,用户看到的状态全是「系统繁忙」。从那以后我每次上线秒杀前都强制走一遍脚本预热、库存对账和唯一索引检查,出事故后熬一夜的代价,比上线前五分钟的检查沉重太多。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询