首先环境介绍下:商城商品可能存在几个端(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,我们在方法上加此注解,在方法内部判断,如果不同,我们就抛出一个自定义异常,这样就会自动进行回滚了。”

“测试一下”

“经过几轮测试,确实可行,就这样做。”

“具体实施为:先生成订单,然后进行扣减,如果捕获到扣减失败的自定义异常,则对生成的订单执行删除标记。但存在一个问题,就是标记订单为删除状态失败的情况,这个订单仍然存在,也是超卖了。”

“可以调整下,改为先进行扣减,扣减成功再生成订单,这样可以避免此问题。”

“嗯,此方法可以解决超卖问题,可能会存在商品扣减成功,但订单未生成的情况。”

“这种问题会存在,但比超卖要好很多。”