seata的官网链接
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
拿经典的下订单问题举例,用户下订单需要减商品库存、并且需要减少用户账户余额。若减少库存之后,调用减少用户账户余额方法时产生了错误,那么会使余额扣减失败,但此时库存已经减少,信息明显不正确。在传统的单体应用中,只对应一个数据库,下单时的减库存、减账户余额可以在一个事务内完成,因此可以解决上述问题。但是在分布式的环境下,这几个服务不止对应一个数据库,可能是一个服务对应于一个数据库。那么在这种情况下,原本的数据库事务就不能解决该问题,需要一个解决分布式事务的方案,那么seata就可以实现这样的一种操作。
XID: 全局唯一事务ID
TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
具体过程:
1.TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
2. XID在微服务调用链路的上下文中传播
3. RM想TC注册分支事务,将其纳入XID对应全局事务的管辖
4. TM向TC发起针对XID的全局提交或回滚决议
5. TC调度XID下管辖的全局分支事务完成提交或回滚请求
此处下载版本为1.3.0
seata下载链接
修改seata的conf目录下的 file.conf 文件
driverClassName = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata?serverTimezone=UTC"
user = "root"
password = "123456"
建立本地建立名为seata数据库
在seata库中建表
利用自带的db_store.sql建表,低版本在conf目录下就有该文件,高版本可以在github的parent目录下找,也可以直接用以下脚本创建:
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;
修改conf目录下的registry.conf文件,对其中的Nacos模块进行修改,将serverAddr修改为 localhost:8848
替换jar包,将mysql-connector-java包改为8.0版本的jar包,seata默认的jar包为5.7版本的,若本地MySQL为5.7则无需进行该项操作。
启动Nacos后启动Seata
建立三个数据库分别存储库存、订单、账户,每个数据库一张业务表,其次建立对应的回滚表。
建立数据库和数据表:
建立订单库和订单表
create database seata_order;
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(11) DEFAULT NULL,
`product_id` bigint(11) DEFAULT NULL,
`count` int(11) DEFAULT NULL,
`money` decimal(11,0) DEFAULT NULL,
`status` int(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
建立库存库和库存表
create database seata_store;
CREATE TABLE `t_store` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) DEFAULT NULL,
`total` int(11) DEFAULT NULL,
`used` int(11) DEFAULT NULL,
`residue` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `t_store` VALUES (1,1,100,50,50);
建立账户库和账户表
create database seata_account;
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(11) DEFAULT NULL,
`total` decimal(10,0) DEFAULT NULL,
`used` decimal(10,0) DEFAULT NULL,
`residue` decimal(10,0) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
INSERT INTO `t_account` VALUES (1,1,1000,700,300);
三个业务数据库都建立回滚表
该代码在
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 DEFAULT CHARSET=utf8;
以下的每一个模块都为一个springboot项目。
引入依赖
注意点就是引入seata依赖时,和自己下载的版本号匹配。
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
修改application.yml配置
server:
port: 2003
spring:
application:
name: seata-account-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
alibaba:
seata:
# 若之前修改成了自己配置的名称,该处则修改为 事务名_tx_group
tx-service-group: my_test_tx_group
datasource: # 链接数据库
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account?serverTimezone=UTC
username: root
password: 123456
feign:
hystrix:
enabled: true
logging:
level:
io:
seata: info
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.lk.alibaba.domain
编写实体类
@Data
public class Account {
private Long id;
private Long user_id;
private BigDecimal total;
private BigDecimal used;
private BigDecimal residue;
}
编写dao
@Mapper
public interface AccountDao {
//减少账户余额的方法
void decrease(@Param("user_id") Long user_id, @Param("money") BigDecimal money);
}
编写mapper.xml
<mapper namespace="com.lk.alibaba.dao.AccountDao">
<update id="decrease">
update t_account
set used=used+#{money},residue=residue-#{money}
where user_id=#{user_id}
update>
mapper>
编写service实现类(AccountService接口省略,只有一个decrease方法,因为设置了睡眠时间为20秒,所以通过feign调用该服务时会抛出异常。本方法作为事务失败的起点。
@Service
public class AccountServiceImpl implements AccountService {
@Resource
private AccountDao accountDao;
@Override
public void decrease(Long user_id, BigDecimal money) {
try {
TimeUnit.SECONDS.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountDao.decrease(user_id,money);
}
}
编写controller
@RestController
public class AccountController {
@Resource
private AccountService accountService;
@RequestMapping(value = "/account/decrease")
public String decrease(Long user_id, BigDecimal money){
accountService.decrease(user_id, money);
return “账户服务完成”;
}
}
编写主启动类
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class Seata2003Application {
public static void main(String[] args) {
SpringApplication.run(Seata2003Application.class,args);
}
}
将seata的配置文件file.conf和registry.conf放到resources目录下
库存模块
订单模块的编写与账号模块的编写基本相同。不同点主要在于application.yml配置中的数据库连接信息(改为seate_store库)、端口号(改为2002)、应用名(改为seata-store-service)。其余的实体类、dao、service、controller基本相同,也是只有一个减库存的方法。
订单模块
引入依赖
与账户模块相同
修改application.yml
和账户模块相同,不同点主要在于application.yml配置中的数据库连接信息(改为seate_order库)、端口号(改为2001)、应用名(改为seata-order-service)
创建实体类
@Data
public class Order {
private Long id;
private Long user_id;
private Long product_id;
private Integer count;
private BigDecimal money;
private Integer status;
}
编写dao
@Mapper
public interface OrderDao {
void add(Order order);
void update(@Param("user_id") Long user_id,@Param("status") Integer status);
}
编写mapper.xml
<mapper namespace="com.lk.alibaba.dao.OrderDao">
<insert id="add" parameterType="Order">
insert into t_order(user_id,product_id,count,money,status)
values(#{user_id},#{product_id},#{count},#{money},0);
insert>
<update id="update">
update t_order set status=1 where user_id=#{user_id} and status=#{status}
update>
mapper>
编写service
将store模块的业务和account模块的业务通过OpenFeign映射过来
@FeignClient(value = "seata-store-service")
public interface StoreService {
@PostMapping(value = "/store/decrease")
String decrease(@RequestParam("product_id") Long product_id,@RequestParam("count") Integer count);
}
@FeignClient(value = "seata-account-service")
public interface AccountService {
@GetMapping(value = "/account/decrease")
String decrease(@RequestParam("user_id") Long user_id,@RequestParam("money") BigDecimal money);
}
编写orderService(省略)的实现类
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private OrderDao orderDao;
@Resource
private StoreService storeService;
@Resource
private AccountService accountService;
@Override
@GlobalTransactional(name = "my-create-order",rollbackFor = Exception.class)
public void add(Order order) {
orderDao.add(order);
storeService.decrease(order.getProduct_id(),order.getCount());
accountService.decrease(order.getUser_id(),order.getMoney());
orderDao.update(order.getUser_id(),order.getStatus());
}
}
编写controller测试
@RestController
public class OrderController {
@Resource
private OrderService orderService;
@GetMapping(value = "/order/add")
public String create(Order order){
orderService.add(order);
return “添加成功”;
}
}
编写主启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class Seata2001Application {
public static void main(String[] args) {
SpringApplication.run(Seata2001Application.class,args);
}
}
将seata的配置文件file.conf和registry.conf放到resources目录下
在orderService中使用到了accountService和storeService,实现分布式事务只需要在orderService的方法上添加@GlobalTransactional(name = "my-create-order",rollbackFor = Exception.class)
即可实现分布式事务,其中name可以随便起,rollbackFor表示任何异常都会回滚事务。
浏览器输入http://localhost:2001/order/add?user_id=1&product_id=1&count=10&money=100
进行下单操作,此时发生了异常,观察三个业务数据库,会发现数据表的数据都没发生改变。
该案例中,seata服务器相当于TC,标记了@GlobalTransactional的方法即为TM,TM向TC发起全局事务的请求,该案例中的orderDao和accountService以及storeService对数据库进行操作,相当于是RM。在案例的基础上再理解以下逻辑: