秒杀接口最外层安全防线是请求进入系统第一刻的身份、资格与操作意图原子化验证;通过nginx+lua预筛、threadlocal固化可信上下文、redis原子变量快照及禁止信任原始参数实现四重防护。

在秒杀接口中,最外层安全防线不是数据库校验,也不是业务层的 if 判断,而是请求进入系统第一刻就完成的身份、资格与操作意图的原子化验证。变量捕获的核心,是把关键决策因子(如用户身份、商品状态、时间窗口、行为指纹)提前固化为不可篡改的上下文变量,并在网关或拦截器阶段完成强校验——不放行,就不进业务逻辑,更不触碰数据库。
用 ThreadLocal + 拦截器固化可信上下文
用户身份、登录态、风控等级等信息不能靠每次从 Redis 或 Session 里查,否则既慢又可能被中间篡改。正确做法是在统一入口(如 Spring MVC 拦截器)中完成一次解析与绑定:
- 解析 JWT 或 Cookie 中的 token,校验签名与有效期,提取 userId、role、riskLevel 等字段
- 将这些字段封装为轻量 VO(如 SeckillContext),存入 ThreadLocal
- 后续所有中间件、服务、DAO 层都直接从 ThreadLocal 获取,不再重复解析或远程调用
- 若拦截器校验失败(如 token 过期、签名无效、风控拦截标记为 high),直接返回 403,不继续流转
用 Nginx + Lua 在接入层做无状态变量预筛
真正扛住百万级并发的第一道闸门不在 Java 应用里,而在 Nginx。这里用变量捕获实现“未进应用,先定生死”:
- 通过 $arg_token、$cookie_uid、$time_iso8601 等内置变量提取原始请求特征
- 用 Lua 脚本做轻量判断:是否在活动时间窗内、是否已限流(基于 redis.incr + expire)、是否命中恶意 UA/IP 黑名单
- 把校验结果写入自定义 header,如 X-Auth-Status: pass 或 X-Block-Reason: rate_limit
- Java 层只信任该 header,不信任原始参数;若 header 缺失或值非 pass,则直接拒掉,不执行任何库存查询
用 Redis 原子变量做资格快照,隔离数据库依赖
库存是否可抢、用户是否已参与、优惠券是否可用——这些判定必须脱离 MySQL,避免锁表和慢查询拖垮整条链路:
- 活动开始前,预热加载商品维度的原子变量:seckill:sku:1001:stock(string 类型,初始值=总库存)
- 用户资格存为带过期时间的 set:seckill:user:12345:used_vouchers,插入时用 SADD + EXPIRE,天然幂等
- 秒杀入口用 Lua 脚本一次性完成「检查库存 > 0」「检查用户未参与」「decr 库存」「添加用户到已参与集合」四步,全部在 Redis 内原子执行
- 脚本返回 1 表示资格通过,返回 0 表示拦截;Java 层仅根据这个整型变量决定是否放行,不查 DB
禁止下游信任原始请求参数,强制使用上游注入变量
防止前端篡改(如改 price、改 skuId、伪造 timestamp)的关键,是让业务层彻底“看不见”原始请求参数:
- Controller 方法不接收 @RequestParam String skuId,而是从 ThreadLocal 或 RequestAttribute 中取已校验的 SeckillContext.skuId
- MyBatis 的 SQL 不拼接 ${skuId},而用 #{context.skuId},且 context 对象由拦截器注入,非用户可控
- 所有日志记录、审计落库、消息投递,都用注入后的变量,而非 request.getParameter()
- 若某环节必须用原始参数(如风控回溯),则只读不用于决策,且打上 “raw_” 前缀明确标注不可信










