文章目录

  • 一、本地事务
    • 1、事务的基本性质
    • 2、事务的隔离级别
    • 3、spring事务的传播行为
    • 4、SpringBoot事务关键点
      • 4.1、事务的自动配置
      • 4.2、事务的坑
    • 5、本地事务问题
  • 二、分布式事务
    • 1、为什么会有分布式事务
    • 2、分布式事务
    • 3、分布式理论
      • 1、CAP理论
      • 2、BASE理论
    • 4、分布式事务几种方案
      • 1、2PC提交(刚性事务)
      • 2、3PC提交(刚性事务)
      • 3、柔性事务-TCC事务补偿型方案#
      • 4、柔性事务-最大努力通知型方案
      • 5、柔性事务-可靠消息+最终一致性方案(异步确保型)
  • 三、分布式事务-Seata 控制分布式事务
    • 1、Seata概念
    • 2、Seata使用
      • 1、创建 UNDO_LOG 表
      • 2、安装事务协调器
      • 3、整合
      • 4、数据源代理
      • 5、微服务导入配置文件
      • 6、启动测试分布式事务
      • 小结:
  • 四、分布式事务-最终一致性库存解锁逻辑
    • 1、Seata的不足
    • 2、高并发场景

一、本地事务

1、事务的基本性质

数据库事务的几个特性:原子性(Atomicity)、一致性(Consistency)、隔离性或独立性(isolation)、持久性(Durability),简称就是 ACID。

  • 原子性:一系列的操作整体不可拆分,要么同时成功,要么同时失败。
  • 一致性:数据在事务的前后,业务整体一致。
    • 转账:A:1000; B:1000; 转 200 事务成功; A:800; B:1200
  • 隔离性:事务之间互相隔离。
  • 持久性:一旦事务成功,数据一定会落盘在数据库。

在以往的单体应用中,我们多个业务操作使用同一条连接操作不同的数据表,一旦有异常,我们可以很容易的整体回滚。

Business:我们具体的业务代码
Storage:库存业务代码,扣减库存
Order:订单业务代码,保存订单
Account:账号业务代码,减账户余额
比如买东西业务,扣库存,下订单,账户扣款,是一个整体,必须同时成功或失败。
本地事务的适用非常简单,在方法上加上@Transactional 注解就可以了。

2、事务的隔离级别

数据库事务的隔离级别有4种,由低到高分别为Read uncommitted 、Read committed 、Repeatable read 、Serializable 。而且,在事务的并发操作中可能会出现脏读,不可重复读,幻读。下面通过事例一一阐述它们的概念与联系。

Read uncommitted
读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。
事例:老板要给程序员发工资,程序员的工资是3.6万/月。但是发工资时老板不小心按错了数字,按成3.9万/月,该钱已经打到程序员的户口,但是事务还没有提交,就在这时,程序员去查看自己这个月的工资,发现比往常多了3千元,以为涨工资了非常高兴。但是老板及时发现了不对,马上回滚差点就提交了的事务,将数字改成3.6万再提交。
分析:实际程序员这个月的工资还是3.6万,但是程序员看到的是3.9万。他看到的是老板还没提交事务时的数据。这就是脏读

那怎么解决脏读呢?Read committed!读提交,能解决脏读问题。

Read committed
读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。
事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时(程序员事务开启),收费系统事先检测到他的卡里有3.6万,就在这个时候!!程序员的妻子要把钱全部转出充当家用,并提交。当收费系统准备扣款时,再检测卡里的金额,发现已经没钱了(第二次检测金额当然要等待妻子转出金额事务提交完)。程序员就会很郁闷,明明卡里是有钱的…
分析:这就是读提交,若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但在这个事例中,出现了一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读

那怎么解决可能的不可重复读问题?Repeatable read !

Repeatable read
重复读,就是在开始读取数据(事务开启)时,不再允许修改操作
事例:程序员拿着信用卡去享受生活(卡里当然是只有3.6万),当他埋单时(事务开启,不允许其他事务的UPDATE修改操作),收费系统事先检测到他的卡里有3.6万。这个时候他的妻子不能转出金额了。接下来收费系统就可以扣款了。
分析:重复读可以解决不可重复读问题。写到这里,应该明白的一点就是,不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作

什么时候会出现幻读?
事例:程序员某一天去消费,花了2千元,然后他的妻子去查看他今天的消费记录(全表扫描FTS,妻子事务开启),看到确实是花了2千元,就在这个时候,程序员花了1万买了一部电脑,即新增INSERT了一条消费记录,并提交。当妻子打印程序员的消费记录清单时(妻子事务提交),发现花了1.2万元,似乎出现了幻觉,这就是幻读。

那怎么解决幻读问题?Serializable!

Serializable 序列化
Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。

值得一提的是:大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。Mysql的默认隔离级别是Repeatable read。

3、spring事务的传播行为

什么叫事务传播行为?听起来挺高端的,其实很简单。
即然是传播,那么至少有两个东西,才可以发生传播。单体不存在传播这个行为。
事务传播行为(propagation behavior)指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
例如:methodA事务方法调用methodB事务方法时,methodB是继续在调用者methodA的事务中运行呢,还是为自己开启一个新事务运行,这就是由methodB的事务传播行为决定的。

Spring定义了七种传播行为:

现在来看看传播行为
PROPAGATION_REQUIRED
如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
可以把事务想像成一个胶囊,在这个场景下方法B用的是方法A产生的胶囊(事务)。

举例有两个方法:

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {methodB();
// do something
}@Transactional(propagation = Propagation.REQUIRED)
public void methodB() {// do something
}

单独调用methodB方法时,因为当前上下文不存在事务,所以会开启一个新的事务。
调用methodA方法时,因为当前上下文不存在事务,所以会开启一个新的事务。当执行到methodB时,methodB发现当前上下文有事务,因此就加入到当前事务中来。

PROPAGATION_SUPPORTS
如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。
举例有两个方法:

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {methodB();
// do something
}// 事务属性为SUPPORTS
@Transactional(propagation = Propagation.SUPPORTS)
public void methodB() {// do something
}

单纯的调用methodB时,methodB方法是非事务的执行的。当调用methdA时,methodB则加入了methodA的事务中,事务地执行。

