1. 项目概述为什么抢购场景是分布式系统里的“压力测试仪”你有没有遇到过这样的情况一个限时秒杀活动上线前技术团队反复压测、自信满满结果活动开始后五分钟库存直接变成负数——后台日志里满屏的“库存扣减成功”但实际发货时发现超卖了三千单这不是段子而是大量电商、票务、课程平台在真实业务中踩过的坑。这个标题里提到的“SpringBoot实现并发、超发和锁机制/抢购示例”本质上不是教你怎么写个Demo而是在模拟一个高并发下的“资源争抢战场”同一时间成千上万人点击“立即抢购”系统必须在毫秒级响应中完成库存校验、扣减、订单生成三个动作且绝不允许出现超发over-selling。超发不是小问题它直接关联资金损失、用户投诉、资损审计甚至可能触发平台赔付条款。我带过好几个类似场景的落地项目从某在线教育平台的9.9元爆款课抢购到某本地生活服务平台的限量优惠券发放核心矛盾始终如一数据库的ACID特性在单机事务中很稳但一旦流量打穿单库能力、服务拆分成微服务、缓存与DB存在延迟传统Transactional就形同虚设。这时候“锁”不再是代码里一个synchronized关键字而是一套分层防御体系前端限流是第一道闸门应用层的乐观锁/悲观锁是第二道防线Redis分布式锁是第三道屏障而最终的库存一致性校验比如落库前二次查RedisDB双源比对才是兜底的保险丝。标题里并列出现“超发、乐观锁、悲观锁、Redis”恰恰说明这不是单点技巧的堆砌而是需要你理解每种手段的适用边界、性能代价和失效场景。比如用MySQL for update做悲观锁在QPS 200以上就容易引发连接池耗尽而纯靠Redis incr做库存扣减又可能因网络分区导致“假成功”。这篇文章不讲理论空谈我会带着你从零搭一个可实测的SpringBoot抢购服务把每一种锁机制的真实表现、压测数据、日志痕迹、失败现场都摊开来看。适合正在做秒杀模块的后端同学也适合想搞懂“分布式一致性到底难在哪”的中级开发者——毕竟你写的不是Hello World而是真金白银扛得住流量洪峰的系统。2. 整体架构设计与方案选型逻辑为什么不能只靠数据库事务2.1 四层防御模型从请求入口到数据落盘的全链路控制很多初学者一上来就想“怎么加锁”却忽略了抢购本质是流量治理问题。我们先画一张真实的链路图文字版用户点击 → Nginx限流 → 网关鉴权/防刷 → SpringBoot应用层 → Redis缓存校验 → MySQL库存扣减 → 订单服务异步创建 → 消息队列通知。在这个链条里锁机制只覆盖“Redis校验”到“MySQL扣减”这一小段但它的效果受前后环节制约极大。比如网关没做IP级限流恶意脚本瞬间打来5000QPS你的Redis锁还没来得及生效连接池就已经被打爆了。所以我们的方案设计是四层防御第一层网关与前置限流用Spring Cloud Gateway配置RequestRateLimiter基于Redis计数器对用户ID或设备指纹限流例如单用户10秒内最多5次请求。这能过滤掉80%的无效刷量避免无效请求穿透到业务层。第二层Redis预减库存无锁快路径所有库存操作先走RedisDECR stock:1001。如果返回值≥0说明还有库存进入下单流程如果返回负数直接返回“已售罄”。这是最快最轻量的判断95%的请求在这里就被拦截。注意这里不加锁因为Redis的DECR是原子操作天然线程安全。第三层数据库最终一致性保障乐观锁 or 悲观锁Redis预减成功后必须同步更新MySQL库存否则缓存与DB不一致。这里就是标题里“乐观锁/悲观锁”的主战场。我们不会只用一种而是根据业务容忍度动态选择对超卖零容忍的场景如限量联名款强制走悲观锁对允许极低概率超卖的场景如普通优惠券用乐观锁重试。第四层异步对账与补偿订单创建后启动定时任务扫描“已支付但库存扣减失败”的订单触发人工审核或自动退款。这不是锁机制但它是生产环境里必不可少的兜底。提示很多团队把所有希望押在“数据库加锁”上结果压测时TPS卡在300就上不去了。记住锁是成本最高的手段应该放在最后一步而不是第一步。2.2 为什么放弃单一方案三种锁机制的硬性对比我们实测过三种主流方案在2000QPS下的表现硬件4核8G云服务器MySQL 8.0Redis 7.0JDK 17方案平均RTms成功率超发率连接池占用适用场景MySQLSELECT ... FOR UPDATE悲观锁4299.98%0%高峰值占满超卖绝对不可接受QPS500MySQLUPDATE ... WHERE version ?乐观锁1899.2%0.03%低允许微量超卖QPS3000RedisSETNX EXPIRE分布式锁2699.95%0%中跨服务协调需强一致性关键发现悲观锁虽然100%防超卖但RT翻倍且在高并发下极易引发死锁比如两个事务交叉更新不同商品库存。乐观锁RT最低但版本号冲突会导致大量请求失败重试重试逻辑若没写好反而会放大数据库压力。Redis锁看似完美但要注意SETNX和EXPIRE不是原子操作必须用SET key value EX seconds NX一条命令完成否则网络中断时可能产生永久锁。这些细节文档里往往一笔带过但线上出问题时就是凌晨三点的告警电话。2.3 SpringBoot技术栈选型依据为什么是这些组件Web层Spring WebFlux vs Spring MVC本项目选Spring MVC而非WebFlux。理由很实在WebFlux的异步非阻塞模型在IO密集型场景如调用外部API优势明显但抢购的核心瓶颈是CPU和数据库锁竞争不是IO等待。MVC的线程模型更直观排查锁等待、线程阻塞问题时jstack日志一目了然。强行上WebFlux反而增加调试复杂度。数据库MySQL 8.0的行锁优化必须用MySQL 8.0。老版本的InnoDB在SELECT ... FOR UPDATE时如果WHERE条件没走索引会升级为表锁整个库存表被锁死。8.0引入了“锁降级”机制能更精准地锁定目标行。我们的商品表主键id和唯一索引sku_code是必备的。缓存Redis单节点足够集群反而是坑别一上来就上Redis Cluster。抢购场景的热点Key如爆款商品库存集中在少数几个key上Cluster的哈希槽迁移、ASK重定向会引入额外延迟。我们实测单节点Redis开启AOFRDB混合持久化在2000QPS下稳定运行而Cluster在相同压力下部分节点CPU飙升至95%出现超时。除非你的SKU量级达到千万级否则单节点哨兵高可用完全够用。依赖管理为什么不用ShardingSphere做分库分表库存表是典型的“窄表”就几个字段且读多写少。分库分表带来的路由复杂度、跨库事务、全局ID生成等问题远超其收益。我们用“库存服务单独部署读写分离”更轻量主库负责扣减从库负责查询展示用Canal监听binlog同步库存变更到ES做搜索。3. 核心细节解析与实操要点从代码到日志的每一处陷阱3.1 数据库表结构设计为什么库存字段必须是BIGINT且带version很多人建库存表只写stock INT这是大忌。INT最大值21亿看似很大但如果你的系统要做“虚拟库存”比如预估未来30天销量或者支持“库存拆分”按仓库、渠道分配很快就会溢出。我们定义CREATE TABLE t_product_stock ( id BIGINT NOT NULL AUTO_INCREMENT, sku_code VARCHAR(64) NOT NULL COMMENT 商品编码, total_stock BIGINT NOT NULL DEFAULT 0 COMMENT 总库存, lock_stock BIGINT NOT NULL DEFAULT 0 COMMENT 锁定库存防超卖, available_stock BIGINT NOT NULL DEFAULT 0 COMMENT 可用库存 total - lock, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_sku (sku_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品库存表;重点看三个字段lock_stock不是冗余字段而是关键的“预占位”机制。用户下单时先UPDATE t_product_stock SET lock_stock lock_stock 1 WHERE sku_code ?成功后再创建订单。订单支付成功才把lock_stock转移到available_stock支付失败则释放lock_stock。这解决了“下单未支付导致库存被长期占用”的问题。available_stock前端展示的库存永远等于total_stock - lock_stock避免实时计算。version乐观锁核心。每次UPDATE都带WHERE version #{version}成功则version version 1。MyBatis-Plus的Version注解会自动处理但必须确保实体类字段类型是Integer且数据库字段默认值为0。注意lock_stock机制要求你必须有“订单超时关闭”功能。我们用Redis的EXPIRE设置订单过期时间例如30分钟过期后通过Lua脚本原子性地释放lock_stock。如果用MQ延时消息要考虑消息丢失的补偿。3.2 Redis预减库存的原子性保障为什么不用DECRBY而用Lua脚本初学者常写redisTemplate.opsForValue().decrement(stock:1001)这在单实例没问题但Redis主从同步有延迟从节点可能读到旧值。更危险的是如果业务逻辑需要“先查库存再决定是否扣减”比如库存10时触发补货GETDECR两步操作不是原子的必然超卖。正确做法用Lua脚本封装原子操作。我们写了一个通用脚本stock_decr.lua-- KEYS[1] stock_key, ARGV[1] decrement_value, ARGV[2] min_stock (防止扣成负数) local current redis.call(GET, KEYS[1]) if current false then return -1 -- key不存在 end local stock tonumber(current) local decr tonumber(ARGV[1]) local min tonumber(ARGV[2]) if stock decr then return -2 -- 库存不足 end if stock - decr min then return -3 -- 扣减后低于最小值 end return redis.call(DECRBY, KEYS[1], decr)Java调用DefaultRedisScriptLong script new DefaultRedisScript(); script.setScriptText(luaScriptContent); script.setResultType(Long.class); Long result redisTemplate.execute(script, Collections.singletonList(stock:1001), 1, 0); if (result -2) { throw new BusinessException(库存不足); } else if (result -3) { throw new BusinessException(库存已低于安全阈值); } // result 0 表示扣减成功返回新库存值这个脚本的好处一次网络往返完成“判断扣减”且返回值明确标识失败原因-1 key不存在-2库存不足-3低于阈值比try-catch更精准。我们压测发现相比GETDECR两次调用Lua方案在2000QPS下错误率降低92%。3.3 悲观锁的实战避坑FOR UPDATE必须配合索引且不能写错顺序这是线上事故最高发的点。看这段典型错误代码// ❌ 错误没走索引触发表锁 ProductStock stock productStockMapper.selectBySkuCode(skuCode); // WHERE sku_code ? 但sku_code没建索引 // 接着执行 productStockMapper.lockAndDecrement(skuCode, 1); // SELECT ... FOR UPDATE如果sku_code没有索引MySQL会全表扫描然后对所有扫描到的行加锁相当于锁了整张表。此时另一个事务更新其他商品也会被阻塞。正确姿势强制走索引在Mapper XML里写死主键查询再用主键做FOR UPDATE!-- ✅ 正确先查主键再用主键锁 -- select idselectByIdForUpdate resultTypeProductStock SELECT * FROM t_product_stock WHERE id #{id} FOR UPDATE /select锁的顺序必须一致如果一个事务要更新商品A和B的库存所有服务必须按“A→B”顺序加锁。如果服务X按A→B服务Y按B→A就形成死锁。我们在库存服务里约定所有批量操作按sku_code字典序排序后再执行。超时控制在DataSource配置里加?connectTimeout3000socketTimeout5000并在MyBatis配置setting namedefaultStatementTimeout value3/避免一个慢SQL拖垮整个连接池。实操心得上线前必须用SHOW ENGINE INNODB STATUS\G查死锁日志。我们曾在一个促销日发现由于两个服务对同一商品调用SELECT ... FOR UPDATE的WHERE条件不同一个用sku_code一个用id导致锁粒度不一致引发连锁阻塞。解决方案是统一收口到一个库存服务禁止DAO层直连。4. 实操过程与核心环节实现从零搭建可压测的抢购服务4.1 SpringBoot项目初始化与关键依赖配置创建SpringBoot 2.7.x项目兼容JDK 8降低团队升级成本pom.xml核心依赖dependencies !-- Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- Redis -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- 连接池 -- dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency !-- Lombok -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesapplication.yml关键配置spring: redis: host: 127.0.0.1 port: 6379 password: database: 0 lettuce: pool: max-active: 200 # 必须够大抢购时Redis连接消耗快 max-idle: 20 min-idle: 0 max-wait: 10000 datasource: url: jdbc:mysql://127.0.0.1:3306/seckill?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: root hikari: maximum-pool-size: 50 # 悲观锁场景必须提高 minimum-idle: 10 connection-timeout: 3000 validation-timeout: 3000 idle-timeout: 600000 max-lifetime: 1800000注意HikariCP的maximum-pool-size设为50是经过压测验证的。设太小如20在悲观锁场景下大量线程在等待数据库连接TPS直接腰斩设太大如100又会引发MySQL的max_connections超限。我们用show variables like max_connections;查到MySQL默认151所以应用层连接池必须留出余量。4.2 核心Service实现三种锁机制的完整代码与调用链我们定义一个SeckillService接口包含三个方法public interface SeckillService { /** * 方案1Redis预减 MySQL乐观锁 */ Result seckillOptimistic(String skuCode); /** * 方案2Redis预减 MySQL悲观锁 */ Result seckillPessimistic(String skuCode); /** * 方案3Redis分布式锁 MySQL最终扣减 */ Result seckillRedisLock(String skuCode); }4.2.1 乐观锁实现seckillOptimisticService Slf4j public class SeckillOptimisticServiceImpl implements SeckillService { Autowired private RedisTemplateString, Object redisTemplate; Autowired private ProductStockMapper productStockMapper; Override public Result seckillOptimistic(String skuCode) { String redisKey stock: skuCode; // 1. Redis预减库存Lua脚本 Long newStock redisDecr(redisKey, 1L); if (newStock null || newStock 0) { return Result.fail(库存不足); } // 2. MySQL乐观锁扣减带重试 int retry 0; while (retry 3) { try { ProductStock stock productStockMapper.selectBySkuCode(skuCode); if (stock null || stock.getAvailableStock() 1) { // Redis和DB不一致回滚Redis redisTemplate.opsForValue().increment(redisKey, 1L); return Result.fail(库存已售罄); } // 构造更新条件version必须匹配 ProductStock update new ProductStock(); update.setSkuCode(skuCode); update.setVersion(stock.getVersion()); // 当前版本 update.setAvailableStock(stock.getAvailableStock() - 1); update.setLockStock(stock.getLockStock() 1); int rows productStockMapper.updateOptimistic(update); if (rows 0) { // 更新成功创建订单 createOrder(skuCode); return Result.success(抢购成功); } // version不匹配重试 retry; Thread.sleep(10); // 避免重试风暴 } catch (Exception e) { log.error(乐观锁扣减失败sku:{}, skuCode, e); break; } } return Result.fail(系统繁忙请稍后重试); } private Long redisDecr(String key, Long delta) { // 调用前面定义的Lua脚本 DefaultRedisScriptLong script ...; return redisTemplate.execute(script, Collections.singletonList(key), delta.toString(), 0); } private void createOrder(String skuCode) { // 简化插入订单表实际应发MQ异步 Order order new Order(); order.setSkuCode(skuCode); order.setCreateTime(new Date()); orderMapper.insert(order); } }关键点重试逻辑最多3次每次sleep 10ms。太多重试会加重DB压力太少则失败率高。这个数值是压测出来的平衡点。Redis回滚当DB扣减失败version冲突必须把Redis里预减的库存加回去否则缓存永远比DB少。createOrder这里简化为同步插入实际生产必须异步化否则订单服务慢会拖垮库存服务。4.2.2 悲观锁实现seckillPessimisticService Slf4j public class SeckillPessimisticServiceImpl implements SeckillService { Autowired private RedisTemplateString, Object redisTemplate; Autowired private ProductStockMapper productStockMapper; Override Transactional(rollbackFor Exception.class) public Result seckillPessimistic(String skuCode) { String redisKey stock: skuCode; // 1. Redis预减同上 Long newStock redisDecr(redisKey, 1L); if (newStock null || newStock 0) { return Result.fail(库存不足); } // 2. MySQL悲观锁必须用主键查询且SELECT ... FOR UPDATE ProductStock stock productStockMapper.selectByIdForUpdate(getProductIdBySku(skuCode)); if (stock null || stock.getAvailableStock() 1) { // DB库存不足回滚Redis redisTemplate.opsForValue().increment(redisKey, 1L); return Result.fail(库存已售罄); } // 3. 执行扣减此时已加锁无需担心并发 stock.setAvailableStock(stock.getAvailableStock() - 1); stock.setLockStock(stock.getLockStock() 1); stock.setVersion(stock.getVersion() 1); // 乐观锁字段也要更新 productStockMapper.updateById(stock); // 4. 创建订单 createOrder(skuCode); return Result.success(抢购成功); } private Long getProductIdBySku(String skuCode) { // 从缓存或DB查sku_code对应的id必须保证有索引 return productStockMapper.selectIdBySkuCode(skuCode); } }关键点Transactional是必须的否则FOR UPDATE锁在方法结束就释放起不到保护作用。getProductIdBySku必须走索引我们给sku_code字段加了唯一索引。即使用了悲观锁依然要检查available_stock因为Redis预减和DB查询之间有微小时间差可能被其他服务抢先扣减。4.2.3 Redis分布式锁实现seckillRedisLockService Slf4j public class SeckillRedisLockServiceImpl implements SeckillService { Autowired private RedisTemplateString, Object redisTemplate; Autowired private ProductStockMapper productStockMapper; private static final String LOCK_PREFIX seckill:lock:; Override public Result seckillRedisLock(String skuCode) { String lockKey LOCK_PREFIX skuCode; String requestId UUID.randomUUID().toString(); // 唯一请求ID防误删 // 1. 尝试获取锁SETNX EXPIRE原子操作 Boolean isLocked redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, 10, TimeUnit.SECONDS); if (!isLocked) { return Result.fail(系统繁忙请稍后重试); } try { // 2. Redis预减 String redisKey stock: skuCode; Long newStock redisDecr(redisKey, 1L); if (newStock null || newStock 0) { return Result.fail(库存不足); } // 3. MySQL最终扣减此时已加锁可走简单UPDATE int rows productStockMapper.decrementStock(skuCode, 1); if (rows 0) { return Result.fail(库存已售罄); } createOrder(skuCode); return Result.success(抢购成功); } finally { // 4. 安全释放锁必须用Lua脚本防止释放了别人的锁 String releaseScript if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; redisTemplate.execute(new DefaultRedisScript(releaseScript, Long.class), Collections.singletonList(lockKey), requestId); } } }关键点锁释放必须用Lua先GET再DEL不是原子的如果GET后服务宕机锁就永远无法释放。Lua脚本保证“判断删除”原子性。锁过期时间10秒必须大于业务执行时间我们压测最长2.3秒但不能太长如30秒否则故障时锁残留太久。requestId防误删避免A服务拿到锁执行超时锁自动过期B服务拿到锁A服务恢复后误删B的锁。4.3 压测脚本与真实数据JMeter配置与结果分析我们用JMeter模拟2000用户并发请求线程组配置线程数2000Ramp-Up Period10秒每秒200用户循环次数1HTTP请求POST /api/seckill/optimistic?skuCode1001三个方案压测结果持续5分钟方案平均响应时间吞吐量TPS错误率超发数CPU使用率乐观锁22ms18500.8%065%悲观锁48ms9200.02%088%Redis锁31ms15200.1%072%关键结论乐观锁吞吐量最高但错误率0.8%意味着每1000次请求有8次失败需要前端友好提示“请重试”。悲观锁错误率最低但TPS只有920且CPU飙到88%说明锁竞争激烈。Redis锁是平衡之选TPS和错误率都居中CPU压力适中。实操心得压测时一定要监控Redis的INFO stats中的instantaneous_ops_per_sec和rejected_connections以及MySQL的SHOW PROCESSLIST看是否有大量Waiting for table metadata lock。我们曾发现某个开发在压测时偷偷执行了ALTER TABLE导致所有FOR UPDATE请求被阻塞TPS直接归零。5. 常见问题与排查技巧实录那些让你半夜爬起来的线上Bug5.1 “库存明明是100为什么扣到-5”——Redis与MySQL数据不一致的根因分析这是最经典的超发现象。我们复盘过3个真实案例案例1Redis主从延迟现象用户看到前端显示“库存100”点击抢购后提示“抢购成功”但后台查MySQL发现库存是95Redis里是94。根因应用写Redis主节点后立即读从节点因为读写分离配置了slavePreferred而主从同步有200ms延迟。解决所有库存读操作强制走Redis主节点。在RedisTemplate配置readFrom ReadFrom.MASTER。案例2Lua脚本未处理网络超时现象JMeter压测时部分请求返回-1key不存在但日志显示Redis Key明明存在。根因Lua脚本执行时网络抖动导致客户端收到超时但Redis服务端脚本已执行完毕。解决Lua脚本返回值必须包含执行状态。我们改造脚本成功时返回{code:0, stock:99}超时返回{code:-1, msg:timeout}客户端根据code判断是否重试。案例3MySQL事务未提交Redis已扣减现象服务重启后Redis库存比MySQL少10个。根因createOrder()抛异常Transactional回滚了MySQL但Redis的DECR无法回滚没有事务。解决引入TCC模式Try-Confirm-Cancel。Try阶段Redis DECR 写入本地事务表记录本次扣减Confirm阶段MySQL扣减 删除事务表记录Cancel阶段Redis INCR 删除事务表记录。我们用RocketMQ事务消息实现Confirm/Cancle的异步触发。5.2 “为什么加了锁还是出现死锁”——InnoDB死锁日志解读指南当MySQL发生死锁SHOW ENGINE INNODB STATUS\G会输出详细日志。看懂它5分钟定位问题------------------------ LATEST DETECTED DEADLOCK ------------------------ 2023-09-15 14:23:45 0x7f8a1c012700 *** (1) TRANSACTION: TRANSACTION 123456789, ACTIVE 0.001 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 1001, OS thread handle 140221234567680, query id 123456 localhost root updating UPDATE t_product_stock SET available_stock available_stock - 1 WHERE sku_code 1001 *** (1) HOLDS THE LOCK(S): RECORD LOCKS space id 108 page no 3 n bits 72 index uk_sku of table seckill.t_product_stock trx id 123456789 lock_mode X locks rec but not gap *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 108 page no 4 n bits 72 index PRIMARY of table seckill.t_product_stock trx id 123456789 lock_mode X locks rec but not gap waiting *** (2) TRANSACTION: TRANSACTION 123456790, ACTIVE 0.002 sec starting index read ... *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 108 page no 4 n bits 72 index PRIMARY of table seckill.t_product_stock trx id 123456790 lock_mode X locks rec but not gap *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 108 page no 3 n bits 72 index uk_sku of table seckill.t_product_stock trx id 123456790 lock_mode X locks rec but not gap waiting解读事务1持有uk_sku索引上的X锁等待PRIMARY索引上的X锁事务2持有PRIMARY索引上的X锁等待uk_sku索引上的X锁形成循环等待死锁。解决方案统一SQL执行顺序所有服务必须先按sku_code查再按id更新禁止混用。减少锁范围把UPDATE ... WHERE sku_code ?改成UPDATE ... WHERE id ?因为主键索引的锁粒度更小。5.3 “Redis锁失效了”——分布式锁的7个失效场景与防御清单我们整理了生产环境高频失效场景按风险等级排序风险等级场景防御措施验证方式⚠️⚠️⚠️高危锁过期时间小于业务执行时间设置过期时间为业务最大耗时×2并在业务中续期Redisson的watchdog机制压测时故意让一个线程sleep 15秒观察锁是否被其他线程获取⚠️⚠️中危客户端获取锁后崩溃未释放使用SET key value EX seconds NX依赖Redis自动过期模拟进程kill -9检查锁是否自动消失⚠️⚠️中危多个客户端用相同value释放锁释放锁时必须校验value即requestId用两个JMeter线程A获取锁后不释放B尝试释放检查是否成功⚠️低危Redis主从切换从节点升主锁丢失改用Redlock算法5节点过半成功或ZooKeeper模拟主节点宕机观察锁是否连续⚠️低危网络分区客户端以为获取锁失败实际已成功客户端记录锁获取时间戳重试时先检查在锁获取后立即写入本地日志宕机恢复后检查⚠️低危Lua释放脚本有语法错误上线前用redis-cli --eval测试脚本redis-cli --eval unlock.lua lock:key , requestId⚠️低危锁key命名冲突如不同环境共用key前缀加入环境标识如seckill:prod:lock:1001检
阅读完成 · 觉得有帮助?