在单体的应用开发场景中涉及并发同步时,大家往往采用Synchronized(同步)或同一个JVM内Lock机制来解决多线程间的同步问题。而在分布式集群工作的开发场景中,就需要一种更加高级的锁机制来处理跨机器的进程之间的数据同步问题,这种跨机器的锁就是分布式锁。接下来本文将为大家分享分布式锁的最佳实践。
01 超卖问题复现
1.1 现象
存在如下的几张表:
商品表
订单表
订单item表
商品的库存为1,但是并发高的时候有多笔订单。
✪ 错误案例一:数据库update相互覆盖
直接在内存中判断是否有库存,计算扣减之后的值更新数据库,并发的情况下会导致相互覆盖发生:
@Transactional(rollbackFor = Exception.class) public Long createOrder() throws Exception { Product product = productMapper.selectByPrimaryKey(purchaseProductId); // ... 忽略校验逻辑 //商品当前库存 Integer currentCount = product.getCount(); //校验库存 if (purchaseProductNum > currentCount) { throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买"); } // 计算剩余库存 Integer leftCount = currentCount - purchaseProductNum; // 更新库存 product.setCount(leftCount); product.setGmtModified(new Date()); productMapper.updateByPrimaryKeySelective(product); Order order = new Order(); // ... 省略 Set orderMapper.insertSelective(order); OrderItem orderItem = new OrderItem(); orderItem.setOrderId(order.getId()); // ... 省略 Set return order.getId(); }
✪ 错误案例二:扣减串行执行,但是库存被扣减为负数
在 SQL 中加入运算避免值的相互覆盖,但是库存的数量变为负数,因为校验库存是否足够还是在内存中执行的,并发情况下都会读到有库存:
@Transactional(rollbackFor = Exception.class) public Long createOrder() throws Exception { Product product = productMapper.selectByPrimaryKey(purchaseProductId); // ... 忽略校验逻辑 //商品当前库存 Integer currentCount = product.getCount(); //校验库存 if (purchaseProductNum > currentCount) { throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买"); } // 使用 set count = count - #{purchaseProductNum,jdbcType=INTEGER}, 更新库存 productMapper.updateProductCount(purchaseProductNum,new Date(),product.getId()); Order order = new Order(); // ... 省略 Set orderMapper.insertSelective(order); OrderItem orderItem = new OrderItem(); orderItem.setOrderId(order.getId()); // ... 省略 Set return order.getId(); }
✪ 错误案例三:使用 synchronized 实现内存中串行校验,但是依旧扣减为负数
因为我们使用的是事务的注解,synchronized加在方法上,方法执行结束的时候锁就会释放,此时的事务还没有提交,另一个线程拿到这把锁之后就会有一次扣减,导致负数。
@Transactional(rollbackFor = Exception.class) public synchronized Long createOrder() throws Exception { Product product = productMapper.selectByPrimaryKey(purchaseProductId); // ... 忽略校验逻辑 //商品当前库存 Integer currentCount = product.getCount(); //校验库存 if (purchaseProductNum > currentCount) { throw new Exception("商品" + purchaseProductId + "仅剩" + currentCount + "件,无法购买"); } // 使用 set count = count - #{purchaseProductNum,jdbcType=INTEGER}, 更新库存 productMapper.updateProductCount(purchaseProductNum,new Date(),product.getId()); Order order = new Order(); // ... 省略 Set orderMapper.insertSelective(order); OrderItem orderItem = new OrderItem(); orderItem.setOrderId(order.getId()); // ... 省略 Set return order.getId(); }
1.2 解决办法
从上面造成问题的原因来看,只要是扣减库存的动作,不是原子性的。多个线程同时操作就会有问题。
- 单体应用:使用本地锁 + 数据库中的行锁解决
- 分布式应用:
- 使用数据库中的乐观锁,加一个 version 字段,利用CAS来实现,会导致大量的 update 失败
- 使用数据库维护一张锁的表 + 悲观锁 select,使用 select for update 实现
- 使用Redis 的 setNX实现分布式锁
- 使用zookeeper的watcher + 有序临时节点来实现可阻塞的分布式锁
- 使用Redisson框架内的分布式锁来实现
- 使用curator 框架内的分布式锁来实现