java代码
库存问题,如何保证不超卖?
时间:2024-01-11
这是一个经典问题,在高并发企业级项目中会经常碰到,而且在面试中也是时常会被问到。这个问题的底层含义其实保证能够合乎逻辑的对数据库进行更新,这个逻辑其实就是保证一个值不被更新成负值。
一、问题原因
看如下代码:
public String buyGoods(Long goodsId, Integer goodsNum) { |
仔细一看没啥毛病,但一旦多线程访问,问题就会出现在第二步,因为同时访问的线程都会得到库存仍然存在的结果,都能够执行第三步,但是第三步如果这些线程去更新,极有可能会更新成负值。
在秒杀系统、售票系统、仓库库存系统等业务场景等等,是无论如何不能出现库存为负值的情况的。
二、数据库保险方案
在库存操作中,一般只会涉及到uodate操作,而在数据库(这里主要指mysql)中,可以利用其自身的特性来帮助解决这个问题。
使用where语句和unsigned 非负字段。
关于数据库是如何执行update语句,可以查看:update语句的执行过程
update t_goods
set goods_inventory = goods_inventory - #{goodsNum}, |
UNSIGNED属性就是将数字类型无符号化,与C、C++这些程序语言中的含义相同。
例如,TINYINT的类型范围是【-128,127】,TINYINY UNSIGNED的范围类型就是0~255。
三、锁方案
数据库行锁:
SELECT * FROM t_goods WHERE id = #{goodsId} for update |
这里直接使用悲观锁(乐观锁就不介绍了,非业务字段实在有些鸡肋),确保库存操作为同步操作。会损耗一定的性能。
redis分布式锁:
import org.redisson.Redisson; import org.redisson.api.RedissonClient; public String buyRedisLock(Long goodsId, Integer goodsNum) { RLock lock = redissonClient.getLock("goods_buy"); try { Goods goods = goodsMapper.selectById(goodsId); if (goods.getGoodsInventory() <= 0) { goodsMapper.updateById(goods); return "购买成功!"; } catch (Exception e) { //没加上锁就表示已经有线程在操作了,本质上还是同步操作 log.error("秒杀失败"); } finally { lock.unlock(); |
四、缓存方案
redis虽然是以缓存数据库著称,但其有很多原子操作。在并发较高的场景下,如秒杀,可以将库存预先缓存到redis中,不仅能减少数据库面临的压力,还能进行库存预扣:
@Resource private StringRedisTemplate stringRedisTemplate; /** String key = "shop-product-stock" + skuCode; Object value = stringRedisTemplate.opsForValue().get(key); //先检查 库存是否充足 //不可在这里直接操作数据库减库存,否则导致数据不安全,因为此时可能有其他线程已经将redis的key修改了 //redis 减少库存,然后才能操作数据库,increment属于原子操作,关于其用法可以参考:redis in Long newStock = stringRedisTemplate.opsForValue().increment(key, -num.longValue()); |
总结:在生产环境中,几类方案各有利弊,根据具体场景去使用。如果用户量小,并发度低,切不是分布式系统,比如小型库存管理系统,使用数据库保险方案足以解决问题。反之,则需要使用到行锁货分布式锁,当然无论是使用数据库机制还是锁机制,都会有一定的性能损耗,这是无可避免的。
当然还有更多优秀的方案,需要结合具体环境去使用。