一、Seata简介、安装
Seate是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务
官网:http://seata-io/zh-cn/
下载地址:http://seata.io/zh-cn/blog/download.html
1、一个典型的分布式事务过程
-
分布式事务处理过程的ID+三组件模型
-
全局唯一的事务ID
-
三组件概念
-
Transaction Corrdinator(
TC
)事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚 -
Transaction Manager(
TM
)事务管理者:定义全局事务的范围,开始全局事务,提交或回滚全局事务 -
Resource Manager(
RM
)资源管理者:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
-
-
-
处理过程
- TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
- XID在微服务调用链路的上下文中传播
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
- TM向TC发起针对XID的全局提交或回滚决议
- TC调度XID下管辖的全部分支事务完成提交或回滚请求
2、使用方法
- 本地@Transactional
全局@GlobalTransactional
seata的分布式交易解决方案
3、seata-server安装
1、官网地址
http://seata.io/zh-=ch下载seata-server-0.9.0.zip
2、seata-server-0.9.0.zip解压配置
-
先备份原始file.conf文件
-
主要修改:自定义事务组名称+事务日志存储模式为DB+数据库连接信息
-
file.conf
-
service模块
service{ vgroup_mapping.my_test_tx_group="fsp_tx_group" default.grouplist="127.0.0.1:8091" enableDegrade=false disable=false max.commit.retry.timeout="-1" max.rollback.retry.timeout="-1" }
-
store模块
## transaction log store store{ ##store mode: file、db mode="db" ##file store file{ dir = "sessionStore" ##分支session大小,如果超过了先尝试压缩lockkey,仍然超出抛出异常 max-branch-session-size = 16384 ##如果超过全局会话大小,则引发异常 max-global-session-size = 512 ##如果超过文件缓冲区大小,请分配新的缓冲区 file-write-buffer-cache-size = 16384 ##恢复批读取大小时 session.reload.read_size = 100 #async,sync } ##database store db{ ##所使用连接池 datasource = "dbcp" db-type = "mysql" driver-class-name = "com.mysql.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata" user = "root" password = "123456" min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } }
-
3、mysql数据库新建库seata
4、在数据库新建表
建表db_store.sql在\seata-server-0.9.0\seata\conf目录里
5、修改seata-server-0.9.0\seata\conf目录下registry.conf配置文件
registry{
#file、nacos、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos{
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
}
6、先启动Nacos在启动seata-server
二、seata原理简介
1、说明
0.9不支持集群,1.0以后支持,工作中还是要用1.0以后的版本
2、TC、TM、RM
分布式事务执行流程:
- TM开启分布式事务(TM向TC注册全局事务记录)
- 按业务场景,编排数据库、服务等事务内资源(RM向TC汇报资源准备状态);
- TM结束分布式事务,事务一阶段阶段(TM通知TC提交/回滚分布式事务)
- TC汇总事务信息,决定分布式事务时提交还是回滚
- TC通知所有RM提交/回滚资源,事务二阶段结束
3、AT模式如何做到对业务无侵入
AT模式
前提
- 基于支持本地ACID事务读关系型数据库
- Java应用,通过JDBC访问数据库
整体机制
两阶段提交协议的演变:
-
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
一阶段提交中,seata会拦截“业务SQL”
- 解析SQL语义,找到“业务SQL”要更新的业务数据,在业务数据被更新前,将其保存为“before image”
- 执行“业务SQL”更新业务数据,在业务数据更新之后
- 其保存成“after image”,最后生成行锁
以上操作全部在一个数据库事务内完成,这样保证了一阶段提交的原子性。
-
二阶段:
-
提交异步化,非常快速的完成
-
回滚通过一阶段的回滚日志进行反向补偿
- 二阶段提交如果时顺利提交的化
因为“业务SQL”在一阶段已经提交至数据库,所以seata框架只需将
一阶段保存的快照数据和行锁删掉,完成数据清理即可
-
二阶段回滚
二阶段如果时回滚的化,seata就需要回滚一阶段已经执行的“业务sql”,还原业务数据
回滚方式便是用“before image”还原业务数据,但是还原前首先要验证脏写,对比“数据库当前业务数据”和“after image”,如果两份数据完全一致说明没有脏写,可以还原数据,如果不一致就说明有脏写,出现脏写就需要转人工处理
-
4、debug
5、补充
三、测试订单、库存、账户业务数据库准备
1、分布式事务也业务说明
在这里我们会创建三个服务,一个订单服务,一个库存服务一个账户服务>
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减单商品的库存,在通过远程掉哦那个账户服务来扣减用户账户里面的月,最后在订单服务中修改订单状态为已完成
该操作跨越三个数据库,有两次远程调用,很明显的分布式事务问题
2、创建业务数据库
-
seate_order:存储订单的数据库
-
创建t_order表
create table 't_order'( 'id' bigint(11) not null auto_increment primary key, 'user_id' bigint(11) default null comment '用户id', 'product_id' bigint(11) default null comment '产品id', 'count' int(11) default null comment '数量', 'money' decimal(11,0) default null comment '金额', 'status' int(1) default null comment '订单状态:0创建中 1已完结' )engine=innodb auto_increment=7 defaule charset=utf8; select * from t_order;
-
-
seata_storage:存储库存的数据库
-
创建t_storage表
create table 't_storage'( 'id' bigint(11) not null auto_increment primary key, 'product_id' bigint(11) default null comment '产品id', 'total' int(11) default null comment '总库存', 'used' int(11) default null comment '已用库存', 'residue' int(1) default null comment '剩余库存' )engine=innodb auto_increment=2 defaule charset=utf8; insert into seata_storage.t_storage('id','product_id','total','used','residue') values('1','1','100','0','100'); select * from t_storage;
-
-
seate_account:存储账户信息的数据库
-
创建t_account表
create table 't_account'( 'id' bigint(11) not null auto_increment primary key, 'user_id' bigint(11) default null comment '用户id', 'total' int(11) default null comment '总额度', 'used' int(11) default null comment '已用余额', 'residue' int(1) default null comment '剩余可用额度' )engine=innodb auto_increment=2 defaule charset=utf8; insert into seate_account.t_account('id','user_id','used','residue') values('1','1','1000','0','1000'); select * from t_account;
-
3、按照上述三库分别建立对应的回滚日志表
订单-库存-账户3个库下都需要建各自的回滚日志表
\seata-server-0.9.0\seata\conf目录下的db_undo_log.sql
drop table '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' datatime not null,
'ext' varchar(100) default null,
primary key('id'),
unique key 'ux_undo_log' ('xid','branck_id')
)engine=innodb auto_increment=1 default charset=utf8;
3、最终结果展示
四、订单、库存、账户微服务准备
下订单—>减库存—>扣余额—>改订单状态
1、新建订单Order-Module
seate-order-service2001
1、添加pom配置
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io-seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>0.9.0</version>
</dependency>
<!--feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--web-actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2、添加yml配置
server:
port: 2001
spring:
application:
name: seata-order-service
cloud:
alibaba:
seata:
#自定义事务组名称需要与seata-server中的对应
tx-service-group: fsp_tx_group
nacos:
discovery:
server-addr: localhost:8848
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_order
username: root
password: 123456
feign:
hystrix:
enabled: false
3、file.conf
resources下创建文件file.conf,从seate安装包中赋值配置文件内容
service{
vgroup_mapping.my_test_tx_group="fsp_tx_group"
default.grouplist="127.0.0.1:8091"
enableDegrade=false
disable=false
max.commit.retry.timeout="-1"
max.rollback.retry.timeout="-1"
}
## transaction log store
store{
##store mode: file、db
mode="db"
##database store
db{
##所使用连接池
datasource = "dbcp"
db-type = "mysql"
driver-class-name = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "123456"
min-conn = 1
max-conn = 3
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
}
4、registry.conf
resources下创建文件registry.conf,从seata安装包下复制
registry{
#file、nacos、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos{
serverAddr = "localhost:8848"
namespace = ""
cluster = "default"
}
}
5、创建实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>{
private Integer code;
private String message;
private T data;
public CommonResult(Integer code,String message){
this(code,message,null);
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order{
private Long id;
private Long userId;
private Long productId;
private Integer count;
priate BigDecimal money;
/**
* 订单状态: 0:创建中 1:已完结
*/
private Integer status;
}
6、创建dao接口
@Mapper//xml文件省略
public interface OrderDao{
//新建订单
void create(order order);
//修改订单状态,从0改为1
void update(@Param("userId") Long userId,@Param("status") Integer status);
}
7、创建serice接口实现
@FeignClient(vlue="seata-storage-serice")
public interface StorageSerice{
@PostMapping(value="/storage/decrease")
CommonResult descrease(@RequestParam("productId")Long productId,
@RequestParam("count")Integer count);
}
@FeignClient(vlue="seata-account-serice")
public interface AccountSerice{
@PostMapping(value="/account/decrease")
CommonResult descrease(@RequestParam("userId")Long userId,
@RequestParam("money")BigDecimal money);
}
@Service
public class OrderSericeImpl implements OrderService{
@Resource
private OrderDao orderDao;
@Resource
private StorageSerice storageService;
@Resource
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户月->修改订单状态
*/
@Override
@GlobalTransactional(nam="fsp-create-order",rollbackFor=Exception.class)
public void create(Order order){
log.info("------->1、新建订单");
orderDao.create(order);
log.info("------->2、订单服务开始调用库存,做扣减count");
storageService.decrease(order.getProductId(),order.getCount());
log.info("------->3、订单微服务开始调用账户,做扣减余额");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("------->4、修改订单状态");
orderDao,update(order.getUserId(),1);
log.info("------->5、订单结束");
}
}
8、创建controller
@RestController
public class OrderController{
@Resource
private OrderService orderService;
@GetMapping("/order/create")
public CommonResult create(Order order){
orderService.create(order);
return new CommonResult(200,"订单创建完成");
}
}
9、主启动类
@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication
public class SeataOrderMainApp2001{
public static void main(String[] args){
SpringApplication.run(SeataOrderMainApp2001.class,args);
}
}
2、新建库存Storage-Module、账户Account-Module同上,省略
3、测试
1、正常下单
2、超时异常,没有加@GlobalTransaction
AccountServiceImpl添加超时
- 当库存和账户进饿扣减后,订单状态没有设置为已经完成,没有从0改为1
- 而且由于feign的重试机制,账户余额可能被多次扣减
3、超时异常,加了@GlobalTransaction
AccountServiceImpl添加超时,下单后数据库数据没有任何改变,分布式事务生效回滚了。