Seata是由Alibaba开源的一个用于解决分布式事务问题的框架
Seata官网地址
Seata是由1+3
组成:
1
:Transaction ID【XID】:全局唯一的事务ID
3
:三个组件,TC(事务协调器)、TM(事务管理器)、RM(资源管理器)
班主任老师向授课老师传达上课请求,并且开启一个直播,直播链接就是XID
老师和同学在互相传播直播链接(XID)
同学们加入直播,受到直播老师的管辖
班主任老师,发现上课的人已经来齐了,给直播老师说"可以开始上课"
直播过程中,不认真听讲,玩手机的,直接踢出会议
①下载SeataServer:seata-server-0.9.0.zip
此处我使用0.9.0版本做示范,其他版本大同小异
GitHub下载地址:https://github.com/seata/seata/releases
②下载完成之后,将默认配置文件备份,然后修改配置文件
修改file.conf:
下面是我的配置
service {
#vgroup->rgroup
vgroup_mapping.zi_tx_group = "default" # 修改为自己的
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
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"
}
## database store
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
driver-class-name = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata?serverTimezone=UTC"
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
}
因为我是mysql8,所以采用8的驱动,如果大家不是的话,可以不用修改,并且前面file.conf里面数据库配置的驱动类也不用修改
-- the table to store GlobalSession data
drop table if exists `global_table`;
create table `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`)
);
-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
`branch_id` bigint not null,
`xid` varchar(128) not null,
`transaction_id` bigint ,
`resource_group_id` varchar(32),
`resource_id` varchar(256) ,
`lock_key` varchar(128) ,
`branch_type` varchar(8) ,
`status` tinyint,
`client_id` varchar(64),
`application_data` varchar(2000),
`gmt_create` datetime,
`gmt_modified` datetime,
primary key (`branch_id`),
key `idx_xid` (`xid`)
);
-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
`row_key` varchar(128) not null,
`xid` varchar(96),
`transaction_id` long ,
`branch_id` long,
`resource_id` varchar(256) ,
`table_name` varchar(32) ,
`pk` varchar(36) ,
`gmt_create` datetime ,
`gmt_modified` datetime,
primary key(`row_key`)
);
⑤启动本地nacos【如果本地没有下载的话,自行下载启动即可】
⑥双击bin目录下的seata-server.bat
表明启动成功
项目说明:
此处演示Seata使用,我们会创建三个服务,一个
订单服务
,一个库存服务
,一个账户服务
该操作跨越三个数据库,有两次远程调用(RPC),因此会涉及到分布式事务问题
业务需求:下订单 - 减库存 - 扣余额 - 改(订单)状态
①建库建表语句【项目中使用到的】
seata_order库及表:
CREATE DATABASE seata_order;
USE seata_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 CHARSET=utf8;
seata_storage库及表:
CREATE DATABASE seata_storage;
USE seata_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(11) DEFAULT NULL COMMENT '剩余库存'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
INSERT INTO t_storage(id, product_id, total, used, residue) VALUES(1,1,100,0,100);
seata_account库及表:
CREATE DATABASE seata_account;
USE seata_account;
CREATE TABLE t_account(
id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
user_id BIGINT(11) DEFAULT NULL COMMENT '用户id',
total DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
used DECIMAL(10,0) DEFAULT NULL COMMENT '已用额度',
residue DECIMAL(10,0) DEFAULT 0 COMMENT '剩余可用额度'
)ENGINE=InnoDB AUTO_INCREMENT=7 CHARSET=utf8;
INSERT INTO t_account(id, user_id, total, used, residue) VALUES(1,1,1000,0,1000);
②为所有数据库创建undo_log表,因为会涉及到事务回滚,方便记录
seata、seata_account、seata_order、seata_storage分别创建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,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
这几个模块的代码这里我就不贴出来了,我将代码提交到了gitee上
Gitee代码地址
注意:下载下来的是我手动添加了异常的代码,如果想要演示正常状态,去掉AccountServiceImpl中的
"TimeUnit.SECONDS.sleep(20);"即可
如果在测试过程中遇到什么问题了,欢迎提问
①数据库状态
用户的user_id为1,用户总金额为1000,剩余金额为1000
订单表中初始没有任何数据
产品的product_id为1,产品总数量为100,剩余库存为100
②发起请求【此时没有设置异常】
在浏览器中输入:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
表明创建订单,用户id为1,产品id为1,订单中产品数量为10,花了100元
浏览器结果:
库存表(t_storage):
订单表(t_order):
账户表(t_account):
模拟Feign超时异常,在AccountServiceImpl中添加超时异常:
@Service
public class AccountServiceImpl implements AccountService {
private static final Logger logger = LoggerFactory.getLogger(AccountServiceImpl.class);
@Autowired
private AccountDao accountDao;
/**
* 扣减账户余额
* @param userId
* @param money
*/
@Override
public void decrease(Long userId, BigDecimal money) {
logger.info("---->account-service扣减账户余额");
//模拟超时异常,全局事务回滚
//暂停几秒钟线程
try{
TimeUnit.SECONDS.sleep(20);
accountDao.decrease(userId, money);
logger.info("----->account-service扣减账户余额结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
再用浏览器发送一次请求:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
可以看到服务器报错:
此时数据库状态:
可以看到下面的订单表,订单状态为0,表明订单未完成
查看库存表与账户表:
此时发现库存和账户都更改了,也就是库存减了,钱扣了,但是订单却没有完成,这显然是不合理的
注意:
并且由于feign的重试机制,库存和账户可能都会扣减多次
@GlobalTransactional
只需要在我们的业务方法上添加即可,这样就可以保证全局事务一致
//创建订单:调用库存服务扣减库存 - 调用账户服务扣减账户余额 - 修改订单状态
@GlobalTransactional
@Override
public void create(Order order) {
log.info("----->开始新建订单");
//1 新建订单
orderDao.createOrder(order);
//2 扣减库存
log.info("----->调用storageService,扣减count");
storageService.decrease(order.getProductId(), order.getCount());
log.info("----->扣减count结束");
//3 扣减账户
log.info("----->调用storageService,扣减money");
accountService.decrease(order.getUserId(), order.getMoney());
log.info("----->扣减money结束");
//4 修改订单状态,从0-1,代表订单完成
log.info("----->修改订单状态");
orderDao.updateOrder(order.getUserId(), 0);
log.info("----->修改订单状态结束");
}
默认Seata使用的是AT模式
其他模式:TCC、Saga、XA模式
官网地址
我们之前在seata数据库中生成了以下几张表:
并且在另外三个数据库中seata_order、seata_account、seata_storage中生成了对应的undo_log
那么,这些表是干什么用的呢?
①我们首先将AccountServiceImpl中的睡眠代码注释掉,然后在AccountServiceImpl中打一个断点,所有服务debug启动,在浏览器中输入:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
然后,让断点停止在那个位置,此时访问数据库
②查看数据库状态
xid:全局事务id
resource_id:对应另外三个业务数据库
branch_type:AT
AT模式对业务无入侵:
保存原快照before image - 执行业务SQL - 保存新快照 after image
"Spring的AOP"
过程:业务SQL - 解析SQL语义 - 提取表元数据 - 【保存原快照before image - 执行业务SQL - 保存新快照 after image - 生成行锁】 - 提交业务SQL、undo/redo log、行锁
【1】前置SQL:before image
前置SQL:select age from t where id = 1; //结果:age=22
前置SQL查询出来之前的age为22
【2】业务SQL
业务SQL:update t set age = 28 where id = 1;
业务SQL修改年龄为28
【3】后置SQL:after image
后置SQL:select age form t where id = 1; //结果:age = 28
【4】假如此时业务出现了异常,需要回滚,则反向update,将age从28改为之前的22
所有的操作都记录在一张表中
注意:
如果此时反向update的时候发现age不是28了(脏写),则会转为人工处理【类比CAS,乐观锁】