PROPAGATION_MANDATORY
如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {methodB();
// do something
}// 事务属性为MANDATORY
@Transactional(propagation = Propagation.MANDATORY)
public void methodB() {// do something
}

当单独调用methodB时,因为当前没有一个活动的事务,则会抛出异常throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);当调用methodA时,methodB则加入到methodA的事务中,事务地执行。

PROPAGATION_REQUIRES_NEW

使用PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager作为事务管理器。
它会开启一个新的事务。如果一个事务已经存在,则先将这个存在的事务挂起。

@Transactional(propagation = Propagation.REQUIRED)
public void methodA() {
doSomeThingA();
methodB();
doSomeThingB();
// do something else
}// 事务属性为REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {// do something
}

当调用

main{
methodA();
}

相当于调用

main(){TransactionManager tm = null;try{//获得一个JTA事务管理器tm = getTransactionManager();tm.begin();//开启一个新的事务Transaction ts1 = tm.getTransaction();doSomeThing();tm.suspend();//挂起当前事务try{tm.begin();//重新开启第二个事务Transaction ts2 = tm.getTransaction();methodB();ts2.commit();//提交第二个事务} Catch(RunTimeException ex) {ts2.rollback();//回滚第二个事务} finally {//释放资源}//methodB执行完后,恢复第一个事务tm.resume(ts1);doSomeThingB();ts1.commit();//提交第一个事务} catch(RunTimeException ex) {ts1.rollback();//回滚第一个事务} finally {//释放资源}
}

在这里,我把ts1称为外层事务,ts2称为内层事务。从上面的代码可以看出,ts2与ts1是两个独立的事务,互不相干。Ts2是否成功并不依赖于 ts1。如果methodA方法在调用methodB方法后的doSomeThingB方法失败了,而methodB方法所做的结果依然被提交。而除了 methodB之外的其它代码导致的结果却被回滚了

PROPAGATION_NOT_SUPPORTED
PROPAGATION_NOT_SUPPORTED 总是非事务地执行,并挂起任何存在的事务。使用PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager作为事务管理器。

PROPAGATION_NEVER
总是非事务地执行,如果存在一个活动事务,则抛出异常。

PROPAGATION_NESTED

如果一个活动的事务存在,则运行在一个嵌套的事务中。 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行。
这是一个嵌套事务,使用JDBC 3.0驱动时,仅仅支持DataSourceTransactionManager作为事务管理器。
需要JDBC 驱动的java.sql.Savepoint类。使用PROPAGATION_NESTED,还需要把PlatformTransactionManager的nestedTransactionAllowed属性设为true(属性值默认为false)。
这里关键是嵌套执行。

@Transactional(propagation = Propagation.REQUIRED)
methodA(){doSomeThingA();methodB();doSomeThingB();
}@Transactional(propagation = Propagation.NEWSTED)
methodB(){……
}

如果单独调用methodB方法,则按REQUIRED属性执行。如果调用methodA方法,相当于下面的效果:

main(){Connection con = null;Savepoint savepoint = null;try{con = getConnection();con.setAutoCommit(false);doSomeThingA();savepoint = con2.setSavepoint();try{methodB();} catch(RuntimeException ex) {con.rollback(savepoint);} finally {//释放资源}doSomeThingB();con.commit();} catch(RuntimeException ex) {con.rollback();} finally {//释放资源}
}

当methodB方法调用之前,调用setSavepoint方法,保存当前的状态到savepoint。如果methodB方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括methodB方法的所有操作。嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。

PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别:
它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。
使用 PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。

使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager使用savepoint支持PROPAGATION_NESTED时,需要JDBC 3.0以上驱动及1.4以上的JDK版本支持。其它的JTATrasactionManager实现可能有不同的支持方式。

PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。

另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。

由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back。

4、SpringBoot事务关键点

4.1、事务的自动配置

TransactionAutoConfiguration

4.2、事务的坑

在同一个类里面,编写两个方法,内部调用的时候,会导致事务设置失效。原因是没有用到代理对象的缘故。

起因
考虑如下一个例子:

@Target (value = {ElementType.METHOD})
@Retention (RetentionPolicy.RUNTIME)
@Documented public @interface MyMonitor {}@Component
@Aspect
public class MyAopAdviseDefine {private Logger logger = LoggerFactory.getLogger(getClass());@Pointcut ( "@annotation(com.xys.demo4.MyMonitor)" )public void pointcut () {}// 定义 advise @Before ( "pointcut()" )public void logMethodInvokeParam (JoinPoint joinPoint) {logger.info( "---Before method {} invoke, param: {}---" , joinPoint.getSignature().toShortString(), joinPoint.getArgs());}
}@Service
public class SomeService {private Logger logger = LoggerFactory.getLogger(getClass());public void hello (String someParam) {logger.info( "---SomeService: hello invoked, param: {}---" , someParam);test();}@MyMonitor public void test () {logger.info( "---SomeService: test invoked---" );}
}@EnableAspectJAutoProxy (proxyTargetClass = true )
@SpringBootAppliMyion
public class MyAopDemo {@AutowiredSomeService someService;public static void main (String[] args) {SpringAppliMyion.run(MyAopDemo.class, args);}@PostConstruct public void aopTest () {someService.hello( "abc" );}
}

在这个例子中,我们定义了一个注解 MyMonitor,这个是一个方法注解, 我们的期望是当有此注解的方法被调用时,需要执行指定的切面逻辑,即执行 MyAopAdviseDefine.logMethodInvokeParam 方法。

在 SomeService 类中,方法 test() 被 MyMonitor 所注解,因此调用 test() 方法时,应该会触发 logMethodInvokeParam 方法的调用. 不过有一点我们需要注意到,我们在 MyAopDemo 测试例子中,并没有直接调用 SomeService.test() 方法,而是调用了 SomeService.hello() 方法,在 hello 方法中,调用了同一个类内部的 SomeService.test() 方法。 按理说, test() 方法被调用时, 会触发 AOP 逻辑, 但是在这个例子中, 我们并没有如愿地看到 MyAopAdviseDefine.logMethodInvokeParam 方法的调用, 这是为什么呢?

这是由于 Spring AOP (包括动态代理和 CGLIB 的 AOP) 的限制导致的。Spring AOP 并不是扩展了一个类(目标对象), 而是使用了一个代理对象来包装目标对象,并拦截目标对象的方法调用。 这样的实现带来的影响是: 在目标对象中调用自己类内部实现的方法时,这些调用并不会转发到代理对象中,甚至代理对象都不知道有此调用的存在。

即考虑到上面的代码中, 我们在 MyAopDemo.aopTest() 中, 调用了 someService.hello("abc") , 这里的 someService bean 其实是 Spring AOP 所自动实例化的一个代理对象, 当调用 hello() 方法时, 先进入到此代理对象的同名方法中, 然后在代理对象中执行 AOP 逻辑(因为 hello 方法并没有注入 AOP 横切逻辑, 因此调用它不会有额外的事情发生), 当代理对象中执行完毕横切逻辑后, 才将调用请求转发到目标对象的 hello() 方法上。因此当代码执行到 hello() 方法内部时, 此时的 this 其实就不是代理对象了, 而是目标对象, 因此再调用 SomeService.test() 自然就没有 AOP 效果了。

解决
弄懂了上面的分析, 那么解决这个问题就十分简单了. 既然 test() 方法调用没有触发 AOP 逻辑的原因是因为我们以目标对象的身份(target object) 来调用的, 那么解决的关键自然就是以代理对象(proxied object)的身份来调用 test() 方法.因此针对于上面的例子, 我们进行如下修改即可:

@Service
public class SomeService {private Logger logger = LoggerFactory.getLogger(getClass());@Autowired private SomeService self;public void hello (String someParam) {logger.info( "---SomeService: hello invoked, param: {}---" , someParam);self.test();}@CatMonitor public void test () {logger.info( "---SomeService: test invoked---" );}
}

上面展示的代码中, 我们使用了一种很 subtle 的方式, 即将 SomeService bean 注入到 self 字段中(这里再次强调的是, SomeService bean 实际上是一个代理对象, 它和 this 引用所指向的对象并不是同一个对象), 因此我们在 hello 方法调用中, 使用 self.test() 的方式来调用 test() 方法, 这样就会触发 AOP 逻辑了。

Spring AOP 导致的 @Transactional 不生效的问题
这个问题同样地会影响到 @Transactional 注解的使用, 因为 @Transactional 注解本质上也是由 AOP 所实现的.
例如我在 stackoverflow 上看到的一个类似的问题: Spring @Transaction method call by the method within the same class, does not work?
这里也记录下来以作参考.
那个哥们遇到的问题如下:

public class UserService {@Transactional public boolean addUser (String userName, String password) {try {// call DAO layer and adds to database.} catch (Throwable e) {TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}}public boolean addUsers (List<User> users) {for (User user : users) {addUser(user.getUserName, user.getPassword);}}
}

他在 addUser 方法上使用 @Transactional 来使用事务功能, 然后他在外部服务中, 通过调用 addUsers 方法批量添加用户. 经过了上面的分析后, 现在我们就可知道其实这里添加注解是不会启动事务功能的, 因为 AOP 逻辑整个都没生效嘛.
解决这个问题的方法有两个, 一个是使用 AspectJ 模式的事务实现:

<tx:annotation-drivenmode="aspectj"/>

另一个就是和我们刚才在上面的例子中的解决方式一样:

public class UserService {private UserService self;public void setSelf (UserService self) {this .self = self;}@Transactional public boolean addUser (String userName, String password) {try {// call DAO layer and adds to database.} catch (Throwable e) {TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}}public boolean addUsers (List<User> users) {for (User user : users) {self.addUser(user.getUserName, user.getPassword);}}
}

5、本地事务问题

在提交订单方法上,我们加了本地事务 @Transactional,当订单创建失败时,会自动回滚相关的数据,但是该方法包含了远程调用锁库存等服务,如何回滚远程的锁库存数据是一个问题;还有一个问题,当远程锁库存成功,但是由于网络等问题,响应超时了,这时还以为锁库存失败了,订单就会自动回滚,这就会出现一个很严重的问题,订单回滚了,而锁定的库存没有回滚。

综上:
1、远程服务假失败:远程服务其实成功了,由于网络故障灯没有返回
导致:订单回滚,库存却扣减

2、远程服务执行完成,下面的其它方法出现问题
导致:已执行的远程请求,肯定不能回滚。

小结:@Transactional 是本地事务,在分布式系统,只能控制住自己的回滚,控制不了其它服务的回滚,所以,是有局限性的,如果方法里边需要调用其它服务的操作,则要使用分布式事务。

二、分布式事务

1、为什么会有分布式事务

分布式系统经常出现的异常,如机器宕机、网络异常、消息丢失、数据错误、不可靠的TCP、存储数据丢失等等。

2、分布式事务

分布式事务是指事务的参与者,支持事务的服务器,资源服务器分别位于分布式系统的不同节点之上,通常一个分布式事物中会涉及到对多个数据源或业务系统的操作。

典型的分布式事务场景:跨银行转操作就涉及调用两个异地银行服务

3、分布式理论

1、CAP理论

CAP理论:一个分布式系统不可能同时满足一致性,可用性和分区容错性这个三个基本需求,最多只能同时满足其中两项
**一致性(Consistency):**数据在多个副本之间是否能够保持一致的特性。
**可用性(Avaliability):**是指系统提供的服务必须一致处于可用状态,对于每一个用户的请求总是在有限的时间内返回结果,超过时间就认为系统是不可用的。
**分区容错性(Partition tolerance):**分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非整个网络环境都发生故障。

CAP定理的应用
放弃P(CA):如果希望能够避免系统出现分区容错性问题,一种较为简单的做法就是将所有的数据(或者是与事物先相关的数据)都放在一个分布式节点上(属于本地都放一个系统),这样虽然无法保证100%系统不会出错,但至少不会碰到由于网络分区带来的负面影响
放弃A(CP):其做法是一旦系统遇到网络分区或其他故障时,那受到影响的服务需要等待一定的时间,应用等待期间系统无法对外提供正常的服务,即不可用
放弃C(AP):这里说的放弃一致性,并不是完全不需要数据一致性,是指放弃数据的强一致性,保留数据的最终一致性。

CP面临的问题:
对于多数大型互联网应用的场景,主机众多,部署分散,而且现在集群的规模越来越大,所以,节点故障、网络故障是常态,而且要保证服务可用性达到 99.999999%(N个9),即保证P和A,舍弃C。

分布式系统中实现一致性的 raft 算法:
1、Raft基础知识
Raft集群包含多个服务器,5个服务器是比较典型的,允许系统容忍两个故障。在任何给定时间,每个服务器都处于以下三种状态之一,领导者(Leader),追随者(Follower)或候选人(Candidate)。 这几个状态见可以相互转换。
Leader:处理所有客户端交互,日志复制等,一般一次只有一个Leader
Follower:类似选民,完全被动
Candidate:可以被选为一个新的领导人

每个节点上都有一个倒计时器 (Election Timeout),时间随机在 150ms 到 300ms 之间。有几种情况会重设 Timeout:
1)、收到选举的请求
2)、收到 Leader 的 Heartbeat

在 Raft 运行过程中,最主要进行两个活动:

1)、选主 Leader Election
2)、复制日志 Log Replication

2、选主 Leader Election
2.1 正常情况下选主

假设现在有如图5个节点,5个节点一开始的状态都是 Follower。

在一个节点倒计时结束 (Timeout) 后,这个节点的状态变成 Candidate 开始选举,它给其他几个节点发送选举请求 (RequestVote)

其他四个节点都返回成功,这个节点的状态由 Candidate 变成了 Leader,并在每个一小段时间后,就给所有的 Follower 发送一个 Heartbeat 以保持所有节点的状态,Follower 收到 Leader 的 Heartbeat 后重设 Timeout。

这是最简单的选主情况,只要有超过一半的节点投支持票了,Candidate 才会被选举为 Leader,5个节点的情况下,3个节点 (包括 Candidate 本身) 投了支持就行。

2.2 Leader 出故障情况下的选主

一开始已经有一个 Leader,所有节点正常运行。

Leader 出故障挂掉了,其他四个 Follower 将进行重新选主。



4个节点的选主过程和5个节点的类似,在选出一个新的 Leader 后,原来的 Leader 恢复了又重新加入了,这个时候怎么处理?在 Raft 里,第几轮选举是有记录的,重新加入的 Leader 是第一轮选举 (Term 1) 选出来的,而现在的 Leader 则是 Term 2,所有原来的 Leader 会自觉降级为 Follower

2.3 多个 Candidate 情况下的选主

假设一开始有4个节点,都还是 Follower。

有两个 Follower 同时 Timeout,都变成了 Candidate 开始选举,分别给一个 Follower 发送了投票请求。

两个 Follower 分别返回了ok,这时两个 Candidate 都只有2票,要3票才能被选成 Leader。

两个 Candidate 会分别给另外一个还没有给自己投票的 Follower 发送投票请求。

但是因为 Follower 在这一轮选举中,都已经投完票了,所以都拒绝了他们的请求。所以在 Term 2 没有 Leader 被选出来。

这时,两个节点的状态是 Candidate,两个是 Follower,但是他们的倒计时器仍然在运行,最先 Timeout 的那个节点会进行发起新一轮 Term 3 的投票。

两个 Follower 在 Term 3 还没投过票,所以返回 OK,这时 Candidate 一共有三票,被选为了 Leader。


如果 Leader Heartbeat 的时间晚于另外一个 Candidate timeout 的时间,另外一个 Candidate 仍然会发送选举请求。

两个 Follower 已经投完票了,拒绝了这个 Candidate 的投票请求。

Leader 进行 Heartbeat, Candidate 收到后状态自动转为 Follower,完成选主。
以上是 Raft 最重要活动之一选主的介绍,以及在不同情况下如何进行选主。

3、 复制日志 Log Replication
3.1 正常情况下复制日志
Raft 在实际应用场景中的一致性更多的是体现在不同节点之间的数据一致性,客户端发送请求到任何一个节点都能收到一致的返回,当一个节点出故障后,其他节点仍然能以已有的数据正常进行。在选主之后的复制日志就是为了达到这个目的。

一开始,Leader 和 两个 Follower 都没有任何数据。

客户端发送请求给 Leader,储存数据 “sally”,Leader 先将数据写在本地日志,这时候数据还是 Uncommitted (还没最终确认,红色表示)

Leader 给两个 Follower 发送 AppendEntries 请求,数据在 Follower 上没有冲突,则将数据暂时写在本地日志,Follower 的数据也还是 Uncommitted。

Follower 将数据写到本地后,返回 OK。Leader 收到后成功返回,只要收到的成功的返回数量超过半数 (包含Leader),Leader 将数据 “sally” 的状态改成 Committed。( 这个时候 Leader 就可以返回给客户端了)

Leader 再次给 Follower 发送 AppendEntries 请求,收到请求后,Follower 将本地日志里 Uncommitted 数据改成 Committed。这样就完成了一整个复制日志的过程,三个节点的数据是一致的,

3.2 Network Partition 情况下进行复制日志

在 Network Partition 的情况下,部分节点之间没办法互相通信,Raft 也能保证在这种情况下数据的一致性。

一开始有 5 个节点处于同一网络状态下。


Network Partition 将节点分成两边,一边有两个节点,一边三个节点。

两个节点这边已经有 Leader 了,来自客户端的数据 “bob” 通过 Leader 同步到 Follower。

因为只有两个节点,少于3个节点,所以 “bob” 的状态仍是 Uncommitted。所以在这里,服务器会返回错误给客户端

另外一个 Partition 有三个节点,进行重新选主。客户端数据 “tom” 发到新的 Leader,通过和上节网络状态下相似的过程,同步到另外两个 Follower。



因为这个 Partition 有3个节点,超过半数,所以数据 “tom” 都 Commit 了。

网络状态恢复,5个节点再次处于同一个网络状态下。但是这里出现了数据冲突 “bob" 和 “tom"

三个节点的 Leader 广播 AppendEntries

两个节点 Partition 的 Leader 自动降级为 Follower,因为这个 Partition 的数据 “bob” 没有 Commit,返回给客户端的是错误,客户端知道请求没有成功,所以 Follower 在收到 AppendEntries 请求时,可以把 “bob“ 删除,然后同步 ”tom”,通过这么一个过程,就完成了在 Network Partition 情况下的复制日志,保证了数据的一致性。

小总结
Raft 是能够实现分布式系统强一致性的算法,每个系统节点有三种状态 Follower,Candidate,Leader。实现 Raft 算法两个最重要的事是:选主和复制日志

Raft 原理动画 (推荐看看):http://thesecretlivesofdata.com/raft/

2、BASE理论

BASE是基本可用,软状态,最终一致性。是对CAP中一致性和可用性权限的结果,是基于CAP定理演化而来的,核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特定,采用适当的方式来使系统达到最终一致性
强一致性、弱一致性、最终一致性:
对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这就是强一致性。
如果能容忍后续的部分或者全部访问不到,则是弱一致性,如果经过一段时间能访问到更新后的数据,则是最终一致性。

4、分布式事务几种方案

1、2PC提交(刚性事务)

二阶段提交协议是将事务的提交过程分成提交事务请求和执行事务提交两个阶段进行处理。

2、3PC提交(刚性事务)

  • 三阶段提,也叫三阶段提交协议,是二阶段提交(2PC)的改进版本。
  • 与两阶段提交不同的是,三阶段提交有两个改动点。引入超时机制。同时在协调者和参与者中都引入超时机制。在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
  • 三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

3、柔性事务-TCC事务补偿型方案#

刚性事务:遵循 ACID 原则,强一致性。
柔性事务:遵循 BASE 理论,最终一致性。
与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。
TCC是服务化的两阶段编程模型,其Try、Confirm、Cancel,3个方法均由业务编码实现
TCC要求每个分支事务实现三个操作:预处理Try,确认Confirm,撤销Cancel。
Try操作做业务检查及资源预留,
Confirm做业务确认操作
Cancel实现一个与Try相反的操作即回滚操作。
TM首先发起所有的分支事务Try操作,任何一个分支事务的Try操作执行失败,TM将会发起所有分支事务的Cancel操作,若Try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试。

4、柔性事务-最大努力通知型方案

最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是消息可能接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务,通知可靠性关键在于接收通知方

5、柔性事务-可靠消息+最终一致性方案(异步确保型)

实现:业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送,只有在得到确认发送指令后,实时消息服务才会真正发送。
在我们的业务中,最终会使用4、5方案来解决分布式事务。

三、分布式事务-Seata 控制分布式事务

1、Seata概念

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

Seata术语

  • TC:事务协调者。维护全局和分支事务的状态,驱动全局事务提交或回滚。
  • TM:事务管理器。定义全局事务的范围:开始全局事务、提交或回滚全局事务
  • RM:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。


RPC(Remote Procedure Call)远程过程调用,简单的理解是一个节点请求另一个节点提供的服务。
我们只需要使用一个 @GlobalTransactional 注解在业务方法上:

@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {......
}

2、Seata使用

1、创建 UNDO_LOG 表

SEATA AT 模式需要 UNDO_LOG 表

– 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE undo_log (
id bigint(20) NOT NULL AUTO_INCREMENT,
branch_id bigint(20) NOT NULL,
xid varchar(100) NOT NULL,
context varchar(128) NOT NULL,
rollback_info longblob NOT NULL,
log_status int(11) NOT NULL,
log_created datetime NOT NULL,
log_modified datetime NOT NULL,
ext varchar(100) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid,branch_id)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

如果每一个微服务都想参与到全局事务中,则每个微服务都需要创建一个回滚日志表 UNDO_LOG。
给我们项目所涉及的微服务数据库都添加undo_log表:gulimall-oms,gulimall-pms, gulimall-ums,gulimall-wms 。

2、安装事务协调器

从 https://github.com/seata/seata/releases, 下载服务器软件包,将其解压缩。

Usage: sh seata-server.sh(for linux and mac) or cmd seata-server.bat(for windows) [options]
Options:
–host, -h
The host to bind.
Default: 0.0.0.0
–port, -p
The port to listen.
Default: 8091
–storeMode, -m
log store mode : file、db
Default: file
–help

e.g.

sh seata-server.sh -p 8091 -h 127.0.0.1 -m file

3、整合

gulimall-common 服务导入seata依赖:
gulimall-common/pom.xml

<!-- 分布式事务seata --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency>


可以看到导入的Seata版本为 seata-all-0.7.1 ,所以,seata服务版本也必须是 0.7.1

下载 seata-server 0.7.1版本到本地,然后解压缩,将解压缩包放到我们开发目录下:

cd /Users/kaiyiwang/javaweb/guli/develop/seata-server-0.7.1/bin

注册中心配置:
/seata-server-0.7.1/conf/registry.conf:修改 registry type=nacos

registry {# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa  指定注册中心type = "nacos"# 如果类型为nacos,则需要配置nacos的服务addrnacos {serverAddr = "localhost:8848"  namespace = "public"cluster = "default"}eureka {serviceUrl = "http://localhost:1001/eureka"application = "default"weight = "1"}redis {serverAddr = "localhost:6379"db = "0"}zk {cluster = "default"serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}consul {cluster = "default"serverAddr = "127.0.0.1:8500"}etcd3 {cluster = "default"serverAddr = "http://localhost:2379"}sofa {serverAddr = "127.0.0.1:9603"application = "default"region = "DEFAULT_ZONE"datacenter = "DefaultDataCenter"cluster = "default"group = "SEATA_GROUP"addressWaitTime = "3000"}file {name = "file.conf"}
}config {# file、nacos 、apollo、zk、consul、etcd3  Seata的配置都在哪里,默认是在该文件 file.conftype = "file"nacos {serverAddr = "localhost"namespace = "public"cluster = "default"}consul {serverAddr = "127.0.0.1:8500"}apollo {app.id = "seata-server"apollo.meta = "http://192.168.1.204:8801"}zk {serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}etcd3 {serverAddr = "http://localhost:2379"}file {name = "file.conf"}
}

Seata服务器配置:
/seata-server-0.7.1/conf/file.conf

transport {# tcp udt unix-domain-sockettype = "TCP"#NIO NATIVEserver = "NIO"#enable heartbeatheartbeat = true#thread factory for nettythread-factory {boss-thread-prefix = "NettyBoss"worker-thread-prefix = "NettyServerNIOWorker"server-executor-thread-prefix = "NettyServerBizHandler"share-boss-worker = falseclient-selector-thread-prefix = "NettyClientSelector"client-selector-thread-size = 1client-worker-thread-prefix = "NettyClientWorkerThread"# netty boss thread size,will not be used for UDTboss-thread-size = 1#auto default pin or 8worker-thread-size = 8}shutdown {# when destroy server, wait secondswait = 3}serialization = "seata"compressor = "none"
}
service {#vgroup->rgroupvgroup_mapping.my_test_tx_group = "default"#only support single nodedefault.grouplist = "127.0.0.1:8091"#degrade current not supportenableDegrade = false#disabledisable = false#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanentmax.commit.retry.timeout = "-1"max.rollback.retry.timeout = "-1"
}client {async.commit.buffer.limit = 10000lock {retry.internal = 10retry.times = 30}report.retry.count = 5
}## transaction log store,事务日志存储在哪里
store {## store mode: file、dbmode = "file"## file storefile {dir = "sessionStore"# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptionsmax-branch-session-size = 16384# globe session size , if exceeded throws exceptionsmax-global-session-size = 512# file buffer size , if exceeded allocate new bufferfile-write-buffer-cache-size = 16384# when recover batch read sizesession.reload.read_size = 100# async, syncflush-disk-mode = async}## database storedb {## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.datasource = "dbcp"## mysql/oracle/h2/oceanbase etc.db-type = "mysql"url = "jdbc:mysql://127.0.0.1:3306/seata"user = "mysql"password = "mysql"min-conn = 1max-conn = 3global.table = "global_table"branch.table = "branch_table"lock-table = "lock_table"query-limit = 100}
}
lock {## the lock store mode: local、remotemode = "remote"local {## store locks in user's database}remote {## store locks in the seata's server}
}
recovery {committing-retry-delay = 30asyn-committing-retry-delay = 30rollbacking-retry-delay = 30timeout-retry-delay = 30
}transaction {undo.data.validation = trueundo.log.serialization = "jackson"
}## metrics settings
metrics {enabled = falseregistry-type = "compact"# multi exporters use comma dividedexporter-list = "prometheus"exporter-prometheus-port = 9898
}

启动Seata服务:

cd /Users/kaiyiwang/javaweb/guli/develop/seata-server-0.7.1/bin
# sh seata-server.sh -p 8091 -h 127.0.0.1 -m file
chmod +x seata-server.sh
sh seata-server.sh -p 8091 -h 127.0.0.1

打印:

2020-09-15 17:31:35.889 INFO [main]io.seata.core.rpc.netty.AbstractRpcRemotingServer.start:179 -Server started …
2020-09-15 17:31:35.903 INFO [main]io.seata.common.loader.EnhancedServiceLoader.loadFile:237 -load RegistryProvider[Nacos] extension by class[io.seata.discovery.registry.nacos.NacosRegistryProvider]

然后再进入到Nacos中心,可以看到Seata服务已经成功注册到Nacos服务中心了。

4、数据源代理

所有想要用到分布式事务的微服务使用 seata DataSourceProxy 代理自己的数据源。
创建Seata数据源配置文件:
gulimall-order/xxx/order/config/MySeataConfig.java

package com.atguigu.gulimall.order.config;import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;import javax.sql.DataSource;/*** Seata数据源代理配置** @doc:* https://github.com/seata/seata-samples/tree/master/springcloud-jpa-seata** @author: kaiyi* @create: 2020-09-15 17:44*/
@Configuration
public class MySeataConfig {@AutowiredDataSourceProperties dataSourceProperties;@Beanpublic DataSource dataSource(DataSourceProperties dataSourceProperties) {HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();if (StringUtils.hasText(dataSourceProperties.getName())) {dataSource.setPoolName(dataSourceProperties.getName());}return new DataSourceProxy(dataSource);}
}

然后将该配置文件放到需要使用seata服务的配置目录下。

5、微服务导入配置文件

每个微服务,都必须导入 registry.conf , file.conf,并且修改file.conf配置应用名为我们微服务名 vgroup_mapping.{application.name}-fescar-server-group = "default" ,否则会找不到。

file.conf

transport {# tcp udt unix-domain-sockettype = "TCP"#NIO NATIVEserver = "NIO"#enable heartbeatheartbeat = true#thread factory for nettythread-factory {boss-thread-prefix = "NettyBoss"worker-thread-prefix = "NettyServerNIOWorker"server-executor-thread-prefix = "NettyServerBizHandler"share-boss-worker = falseclient-selector-thread-prefix = "NettyClientSelector"client-selector-thread-size = 1client-worker-thread-prefix = "NettyClientWorkerThread"# netty boss thread size,will not be used for UDTboss-thread-size = 1#auto default pin or 8worker-thread-size = 8}shutdown {# when destroy server, wait secondswait = 3}serialization = "seata"compressor = "none"
}
service {#vgroup->rgroup#vgroup_mapping.my_test_tx_group = "default" # 默认的vgroup_mapping.gulimall-order-fescar-service-group = "default"#only support single nodedefault.grouplist = "127.0.0.1:8091"#degrade current not supportenableDegrade = false#disabledisable = false#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanentmax.commit.retry.timeout = "-1"max.rollback.retry.timeout = "-1"
}client {async.commit.buffer.limit = 10000lock {retry.internal = 10retry.times = 30}report.retry.count = 5
}## transaction log store
store {## store mode: file、dbmode = "file"## file storefile {dir = "sessionStore"# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptionsmax-branch-session-size = 16384# globe session size , if exceeded throws exceptionsmax-global-session-size = 512# file buffer size , if exceeded allocate new bufferfile-write-buffer-cache-size = 16384# when recover batch read sizesession.reload.read_size = 100# async, syncflush-disk-mode = async}## database storedb {## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.datasource = "dbcp"## mysql/oracle/h2/oceanbase etc.db-type = "mysql"url = "jdbc:mysql://127.0.0.1:3306/seata"user = "mysql"password = "mysql"min-conn = 1max-conn = 3global.table = "global_table"branch.table = "branch_table"lock-table = "lock_table"query-limit = 100}
}
lock {## the lock store mode: local、remotemode = "remote"local {## store locks in user's database}remote {## store locks in the seata's server}
}
recovery {committing-retry-delay = 30asyn-committing-retry-delay = 30rollbacking-retry-delay = 30timeout-retry-delay = 30
}transaction {undo.data.validation = trueundo.log.serialization = "jackson"
}## metrics settings
metrics {enabled = falseregistry-type = "compact"# multi exporters use comma dividedexporter-list = "prometheus"exporter-prometheus-port = 9898
}

其他微服务也需要引入这两个文件,其他微服务引入的时候,要记得file.conf文件修改 application.name名字,如仓库服务:

#仓储服务
vgroup_mapping.gulimall-ware-fescar-service-group = “default”

6、启动测试分布式事务

  • 给分布式大事务的入口标注@GlobalTransactional
  • 每一个远程的小事务用@Transactional

项目代码:
gulimall-order/xxx/order/service/impl/OrderServiceImpl.java

 /*** 提交订单* @param vo* @return*/// @Transactional(isolation = Isolation.READ_COMMITTED) 设置事务的隔离级别// @Transactional(propagation = Propagation.REQUIRED)   设置事务的传播级别// 开启Seata全局性事务@GlobalTransactional(rollbackFor = Exception.class)@Transactional@Overridepublic SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {confirmVoThreadLocal.set(vo);SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();//去创建、下订单、验令牌、验价格、锁定库存...//获取当前用户登录的信息MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();responseVo.setCode(0);//1、验证令牌是否合法【令牌的对比和删除必须保证原子性】String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";String orderToken = vo.getOrderToken();//通过lua脚本原子验证令牌和删除令牌Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),orderToken);if (result == 0L) {//令牌验证失败responseVo.setCode(1);return responseVo;} else {//令牌验证成功//1、创建订单、订单项等信息OrderCreateTo order = createOrder();//2、验证价格BigDecimal payAmount = order.getOrder().getPayAmount();BigDecimal payPrice = vo.getPayPrice();if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {//金额对比//TODO 3、保存订单saveOrder(order);//4、库存锁定,只要有异常,回滚订单数据//订单号、所有订单项信息(skuId,skuNum,skuName)WareSkuLockVo lockVo = new WareSkuLockVo();lockVo.setOrderSn(order.getOrder().getOrderSn());//获取出要锁定的商品数据信息List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {OrderItemVo orderItemVo = new OrderItemVo();orderItemVo.setSkuId(item.getSkuId());orderItemVo.setCount(item.getSkuQuantity());orderItemVo.setTitle(item.getSkuName());return orderItemVo;}).collect(Collectors.toList());lockVo.setLocks(orderItemVos);//TODO 调用远程锁定库存的方法//出现的问题:扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)//为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,可以发消息给库存服务R r = wmsFeignService.orderLockStock(lockVo);if (r.getCode() == 0) {//锁定成功responseVo.setOrder(order.getOrder());// int i = 10/0;//删除购物车里的数据redisTemplate.delete(CartConstant.CART_PREFIX + memberResponseVo.getId());return responseVo;} else {//锁定失败String msg = (String) r.get("msg");throw new NoStockException(msg);// responseVo.setCode(3);// return responseVo;}} else {responseVo.setCode(2);return responseVo;}}}

锁定库存业务:
gulimall-ware/xxx/ware/service/impl/WareSkuServiceImpl.java

/*** 为某个订单锁定库存* @param vo* @return*/@Transactional(rollbackFor = Exception.class)@Overridepublic boolean orderLockStock(WareSkuLockVo vo) {//1、按照下单的收货地址,找到一个就近仓库,锁定库存//2、找到每个商品在哪个仓库都有库存List<OrderItemVo> locks = vo.getLocks();List<SkuWareHasStock> collect = locks.stream().map((item) -> {SkuWareHasStock stock = new SkuWareHasStock();Long skuId = item.getSkuId();stock.setSkuId(skuId);stock.setNum(item.getCount());//查询这个商品在哪个仓库有库存List<Long> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);stock.setWareId(wareIdList);return stock;}).collect(Collectors.toList());//2、锁定库存for (SkuWareHasStock hasStock : collect) {boolean skuStocked = false;Long skuId = hasStock.getSkuId();List<Long> wareIds = hasStock.getWareId();if (org.springframework.util.StringUtils.isEmpty(wareIds)) {//没有任何仓库有这个商品的库存throw new NoStockException(skuId);}for (Long wareId : wareIds) {//锁定成功就返回1,失败就返回0Long count = wareSkuDao.lockSkuStock(skuId,wareId,hasStock.getNum());if (count == 1) {skuStocked = true;break;} else {//当前仓库锁失败,重试下一个仓库}}if (skuStocked == false) {//当前商品所有仓库都没有锁住throw new NoStockException(skuId);}}//3、肯定全部都是锁定成功的return true;}@Dataclass SkuWareHasStock {private Long skuId;private Integer num;private List<Long> wareId;}

调试:
问题:
在引入seata后,启动服务报io.seata.common.exception.FrameworkException: can not register RM,err:can not connect to services-server的错误。
解决:
参考:(1)https://github.com/seata/seata/issues/2522 (2)https://seata.io/zh-cn/docs/overview/faq.html

https://github.com/seata/seata-samples/tree/master/springcloud-jpa-seata
file.conf 的 service.vgroup_mapping 配置必须和spring.application.name一致
在 org.springframework.cloud:spring-cloud-starter-alibaba-seata的org.springframework.cloud.alibaba.seata.GlobalTransactionAutoConfiguration类中,默认会使用 ${spring.application.name}-fescar-service-group作为服务名注册到 Seata Server上,如果和file.conf中的配置不一致,会提示 no available server to connect错误
也可以通过配置 spring.cloud.alibaba.seata.tx-service-group修改后缀,但是必须和file.conf中的配置保持一致

小结:

使用Seata之后,我们可以测试看到远程调用增加的库存在执行后边积分失败时库存也回滚了,所以,Seata还是非常强大的。

四、分布式事务-最终一致性库存解锁逻辑

1、Seata的不足

Seata的AT模式是二阶段提交协议(2PC),第一阶段将本地事务直接提交,第二阶段想要回滚的时候,是通过回滚日志(日志表)做的反向补偿,数据库原来是多少又改了回来。
Seata应用场景:后台管理系统,比如添加商品,优惠、库存、积分、会员要成功都成功,要失败都失败,对于并发性能不高的可以使用Seata来处理分布式事务。
如果并发性能要求很高的,比如下单,则需要使用最终一致性,RMQ发消息,保证消息的可靠性(发送端和接收端确认),不能达到强一致性,但能达到软柔性事务的最终一致性。
下单属于高并发场景,为了保证高并发,不推荐使用seata,Seata用了很多锁机制,因为是加锁,相当于把并发变为串行了,如果多个订单下来,就得进行排队,等待上一个人处理完了,释放了锁,才能继续下单,这样系统可能就没法用了,提升不了效率,可以发消息给库存服务。

2、高并发场景

在高并发场景下,不考虑 2PC 和 TCC 模式(这两种属于刚性事务),可以使用 最大努力通知型方案可靠消息+最终一致性方案,这两种是通过消息来实现的,并且都是柔性事务。
为了保证高并发,库存服务自己回滚,可以发消息给库存服务。库存服务本身也可以使用自动解锁模式,要参与消息队列。

在锁库存的时候,需要给数据库增加记录,锁的数量和SKU,以及仓库,如果锁失败,库存锁表里边没有这条记录,可以使用定时任务来处理,不过使用定时任务来处理,是非常麻烦的事情,可以使用延时队列来处理,延时队列做一个定时工作。

本地事务与分布式事务相关推荐

  1. 【事务】本地事务和分布式事务的区别

    整理下事务相关的知识点: 文章目录 一.事务 MySQL事务 事务的实现 事务的ACID特性 事务的隔离级别 二.本地事务 三.分布式事务 认识分布式事务 CAP理论 BASE理论 一.事务 先复习下 ...

  2. 尚硅谷谷粒商城第六天 本地事务、分布式事务及seata

    1. 本地事务 商品新增功能非常复杂,商品管理微服务在service层中调用保存spu和sku相关的方法,为了保证数据的一致性,必然会使用事务. 在JavaEE企业级开发的应用领域,为了保证数据的完整 ...

  3. 分布式事务:本地事务与分布式事务

    分布式事务:本地事务与分布式事务 我们编写的扣减库存与保存订单是在两个服务中存在的.因为扣减库存后出现某些原因导致整一个下单流程出现错误,此刻会出现这样的状况:保存订单的本地事务(加了@Transac ...

  4. 分布式事务:分布式事务原理概述

    1.什么是分布式事务 分布式事务就是指事务的资源分别位于不同的分布式系统的不同节点之上的事务: 2.分布式事务产生的原因 2.1.数据库分库分表 在单库单表场景下,当业务数据量达到单库单表的极限时,就 ...

  5. Java事务之八——分布式事务(Spring+JTA+Atomikos+Hibernate+JMS)

    在本系列先前的文章中,我们主要讲解了JDBC对本地事务的处理,本篇文章将讲到一个分布式事务的例子. 请通过以下方式下载github源代码: git clone https://github.com/d ...

  6. 【网站架构】一招搞定90%的分布式事务,实打实介绍数据库事务、分布式事务的工作原理应用场景

    大家好,欢迎来到停止重构的频道.本期,我们来聊一下数据库事务以及分布式事务. 大家都在强调事务的重要性,而分布式事务也说是微服务必备的.但又说事务会影响性能,分布式事务更是很复杂的东西.使得大家都很迷 ...

  7. 分布式事务解决方案分布式事务原理

    分布式事务解决方案&分布式事务原理 0. 前言 1. 单数据源事务 & 多数据源事务 2. 常见分布式事务解决方案 2.0.什么是分布式事务 2.1. 分布式事务模型 2.2. 二将军 ...

  8. springcloud分布式事务_Springcloud 分布式事务集成Naco Seata

    前言:分布式系统架构中,最最费劲的是分布式事务,分布式事务解决方案网上大致分为两种 消息一致性 基于TCC分布式事务 不管基于那种解决方案,都是对侵入的代码植入,以大量的代码或者消息来作为代价,来实现 ...

  9. Part2_事务与分布式事务原理与实现_沈询_fixed ver—在线播放—优酷网,视频高清在线观看...

    Part2_事务与分布式事务原理与实现_沈询_fixed ver-在线播放-优酷网,视频高清在线观看 Part2_事务与分布式事务原理与实现_沈询_fixed ver-在线播放-优酷网,视频高清在线观 ...

  10. 什么是事务? 事务的隔离级别和事务运行的模式分别是什么?spring 事务和分布式事务实现方式有哪些?

    目录 什么是事务? 事务的隔离级别: 事务运行的模式: spring 事务实现方式: 分布式事务实现方式: 什么是事务? 百度百科中解释:指作为单个逻辑工作单元执行的一系列操作,此操作是对数据库的操作 ...

最新文章

  1. Linux下C语言的fgets与fputs
  2. 使用 pm2-web 监控 pm2 服务运行状态
  3. ESP8266 Flash
  4. Binary Tree Non-recursive Traversal
  5. .Net Core手撸一个基于Token的权限认证
  6. 实例53:python
  7. vux radio 改造为 tree 代码片段
  8. java properties用法_java中Properties文件加载和使用方法
  9. MySQL数据库安装与配置详解
  10. 防爆技术在工业电子秤中的最新应用(转)
  11. LINUX设备驱动之platform总线
  12. Fluent软件零基础入门到精通教程
  13. .Net Frame安装心得
  14. 主流集成控制系统:EPICS和TANGO
  15. 更深一步了解,电容是起什么作用的?
  16. mdf文件和ldf文件是什么?
  17. 多数投票算法(Boyer-Moore Algorithm)
  18. 【调剂】2021年齐齐哈尔大学朱磊教授课题组招收通信与信息系统、电子信息相关专业硕士调剂...
  19. threejs效果记录
  20. 计算机虚拟化技术论文,虚拟化技术在计算机技术中的运用

热门文章

  1. 计算机毕业设计Java“小蜜蜂”校园代取快递系统(源码+系统+mysql数据库+lw文档)
  2. 单目图像3D物体的姿态检测
  3. QT开发--Visual Studio2013--配置代码32bit、64bit环境
  4. 几款炫酷的CSS代码样式
  5. 云音乐vue开发日记
  6. 爬取王者荣耀皮肤-点券领取
  7. 知路,然后智行远;懂行,所以万业兴
  8. 高颜值蓝牙耳机有哪些?音质好颜值高的蓝牙耳机推荐
  9. 微信小程序多表查询踩坑记录
  10. web前端开发基础知识整理以及前端视频教程