首先环境介绍下:商城商品可能存在几个端(PC、APP),其次每个端对应的服务端又可能做了负载均衡(即也有多个服务端)。
要实现的目标和功能:保证商品不会出现超卖的情况。超卖商品后,无法对商品进行发货,是一种不负责任的行为。
方案实现讨论流程
“要实现不超卖,首先商品库存的扣减不能使用框架进行更新,因为框架是设置值,如果在这段时间,又有人购买了,则商品库存必然会出现问题。要采用手写SQL方式。并且sql中还要判断是否大于等于指定的购买量。”
UPDATE `SKU_Info` SET skuNum=skuNum-1000 WHERE id='00293cb7-d8cf-4470-a66d-bb45ca2b130000293cb7-d8cf-4470-a66d-bb45ca2b1300' AND skuNum>=1000;
“要实现不超卖,我们可以对方法加上同步锁,这样可以解决”。
“方法加上同步锁后,用户下单将会出现排队的情况,性能有问题。”
“那我们可以实现对同一商品进行加锁,这样可以解决购买不同商品不会相互阻塞。如果有包含关系,也应该加锁。比如A用户购买商品1和商品2,B用户购买商品1,因为他们都有商品1,则应该加锁。”
“你这个方案应该可以解决问题,采用分布式锁的方式可以解决,我们可以使用redis来做。”
“是的,确实可以解决问题,并且多个服务端也不存在问题了,就这么干。”
“我们可以对订单中的所有商品的sku值进行排序,拼接成一个skuId值,然后MD5的值作为key,其它订单进来方法时,按同样的操作进行检测是否正在下单,如果是,则等待。”
“你这种方案忽略了商品不同的情况,就比如上面的例子中,A购买商品1和商品2,B购买商品1,那么他们的key是不同的,因而达不到效果。”
“我们可以对每个商品sku的id定义个锁,这样每次购买时,我们针对每个商品进行检测,这样就可以了,绝对能够保证同步。”
“这种方法可行,不过还是存在一个问题,服务端与redis的连接次数会比较多,如果一个用户下单商品种类较多,那么仍然会比较慢,但这确实不失为一个好的方案。”
“既然这个方案仍然有可能有问题,那么还有没有其它的方案。”
“数据库本身是有锁的,可以实现锁同步的问题,那么有没有办法使用到数据库的锁来解决这个问题?”
“对呀,我们可以写SQL语句去循环扣减库存,最后判断数据库影响行数与商品种类是否匹配?如果不匹配,则是扣减失败,进行还原,如果匹配,则扣减成功!”
“经过测试,我们用的MySQL不支持这种方案,里面需要用到if判断,而if判断必须要在存储过程中才能使用。”
“那我们可以使用存储过程来做。代码如下”
DELIMITER $$ USE anke_skucenter$$ CREATE PROCEDURE minusSkuNum() BEGIN SET AUTOCOMMIT=0; START TRANSACTION; UPDATE SKU_Info SET skuNum=skuNum-100 WHERE id='0031394c-8058-49f5-9ba9-f971480ac2f2' AND skuNum>=100; IF(SELECT ROW_COUNT()<=0)THEN ROLLBACK; END IF; UPDATE `SKU_Info` SET skuNum=skuNum-1000 WHERE id='00293cb7-d8cf-4470-a66d-bb45ca2b130000293cb7-d8cf-4470-a66d-bb45ca2b1300' AND skuNum>=1000; IF(SELECT ROW_COUNT()<=0)THEN ROLLBACK; END IF; COMMIT; SET AUTOCOMMIT=1; END$$
“这个是初步的存储过程,仍然需要将update语句变更为循环,改变传入参数为商品id和数量,有谁会写?”
“额,目前大家都不会写,并且这个循环看上去也挺复杂的。”
“那么我们能不能在mybatis中获取多条更新语句的影响行数?”
“不能,没有任何框架支持,并且mysql本身就不支持,要不然也不会需要存储过程了。”
“既然多条SQL不行,能不能放到一条SQL中去做更新呢?”
“先baidu下”
“哈哈,找到了,我们查询的时候有时候回用到case when,那么我们更新的时候是否可以使用这个呢?尝试代码如下:”
update SKU_Info set skuNum=skuNum-(case when id='0031394c-8058-49f5-9ba9-f971480ac2f2' then 100 when id='00293cb7-d8cf-4470-a66d-bb45ca2b1300' then 1000 end) where (id='0031394c-8058-49f5-9ba9-f971480ac2f2' AND skuNum>=100) or (id='00293cb7-d8cf-4470-a66d-bb45ca2b1300' AND skuNum>=1000);
“经过测试,该段代码执行正常,并且能够正常返回需要的影响行数。”
“这个循环的SQL编写在mybatis中不难,那么判断最后扣减结果与商品种类不同时,如何进行补偿呢?”
“这个可以使用@Transactional,我们在方法上加此注解,在方法内部判断,如果不同,我们就抛出一个自定义异常,这样就会自动进行回滚了。”
“测试一下”
“经过几轮测试,确实可行,就这样做。”
“具体实施为:先生成订单,然后进行扣减,如果捕获到扣减失败的自定义异常,则对生成的订单执行删除标记。但存在一个问题,就是标记订单为删除状态失败的情况,这个订单仍然存在,也是超卖了。”
“可以调整下,改为先进行扣减,扣减成功再生成订单,这样可以避免此问题。”
“嗯,此方法可以解决超卖问题,可能会存在商品扣减成功,但订单未生成的情况。”
“这种问题会存在,但比超卖要好很多。”
“嗯嗯”
“嗯嗯”