Redis防止订单超卖

背景

想象一下,你开了一家超火爆的奶茶店,每天限量 100 杯招牌奶茶。顾客们都在线疯狂抢购。
核心问题就是:如何确保这 100 杯奶茶不会被第 101 个顾客抢走? 简单地在数据库里减库存 stock = stock - 1是绝对不行的,高并发下瞬间就超卖了!

解析

核心目的就是要解决在高并发下单的场景下,保证商品库存扣减的原子性一致性

Redis是基于内存的,读写速度快,能抗住高并发。Redis可以使用Lua脚本实现CAS语义,可以将判断库存和扣减库存绑定为一个原子操作,确保在操作执行的时候不会出错。

实现

  1. 首先编写一个lua脚本
-- KEYS[1]: 库存的键名,例如: stock:product_1001
-- ARGV[1]: 要购买的数量 (通常是 1,字符串类型,需要转数字)
local stock_key = KEYS[1]
local quantity_to_buy = tonumber(ARGV[1])

-- 1. 获取当前库存
local current_stock = tonumber(redis.call('GET', stock_key))
if current_stock == nil then
current_stock = 0 -- 如果键不存在,视为库存0
end

-- 2. 检查库存是否足够
if current_stock < quantity_to_buy then
-- 库存不足,返回 -1 (或者返回一个特定的错误码/标识)
return -1
end

-- 3. 扣减库存
redis.call('DECRBY', stock_key, quantity_to_buy)

-- 4. 返回扣减后的库存 (或者返回 1 表示成功)
return tonumber(redis.call('GET', stock_key)) -- 返回最新库存
-- -- 或者 return 1 表示成功扣减
  1. 加载和执行Lua脚本
@Component
public class StockService {

@Autowired
private RedisTemplate<String, Object> redisTemplate; // 或 StringRedisTemplate

// 定义 Lua 脚本对象 static初始化块确保线程安全且只加载一次
private static final DefaultRedisScript<Long> DEDUCT_STOCK_SCRIPT;

static {
// 1. 创建 DefaultRedisScript 实例
DEDUCT_STOCK_SCRIPT = new DefaultRedisScript<>();

// 2. 设置脚本资源位置 (Spring 会自动读取 classpath 下的文件)
// 假设 lua 脚本放在 src/main/resources/lua/ 目录下
DEDUCT_STOCK_SCRIPT.setLocation(new ClassPathResource("lua/deduct_stock.lua"));

// 3. 设置脚本执行结果的 Java 类型
DEDUCT_STOCK_SCRIPT.setResultType(Long.class);
}

public String deductStock(String productId, int quantityToBuy) {
String stockKey = "stock:product_" + productId; // 构造库存键

// 执行 Lua 脚本
// 参数说明:
// DEDUCT_STOCK_SCRIPT: 脚本对象
// Collections.singletonList(stockKey): KEYS 参数列表 (只有一个元素)
// quantityToBuy: ARGV 参数列表 (只有一个元素,脚本内会转成数字)
Long result = redisTemplate.execute(
DEDUCT_STOCK_SCRIPT,
Collections.singletonList(stockKey), // KEYS
quantityToBuy // ARGV
);

// 处理脚本执行结果
if (result == null) {
// 脚本执行可能出错 (如键类型不对)
return "系统错误";
} else if (result == -1) { // 根据脚本设计,-1 表示库存不足
return "库存不足";
} else {
// result 是扣减后的库存 (或者如果是 return 1,这里判断 result == 1)
// 库存扣减成功!执行后续业务逻辑(创建订单等)
// 注意:如果后续业务失败,同样需要回补库存!
return "扣减成功,剩余库存: " + result;
}
}
}