谷粒商城-高级-71 -商城业务-分布式事务-本地事务与分布式事务

一、本地事务

1、事务的基本性质

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

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

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

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

在代码中我们可以指定事务的隔离级别:

@Transactional(isolation=Isolation.READ_COMMITED)

3、事务的传播行为

1、支持当前事务:
TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。

2、不支持当前事务:
TransactionDefinition.PROPAGATION_REQUIRED_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGETION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

3、其他
TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。

示例:

@Transactional  // a事务的所有设置就传播到了和他共用一个事务的方法。
public void a() {

// b,c是否和a共用一个事务?
b();   // b需要一个事务,这时b和a共用一个事务,即在a事务里边
c();   // c是一个新事务

int i = 10 / 0;   // 哪个会回滚呢?
}

@Transactional(propagation=Propagation.REQUIRED)
public void b() {
}

@Transactional(propagation=Propagation.REQUIRED_NEW)
public void c() {
}

如果上边的代码 int i = 10 / 0;执行,哪个会回滚呢? 由于b和a是在同一个事务里边,所以,两个都会回滚,而c是一个新事物,所以,不会回滚。

我们还可以给事务添加超时时间 @Transactional(timeout =20),如果共用事务,则子事务的设置就不起作用了,会用父类的事务。

@Transactional(timeout =20)   // a事务的所有设置就传播到了和他共用一个事务的方法。
public void a() {

// b,c是否和a共用一个事务?
b();   // b需要一个事务,这时b和a共用一个事务,即在a事务里边
c();   // c是一个新事务

int i = 10 / 0;   // 哪个会回滚呢?
}

// b这里设置的超时时间不起作用,因为它用的是a的事务,所以,超时时间和a的一样
@Transactional(propagation=Propagation.REQUIRED,  timeout =2 )
public void b() {
}

@Transactional(propagation=Propagation.REQUIRED_NEW, ,  timeout =30)
public void c() {
}

4、SpringBoot事务关键点

4.1、事务的自动配置

TransactionAutoConfiguration

4.2、事务的坑

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

@Transactional(timeout =20)   // a事务的所有设置就传播到了和他共用一个事务的方法。
public void a() {

// b,c做任何设置都没用,都是和a共用一个事务,事务最大的一个特性就是代理
// b,c是否和a共用一个事务?
b();   // b需要一个事务,这时b和a共用一个事务,即在a事务里边
c();   // c是一个新事务

int i = 10 / 0;   // 哪个会回滚呢?
}

// b这里设置的超时时间不起作用,因为它用的是a的事务,所以,超时时间和a的一样
@Transactional(propagation=Propagation.REQUIRED,  timeout =2 )
public void b() {
}

@Transactional(propagation=Propagation.REQUIRED_NEW, ,  timeout =30)
public void c() {
}

事务使用代理对象来控制的,上边的b,c和a是相同的对象,同一个对象内事务方法互调默认失败,原因是绕过了代理对象,

解决:
1)、导入spring-boot-starter-aop,引入了 aspectj
2)、@EnableTransactionManagement(proxyTargetClass=true)
3)、@EnableAspectJAutoProxy(exposeProxy=true);开启 aspectj 动态代理功能。以后所有的动态代理都是 aspectj 创建(即使没有接口也可以创建动态代理),对外暴露代理对象。
4)、本类互调用调用对象(OrderServiceImpl obj = AopContext.currentProxy()

如果有本类互调的可以使用上边的解决方案。

5、本地事务问题

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

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

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

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

分布式事务:最大原因,网络问题+分布式机器。


相关文章:
事务的四种隔离级别
微服务分布式事务4种解决方案实战

为者常成,行者常至