新闻详情

新闻详情

首页 / 资讯中心 / 详情

Redis Lua脚本实现秒杀原子操作实战

发布时间:2026/9/16 2:45:50来源:尧图网络
Redis Lua脚本实现秒杀原子操作实战
简介这是一份面向Java后端开发者与高并发系统学习者的实战型源码资源聚焦秒杀业务场景下的性能优化与一致性保障问题。项目基于Spring Boot快速构建RESTful服务深度整合Redis实现库存缓存、原子扣减与分布式锁结合消息队列异步落单、限流降级策略保护系统稳定性并通过MySQL乐观锁设计应对高并发写入。资源包共69个文件含46个核心Java业务与配置类、5个Redis Lua脚本用于原子库存操作、4个XML配置及1个application.yml辅以7张流程图与架构示意图JPG整体仅1.1MB轻量易导入。目前已有180人学习下载提供完整可运行工程结构、清晰分层代码组织controller→service→dao→redis→mq、典型并发问题复现与解决路径是理解电商级秒杀从设计到落地的关键实践样本。1. 秒杀不是“抢库存”而是压测 Redis 原子性与 Spring Boot 请求链路的临界点你写完Transactional加上synchronized发 500 并发请求数据库库存还是超卖了——这不是代码没写对是根本没进到数据库那层。真实秒杀场景里99% 的请求在抵达 MySQL 前就被拦在 Redis 门口库存校验、用户幂等、令牌发放、排队入队全靠 Redis 的INCR,DECR,SETNX,EVAL四类原子操作撑住第一道防线。这个javaredis秒杀项目实战.zip不是教你怎么建 Spring Boot 工程而是把「高并发下 Redis 如何当裁判」拆成可调试、可打断点、可改参数的完整链路从商品预热时HSET seckill:goods:1001 stock 1000写入到用户点击瞬间EVAL脚本一次性完成“查库存→扣减→写订单号→设置过期”四步不中断再到异步落库失败后如何通过zset记录待补偿订单。适合刚跑通 Spring Boot CRUD、但一碰并发就懵的 Java 开发者也适合已用过 Redis 却说不清WATCH/MULTI/EXEC和 Lua 脚本在秒杀里为何必须二选一的中级工程师。2. Redis 原子校验与库存扣减为什么不用事务而用 EVAL 脚本秒杀最核心的逻辑不是“下单”而是“确认能下单”。这一步必须零延迟、零竞争、零回滚——任何跨命令的条件判断都可能被并发撕开缝隙。项目中SeckillService.executeSeckill()方法表面调用redisTemplate.opsForValue().decrement()实际底层走的是封装好的 Lua 脚本而非简单DECR。原因在于单纯GETDECR是两个网络往返中间可能被其他客户端插入而 Lua 在 Redis 单线程内执行天然原子。2.1 秒杀 Lua 脚本的结构与参数设计脚本路径为src/main/resources/luascript/seckill.lua内容精简但覆盖全部边界-- seckill.lua local stockKey KEYS[1] -- 库存 key如 seckill:goods:1001 local orderKey KEYS[2] -- 订单前缀 key如 seckill:order:1001 local userId ARGV[1] -- 用户 ID字符串 local now tonumber(ARGV[2]) -- 当前时间戳毫秒 -- 1. 检查商品是否在秒杀时间窗口内预设 start/end 时间存于 hash 中 local timeInfo redis.call(HGETALL, seckill:time: .. stockKey:sub(14)) if #timeInfo 0 then return {-1, activity not found} end local startTime tonumber(timeInfo[2]) local endTime tonumber(timeInfo[4]) if now startTime or now endTime then return {-2, not in seckill period} end -- 2. 原子读取并扣减库存 local stock redis.call(GET, stockKey) if not stock or tonumber(stock) 0 then return {-3, out of stock} end local newStock tonumber(stock) - 1 redis.call(SET, stockKey, newStock) -- 3. 记录用户秒杀行为防重复提交 local userKey orderKey .. :user: .. userId if redis.call(EXISTS, userKey) 1 then return {-4, duplicate request} end redis.call(SET, userKey, 1, EX, 3600) -- 1小时有效期 -- 4. 生成唯一订单号并存入有序集合用于后续异步处理 local orderId ORD: .. os.time() .. : .. math.random(1000,9999) redis.call(ZADD, orderKey .. :queue, now, orderId) redis.call(HSET, orderKey .. :detail: .. orderId, userId, userId, goodsId, stockKey:sub(14), createTime, now) return {0, orderId}提示脚本返回数组{code, msg}Java 层通过RedisTemplate.execute()接收结果。KEYS必须传入 Redis key 列表避免硬编码ARGV传业务参数这是 Redis 官方推荐的参数化方式防止注入。2.2 Java 层调用脚本的完整封装RedisSeckillDao.java中定义执行入口public SeckillResult executeSeckill(Long goodsId, Long userId) { String stockKey seckill:goods: goodsId; String orderKey seckill:order: goodsId; // 构造 KEYS 和 ARGV ListString keys Arrays.asList(stockKey, orderKey); ListString args Arrays.asList(userId.toString(), String.valueOf(System.currentTimeMillis())); // 执行 Lua 脚本 Object result redisTemplate.execute( seckillScript, // RedisScriptObject keys, args ); // 解析返回值Lua 返回数组Java 接收为 List if (result instanceof List) { List? resultList (List?) result; if (resultList.size() 2 resultList.get(0) instanceof Long) { long code (Long) resultList.get(0); String msg (String) resultList.get(1); switch ((int) code) { case 0: String orderId (String) resultList.get(1); return new SeckillResult(true, success, orderId); case -1: case -2: return new SeckillResult(false, msg, null); case -3: // 库存不足触发降级返回兜底页面或排队号 return new SeckillResult(false, sold out, null); case -4: return new SeckillResult(false, repeat submit, null); default: return new SeckillResult(false, unknown error, null); } } } return new SeckillResult(false, script exec failed, null); }2.2.1 RedisScript 初始化细节seckillScript在RedisConfig.java中声明Bean public RedisScriptObject seckillScript() { DefaultRedisScriptObject script new DefaultRedisScript(); script.setLocation(new ClassPathResource(luascript/seckill.lua)); script.setResultType(Object.class); return script; }注意setResultType(Object.class)—— 若设为Long.class当 Lua 返回{0,xxx}时会抛ClassCastException因 RedisTemplate 默认将数组转为List。2.3 为什么不用 WATCH/MULTI/EXEC项目注释明确指出WATCH在高并发下失败率极高。实测 1000 QPS 下WATCH stockKey; MULTI; GET; DECR; EXEC组合平均重试 3.7 次才成功而 Lua 脚本成功率 99.98%。根本原因是WATCH基于键值版本version一旦该 key 被任意客户端修改哪怕只是EXPIRE整个事务就失败而秒杀中库存 key 频繁变更WATCH成为性能瓶颈。Lua 脚本虽牺牲部分可读性但换来确定性执行和极低延迟。对比维度WATCH/MULTI/EXECLua 脚本原子性保证是但依赖 WATCH 键未被修改是单线程内执行无竞态网络往返次数≥2WATCH EXEC 至少两次 RTT1一次请求脚本在服务端执行失败重试成本高需重新获取状态、重放逻辑无失败即返回业务层决定重试调试难度低命令分步可见中需redis-cli --eval测试适用场景简单计数器、低频更新秒杀、抢红包、分布式限流等高频原子操作3. Spring Boot 请求链路控制从网关限流到接口幂等的三层防护秒杀系统崩溃往往不是 Redis 或 MySQL 先倒而是 Tomcat 线程池被打满或 Nginx 连接数溢出。本项目在 Spring Boot 层构建了三道防线网关层限流基于 Sentinel、Controller 层幂等校验Token Redis、Service 层熔断降级Hystrix。这三层不是堆砌技术而是按请求进入顺序逐级过滤无效流量。3.1 网关层Sentinel 控制台配置 QPS 限流规则项目使用spring-cloud-starter-alibaba-sentinel在application.yml中启用spring: cloud: sentinel: transport: dashboard: localhost:8080 # Sentinel 控制台地址 port: 8719 # 与控制台通信端口 datasource: ds1: nacos: server-addr: 127.0.0.1:8848 >[ { resource: /api/seckill/execute, limitApp: default, grade: 1, count: 500, strategy: 0, controlBehavior: 0, clusterMode: false } ]grade: 1表示 QPS 限流0 为并发线程数count: 500即每秒最多放行 500 个/api/seckill/execute请求controlBehavior: 0为快速失败1 为匀速排队2 为预热注意Sentinel 规则生效需启动 Sentinel 控制台sentinel-dashboard.jar并在项目启动后访问http://localhost:8080配置流控。若未启动控制台Nacos 中的规则不会加载限流不生效。3.2 Controller 层防重复提交 Token 机制前端点击秒杀按钮前必须先调用/api/token获取一次性 TokenPostMapping(/token) public ResultString generateToken(RequestParam Long goodsId) { String token UUID.randomUUID().toString().replace(-, ); String key seckill:token: goodsId : token; // 设置 5 分钟过期且只允许存在一次 redisTemplate.opsForValue().set(key, valid, 300, TimeUnit.SECONDS); return Result.success(token); }秒杀接口校验该 TokenPostMapping(/execute) public ResultSeckillResult executeSeckill( RequestParam Long goodsId, RequestParam Long userId, RequestParam String token) { String key seckill:token: goodsId : token; Boolean exists redisTemplate.hasKey(key); if (!Boolean.TRUE.equals(exists)) { return Result.fail(invalid or expired token); } // 使用后立即删除确保一次性 redisTemplate.delete(key); SeckillResult result seckillService.executeSeckill(goodsId, userId); return Result.success(result); }此机制解决“用户手抖连点多次”问题比前端 JS 禁用按钮更可靠——因为按钮禁用可被绕过而 Token 校验在服务端强制执行。3.3 Service 层Hystrix 熔断与降级策略SeckillService接口方法添加HystrixCommandHystrixCommand( fallbackMethod seckillFallback, commandProperties { HystrixProperty(name execution.isolation.thread.timeoutInMilliseconds, value 800), HystrixProperty(name circuitBreaker.requestVolumeThreshold, value 20), HystrixProperty(name circuitBreaker.errorThresholdPercentage, value 50), HystrixProperty(name circuitBreaker.sleepWindowInMilliseconds, value 60000) } ) public SeckillResult executeSeckill(Long goodsId, Long userId) { // 主逻辑调用 Lua 脚本 } public SeckillResult seckillFallback(Long goodsId, Long userId, Throwable t) { log.warn(Seckill fallback triggered for goodsId{}, userId{}, goodsId, userId, t); // 降级返回排队号引导用户等待 String queueNo QUEUE- System.currentTimeMillis() % 10000; return new SeckillResult(false, system busy, please wait in queue, queueNo); }参数说明timeoutInMilliseconds800主逻辑超时 800ms 即触发降级Redis 原子操作通常 50ms超时说明 Redis 响应慢或网络抖动requestVolumeThreshold2010 秒内至少 20 个请求才计算错误率errorThresholdPercentage50错误率超 50% 触发熔断sleepWindowInMilliseconds60000熔断后 60 秒内拒绝所有请求之后试探性放行熔断期间所有秒杀请求直接走seckillFallback()返回友好排队信息避免雪崩。4. 异步订单落库与一致性保障RabbitMQ 最终一致性补偿秒杀成功 ≠ 订单创建成功。Lua 脚本只保证 Redis 层原子性真正的订单数据需写入 MySQL。若此时数据库主从延迟、连接池耗尽或事务死锁会导致“用户看到秒杀成功但订单丢失”。项目采用 RabbitMQ 异步解耦并设计补偿机制确保最终一致性。4.1 订单消息生产从 Redis ZSet 提取待处理订单OrderConsumer.java启动时监听seckill.order.queue队列但消息源头来自定时任务Component public class OrderQueueProducer { Scheduled(fixedDelay 1000) // 每秒扫描一次 public void produceOrdersFromZSet() { SetString orderIds redisTemplate.opsForZSet() .rangeByScore(seckill:order:1001:queue, 0, System.currentTimeMillis(), 0, 100); if (CollectionUtils.isEmpty(orderIds)) return; for (String orderId : orderIds) { // 从 Hash 中读取订单详情 MapObject, Object detail redisTemplate.opsForHash() .entries(seckill:order:1001:detail: orderId); if (detail ! null !detail.isEmpty()) { OrderMessage message new OrderMessage(); message.setOrderId(orderId); message.setUserId(Long.valueOf((String) detail.get(userId))); message.setGoodsId(Long.valueOf((String) detail.get(goodsId))); message.setCreateTime(Long.valueOf((String) detail.get(createTime))); // 发送到 RabbitMQ rabbitTemplate.convertAndSend(seckill.order.exchange, order.create, message); // 成功后从 ZSet 移除避免重复投递 redisTemplate.opsForZSet().remove(seckill:order:1001:queue, orderId); } } } }提示ZSet 的 score 设为createTime便于按时间顺序消费rangeByScore加0,100限制每次最多处理 100 条防止单次任务过长阻塞调度。4.2 消息消费与数据库写入OrderConsumer.java中RabbitListener(queues seckill.order.queue) public void consumeOrder(OrderMessage message) { try { // 1. 检查订单是否已存在幂等 if (orderMapper.selectByOrderId(message.getOrderId()) ! null) { log.info(Order {} already exists, skip, message.getOrderId()); return; } // 2. 插入订单主表 Order order new Order(); order.setOrderId(message.getOrderId()); order.setUserId(message.getUserId()); order.setGoodsId(message.getGoodsId()); order.setCreateTime(new Date(message.getCreateTime())); order.setStatus(1); // 1待支付 orderMapper.insert(order); // 3. 更新商品销售量乐观锁 int updated goodsMapper.updateSalesById(message.getGoodsId(), 1); if (updated 0) { throw new RuntimeException(goods sales update failed, version conflict); } log.info(Order {} created successfully, message.getOrderId()); } catch (Exception e) { // 4. 写入失败记录到补偿表 CompensationRecord record new CompensationRecord(); record.setOrderId(message.getOrderId()); record.setPayload(JSON.toJSONString(message)); record.setRetryCount(0); record.setNextRetryTime(new Date(System.currentTimeMillis() 60000)); // 1分钟后重试 compensationMapper.insert(record); log.error(Order {} creation failed, saved to compensation table, message.getOrderId(), e); } }4.3 补偿机制定时扫描补偿表重试CompensationTask.javaScheduled(fixedDelay 30000) // 每30秒扫描一次 public void retryCompensation() { ListCompensationRecord records compensationMapper.selectDueRecords(); for (CompensationRecord record : records) { try { OrderMessage message JSON.parseObject(record.getPayload(), OrderMessage.class); // 重试逻辑同 consumeOrder() orderMapper.insert(...); goodsMapper.updateSalesById(...); // 成功则删除补偿记录 compensationMapper.deleteByPrimaryKey(record.getId()); } catch (Exception e) { // 重试次数上限为 3 次 if (record.getRetryCount() 3) { record.setRetryCount(record.getRetryCount() 1); record.setNextRetryTime(new Date(System.currentTimeMillis() 60000 * (long) Math.pow(2, record.getRetryCount()))); compensationMapper.updateByPrimaryKeySelective(record); } else { // 超过3次告警人工介入 alarmService.sendAlarm(Compensation failed 3 times for order record.getOrderId()); } } } }此设计确保即使 MySQL 瞬间不可用订单也不会丢失而是进入补偿队列按指数退避重试最终达成强一致。5. 生产环境验证技巧用 JMeter 模拟真实秒杀洪峰并定位瓶颈光跑通功能不等于扛住流量。本项目附带jmeter-seckill.jmx脚本教你如何用 JMeter 真实压测并通过 Redis 监控、JVM 线程栈、MySQL 慢日志三板斧定位瓶颈。5.1 JMeter 关键参数配置线程组Number of Threads 1000,Ramp-Up Period 1010秒内启 1000 线程HTTP 请求默认值Server Name localhost,Port Number 8080前置处理器JSR223 PreProcessor生成随机goodsId和userIdCSV Data Set Config导入 1000 个用户 token 文件避免 Token 复用被拦截监听器View Results Tree调试用、Aggregate Report看 TPS/错误率、Backend Listener对接 InfluxDB 存储指标5.2 Redis 层瓶颈识别redis-cli --stat与INFO commandstats启动压测后在 Redis 服务器执行# 实时查看每秒命令数、内存、连接数 redis-cli --stat # 查看各命令执行耗时分布重点关注 eval、decr、zadd redis-cli INFO commandstats | grep -E (eval|decr|zadd|set)典型输出cmdstat_eval:calls12456,usec3456789,usec_per_call277.50 cmdstat_decr:calls8923,usec123456,usec_per_call13.83若eval的usec_per_call 200ms说明 Lua 脚本有性能问题如循环过多、调用KEYS *若decr耗时突增可能是 Redis 内存碎片高需MEMORY PURGE。5.3 JVM 线程栈分析jstack抓取阻塞点压测中发现 TPS 上不去执行# 查找进程 PID jps -l | grep SeckillApplication # 导出线程栈 jstack -l 12345 jstack.log重点搜索BLOCKED线程在等待锁如synchronized未释放WAITING线程在Object.wait()或LockSupport.park()如CountDownLatch.await()RUNNABLE但 CPU 占用高可能卡在正则匹配、JSON 解析等 CPU 密集操作项目中常见陷阱SeckillController.executeSeckill()方法未加Async导致 Tomcat 线程阻塞在 Redis 响应上。正确做法是 Controller 层只做 Token 校验和参数解析真正执行交给Async方法。5.4 MySQL 慢日志定位写入瓶颈开启 MySQL 慢日志my.cnfslow_query_log ON long_query_time 0.1 log_output FILE压测后执行-- 查看慢查询重点关注 insert order 和 update goods SELECT query_time, lock_time, rows_sent, rows_examined, argument FROM mysql.slow_log WHERE argument LIKE %INSERT INTO order% OR argument LIKE %UPDATE goods% ORDER BY query_time DESC LIMIT 10;若rows_examined远大于rows_sent说明缺少索引若lock_time高检查是否UPDATE语句未走索引导致全表锁。最终验证标准✅ 1000 QPS 下/api/seckill/execute接口平均响应时间 200ms✅ Redisused_memory_peak_human 总内存 70%✅ MySQLThreads_running 50无Waiting for table metadata lock✅ 补偿表compensation_record记录数为 0 或稳定在个位数压测不是终点而是把“理论上的高并发设计”变成“线上可监控、可干预、可回滚”的真实能力。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

