老板现在喊我大哥,原因是我用阿里分布式事务框架Seata解决了长久以来困扰公司的分布式事务问题
大家好,我是曹尼玛从大学毕业5年,一直努力学习,努力工作,追求新技术,不保守。上个月我来到一家新公司上班,月薪20K,这家公司老板人很好,对员工很关爱,公司氛围不错,同事们也努力把公司项目搞搞好。除了那个混日子的10年开发经验的老王(老板小舅子)由于老板对客户比较负责,业界口碑好,公司的项目越做越大,单子也越来越多,但是很多客户项目要求分布式微服务,因为大部分是金融外贸项目,搞了微服务后,偶尔出现
大家好,我是曹尼玛
从大学毕业5年,一直努力学习,努力工作,追求新技术,不保守。
上个月我来到一家新公司上班,月薪20K,这家公司老板人很好,对员工很关爱,公司氛围不错,同事们也努力把公司项目搞搞好。除了那个混日子的10年开发经验的老王(老板小舅子)
由于老板对客户比较负责,业界口碑好,公司的项目越做越大,单子也越来越多,但是很多客户项目要求分布式微服务,因为大部分是金融外贸项目,搞了微服务后,偶尔出现分布式事务不一致性问题,客户很是反感,前段时间,公司用TCC试水,发现这个方案代码侵入性太强,维护也不方便,人工投入太大,也容易编码出错。
后来用基于RocketMQ消息队列实现可靠消息最终一致性方案代替,能实现要求,比TCC好维护点,但是实现起来,终究得依赖消息中间件,而且逻辑也有点繁琐,公司项目涉及到得分布式事务模块还是很多的,搞得天罗地网似的。
公司急于寻求一种简单快捷高效的低代码侵入式的分布式事务解决方案。
作为新技术控,我一直关注新技术,最近2年,阿里分布式事务解决方案Seata很火,人气很足!
也深入研究过,现在最新版本1.4.2,基本成熟了,可以商用!作为Spring Cloud Alibaba服务组件,很好集成到项目里,而且我都深入测试过!
所以我鼓起勇气,直接走到老板办公室,让老板试试 Seata 吧;
老板不是一个思想保守的人,乐于尝试!他说他先研究下Seata。
第二天,老板叫我进办公室,直接说“公司的438项目,有一个sb模块涉及分布式事务,你用seata搞下”。我连忙说”OK,没问题,保证完成任务“!
由于我对seata比较熟悉,MD笔记整理得井井有条。所以一顿猛操,seata-server配置下,启动,微服务项目applicaton.yml配置下,@GlobalTransactional加上,测试下,没问题,一上午就撸完了,跟老板说”可以让测试部的小姐姐测试下了“。
老板惊呆了,这么快就搞完了?半信半疑
下午测试部的小姐姐们仔细功能测试,压测,没毛病!偶尔服务调用超时也是自动回滚事务,所以不影响业务,界面有友好用户提示”系统繁忙,请稍后重试“!
老板听到测试部的汇报后,直接让版本更新放到线上让客户使用!
用了一个多月,根据客户反馈,以及日志系统查看,这次seata解决方案非常OK!
老板让漂亮的小秘把我叫到办公室,然后塞给我一沓厚厚的信封,跟我说 ”曹大哥,明天开始,那个混饭吃胡日子不思进取的10年经验的老王就要滚蛋了,以后技术总监的位置就是你的了,薪资是40K,15薪资+项目奖金+年终奖“,”另外还有特殊福利“,顺便喵看了下旁边芳龄18的小敏秘书!
由于我过于激动,那天说话老是合不拢嘴!
3年以后,我成为了公司技术一把手,年入百万;
第4年,我当爸爸了,选了个风和日丽的日子,开着保时捷Panamera,带着满月的儿子和小敏一起去拍满月照!
阿里Seata作为一个优秀得分布式事务解决方案,让穷屌丝的我逆袭了,我决定公开我的seata笔记,顺便还录制了一套Seata视频教程,希望对大家有帮助!
1 阿里分布式事务框架Seata简介
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
中文官方站点:https://seata.io/zh-cn/
seata github开源地址:https://github.com/seata
2 分布式事务简介
2.1 本地事务
在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫数据库事务,由于应用主要靠关系数据库来控制事务,而数据库通常和应用在同一个服务器,所以基于关系型数据库的事务又被称为本地事务。
数据库事务的四大特性:ACID
A(Atomic):原子性,构成事务的所有操作,要么都执行完成,要么全部不执行,不可能出现部分成功部分失败的情况。
C(Consistency):一致性,在事务执行前后,数据库的一致性约束没有被破坏。比如:张三向李四转 100 元,转账前和转账后的数据是正确状态这叫一致性,如果出现张三转出 100 元,李四账户没有增加 100 元这就出现了数 据错误,就没有达到一致性。
I(Isolation):隔离性,数据库中的事务一般都是并发的,隔离性是指并发的两个事务的执行互不干扰,一个事务不能看到其他事务的运行过程的中间状态。通过配置事务隔离级别可以比避免脏读、重复读问题。
D(Durability):持久性,事务完成之后,该事务对数据的更改会持久到数据库,且不会被回滚。
数据库事务在实现时会将一次事务的所有操作全部纳入到一个不可分割的执行单元,该执行单元的所有操作要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚。
2.2 分布式事务
随着互联网的快速发展,软件系统由原来的单体应用转变为分布式应用
分布式系统会把一个应用系统拆分为可独立部署的多个服务,因此需要服务与服务之间远程协作才能完成事务操作,这种分布式系统环境下由不同的服务之间通过网络远程协作完成事务称之为分布式事务,例如用户注册送积分事务、创建订单减库存事务,银行转账事务等都是分布式事务。
3 SpringCloud Alibaba分布式基础案例搭建
我们模拟一个简单下单业务,客户端调用rest对外服务,rest服务再调用订单服务实现创建订单和账户服务实现账户扣钱操作,最终来完整下单业务;
2.1 案例架构设计
所有服务都注册到nacos中,方便feign远程调用;订单服务,账户服务各自有独立数据库;架构设计如下图:
整体项目结构如下图:
seatatest
是父项目,主要是做一些依赖管理,依赖版本管理,管理所有子module项目;
seata-common
子项目,主要是引入其他子项目需要的公共依赖,以及公共实体,工具类,配置类的统一封装;
seata-order
子项目,主要提供订单服务,生成订单;
seata-account
子项目,主要提供账户服务,根据订单扣钱操作;
seata-web
子项目,主要处理客户端下单请求,feign远程调用order,和account服务接口,最终完成下单处理;
2.2 数据库设计
我们新建两个数据库,分别是db_order
(订单数据库),db_account
(账户数据库),
db_order
数据库里面新建表t_order
订单表:
CREATE TABLE `t_order` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`orderNo` varchar(100) DEFAULT NULL,
`userId` int(11) DEFAULT NULL,
`count` int(11) DEFAULT NULL,
`amount` int(11) DEFAULT NULL,
`remark` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8
db_account
数据库里面新建表t_account
用户账户表:
CREATE TABLE `t_account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userId` int(11) DEFAULT NULL,
`balance` int(11) DEFAULT NULL,
`remark` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
插入数据:
insert into `t_account` (`id`, `userId`, `balance`, `remark`) values('1','1','2000','jack的账户');
insert into `t_account` (`id`, `userId`, `balance`, `remark`) values('2','2','1000','marry的账户');
2.3 seatatest父项目搭建
seatatest是父项目,主要是做一些依赖管理,依赖版本管理,管理所有子module项目;
注意,它的packaging
类型是pom
pom.xml:
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.java1234</groupId>
<artifactId>seatatest</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>seata-common</module>
<module>seata-order</module>
<module>seata-account</module>
<module>seata-web</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<druid.version>1.1.10</druid.version>
<spring-cloud.version>Hoxton.SR8</spring-cloud.version>
<springboot.version>2.3.2.RELEASE</springboot.version>
<springcloudalibaba.version>2.2.4.RELEASE</springcloudalibaba.version>
<fastjson.version>1.2.35</fastjson.version>
<commons-lang3.version>3.6</commons-lang3.version>
<seata-common.version>1.0-SNAPSHOT</seata-common.version>
<mybatis.version>2.1.0</mybatis.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${springboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${springcloudalibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>com.java1234</groupId>
<artifactId>seata-common</artifactId>
<version>${seata-common.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.4 seata-common子项目搭建
seata-common
子项目,主要是引入其他子项目需要的公共依赖,以及公共实体,工具类,配置类的统一封装;
项目结构:
pom.xml:
<?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>seatatest</artifactId>
<groupId>com.java1234</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-common</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<!-- spring boot redis 缓存引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lettuce pool 缓存连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
</project>
订单实体Order
:
package com.java1234.entity;
/**订单表实体
* @author java1234_小锋
* @site www.java1234.com
* @company 南通小锋网络科技有限公司
* @create 2021-07-13 10:26
*/
public class Order {
private Integer id; // 编号
private String orderNo; // 订单号
private Integer userId; // 用户编号
private Integer count; // 购买数量
private Integer amount; // 购买金额
private String remark; // 备注
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getOrderNo() {
return orderNo;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Integer getAmount() {
return amount;
}
public void setAmount(Integer amount) {
this.amount = amount;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}
账户实体Account
:
package com.java1234.entity;
/**
* 用户账户表
* @author java1234_小锋
* @site www.java1234.com
* @company 南通小锋网络科技有限公司
* @create 2021-07-13 10:36
*/
public class Account {
private Integer id; // 编号
private Integer userId; // 用户编号
private Integer balance; // 账户余额
private String remark; // 备注
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getUserId() {
return userId;
}
public void setUserId(Integer userId) {
this.userId = userId;
}
public Integer getBalance() {
return balance;
}
public void setBalance(Integer balance) {
this.balance = balance;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}
2.5 seata-order子项目搭建
seata-order
子项目,主要提供订单服务,生成订单;
项目结构:
pom.xml
:
<?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>seatatest</artifactId>
<groupId>com.java1234</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-order</artifactId>
<dependencies>
<dependency>
<groupId>com.java1234</groupId>
<artifactId>seata-common</artifactId>
</dependency>
</dependencies>
</project>
application.yml
:
server:
port: 8081
servlet:
context-path: /
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_order?serverTimezone=Asia/Shanghai
username: root
password: 123456
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: seata-order
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml
启动类OrderApplication
:
package com.java1234;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@MapperScan("com.java1234.mapper")
@EnableDiscoveryClient
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
OrderMapper.xml
:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.java1234.mapper.OrderMapper">
<select id="createOrder" parameterType="com.java1234.entity.Order" >
insert into t_order values(null,#{orderNo},#{userId},#{count},#{amount},#{remark})
</select>
</mapper>
OrderMapper
接口:
package com.java1234.mapper;
import com.java1234.entity.Order;
/**
* @author java1234_小锋
* @site www.java1234.com
* @company 南通小锋网络科技有限公司
* @create 2021-07-13 10:43
*/
public interface OrderMapper {
/**
* 创建订单
* @param order
*/
void createOrder(Order order);
}
OrderService
接口:
package com.java1234.service;
import com.java1234.entity.Order;
/**
* 订单service接口
* @author java1234_小锋
* @site www.java1234.com
* @company 南通小锋网络科技有限公司
* @create 2021-07-14 11:00
*/
public interface OrderService {
/**
* 创建订单
* @param order
*/
void createOrder(Order order);
}
OrderServiceImpl
实现类:
package com.java1234.service.impl;
import com.java1234.entity.Order;
import com.java1234.mapper.OrderMapper;
import com.java1234.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author java1234_小锋
* @site www.java1234.com
* @company 南通小锋网络科技有限公司
* @create 2021-07-14 11:02
*/
@Service("orderService")
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Override
public void createOrder(Order order) {
orderMapper.createOrder(order);
}
}
OrderController
:
package com.java1234.controller;
import com.java1234.entity.Order;
import com.java1234.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
/**
* @author java1234_小锋
* @site www.java1234.com
* @company 南通小锋网络科技有限公司
* @create 2021-07-14 11:13
*/
@RestController
@RequestMapping("/seata")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 创建订单
* @param order
* @return
*/
@PostMapping("/createOrder")
public boolean createOrder(@RequestBody Order order){
System.out.println("order:"+order);
order.setOrderNo(UUID.randomUUID().toString()); // 生成订单ID
orderService.createOrder(order);
return true;
}
}
2.6 seata-account子项目搭建
seata-account
子项目,主要提供账户服务,根据订单扣钱操作;
项目结构:
pom.xml
:
<?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>seatatest</artifactId>
<groupId>com.java1234</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-account</artifactId>
<dependencies>
<dependency>
<groupId>com.java1234</groupId>
<artifactId>seata-common</artifactId>
</dependency>
</dependencies>
</project>
application.yml
:
server:
port: 8082
servlet:
context-path: /
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_account?serverTimezone=Asia/Shanghai
username: root
password: 123456
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: seata-account
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml
AccountApplication
启动类:
package com.java1234;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@MapperScan("com.java1234.mapper")
@EnableDiscoveryClient
public class AccountApplication {
public static void main(String[] args) {
SpringApplication.run(AccountApplication.class, args);
}
}
AccountMapper.xml
:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.java1234.mapper.AccountMapper">
<select id="decrease" parameterType="Map" >
UPDATE t_account SET balance=balance-#{amount} WHERE userId=#{userId}
</select>
</mapper>
AccountMapper
接口:
package com.java1234.mapper;
import java.util.Map;
/**
* 账户Mapper接口
* @author java1234_小锋
* @site www.java1234.com
* @company 南通小锋网络科技有限公司
* @create 2021-07-13 10:43
*/
public interface AccountMapper {
/**
* 账户扣钱
*/
void decrease(Map map);
}
AccountService
接口:
package com.java1234.service;
import java.util.Map;
/**
* 账户service接口
* @author java1234_小锋
* @site www.java1234.com
* @company 南通小锋网络科技有限公司
* @create 2021-07-15 12:58
*/
public interface AccountService {
/**
* 账户扣钱
*/
void decrease(Map map);
}
AccountServiceImpl
接口实现类:
package com.java1234.service.impl;
import com.java1234.mapper.AccountMapper;
import com.java1234.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* 账户Service实现类
* @author java1234_小锋
* @site www.java1234.com
* @company 南通小锋网络科技有限公司
* @create 2021-07-15 13:00
*/
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public void decrease(Map map) {
accountMapper.decrease(map);
}
}
AccountController
:
package com.java1234.controller;
import com.java1234.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 账户Controller
* @author java1234_小锋
* @site www.java1234.com
* @company 南通小锋网络科技有限公司
* @create 2021-07-15 13:06
*/
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private AccountService accountService;
/**
* 给指定用户账户扣钱
* @param amount
* @param userId
* @return
*/
@PostMapping("/decrease")
public boolean decrease(@RequestParam("amount")Integer amount, @RequestParam("userId")Integer userId){
System.out.println("amount:"+amount+",userId:"+userId);
Map<String,Object> map=new HashMap<>();
map.put("amount",amount);
map.put("userId",userId);
accountService.decrease(map);
return true;
}
}
2.7 seata-web子项目搭建
seata-web
子项目,主要处理客户端下单请求,feign远程调用order,和account服务接口,最终完成下单处理;
项目结构:
pom.xml
:
<?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>seatatest</artifactId>
<groupId>com.java1234</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seata-web</artifactId>
<dependencies>
<dependency>
<groupId>com.java1234</groupId>
<artifactId>seata-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
</project>
application.yml
:
server:
port: 80
servlet:
context-path: /
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: seata-web
WebApplication
:
package com.java1234;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
@EnableFeignClients(basePackages = "com.java1234.feign")
@EnableDiscoveryClient
public class WebApplication {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
}
}
OrderFeignService
:
package com.java1234.feign;
import com.java1234.entity.Order;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* 订单接口feign远程调用
* @author java1234_小锋
* @site www.java1234.com
* @company 南通小锋网络科技有限公司
* @create 2021-07-16 10:55
*/
@FeignClient("seata-order")
public interface OrderFeignService {
/**
* 创建订单
* @param order
* @return
*/
@PostMapping("/seata/createOrder")
public boolean createOrder(@RequestBody Order order);
}
AccountFeignService
:
package com.java1234.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* 账号接口feign远程调用
* @author java1234_小锋
* @site www.java1234.com
* @company 南通小锋网络科技有限公司
* @create 2021-07-16 10:55
*/
@FeignClient("seata-account")
public interface AccountFeignService {
/**
* 账号扣钱
* @param amount
* @param userId
* @return
*/
@PostMapping("/account/decrease")
public boolean decrease(@RequestParam("amount")Integer amount, @RequestParam("userId")Integer userId);
}
WebController
:
package com.java1234.controller;
import com.java1234.entity.Order;
import com.java1234.feign.AccountFeignService;
import com.java1234.feign.OrderFeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* web-rest接口
* @author java1234_小锋
* @site www.java1234.com
* @company 南通小锋网络科技有限公司
* @create 2021-07-15 16:32
*/
@RestController
public class WebController {
@Autowired
private OrderFeignService orderFeignService;
@Autowired
private AccountFeignService accountFeignService;
/**
* 下单 1,创建订单 2,账户扣钱
* @param order
* @return
*/
@PostMapping("/shopping")
public boolean shopping(Order order){
orderFeignService.createOrder(order); // 创建订单
accountFeignService.decrease(order.getAmount(),order.getUserId()); // 账户扣钱
return true;
}
}
2.8 postman测试
首先启动Nacos服务注册中心:
项目启动:
Nacos控制台:
服务注册成功!
测试接口:http://localhost/shopping
接口测试OK:
订单表生成订单:
账户表id=1的jack账户成功扣减180元;
4 分布式事务问题模拟
当多个服务调用过程中,其中一个或者多个服务执行失败,出现异常情况的时候,导致数据不一致性,这样就出现了分布式事务问题;
我们来模拟下这个问题,我们运行过程中,让账户扣钱操作执行失败;
我们修改seata-account
里面AccountController
类的decrease
方法:
/**
* 给指定用户账户扣钱
* @param amount
* @param userId
* @return
*/
@PostMapping("/decrease")
public boolean decrease(@RequestParam("amount")Integer amount, @RequestParam("userId")Integer userId)throws Exception{
System.out.println("amount:"+amount+",userId:"+userId);
if(userId==null || userId==1){
throw new Exception("模拟异常");
}
Map<String,Object> map=new HashMap<>();
map.put("amount",amount);
map.put("userId",userId);
accountService.decrease(map);
return true;
}
当userId=1的时候,模拟抛出异常,下面的就不执行;
数据库数据先恢复原来的,postman再测试:
出现 500异常;
这时候我们去查看数据库发现,t_order表有数据,订单已经生成了;
再看t_account表,数据没变化:
这样的话,就导致了业务上的数据不一致性问题,也就是分布式事务问题,我们需要解决这个问题;
5 分布式事务常见解决方案
5.1 CAP理论
CAP理论:一个分布式系统不可能同时满足一致性,可用性和分区容错性这个三个基本需求,最多只能同时满足其中两项
一致性( C ):数据在多个副本之间是否能够保持一致的特性。
可用性( A ):是指系统提供的服务必须一致处于可用状态,对于每一个用户的请求总是在有限的时间内返回结果,超过时间就认为系统是不可用的
分区容错性( P ):分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非整个网络环境都发生故障。
5.2 CAP定理的应用
放弃P(CA):如果希望能够避免系统出现分区容错性问题,一种较为简单的做法就是将所有的数据(或者是与事物先相关的数据)都放在一个分布式节点上,这样虽然无法保证100%系统不会出错,但至少不会碰到由于网络分区带来的负面影响
放弃A(CP):其做法是一旦系统遇到网络分区或其他故障时,那受到影响的服务需要等待一定的时间,应用等待期间系统无法对外提供正常的服务,即不可用
放弃C(AP):这里说的放弃一致性,并不是完全不需要数据一致性,是指放弃数据的强一致性,保留数据的最终一致性。
5.3 BASE理论
BASE是基本可用,软状态,最终一致性。是对CAP中一致性和可用性权限的结果,是基于CAP定理演化而来的,核心思想是即使无法做到强一致性,但每个应用都可以根据自身的业务特定,采用适当的方式来使系统达到最终一致性
5.4 2PC提交
- 二阶段提交协议是将事务的提交过程分成提交事务请求和执行事务提交两个阶段进行处理。
阶段1:提交事务请求
- 事务询问:协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应
- 执行事务:各参与者节点执行事务操作,并将Undo和Redo信息记入事务日志中
- 如果参与者成功执事务操作,就反馈给协调者Yes响应,表示事物可以执行,如果没有成功执行事务,就反馈给协调者No响应,表示事务不可以执行
- 二阶段提交一些的阶段一夜被称为投票阶段,即各参与者投票票表明是否可以继续执行接下去的事务提交操作
阶段二:执行事务提交
-
假如协调者从所有的参与者或得反馈都是Yes响应,那么就会执行事务提交。
-
发送提交请求:协调者向所有参与者节点发出Commit请求
-
事务提交:参与者接受到Commit请求后,会正式执行事务提交操作,并在完成提交之后放弃整个事务执行期间占用的事务资源
-
反馈事务提交结果:参与者在完成事物提交之后,向协调者发送ACK消息
-
完成事务:协调者接收到所有参与者反馈的ACK消息后,完成事务
中断事务
-
假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就中断事务。
-
发送回滚请求:协调者向所有参与者节点发出Rollback请求
-
事务回滚:参与者接收到Rollback请求后,会利用其在阶段一种记录的Undo信息执行事物回滚操作,并在完成回滚之后释放事务执行期间占用的资源。
-
反馈事务回滚结果:参与则在完成事务回滚之后,向协调者发送ACK消息
-
中断事务:协调者接收到所有参与者反馈的ACk消息后,完成事务中断
优缺点
-
原理简单,实现方便
-
缺点是同步阻塞,单点问题,脑裂,保守
5.5 3PC提交
-
三阶段提,也叫三阶段提交协议,是二阶段提交(2PC)的改进版本。
-
与两阶段提交不同的是,三阶段提交有两个改动点。引入超时机制。同时在协调者和参与者中都引入超时机制。在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
-
三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
5.6 TCC分布式事务
-
TCC是服务化的两阶段编程模型,其Try、Confirm、Cancel,3个方法均由业务编码实现
-
TCC要求每个分支事务实现三个操作:预处理Try,确认Confirm,撤销Cancel。
-
Try操作做业务检查及资源预留,
-
Confirm做业务确认操作
-
Cancel实现一个与Try相反的操作即回滚操作。
-
TM首先发起所有的分支事务Try操作,任何一个分支事务的Try操作执行失败,TM将会发起所有分支事务的Cancel操作,若Try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试。
TCC的三个阶段
-
Try阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirmy一起才能构成一个完整的业务逻辑
-
Confirm阶段是做确认提交,Try阶段所有分支事务执行成功后开始执行Confirm,通常情况下,采用TCC则认为Confirm阶段是不会出错的,即:只要Try成功,Confirm一定成功,若Confirm阶段真的出错,需要引入重试机制或人工处理
-
Cancel阶段是在业务执行错误需要回滚到状态下执行分支事务的取消,预留资源的释放,通常情况下,采用TCC则认为Cancel阶段也一定是真功的,若Cance阶段真的出错,需要引入重试机制或人工处理
-
TM事务管理器:TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM的角色,TM独立出来是为了公用组件,是为了考虑系统结构和软件的复用
-
TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条,用来记录事务上下文,追踪和记录状态,用于Confirm和cacel失败需要进行重试,因此需要实现幂等
TCC的三种异常处理情况
幂等处理
- 因为网络抖动等原因,分布式事务框架可能会重复调用同一个分布式事务中的一个分支事务的二阶段接口。所以分支事务的二阶段接口Confirm/Cancel需要能够保证幂等性。如果二阶段接口不能保证幂等性,则会产生严重的问题,造成资源的重复使用或者重复释放,进而导致业务故障。
- 对于幂等类型的问题,通常的手段是引入幂等字段进行防重放攻击。对于分布式事务框架中的幂等问题,同样可以祭出这一利器。
- 幂等记录的插入时机是参与者的Try方法,此时的分支事务状态会被初始化为INIT。然后当二阶段的Confirm/Cancel执行时会将其状态置为CONFIRMED/ROLLBACKED。
- 当TC重复调用二阶段接口时,参与者会先获取事务状态控制表的对应记录查看其事务状态。如果状态已经为CONFIRMED/ROLLBACKED,那么表示参与者已经处理完其分内之事,不需要再次执行,可以直接返回幂等成功的结果给TC,帮助其推进分布式事务。
空回滚
- 当没有调用参与方Try方法的情况下,就调用了二阶段的Cancel方法,Cancel方法需要有办法识别出此时Try有没有执行。如果Try还没执行,表示这个Cancel操作是无效的,即本次Cancel属于空回滚;如果Try已经执行,那么执行的是正常的回滚逻辑。
- 要应对空回滚的问题,就需要让参与者在二阶段的Cancel方法中有办法识别到一阶段的Try是否已经执行。很显然,可以继续利用事务状态控制表来实现这个功能。
- 当Try方法被成功执行后,会插入一条记录,标识该分支事务处于INIT状态。所以后续当二阶段的Cancel方法被调用时,可以通过查询控制表的对应记录进行判断。如果记录存在且状态为INIT,就表示一阶段已成功执行,可以正常执行回滚操作,释放预留的资源;如果记录不存在则表示一阶段未执行,本次为空回滚,不释放任何资源。
资源悬挂
- 问题:TC回滚事务调用二阶段完成空回滚后,一阶段执行成功
- 解决:事务状态控制记录作为控制手段,二阶段发现无记录时插入记录,一阶段执行时检查记录是否存在
TCC和2PC比较
- 2PC通常都是在跨库的DB层面,而TCC则在应用层面处理,需要通过业务逻辑实现,这种分布式事务的实现方式优势在于,可以让应用自己定义数据操作的粒度,使得降低锁冲突,提高吞吐量成为可能
- 而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现Try,confirm,cancel三个操作。此外,其实现难度也比较大,需要按照网络状态,系统故障的不同失败原因实现不同的回滚策略
5.7 消息队列实现可靠消息最终一致性
- 可靠消息最终一致性就是保证消息从生产方经过消息中间件传递到消费方的一致性
- RocketMQ主要解决了两个功能:本地事务与消息发送的原子性问题。事务参与方接收消息的可靠性
- 可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景,引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦
5.8 最大努力通知
最大努力通知与可靠消息一致性有什么不同
- 可靠消息一致性,发起通知方需要保证将消息发出去,并且将消息发送到接收通知方,消息的可靠性由发起通知方保证
- 最大努力通知,发起通知方尽最大的努力将业务处理结果通知为接收通知方,但是消息可能接收不到,此时需要接收通知方主动调用发起通知方的接口查询业务,通知可靠性关键在于接收通知方
两者的应用场景
- 可靠消息一致性关注的是交易过程的事务一致,以异步的方式完成交易
- 最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去
基于MQ的ack机制实现最大努力通知
- 利用MQ的ack机制由MQ向接收通知方发送消息通知,发起方将普通消息发送到MQ
- 接收通知监听MQ,接收消息,业务处理完成回应ACK
- 接收通知方如果没有回应ACK则MQ会重复通知,按照时间间隔的方式,逐步拉大通知间隔
- 此方案适用于内部微服务之间的通知,不适应与通知外部平台
方案二:增加一个通知服务区进行通知,提供外部第三方时适用
5.9 分布式事务方案对比分析
- 2PC 最大的一个诟病是一个阻塞协议。RM在执行分支事务后需要等待TM的决定,此时服务会阻塞锁定资源。由于其阻塞机制和最差时间复杂度高,因此,这种设计不能适应随着事务涉及的服务数量增加而扩展的需要,很难用于并发较高以及子事务生命周期较长的分布式服务中
- 如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面处理,需要通过业务逻辑来实现。这种分布式事务的优势在于,可以让应用自定义数据操作的粒度,使得降低锁冲突,提高吞吐量成为可能。而不足之处在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现三个操作。此外,其实现难度也比较大,需要按照网络状态,系统故障等不同失败原因实现不同的策略。
- 可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦,典型的场景:注册送积分,登陆送优惠券等
- 最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务,允许发起通知方业务处理失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都不会影响到接收通知方的后续处理,发起通知方需提供查询执行情况接口,用于接收通知方校对结果,典型的应用场景:银行通知,支付结果通知等。
6 seata原理详解
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
6.1 Seata术语
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
Seata管理分布式事务的典型生命周期
- TM要求TC开始新的全局事务。 TC生成表示全局事务的XID。
- XID通过微服务的调用链传播。
- RM将本地事务注册为XID到TC的相应全局事务的分支。
- TM要求TC提交或回滚XID的相应全局事务。
- TC在XID的相应全局事务下驱动所有分支事务,以完成分支提交或回滚。
分布式事务是由一批分支事务组成的全局事务,通常分支事务就是本地事务。
6.2 AT 模式
前提
- 基于支持本地 ACID 事务的关系型数据库。
- Java 应用,通过 JDBC 访问数据库。
整体机制
两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
写隔离
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
工作机制
以一个示例来说明整个 AT 分支的工作过程。
业务表:product
Field | Type | Key |
---|---|---|
id | bigint(20) | PRI |
name | varchar(100) | |
since | varchar(100) |
AT 分支事务的业务逻辑:
update product set name = 'GTS' where name = 'TXC';
一阶段
过程:
- 解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
- 查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。
select id, name, since from product where name = 'TXC';
得到前镜像:
id | name | since |
---|---|---|
1 | TXC | 2014 |
- 执行业务 SQL:更新这条记录的 name 为 ‘GTS’。
- 查询后镜像:根据前镜像的结果,通过 主键 定位数据。
select id, name, since from product where id = 1;
得到后镜像:
id | name | since |
---|---|---|
1 | GTS | 2014 |
- 插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到
UNDO_LOG
表中。
{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}
- 提交前,向 TC 注册分支:申请
product
表中,主键值等于 1 的记录的 全局锁 。 - 本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
- 将本地事务提交的结果上报给 TC。
二阶段-回滚
- 收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
- 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:
update product set name = 'TXC' where id = 1;
- 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
二阶段-提交
- 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
- 异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
附录
回滚日志表
UNDO_LOG Table:不同数据库在类型上会略有差别。
以 MySQL 为例:
Field | Type |
---|---|
branch_id | bigint PK |
xid | varchar(100) |
context | varchar(128) |
rollback_info | longblob |
log_status | tinyint |
log_created | datetime |
log_modified | datetime |
-- 注意此处0.7.0+ 增加字段 context
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;
6.3 TCC 模式
回顾总览中的描述:一个分布式的全局事务,整体是 两阶段提交 的模型。全局事务是由若干分支事务组成的,分支事务要满足 两阶段提交 的模型要求,即需要每个分支事务都具备自己的:
- 一阶段 prepare 行为
- 二阶段 commit 或 rollback 行为
根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 Manual (Branch) Transaction Mode.
AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库:
- 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
- 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
- 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
- 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
- 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
6.4 Saga 模式
Saga模式是SEATA提供的长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
理论基础:Hector & Kenneth 发表论⽂ Sagas (1987)
适用场景:
- 业务流程长、业务流程多
- 参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口
优势:
- 一阶段提交本地事务,无锁,高性能
- 事件驱动架构,参与者可异步执行,高吞吐
- 补偿服务易于实现
缺点:
- 不保证隔离性(应对方案见用户文档)
7 应用seata AT模式方案解决分布式事务问题
seata主推的是AT模式强一致性解决方案,所以我们采用这个方案来解决前面案例的分布式问题。
7.1 下载和运行seata server
seata server地址:https://github.com/seata/seata/releases
最新版本 v1.4.2
我们用windows演示,下载seata-server-1.4.2.zip
seata-server配置文件:
seata server所有的配置都在conf文件夹内,该文件夹内有两个文件我们必须要详细介绍下。
seata server默认使用file(文件方式)进行存储事务日志、事务运行信息,我们可以通过-m db脚本参数的形式来指定,目前仅支持file、db这两种方式。
file.conf
该文件用于配置存储方式、透传事务信息的NIO等信息,默认对应registry.conf文件内的file方式配置。
registry.conf
seata server核心配置文件,可以通过该文件配置服务注册方式、配置读取方式。
注册方式目前支持file 、nacos 、eureka、redis、zk、consul、etcd3、sofa等方式,默认为file,对应读取file.conf内的注册方式信息。
读取配置信息的方式支持file、nacos 、apollo、zk、consul、etcd3等方式,默认为file,对应读取file.conf文件内的配置。
file.conf
我们加一个service
配置:
service {
#vgroup->rgroup 服务分组 集群分组默认为"default"
vgroupMapping.my_test_tx_group = "default"
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support 服务降级达到多次错误不走seata
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
这里事务分组概念,我们最后单独讲,主要是为了集群故障的时候,能快速切换,实现高可用;
配置分组映射,集群分组再配置grouplist,暴露server地址,提供给seata-client调用;
最后我们运行bin目录下的seata-server.bat启动seata-server:
启动OK,监听端口8091
7.2 每个数据库都建一个undo_log回滚日志表
根据seata AT模式规范,我们新建undo_log表,db_account和db_order都要建;
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=3 DEFAULT CHARSET=utf8
7.3 项目里添加seata-client maven依赖
我们只需要在seata-common公共模块项目里添加seata依赖,其他子模块都有了,方便;
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
7.4 项目里配置seata分布式事务
首先是每个项目都要配置seata支持,请求seata-server,根据配置;
再通过@GlobalTransactional注解,开启全局分布式事务;
seata-order子项目 application.yml配置改下:
server:
port: 8081
servlet:
context-path: /
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_order?serverTimezone=Asia/Shanghai
username: root
password: 123456
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alibaba:
seata:
tx-service-group: my_test_tx_group
application:
name: seata-order
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml
seata:
service:
vgroup-mapping:
my_test_tx_group: default
grouplist:
default: 127.0.0.1:8091
enable-degrade: false
disable-global-transaction: false
主要多了服务分组和集群分组的配置:
同理,seata-account下的application.yml配置:
server:
port: 8082
servlet:
context-path: /
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_account?serverTimezone=Asia/Shanghai
username: root
password: 123456
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alibaba:
seata:
tx-service-group: my_test_tx_group
application:
name: seata-account
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml
seata:
service:
vgroup-mapping:
my_test_tx_group: default
grouplist:
default: 127.0.0.1:8091
enable-degrade: false
disable-global-transaction: false
seata-web下的application.yml配置:
server:
port: 80
servlet:
context-path: /
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
alibaba:
seata:
tx-service-group: my_test_tx_group
application:
name: seata-web
seata:
service:
vgroup-mapping:
my_test_tx_group: default
grouplist:
java1234-seata-server: 127.0.0.1:8091
enable-degrade: false
disable-global-transaction: false
seata-web项目->WebController->shopping方法,添加@GlobalTransactional注解
7.5 分布式案例测试
三个子项目一起运行;
两个RM在seata-server注册成功;
seata-web项目的RM,TM注册成功;
seata-order项目的TM注册成功;
seata-account项目的TM注册成功;
我们用postman 发送 http://localhost/shopping post请求
执行发现,订单表,和日志表都没有数据,说明测试OK;
7.6 setata二阶段提交过程剖析
因为是执行是一瞬间的,很多小伙伴根本无法感受到内部的二阶段提交过程。
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
所以我们在再seata-account项目里搞个断点,来看下回滚的一个流程;
seata-account项目debug启动;
再次postman测试,我们立刻查看db_order数据库表信息(比较难捕获,因为提交事务超时就立刻回滚,看不到信息)
订单服务执行完的时候,
db_order有数据
undo_log也有数据
说明一阶段业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
然后下面,如果提交事务超时或者有接口调用异常,则分支事务立刻根据回滚日志,立刻进行反向补偿,最后回滚日志会被清空;
如果接口调用一切正常,日志清空,全局事务提交,完成本次事务操作;
8 seata事务日志mysql持久化配置
seata默认事务支持是file文件存储,不怎么好,不方便查看和管理;所以我们一般是db存储;
修改conf下的file.conf配置文件
把mode改成"db",
然后下方mysql配置改成你们对应的配置即可,数据库要自己先建立,比如我这边定义是db_seata
还有三个表,global_table,branch_table,lock_table分别是全局事务会话表,分支事务会话表,锁数据表;
建表语句获取:https://gitee.com/dhb414/seata/blob/master/script/server/db/mysql.sql#
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
因为都是临时数据,最终提交事务会被清理,我们要看数据的话,还是需要服务代码里打断点,debug进行捕获;
下面这个是锋哥捕获到的一个数据;
9 nacos注册中心和配置中心支持
Seata支持注册服务到Nacos,以及支持Seata所有配置放到Nacos配置中心,在Nacos中统一维护;
具体步骤如下:
9.1 项目里添加nacos-client依赖
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.4.0</version>
</dependency>
9.2 集成Nacos注册中心支持
项目里 application.yml
添加注册信息配置
seata:
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group : "SEATA_GROUP"
namespace: ""
username: "nacos"
password: "nacos"
seata-server端配置注册中心
在 registry.conf
中加入对应配置中心
registry {
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
}
9.3 集成Nacos配置中心支持
在 application.yml
中加入对应的配置中心
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group : "SEATA_GROUP"
namespace: ""
username: "nacos"
password: "nacos"
在 registry.conf
中加入对应配置中心
config {
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
username = "nacos"
password = "nacos"
}
}
因为我们Nacos没有开启认证功能,所有不需要用户名密码,最终配置如下:
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace: ""
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: "SEATA_GROUP"
namespace: ""
作为Nacos配置中心,我们需要把seata的一些配置上传到Nacos,配置比较多,官方给了我们一个 config.txt
我们修改后,上传到Nacos。
config.txt
下载地址:https://github.com/seata/seata/blob/develop/script/config-center/config.txt
我们做一些修改:
修改好的文件,放seata目录下:
之后运行仓库中提供的nacos脚本,将信息提交到nacos控制台,如果有需要更改,可直接通过控制台更改
脚本地址:https://github.com/seata/seata/blob/develop/script/config-center/nacos/nacos-config.sh
我们把 nacos-config.sh
文件放到 conf
目录下
右击鼠标,选 Git Bash Here
我们执行命令
sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP
完整命令是:
sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 0af6e97b-a684-4647-b696-7c6d42aecce7 -u nacos -w nacos
注:命令解析:-h -p 指定nacos的端口地址;-g 指定配置的分组,注意,是配置的分组;-t 指定命名空间id; -u -w指定nacos的用户名和密码,同样,这里开启了nacos注册和配置认证的才需要指定。
等执行完:
我们会发现Nacos配置中心就有seata的配置了;
这样,我们的seata配置就可以在nacos维护了,很方便;
9.4 测试
我们要重新启动seata-server,以及重启三个项目,用postman测试,测试OK;
10 seata事务分组介绍
事务分组是什么?
事务分组是seata的资源逻辑,类似于服务实例。在file.conf中的my_test_tx_group就是一个事务分组。
通过事务分组如何找到后端集群?
- 首先程序中配置了事务分组(GlobalTransactionScanner 构造方法的txServiceGroup参数)
- 程序会通过用户配置的配置中心去寻找service.vgroupMapping .[事务分组配置项],取得配置项的值就是TC集群的名称
- 拿到集群名称程序通过一定的前后缀+集群名称去构造服务名,各配置中心的服务名实现不同
- 拿到服务名去相应的注册中心去拉取相应服务名的服务列表,获得后端真实的TC服务列表
为什么这么设计,不直接取服务名?
这里多了一层获取事务分组到映射集群的配置。这样设计后,事务分组可以作为资源的逻辑隔离单位,出现某集群故障时可以快速failover,只切换对应分组,可以把故障缩减到服务级别,但前提也是你有足够server集群。
11 配套原创Seata视频教程+源码+文档领取
假如看帖子还有点吃力的话,锋哥专门录制了一套配套视频教程,附带配套源码文档;
视频教程高清1920*1080 非A&V画质;
锋哥视频一贯风格,通俗易懂,无废话,不浪费大家时间和精力,省点时间晚上去把妹!
如需【 seata视频教程+文档+源码 】可以关注【 java1234 】微信公众号,关注后回复【seata】领取
不会上面操作的,可以加锋哥WX:【 java3459 】 领取
感谢CSDN和CSDN小伙伴的支持,下期更精彩,敬请期待!
记得点点锋哥的CSDN博客【关注】哦!
更多推荐
所有评论(0)