springboot 结合mysql、redis+lua 实现库存扣减方案,防止超卖
表结构
库存表:
订单表:
方案1:采用mysql 自带行级锁
select * from t_stock for update;
当前事务提交之后,其他线程才能获取锁。判断库存是否大于或等于当前需要购买的数量,否则返回库存不足。
@Transactional
@Override
public int createOrderForUpdate(Integer productId, Integer count) {
Stock stock = stockMapper.selectForUpdate(productId);
if (stock.getStock() < count) {
throw new StockLackException("库存不足");
}
Order order = new Order();
order.setUserId((int) Thread.currentThread().getId());
order.setProductId(productId);
order.setCount(count);
order.setOrderTime(new Date());
// 创建订单
baseMapper.insert(order);
// 扣减库存
stockMapper.stockForUpdate(stock.getId(), count);
return 1;
}
方案2:基于版本号的乐观锁
update t_stock set stock = stock - #{count}, version = version + 1
where version = #{version} and stock >= #{count} and id = #{id}
返回结果为此次更新影响的行数,如果影响的行数大于0,表示此次更新库存充足,否则返回库存不足。
@Transactional
@Override
public int createOrderByVersion(Integer productId, Integer count) {
Stock stockByProductId = getStockByProductId(productId);
Integer version = stockByProductId.getVersion();
int i = stockMapper.stockByVersion(stockByProductId.getId(), count, version);
if (i > 0) {
// 不存在并发,创建订单
Order order = new Order();
order.setUserId((int) Thread.currentThread().getId());
order.setProductId(productId);
order.setCount(count);
order.setOrderTime(new Date());
baseMapper.insert(order);
return 1;
}
throw new StockLackException("库存不足");
}
方案3:基于redis 分布式锁
每次只允许一个线程操作,库存减为0时,返回库存不足。
@Override
public int createOrderByRedisLock(Integer productId, Integer count) {
String key = "stock:" + productId;
int stock = (int) redisTemplate.opsForValue().get(key);
if (stock <= 0) {
throw new StockLackException("库存不足");
}
RLock lock = redissonClient.getLock(LOCK_KEY);
if (lock.tryLock()) {
try {
int stock1 = (int) redisTemplate.opsForValue().get(key);
if (stock1 >= count) {
Order order = new Order();
order.setUserId((int) Thread.currentThread().getId());
order.setProductId(productId);
order.setCount(count);
order.setOrderTime(new Date());
baseMapper.insert(order);
redisTemplate.opsForValue().decrement(key, count);
} else {
throw new StockLackException("库存不足");
}
} finally {
lock.unlock();
}
}
return 1;
}
方案4:redis + lua 脚本原子操作
local key = KEYS[1] -- 获取第一个参数作为键名
local incrementBy = tonumber(ARGV[1]) -- 获取第二个参数作为增量值,并将其转换为数字类型
local stock = redis.call("GET", key) -- 通过GET命令获取键的当前值
if nil == stock or not stock then
return -2 -- 库存还未初始化
elseif tonumber(stock) >= incrementBy then
return redis.call('DECRBY', key, incrementBy) -- 库存充足
else
return -1 -- 库存不足
end
原理与方案3类似,将库存判断与扣减的过程原子化,省去加锁的过程。
返回 -2 时,表示库存未初始化,需要先初始化库存到缓存中,而且只能有一个线程执行初始化的操作,
所以这里也需要加锁,初始化之前进行一次非空判断,防止重复初始化;初始化完成后,重新校验一次库存。
@Override
public int createOrderByRedisLua(Integer productId, Integer count) {
// >=0 库存充足 -1 库存不足 -2 库存未初始化
long validateStock = validateStock(productId, count);
if (-2 == validateStock) {
String key = "stock:" + productId;
RLock lock = redissonClient.getLock(LOCK_KEY);
if (lock.tryLock()) {
try {
Object o = redisTemplate.opsForValue().get(key);
if (o == null) {
Stock stockByProductId = getStockByProductId(productId);
redisTemplate.opsForValue().set(key, stockByProductId.getStock());
}
} finally {
lock.unlock();
}
}
validateStock = validateStock(productId, count);
}
if (-1 == validateStock) {
throw new StockLackException("库存不足");
}
if (validateStock >= 0) {
Order order = new Order();
order.setUserId((int) Thread.currentThread().getId());
order.setProductId(productId);
order.setCount(count);
order.setOrderTime(new Date());
baseMapper.insert(order);
}
return 1;
}
private long validateStock(Integer productId, Integer count) {
long stock = redisTemplate.execute(defaultRedisScript,
Collections.singletonList("stock:" + productId), count);
return stock;
}