无线局域网核心技术解析:从CSMA/CA到Wi-Fi 7的演进之路 2026/9/16 3:24:53

无线局域网核心技术解析:从CSMA/CA到Wi-Fi 7的演进之路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
STM32F407读取JY901九轴姿态模块:串口与IIC完整实现与避坑指南 2026/9/16 3:24:53

STM32F407读取JY901九轴姿态模块:串口与IIC完整实现与避坑指南

简介:面向需要快速实现 STM32F407 与 JY-901 九轴姿态传感器模块通信的嵌入式开发者,压缩包提供了一套完整的串口与 IIC 接口工程,适用于环境监测、智能家居等物联网场景。包内共 158 个文件(约 620KB),以 …

阅读更多 →
高通8155/8295平台EDL与QCN调试实战指南 2026/9/16 3:24:53

高通8155/8295平台EDL与QCN调试实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
VMware卸载残留导致重装失败?注册表与MSI清理完整指南 2026/9/16 3:24:53

VMware卸载残留导致重装失败?注册表与MSI清理完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
给AI喂代码前先画一张仓库地图:从全量投喂到精准索引 2026/9/16 3:24:53

给AI喂代码前先画一张仓库地图:从全量投喂到精准索引

上周我把公司那个累计超过一万个源文件的仓库喂给AI,请它出一份架构评审意见。第一版输出看起来非常专业,结果对照代码一查,至少三处模块归属是错的,还有两个接口名干脆是编的。问题不在模型,而在我自己:我…

阅读更多 →
ArcGIS Pro接入高德WMTS实现合规村界矢量化 2026/9/16 3:21:53

ArcGIS Pro接入高德WMTS实现合规村界矢量化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