库存,订单,积分的分布式事务
一个订单支付之后,我们需要做下面的步骤:
-
更改订单的状态为“已支付”
-
扣减商品库存
-
给会员增加积分
-
创建销售出库单通知仓库发货
减库存的业务实现
减库存可以采用同步调用(Feign的方式),也可以采用异步调用(RabbitMQ传递消息),我们这里采用同步调用,接下来我们分析为什么
如果我们采用异步调用的方式,减库存的这条消息发送到MQ就不管了,那么到底库存减成功了没有呢?这我们并不知道,如果库存不足,那么我们减库存失败,但是service的业务不会回滚,这个问题就是分布式事务问题,即跨服务的事务。减库存这个业务从订单微服务跨越到了商品微服务,而事务是由Spring来管理的,两套tomcat两套Spring,本身没有任何关联,但是却是一个事务,如果采用异步,这边的微服务执行失败另一边的微服务并不知道,破坏了事务的一致性,我们解决的方案是什么呢?
变异步调用为同步调用,如果一个微服务执行失败就会抛出异常,事务自然回滚(减库存的操作只能放在创建订单业务的最后,因为减库存执行失败事务自然回滚订单也不会创建成功,但是如果上来就先减库存,那玩意订单创建失败库存无法回滚),但是这种方案也不是最优的,因为我们没做优惠券功能,当我们做了优惠券功能,那计算优惠和减库存哪个放在最后呢?哪个放在最后都不可行,这时候就必须解决分布式事务问题了
解决分布式事务问题:
2PC(两阶段提交):
第一阶段,事务开始执行发送一条消息给相关的微服务告诉它们这个业务要开始执行,执行完毕后返回一条消息,告诉这个微服务业务执行成功了没有;
第二阶,如果上一阶段返回的消息是执行成功,那么再发送一条消息告诉所有微服务事务执行成功了,相关所有事务都可以提交了,如果第一阶段有一个微服务执行失败,则所有事务都回滚
缺点:实现复杂、事务执行过程数据锁定的范围太大了,在本业务未执行完毕之前,数据库相关的表都是锁定状态,因此这种处理方式性能较差,在高并发的业务中较少使用。
TCC(try-confirm-cancel):这种处理方式的前提是面对事务都要有一套确认事务执行的业务,一套取消执行的业务(即补偿业务)。比如说减库存这个业务,确认事务就是减库存,补偿事务就是加库存。这种处理方式时所有业务都开始执行,互相不等待,完成了就提交,解决了两阶段提交问题中数据大面积锁定的情况,但是如果业务A已经提交了,但是业务B失败了,没关系,会调用所有的补偿事务,这种解决方案不是靠事务回滚的方式,靠的是事务的补偿
缺点:解决了业务问题,但是使得业务变得复杂了,写一个业务必须写一个确定执行业务方法和一个补偿业务方法,除此之外还要考虑补偿方案的失败问题,当补偿方案也执行失败了呢,这时候就要考虑重试问题、人工介入问题
异步确保:执行时发送一条消息,另一方接受消息,如果执行不成功会一直重试,直到成功
缺点:事务无法回滚,不合适减库存这个业务
2PC+MQ:两阶段提交方式结合异步确保
综上,在电商行业中适用的还是TCC,虽然业务变得复杂了,但是行之有效;如果是转账业务,适合异步确保,转账业务只需要消息可靠就可以,执行时间晚一点也无妨,所以异步确保的关键点是消息的可靠
但是在我们这个小项目中,无需把业务变得这么复杂,接下来讨论我们采用的同步调用的解决方案。
同步调用中加锁实现方式:
先查询库存,然后if判断,库存足够就减库存
逻辑是对的,但是这么做有线程上的安全问题,当线程很多的时候,有可能引发超卖问题
加锁:synchronized
性能太差了,只有一个线程可以执行,当搭了集群时synchronized只锁住了当前一个tomcat,看起来是可行的,但是在分布式系统下是不安全的
分布式锁:zookeeper
zookeeper是树结构,它利用节点的唯一性来实现,加了分布式锁以后,任何一个逻辑进入到减库存这个地方,都会创建一个节点,创建成功就认为得到了锁,继续执行代码;反之则失败,返回或者wait,因此只有一个人可以拿到这个锁,执行完毕后删除节点释放锁,其他人可以再次创建锁
zookeeper可以创建临时节点,当服务器宕机或者断开连接,会自动删除节点,自动释放锁
缺点:实现起来太复杂
Redis:SETNX命令
原理类似于上述的 节点 ,只能set不存在的key,如果不存在则创建;如果存在它会set失败,并返回0,拿到锁以后可以使用del命令释放锁
缺点是存在搜索问题,假如SETNX成功,成功之后开始执行代码,但是此时服务器宕机,那del释放锁的命令一直没有执行,相当于这个锁一直被拿着,那么这个值将无法再被set成功
但是这里不推荐加锁实现,因为用了锁,就变成单线程了,相当于一执行这段代码就把数据库锁死,同一时刻只能有一个人来操作,这样的实现类似于悲观锁,默认线程安全问题一定会发生,在面对高并发时,往往性能很差。
那既然不推荐悲观锁,是不是可以采用乐观锁呢?乐观锁是默认线程安全问题不会发生,不加锁,但是不加锁会有线程安全问题,那怎么处理这件事情呢?
——我们不做查询不做判断,业务执行到减库存代码这里之后直接开始减库存,唉?这不是会超卖吗?不要紧,我们的sql内部可以加条件来判断,失败则事务回滚,所有人不论怎么操作,最后都会来操作数据库,但是数据库写了判断语句来判断库存,每个人来执行都会被判断,本质上还是乐观锁。如果执行失败会反馈失败信息,而不像是悲观锁那样线程阻塞,导致一直等待,性能上来将,这种处理方式优于加锁,我们的sql语句如下:
"UPDATE tb_stock SET stock = stock - #{num} WHERE sku_id = #{id} AND stock >= #{num}"
高并发场景库存如何计算
https://blog.csdn.net/u012946310/article/details/82878650
https://www.cnblogs.com/shoshana-kong/p/10889998.html
https://blog.csdn.net/sinat_38570489/article/details/91363054