订单业务中的重要问题:超卖问题的解决方案
我在做过的一些项目中都涉及到了订单的业务,如果你的项目中有关于订单的业务模块,那肯定说明你的项目中有卖商品的功能,所以有买卖场景就面临一个很常见的一个问题,那就是超卖问题,下面我就整理一下我在做项目的时候使用的一种很好用的解决方案来避免出现超卖问题。
什么是超卖问题,以及超卖问题是如何产生的?
超卖问题,通俗的来说就是我们商家只有100件库存但是卖出去了100+件商品,出现了多买的问题。
出现的场景:举个简单的例子:比如此时只剩下了最后一个商品,有两个人在抢这最后一个商品,此时A进行到了下单的页面,但是网速很慢卡在了这里,此时库存的数量还是没有扣除的一直在转圈圈,但是此时另一个人的网速快已经下单并且支付成功了,此时数据库中的库存数量已经减一变成了0,突然此时A结束转圈了去支付了,也下单了,那么库存就变成了-1,这只是有两个人在同时抢,那么你想想如果有10w人在一起抢呢?这就是典型的超卖问题的出现场景
方案:类似于乐观锁的Redis事务方案
我们知道MySQL数据库中有一个乐观锁机制,乐观锁使用了版本号的原理来进行判断,在进行更新操作之前会去比较此时的版本号和刚开始操作的时候的版本号是不是一致,如果是一致的那么就可以让这个更新操作正常执行。如果此时的版本号不一致了,那么就无法进行更新操作了。拿上面的例子来说,A因为网速慢所以后加载出来购买的页面,那么当A去提交事务的时候去判断版本号发现此时的版本号和一开始的版本号其实是不一致的,就不让这个事务执行,对应的结果就是A无法买到这个商品,所以也就不会超卖了。那么可能会有人说了,我们为什么不直接使用MySQL的这个乐观锁的机制呢?
首先,如果我们要使用这个MySQL的乐观锁机制我们需要对我们之前设计的表进行更改,我们可以需要增加字段用来作为版本号判断,而且可能有很多的表都会涉及到这个机制,所以我们就要修改更多的数据库。你想一下在实际的项目中,你们的代码都写了上万行了,突然因为一个方案去重新设计数据库肯定是不合适的。
如果你们的项目和我这个项目一样使用到了redis来作为缓存的话,那么可以使用我的这个方案,那就是将订单的信息存到redis中,利用redis的事务机制来解决超卖问题。 注意:redis的事务和MySQL的事务可不是一个东东
具体来说就是这样的:
-
我们将创建的订单的信息放到redis中缓存起来
-
利用redis的事务机制,下面附上代码,是基于spring boot的项目
redisTemplate.execute(new SessionCallback() { //重写里面的execute方法 @Override public Object execute(RedisOperations operations) throws DataAccessException { //这里这个key的版本号来进行乐观锁机制 operations.watch(你要监听的key,也就是你生成订单放到redis中的key); //标志着开启一个事务 operations.multi(); //下面只是一个伪代码,举个例子 //更新你这个订单被谁买了 redisTemplate.opsForValue().set(key,value); //抢单成功之后将这个订单删除,防止被别人买到 redisTemplate.delete(key); //类型于MySQL中的提交事务 return operations.exec(); } });
-
在上面的代码中,我们将我们要原子操作的命令放到operations.multi();和 return operations.exec();之间,类比于MySQL中就是将一个原子操作放到一个事务中,最后将事务提交,这样redis在做这个操作的时候,watch的这个key就类似于MySQL乐观锁中用来作为版本号的字段,如果有多个任务来进行操作的话会根据这个key来判断是不是应该去执行,只有这个key的版本一致的时候才会去执行成功。否则是执行异常的。
-
最后还要多说一句,如果你使用了上述的这个方案的话,那么redis的可靠性要得到保证,毕竟你的订单信息存在了redis中,如果宕机了没有好的处理方案的话,那你的订单信息可就都没了,。所以推荐你将redis的持久化策略调成aof模式,如果有条件的话还可以使用主备模式来保证高可用性。
上面的这个方案,简单可行,但是前提是你的项目用到了redis来存你的订单信息