商品超卖问题

12/12/2015来源:Java教程人气:1577

背景

在公司里面我负责的是积分商城一块,里面的积分商品也跟其它商品一样,超卖是绝对不可以的。。。。


刚接手到积分商城

我刚来的时候,积分商城已经有了自家优惠券的功能,整个商城就2件商品:满5减1+满10减2.
我要做的第一个功能就是添加新的功能:第三方优惠券(其实就是跟我饿了么什么的一样啦)。自家的优惠券是通过模版生成的,给用户一个兑换码;第三方优惠券是从数据库里拿的(第三方给的),同样是给用户一个兑换码。
具体实现的思路就是:找到这个商品,然后通过这个商品的某个字段来找到对应的兑换码,这里的兑换码会有很多,那么给他一张就行了。发兑换码肯定是分为2步的:1.从库里面取一张,2.将取出的这一张状态标记为已使用。这里会出现一个问题,并发的情况下,也就是1.2步之间如果打个断点,那么2个人会得到相同的一张兑换码,而且第二次update的时候是对数据库没有影响的。
这里的解决方法就有很多了:

1.sql不怕累的:来个存储过程;
2.java不怕累的:根据update返回的结果是1还是0(有没有有效的update),来各种判断各种处理;
3.不怕慢的:把2个操作放在同一个事务里面,,其实我最早的想法就是这样的,因为以前做项目没啥效率要求,我也一直觉得这样很好。但是我们公司的读写是分离的,从代码里面就能看到读的slave是不带事务的,至于他们使用的是否为同一个数据库,还得看生产环境上怎么配。当时我就想把这里读的那个也换成master来,然后加个事务,这样基本就可以了。但是带我的那个非常不满意,表示如果把读加上了事务会非常的慢,所以被否定了。。。现在想想也是有些道理,这个能否实现还得看事务隔离级别的设置,而隔离级别要是满足了说不定运行起来还真的阻塞的哭了。。
4.最终的实现方案:我们最后用的是redis,所有的优惠券统一从redis中取,如果redis中没有,那就一次性从数据库中拿出5条来。使用了这个方法,让我对redis的了解多了不少。后面商品修改的时候,由于redis中的缓存也让我吃了不少苦---就是找不到问题所在,最后发现原来问题出在缓存没清。。。

小结

第三方优惠券的处理方案:把优惠券放倒缓存中,然后统一从缓存中取。


后来商城的抢购活动

第三方提供了10件衬衫,要求我们搞个抢购。
当时给的时间也不多,也没准备改啥代码,就插了10条数据到第三方兑换券的数据库,把衬衫当做第三方优惠券来看,然后最后哪10个人抢到了再从兑换记录里面找。
之前用redis处理了并发的问题,所以对于这个抢购还是比较自信的。。最终也是没有出现问题。


v3.0要求添加抽奖功能

也就是后天要上线的新功能,这两天可把我给忙坏了;当时听到要添加抽奖功能的消息时,可把我给高兴坏了,可有好玩的东西写了。
当时脑袋里浮现出来的就是各种贵重物品抽抽抽,产品也说到时候抽iphone啥的。
这个可千万要小心,抽奖这事可别抽1个iphone搞出10个同时中奖来。。。
我的想法时这样的:

第三方券能够通过实实在在存在的券来控制,而其它的东西,没有实际存在的券,该怎么实现并发不出问题呢?
第一个想法还是用缓存,这次把商品的剩余数量给放到redis中来,把分布式的并发交给redis来处理,每次get and 减一 嘛。
这个功能redis里好像是有,但是在sPRing-data的redis封装里面我没找到对应的方法。

于是我就想去找找以前的自家优惠券是怎么写的,怎么控制不超买的,因为这个通过模版生成的东西也是通过商品的剩余数量来控制的,也并没有实际存在的券。
代码看完了,也没发现哪里控制了并发,更没发现哪里用到了缓存。
喜出望外,为啥?因为我发现了带我的人代码的问题,终于可以修复一下他的代码来展现一下自己了。
于是我去网上搜有关 抢购超卖 的处理方案,基本上就是

1.缓存,就是我上面说的
2.消息队列,应该就是序列化吧,别抢,都给我排队去
3.存储过程,我都懒得看。因为我知道我看明白了我也懒得写那么长的sql的
4.update table set a=a-1 where a>=1 and id =x ,行锁

这里我选择了第4中方法,因为带我的人说,不需要用缓存,使用sql就能完成,有行锁。
网上的说法是,请求量很大的时候行锁会使数据库压力非常大。我也觉得很有道理,不过想想我们积分商城的访问量,就先用这个简单的把,哪天访问量吃不消的,让我改成缓存我也乐意。


一顿改

翻了一下原来的代码,发现并发是肯定会出问题的,为此我还测试了下,打了个断点,的确出问题了。
之前的sql是这样写的:

update table set a=a-1 where id =x

然后剩余数量这字段还是unsigned的,所以报了out of range的错,这明显就是要超卖,而数据库缺报错了,不过2个用户都提示兑换成功了。。
sql改了后,还是有问题,为啥,因为返回值没用上啊。
之前的剩余数量可以说就是用来标记一下还有多少个,用来平常用用的。
现在我把它提到了方法的第一行,判断返回值是否为0,多了一个处理并发的功能。

小结

简单处理并发:

update table set a=a-1 where a>=1 and id =x
根据返回结果来看,如果为0,表示卖完了。不为0再继续下面的操作