1. 数据库实现
update goods set stock = stock - 1 where id = #{goodsId} and stock > 0
使用一个sql实现库存的减少,InnoDB存在行锁在更新时会锁住该行,所以同一时刻只有一个用户能成功将库存减1,并且stock > 0保证了库存不会被更新成负数。通过该语句返回的值是否大于0判断库存扣减是否成功。
具体代码如下所示:
@Transactional(rollbackFor = Exception.class)public void seckill(Long goodsId, int num) {Goods goods = goodsDao.selectById(goodsId);if (goods.getStock() <= 0) {throw new BizException("售罄");}// 减库存int row = goodsDao.decreaseStock(goodsId);if (row <= 0) {throw new BizException("售罄");}// 生成订单Order order = new Order(goodsId, num);orderDao.insert(order);}
2. redis实现
方案一 使用redis的自增功能实现
存在的问题:查询和自增两个操作非原子,高并发情况下可能出现库存被减至负值。
@Transactional(rollbackFor = Exception.class)public void seckillRedis(Long goodsId, int num) {String key = "goods:" + goodsId;Map<Object, Object> map = redisTemplate.opsForHash().entries(key);Goods goods = BeanUtil.mapToBean(map, Goods.class, true, null);if (goods.getStock() < 0) {throw new BizException("售罄");}Long stock = redisTemplate.opsForHash().increment(key, "stock", -num);if (stock < 0) {throw new BizException("售罄");}// 生成订单Order order = new Order(goodsId, num);orderDao.insert(order);}
方案二 使用lua脚本
为了解决方案一种的问题,使用lua脚本将查询和自增两个操作合成一个操作进行。
lua脚本如下所示:
if (redis.call('hexists', KEYS[1], KEYS[2]) == 1) thenlocal stock = tonumber(redis.call('hget', KEYS[1], KEYS[2]));if (stock >= 1) thenredis.call('hincrby', KEYS[1], KEYS[2], -1);return stock;end;return 0;end;
代码如下所示:
@Beanpublic DefaultRedisScript<Long> stockScript() {DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();//放在和application.yml 同层目录下redisScript.setLocation(new ClassPathResource("stock.lua"));redisScript.setResultType(Long.class);return redisScript;}@AutowiredDefaultRedisScript<Long> defaultRedisScript;@Override@Transactional(rollbackFor = Exception.class)public void seckillRedisLua(Long goodsId, int num) {List<Object> keys = new ArrayList<>();keys.add("goods:" + goodsId);keys.add("stock");keys.add("" + num);Long stock = redisTemplate.execute(defaultRedisScript, keys);if (stock == null || stock < 1) {throw new BizException("售罄");}// 生成订单Order order = new Order(goodsId, num);orderDao.insert(order);}
3. 使用jmeter压测结果
使用数据库和redis lua脚本这两个方案不会出现库存超卖的现象。
