本地事务
四大特性
- 隔离性:多事务并发访问时,事务之间相互隔离
- 原子性:事务中所有的操作要么全部成功,要么全部失败
- 持久性:事务执行成功后,对应数据永久保存到数据库
- 一致性:事务执行前后数据不会被破坏,假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的
隔离等级
- ReadUnCommit:多个不同的事务操作同一条数据,事务A读取到了事务B未提交的数据(脏读)
- ReadCommit:多个不同的事务操作同一条数据,可以有效的解决脏读的问题,但是不能保证同一事务中的相同查询操作的结果一致(不可重复读)
- ReadRepeatable:多个不同的事务操作同一条数据,可以有效的解决不可重复读的问题,但是可能会出现幻读的问题(幻读)
- Serializable:该数据库隔离等级最高,多个不同的事务操作同一条数据,可以有效的解决脏读、不可重复度读、幻读的问题,但是性能是个问题
@Transactional(Spring声明式事务注解)
基于AOP面向切面,它将具体业务与事务处理部分解耦,所以在实际开发中声明式事务用的比较多
声明式事务也有两种实现方式,一是基于TX和AOP的xml配置文件方式,二种就是基于@Transactional注解(作用在接口、类、类方法)
作用在类上时表示该类所有的public方法都配置相同的事务属性信息,若其方法也配置了@Transactional,则方法的事务会覆盖类的事务配置信息
不推荐作用在接口上,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效
-
隔离等级(4)
READ_UNCOMMITTED:允许另外一个事务可以看到这个事务未提交的数据
READ_COMMITTED:保证一个事物提交后才能被另外一个事务读取
REPEATABLE_READ:防止脏读,不可重复读
SERIALIZABLE:串化读 -
传播行为(7)
PROPAGATION_required(默认):当前存在事务则加入该事务,否则创建一个新的事务
PROPAGATION_nested:当前存在事务则创建一个事务作为当前事务的嵌套事务来运行
否则等价于默认传播行为
PROPAGATION_supports:当前存在事务则加入该事,否则以非事务继续运行
PROPAGATION_mandatory:当前存在事务则加入该事务,否则抛出异常
PROPAGATION_requires_new:当前存在事务则挂起,然后创建一个新的事务
PROPAGATION_not_supported:当前存在事务则挂起,然后以非事务继续运行
PROPAGATION_never:当前存在事务则抛出异常,然后以非事务方式运行 -
失效场景
@Transactional修饰的方法必须为public,否则事务不会生效
发生的异常被捕获了,则事务不会生效
如果updateA被@Transactional修饰,即使updateB没有,因为updateB是在updateA中调用,所有这两个方法都在同一个事物中
分布式事务场景
- 单体应用中访问多个数据库实例(跨数据库实例)
- 多个微服务应用(跨JVM)访问同一个数据库实例
分布式事务解决方案
- 传统全局事务(2PC)
由AP(微服务应用)、TM(事务管理器)、RM(资源管理器)三个部分组成,整个事务分为2阶段:
a、准备阶段:AP各自将本地事务进行预提交并将结果反馈给TM(如果TM出现故障则会导致全局事务不一致)
b、提交阶段:TM根据AP反馈结果对各个RM进行统一提交或回滚(若提交阶段出现失败则会导致全局事务不一致)
基于数据库层面的分布式事务,各个RM持有的数据库资源锁需要等到提交阶段结束后才能被释放(保证了事务的强一致性,但牺牲了数据库的链接数量)
- Seata(2PC)
将分布式事务理解成多个本地分支事务的全局事务(负责协调各本地分支事务,要么全部成功,要么全部失败)
分别由以下3个组件组成:
TC 事务协调器【安装的Seata服务】:维护全局和分支事务的状态,驱动全局事务提交或回滚
TM 事务管理器【与每个需要部署分布式事务的微服务对应】:定义全局事务的范围:开始全局事务、提交或回滚全局事务
RM 资源管理器【与每个需要部署分布式事务的微服务对应】:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
传统2PC中各自本地分支事务持有的数据库资源锁都要保持到“提交阶段”完成后才被释放,而Seata则是在“准备阶段”就将本地分支事务提交了,避免了在提交阶段长期持有数据库资源锁的问题
- 基于SpringCloudAlibaba的微服务项目如何配置Seata1.4.2,实现分布式事务
// 为需要引入Seata分布式事务的微服务添加依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!-- 排除当前SpringCloudAlibaba主版本引入seata 1.3.0 -->
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 重新引入seata 1.4.2 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
/**
* 为需要实现分布式事务的方法添加 @GlobalTransactional 注解
*/
@PostMapping("createOrder")
@GlobalTransactional
public BaseResult<String> createOrder() {
System.out.println("xid========================" + RootContext.getXID());
// 扣库存(分布式事务)
SellerProductVo sellerProductVo = new SellerProductVo();
sellerProductVo.setId(1000000L);
sellerProductVo.setStock(1);
BaseResult<String> baseResultForChangeStock = productFeign.changeStock(sellerProductVo);
if (!baseResultForChangeStock.isSuccess()) {
throw new BaseException(baseResultForChangeStock.getMsg());
}
// 创建订单(分布式事务)
BuyerOrderVo buyerOrderVo = new BuyerOrderVo();
buyerOrderVo.setCreateTime(new Date());
buyerOrderVo.setCode("order1000000");
buyerOrderVo.setProductId(1000000L);
buyerOrderVo.setPayState(SysStateEnum.ORDER_STATE_ING.getValue());
BaseResult<String> baseResultForCreateOrder = orderFeign.createOrder(buyerOrderVo);
if (!baseResultForCreateOrder.isSuccess()) {
throw new BaseException(baseResultForChangeStock.getMsg());
}
// 扣减账户金额(本地事务)
boolean updateBuyerFlag = buyerService.update(new UpdateWrapper<BuyerEntity>().set("grade", 0).eq("account", "31011225"));
if (!updateBuyerFlag) {
throw new BaseException("扣减账户金额失败");
}
// 模拟异常,测试分布式事务是否生效
System.out.println(0/0);
return new BaseResult<String>().success(buyerOrderVo.getCode());
}
- TCC
要求每个本地分支事务必须实现以下3个操作:Try(预处理 ,业务检查及资源预留)、 Confirm(确认操作)、Cancel(取消操作),三个阶段都需要开发者编写相关的业务代码来实现,开发成功较高,如果Confirm、Cancel操作失败还需要人为编写业务代码进行重试(幂等处理) - 最终一致(RocketMQ消息中间件)和最大努力通知
1、本地订单持久化之前,先发送一个扣减库存的半事务消息给RocketMQ,RocketMQ收到半事务消息后并做出成功响应
2、本地事务收到成功响应后开始提交订单:
2.1、订单提交失败,本地事务告知RocketMQ结果,RocketMQ对该消息进行周期性存储
2.2、订单提交成功,本地事务告知RocketMQ结果,RocketMQ对扣减库存的半事务消息进行投递
2.3、由于网络等其它原因,RocketMQ一直没有收到本地订单的提交结果通知,则会主动发起本地订单状态回查(需要开发者提供相关接口),根据回查结果决定是否对扣减库存的半事务消息进行投递
3、如果扣减库存的半事务消息执行失败了,则触发重新投递,超过指定次数后进入死性队列进行周期性存储,针对死性队列的数据需要人为干预
最大努力通知需要开发者自定义相关表来存储失败消息以保证被下游系统消费,开发成本高,代码耦合大