1. 数据库实现

  1. update goods set stock = stock - 1 where id = #{goodsId} and stock > 0

使用一个sql实现库存的减少,InnoDB存在行锁在更新时会锁住该行,所以同一时刻只有一个用户能成功将库存减1,并且stock > 0保证了库存不会被更新成负数。通过该语句返回的值是否大于0判断库存扣减是否成功。

具体代码如下所示:

  1. @Transactional(rollbackFor = Exception.class)
  2. public void seckill(Long goodsId, int num) {
  3. Goods goods = goodsDao.selectById(goodsId);
  4. if (goods.getStock() <= 0) {
  5. throw new BizException("售罄");
  6. }
  7. // 减库存
  8. int row = goodsDao.decreaseStock(goodsId);
  9. if (row <= 0) {
  10. throw new BizException("售罄");
  11. }
  12. // 生成订单
  13. Order order = new Order(goodsId, num);
  14. orderDao.insert(order);
  15. }

2. redis实现

方案一 使用redis的自增功能实现

存在的问题:查询和自增两个操作非原子,高并发情况下可能出现库存被减至负值。

  1. @Transactional(rollbackFor = Exception.class)
  2. public void seckillRedis(Long goodsId, int num) {
  3. String key = "goods:" + goodsId;
  4. Map<Object, Object> map = redisTemplate.opsForHash().entries(key);
  5. Goods goods = BeanUtil.mapToBean(map, Goods.class, true, null);
  6. if (goods.getStock() < 0) {
  7. throw new BizException("售罄");
  8. }
  9. Long stock = redisTemplate.opsForHash().increment(key, "stock", -num);
  10. if (stock < 0) {
  11. throw new BizException("售罄");
  12. }
  13. // 生成订单
  14. Order order = new Order(goodsId, num);
  15. orderDao.insert(order);
  16. }

方案二 使用lua脚本

为了解决方案一种的问题,使用lua脚本将查询和自增两个操作合成一个操作进行。
lua脚本如下所示:

  1. if (redis.call('hexists', KEYS[1], KEYS[2]) == 1) then
  2. local stock = tonumber(redis.call('hget', KEYS[1], KEYS[2]));
  3. if (stock >= 1) then
  4. redis.call('hincrby', KEYS[1], KEYS[2], -1);
  5. return stock;
  6. end;
  7. return 0;
  8. end;

代码如下所示:

  1. @Bean
  2. public DefaultRedisScript<Long> stockScript() {
  3. DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
  4. //放在和application.yml 同层目录下
  5. redisScript.setLocation(new ClassPathResource("stock.lua"));
  6. redisScript.setResultType(Long.class);
  7. return redisScript;
  8. }
  9. @Autowired
  10. DefaultRedisScript<Long> defaultRedisScript;
  11. @Override
  12. @Transactional(rollbackFor = Exception.class)
  13. public void seckillRedisLua(Long goodsId, int num) {
  14. List<Object> keys = new ArrayList<>();
  15. keys.add("goods:" + goodsId);
  16. keys.add("stock");
  17. keys.add("" + num);
  18. Long stock = redisTemplate.execute(defaultRedisScript, keys);
  19. if (stock == null || stock < 1) {
  20. throw new BizException("售罄");
  21. }
  22. // 生成订单
  23. Order order = new Order(goodsId, num);
  24. orderDao.insert(order);
  25. }

3. 使用jmeter压测结果

使用数据库和redis lua脚本这两个方案不会出现库存超卖的现象。