软件工程补完计划 ——哈基米噢南北绿豆小组
2025/12/28 19:29:41
用 Lua 脚本实现原子性库存扣减
一 设计思路与原子性原理
二 单品扣减脚本与 Java 调用
Lua 脚本 seckill.lua
-- KEYS[1] 库存key,KEYS[2] 已购用户集合key-- ARGV[1] 购买数量,ARGV[2] 用户IDlocalstockKey=KEYS[1]localboughtKey=KEYS[2]localquantity=tonumber(ARGV[1])localuserId=ARGV[2]-- 1) 重复下单校验ifredis.call('SISMEMBER',boughtKey,userId)==1thenreturn-2end-- 2) 库存校验localremain=tonumber(redis.call('GET',stockKey))ifnotremainorremain<quantitythenreturn-1end-- 3) 扣减库存 & 记录用户redis.call('DECRBY',stockKey,quantity)redis.call('SADD',boughtKey,userId)returnremain-quantityJava 调用(Spring Boot + StringRedisTemplate)
@ServicepublicclassStockService{@AutowiredprivateStringRedisTemplateredisTemplate;privatestaticfinalStringLUA_SHA="stock:deduct:lua";// 预加载后的 SHA// 预加载脚本(应用启动或首次调用时)@PostConstructpublicvoidinit(){Stringscript=""" local stockKey = KEYS[1] local boughtKey = KEYS[2] local quantity = tonumber(ARGV[1]) local userId = ARGV[2] if redis.call('SISMEMBER', boughtKey, userId) == 1 then return -2 end local remain = tonumber(redis.call('GET', stockKey)) if not remain or remain < quantity then return -1 end redis.call('DECRBY', stockKey, quantity) redis.call('SADD', boughtKey, userId) return remain - quantity """;LUA_SHA=redisTemplate.execute((RedisCallback<String>)conn->conn.scriptLoad(script.getBytes(StandardCharsets.UTF_8)));}// 扣减库存publicLongdeduct(Stringsku,intquantity,LonguserId){List<String>keys=Arrays.asList("stock:"+sku,"bought:"+sku);Objectres=redisTemplate.execute((RedisConnectionconn)->conn.evalSha(LUA_SHA,ReturnType.INTEGER,keys.size(),keys.stream().map(String::getBytes).toArray(),String.valueOf(quantity).getBytes(),String.valueOf(userId).getBytes()));return(Long)res;// >=0 成功剩余;-1 售罄;-2 重复}}三 多商品原子扣减脚本
Lua 脚本 multi_deduct.lua
-- KEYS: 库存key 列表;ARGV: 购买数量列表(与 KEYS 一一对应)localstocks=redis.call('MGET',unpack(KEYS))localargs={unpack(ARGV)}-- 1) 任一不足,收集不足项并返回localinsufficient={}fori=1,#stocksdolocalremain=tonumber(stocks[i])ifnotremainorremain<tonumber(args[i])thentable.insert(insufficient,KEYS[i]..'='..tostring(remainor0))endendif#insufficient>0thenreturninsufficientend-- 2) 全部充足,逐个扣减fori=1,#stocksdoredis.call('DECRBY',KEYS[i],tonumber(args[i]))endreturn{}-- 空表表示全部成功Java 调用
publicList<String>multiDeduct(Map<String,Integer>skuQtyMap){List<String>keys=newArrayList<>(skuQtyMap.keySet());List<String>qtys=keys.stream().map(k->String.valueOf(skuQtyMap.get(k))).toList();Objectres=redisTemplate.execute((RedisConnectionconn)->conn.evalSha(LUA_SHA_MULTI,ReturnType.MULTI,keys.size(),keys.stream().map(String::getBytes).toArray(),qtys.stream().map(String::getBytes).toArray()));// 返回空列表表示成功;非空为“库存不足清单”return(List<String>)res;}🔥 关注公众号【云技纵横】,开始更新redis缓存进阶,包含手写缓存注解,缓存雪崩等内容哟!
四 生产级注意事项与兜底