1、事务简介
事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。在关系数据库中,一个事务由一组SQL语句组成,事务具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID原则。
- 原子性(atomicity): 事务中的操作要么都发生,要么都不发生
- 一致性(consistency): 事务从一个一致性的状态变到另一个一致性的状态,
- 隔离性(isolation): 事务之间不能相互干扰、相互隔离,隔离又分为四个级别: 读未提交(read uncommitted), 读已提交(read committed,解决脏读)、可重复读(rpeatable read ,解决不可重复读)、串行化(serializable 解决幻读)
- 持久性(durability): 持久性也称为永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变是永久的,接下来的操作或故障不应该对其有任何影响。
1.1、本地事务
@Translation
大多数场景下,我们的应用都只需要操作单一的数据库,这种情况的事务我们称之为本地事务(Local Transation
)。本地事务的ACID特性是数据库直接提供支持。本地事务应用架构如下所示:
1.2、常见的分布式事务解决方案
- seata阿里分布式事务框架
- 消息队列
- saga
- XA
他们都有一个共同点,都是两阶段(2PC)。两阶段是指完成整个分布式事务,划分成两个步骤完成。
这四种常见的分布式事务解决方法,分别对应着分布式事务的四种模式:AT、TCC、Sage、XA
;
2、Seata是什么
seata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata将为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Service 全局事务服务)
2.1、Seata的三大角色
TC(Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚
TM(Transaction Manager) - 事务管理器
RM(Resource) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,TC为单独部署的Server服务端,TM和RM为嵌入到应用中的Client客户端。
2.2、AT(auto transcation)模式
AT模式是一种无侵入的分布式事务解决方案
阿里的seata框架,实现了该模式
在AT模式下,用户只需关注自己的“业务SQL”,用户的“业务SQL”作为第一阶段,Seata框架会自动生成事务的二阶段提交和回滚操作。
AT模式如何做到对业务的无侵入:
- 一阶段
在一阶段中,Seata会拦截“业务SQL
”,首先解析SQL语义,找到“业务SQL要更新的业务数据,在业务数据被更新前,将其保存成“before image
",然后执行”业务SQL“更新业务数据,在业务数据更新之后,再将其保存成”after image
“,最后生成行锁
,以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
- 二阶段提交
二阶段如果是提交的话,因为业务“SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁进行删掉,完成数据清理即可。
- 二阶段回滚:
二阶段如果是回滚的话,Seata就需要一阶段已经执行的“业务SQL”,还原业务数据,回滚方式便是用“before image"还原业务数据;但在还原前要首先要校验脏写,对比数据库当前业务数据和after image,如果两份数据一致就说明没有脏写,可以还原数据,如果不一致就说明有脏写需要人工干预处理。
2.3、TCC模式
- 侵入性强,并且得自己实现相关事务控制逻辑
- 整个过程中基本没有锁,性能更强
TCC模式需要用户根据自己的业务场景实现Try
,Confirm
和Cancel
三个操作;事务发起方在一阶段执行try方式,在二阶段提交执行Contirm方法;二阶段回滚执行Cancel方法。
2.4、Saga模式
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
2.5、XA模式
前提
- 支持XA 事务的数据库。
- Java 应用,通过 JDBC 访问数据库。
整体机制
在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种 事务模式。
- 执行阶段:
-
- 可回滚:业务 SQL 操作放在 XA 分支中进行,由资源对 XA 协议的支持来保证 可回滚
- 持久化:XA 分支完成后,执行 XA prepare,同样,由资源对 XA 协议的支持来保证 持久化(即,之后任何意外都不会造成无法回滚的情况)
- 完成阶段:
-
- 分支提交:执行 XA 分支的 commit
- 分支回滚:执行 XA 分支的 rollback
3、Seata快速开始
3.1、Seata Server(TC)环境搭建
Server端存储模式(store.mode)支持三种:
- file : 单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高(默认)
- db(5.7+) :高可用模式,全局事务会话信息通过db共享,相应性能差些
- redis : Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险;请提前配置当前场景的redis持久化配置
3.1.2、db+Nacos方式部署
资源下载地址: 【1.3.0版本windows为例】
https://github.com/seata/seata/releases
1、打开config/file.conf
2、修改mode=“db”
3、修改数据库连接信息(url,username,password)
4、创建数据库(seata)
5、下载相关需要的资源
解压完成后,我们只需要要这个script
文件夹即可
将这个文件夹放入我们的steta目录(方便我们用里面的一些资源)
6、引入sql,script/server/db/mysql.sql
7、打开conf/registry.conf文件进行修改
registry部分:
config部分:
8、修改script/config-center/config.txt,为了等一下导入配置
注意点:
配置的事务分组,要与客户端配置的事务分组保持一致
- 客户端properties配置:springcloud.alibaba.seata.tx-service-group=my_test_tx_troup
- default需要跟客户端和registry.conf中registry中的cluster保持一致
事务分组:异地机房停电容错机制
my_test_tx_group
可以自定义 比如(guangzhou,shanghai),对应的client也要配置
9、配置参数同步到Nacos
1、进入script/config-center/nacos
如果你的ip和端口都是默认的话,直接双击即可。否则可以使用下面的启动方式
sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 93d7e8bc-389c-45e1-99a4-1b14a3309d4a
参数说明:
-h: nacos地址
-p: nacos端口号
-g: 配置分组,默认为SEATA_GROUP
-t: Nacos命名空间ID字段,默认为空
在git bash里面执行命令即可
10、打开nacos进行查看,所有配置成功同步
11、启动seata
找到seata/bin/seata-server.bat双击启动即可
所有启动参数
参数 | 全写 | 作用 | 备注 |
-h | –host | 指定在注册中心注册的ip | 不指定时获取当前ip,外部访问部署建议指定 |
-p | –port | 指定server启动的端口 | 默认8091 |
-m | –storeMode | 事务日志存储方式 | 支持file,db,redis,默认为file,注意:redis需seata-server1.3版本及以上 |
-n | –serverNode | 用户指定seata-server节点id | 如1,2,3默认为1 |
-e | –seataEnv | 指定seata-server运行环境 | 如dev,test,服务启动会使用registry-dev.conf这样的配置 |
例子:bin/seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e tset
启动集群方式:
bin/seata-server.sh -p 8091 -n 1
bin/seata-server.sh -p 8092 -n 2
bin/seata-server.sh -p 8093 -n 3
这时候我们的steata已经进来了
3.2、分布式事务代码搭建
3.2.1、创建两个数据库
- 订单数据库
CREATE TABLE `order` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单id', `product_id` bigint NOT NULL COMMENT '商品id', `total_amount` int NOT NULL COMMENT '商品数量', `status` tinyint NOT NULL COMMENT '状态', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- 库存数据库
CREATE TABLE `stock` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '库存id', `product_id` bigint NOT NULL COMMENT '商品id', `count` bigint NOT NULL COMMENT '库存数量', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3.2.2、创建几个服务模块
- seata父模块
pom文件如下
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud-alibaba</artifactId> <groupId>com.lili</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <packaging>pom</packaging> <modules> <module>seata_order</module> <module>seata_stock</module> </modules> <modelVersion>4.0.0</modelVersion> <artifactId>seata</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--web场景启动器--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--mybatisPlus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> </dependencies> </project>
- seata_order子模块
pom文件如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>seata</artifactId> <groupId>com.lili</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>seata_order</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--nacos 服务 注册与发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--添加openfeign的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> </project>
yml文件:
server: port: 8888 # 服务名称 spring: application: name: order-seata-server cloud: nacos: discovery: server-addr: 101.34.254.160:8847 username: nacos password: nacos namespace: public datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf-8&serverTimezone=UTC username: root password: root
启动类
@EnableFeignClients @SpringBootApplication public class SeataOrderApplication { public static void main(String[] args) { SpringApplication.run(SeataOrderApplication.class,args); } }
实体类:
@Data @AllArgsConstructor @NoArgsConstructor @TableName("ll_order") public class Order implements Serializable { private static final long serialVersionUID = 1L; /** * 订单id */ @TableId(value = "id",type = IdType.AUTO) private Long id; /** * 商品id */ @TableField("product_id") private Long productId; /** * 商品数量 */ @TableField("total_amount") private Integer totalAmount; /** * 状态 */ @TableField("status") private Integer status; }
mapper层
@Mapper public interface OrderMapper extends BaseMapper<Order> { }
service层接口
public interface OrderService extends IService<Order> { void addOrder(); }
service层实现类
@Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService { @Autowired StockFeignService stockFeignService; @Autowired OrderMapper orderMapper; @Override public void addOrder(){ // 模拟添加订单信息 Order order = new Order(); order.setStatus(0); order.setProductId(10L); order.setTotalAmount(20); // 添加订单 orderMapper.insert(order); // 减少库存 stockFeignService.updateStock(); } }
feign下的调用库存接口
@FeignClient(value = "stock-seata-server",path = "/stock") public interface StockFeignService { @RequestMapping("/updateStock") void updateStock(); }
controller层
@RestController @RequestMapping("/order") public class OrderController { @Autowired public OrderService orderService; @RequestMapping("/addOrder") public void addOrder() { orderService.addOrder(); } }
- seata_stock子模块
pom文件如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>seata</artifactId> <groupId>com.lili</groupId> <version>0.0.1-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>seata_stock</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--nacos 服务 注册与发现--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--添加openfeign的依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> </project>
yml文件:
server: port: 9999 # 服务名称 spring: application: name: stock-seata-server cloud: nacos: discovery: server-addr: 101.34.254.160:8847 username: nacos password: nacos namespace: public datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/seata_stock?characterEncoding=utf-8&serverTimezone=UTC username: root password: root
实体类:
@Data @AllArgsConstructor @NoArgsConstructor @TableName("ll_stock") public class Stock implements Serializable { private static final long serialVersionUID = 1L; /** * 库存id */ @TableId(value = "id", type = IdType.AUTO) private Long id; /** * 商品id */ @TableField("product_id") private Long productId; /** * 库存数量 */ @TableField("count") private Long count; }
mapper层
@Mapper public interface StockMapper extends BaseMapper<Stock> { }
service层接口
public interface StockService extends IService<Stock> { void updateStock(); }
service层实现类
@Service public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> implements StockService { @Autowired StockMapper stockMapper; @Override public void updateStock() { // 模拟修改数据 Stock stock = new Stock(); stock.setId(1L); stock.setCount(900L-20L); stockMapper.updateById(stock); } }
controller层
@RestController @RequestMapping("/stock") public class StockController { @Autowired public StockService stockService; @RequestMapping("/updateStock") public void updateStock(){ stockService .updateStock(); } }
3.3.3、使用@Translation注解测试
服务消费方
@Transactional(rollbackFor = Exception.class ) public void addOrder(){ // 模拟添加订单信息 Order order = new Order(); order.setStatus(0); order.setProductId(10L); order.setTotalAmount(20); // 添加订单 orderMapper.insert(order); // 更新库存 stockFeignService.updateStock(); // 测试异常 int i= 1/0; }
进行调用测试
java.lang.ArithmeticException: / by zero
控制台正常报错,我们来看看数据库
订单表成功回滚,但是库存表却已经被改变了
3.3.4、整合seata(@GlobalTransactional
)
第一步,两个服务都需添加下列依赖
<!--seata依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency>
第二步:各微服务对应数据库中添加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, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
第三步:
配置事务的分组,这个要与前面设置的分组相对应(两个服务都需要配置)
spring: cloud: alibaba: seata: tx-service-group: my_test_tx_group
配置seata的注册中心和配置中心(两个服务都需配置)
#配置seata的注册中心,告诉seata client怎么去访问seate server(TC) seata: registry: type: nacos nacos: server-addr: 101.34.254.160:8847 #seate server所在的nacos服务地址 application: seata-server #seate server 的服务名 username: nacos password: nacos group: SEATA_GROUP # seate server 所在的组,默认就是SEATA_GROUP,没有改可以不用配置 config: # 配置中心 type: nacos nacos: server-addr: 101.34.254.160:8847 username: nacos password: nacos group: SEATA_GROUP # 如果是默认空间则可以不用添加 namespace: 93d7e8bc-389c-45e1-99a4-1b14a3309d4a
最后,方法上加上@GlobalTransaction
注解,重新测试
@GlobalTransactional public void addOrder(){ // 模拟添加订单信息 Order order = new Order(); order.setStatus(0); order.setProductId(10L); order.setTotalAmount(20); // 添加订单 orderMapper.insert(order); // 减少库存 stockFeignService.updateStock(); // 测试异常 int i= 1/0; }
运行保存后,发现我们的数据成功回滚,分布式事务到这里已经完全配置成功了。