自己搭建电商平台初期,原来“超卖,java 书籍百度网盘
product.setCount(leftCount);product.setTimeModified(new Date());product.setUpdateUser("kdaddy");productMapper.updateByPrimaryKeySelective(product);//生成订单 KdOrder order = new KdOrder();order.setOrderAmount(product.getPrice().multiply(new BigDecimal(purchaseProductNum)));order.setOrderStatus(1);//待处理 order.setReceiverName("kdaddy");order.setReceiverMobile("13311112222");order.setTimeCreated(new Date());order.setTimeModified(new Date());order.setCreateUser("kdaddy");order.setUpdateUser("kdaddy");orderMapper.insertSelective(order);
KdOrderItem orderItem = new KdOrderItem();orderItem.setOrderId(order.getId());orderItem.setProductId(product.getId());orderItem.setPurchasePrice(product.getPrice());orderItem.setPurchaseNum(purchaseProductNum);orderItem.setCreateUser("kdaddy");orderItem.setTimeCreated(new Date());orderItem.setTimeModified(new Date());orderItem.setUpdateUser("kdaddy");orderItemMapper.insertSelective(orderItem);return order.getId();}}
通过以上代码我们可以看到的是库存的扣减在内存中完成。那么我们再看一下具体的单元测试代码:
@SpringBootTestclass DistributeApplicationTests {@Autowiredprivate OrderService orderService;
@Testpublic void concurrentOrder() throws InterruptedException {//简单来说表示计数器 CountDownLatch cdl = new CountDownLatch(5);//用来进行等待五个线程同时并发的场景 CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
ExecutorService es = Executors.newFixedThreadPool(5);for (int i =0;i<5;i++){es.execute(()->{try {//等待五个线程同时并发的场景 cyclicBarrier.await();Integer orderId = orderService.createOrder();System.out.println("订单 id:"+orderId);} catch (Exception e) {e.printStackTrace();}finally {cdl.countDown();}});}//避免提前关闭数据库连接池 cdl.await();es.shutdown();}}
代码执完毕之后我们看一下结果:
订单 id:1 订单 id:2 订单 id:3 订单 id:4 订单 id:5
很显然,数据库中虽然只有一个库存,但是产生了五个下单记录,如下图:
这也就产生了超卖的现象,那么如何才能解决这个问题呢?
单体架构中,利用数据库行锁解决电商超卖问题。
那么如果是这种解决方案的话,我们就要将我们扣减库存的动作下沉到我们的数据库中,利用数据库的行锁解决并发情况下同时操作的问题,我们来看一下代码的改造点。
@Service@Slf4jpublic class OrderServiceOptimizeOne {.....篇幅限制,此处省略,具体可参考 github 源码 @Transactional(rollbackFor = Exception.class)public Integer createOrder() throws Exception{KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId);if (product==null){throw new Exception("购买商品:"+purchaseProductId+"不存在");}
//商品当前库存 Integer currentCount = product.getCount();//校验库存 if (purchaseProductNum > currentCount){throw new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");}
//在数据库中完成减量操作 productMapper.updateProductCount(purchaseProductNum,"kd",new Date(),product.getId());//生成订单.....篇幅限制,此处省略,具体可参考 github 源码 return order.getId();}}
我们再来看一下执行的结果
从上述结果中,我们发现我们的订单数量依旧是 5 个订单,但是库存数量此时不再是 0,而是由 1 变成了-4,这样的结果显然依旧不是我们想要的,那么此时其实又是超卖的另外一种现象。我们来看一下超卖现象二所产生的原因。
超卖的第二种现象案例
上述其实是第二种现象,那么产生的原因是什么呢?其实是在校验库存的时候出现了问题,在校验库存的时候是并发进行对库存的校验,五个线程同时拿到了库存,并且发现库存数量都为 1,造成了库存充足的假象。此时由于写操作的时候具有 update 的行锁,所以会依次扣减执行,扣减操作的时候并无校验逻辑。因此就产生了这种超卖显现。简单的如下图所示:
[图片上传失败...(image-79e445-1612668719676)]
解决方案一:
单体架构中,利用数据库行锁解决电商超卖问题。就针对当前该案例,其实我们的解决方式也比较简单,就是更新完毕之后,我们立即查询一下库存的数量是否大于等于 0 即可。如果为负数的时候,我们直接抛出异常即可。(当然由于此种操作并未涉及到锁的知识,所以此方案仅做提出,不做实际代码实践)
解决方案二:
校验库存和扣减库存的时候统一加锁,让其成为原子性的操作,并发的时候只有获取锁的时候才会去读库库存并且扣减库存操作。当扣减结束之后,释放锁,确保库存不会扣成负数。那此时我们就需要用到前面博文提到的 java 中的两个锁的关键字 synchronized 关键字 和 ReentrantLock。
关于 synchronized 关键字的用法在之前的博文中也提到过,有方法锁和代码块锁两种方式,我们一次来通过实践看一下代码,首先是通过方法锁的方式,具体的代码如下:
//`sync
hronized`方法块锁 @Service@Slf4jpublic class OrderServiceSync01 {.....篇幅限制,此处省略,具体可参考 github 源码 @Transactional(rollbackFor = Exception.class)public synchronized Integer createOrder() throws Exception{KdProduct product = productMapper.selectByPrimaryKey(purchaseProductId);if (product==null){throw new Exception("购买商品:"+purchaseProductId+"不存在");}
//商品当前库存 Integer currentCount = product.getCount();//校验库存 if (purchaseProductNum > currentCount){throw new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");}
//在数据库中完成减量操作 productMapper.updateProductCount(purchaseProductNum,"kd",new Date(),product.getId());//生成订单.....篇幅限制,此处省略,具体可参考 github 源码 return order.getId();}}
此时我们看一下运行的结果。
[pool-1-thread-2] c.k.d.service.OrderServiceSync01 : pool-1-thread-2 库存数 1[pool-1-thread-1] c.k.d.service.OrderServiceSync01 : pool-1-thread-1 库存数 1
评论