一、分布式事务
1.1 分布式事务问题
在分布式系统,跨越多个服务和数据源,每个服务都是分支事务,要保证所有分支事务的最终一致性。
1.2 CAP理论
- 一致性(consistency)
分布式的数据一致性。- 可用性(availability)
微服务里的一个服务是否可以提供服务。- 分区容错(partition)
当集群节点出现网络连接失败,数据无法同步,如果保证可用性,就无法保证数据一致性,反之则无法保证可用性。
1.3 AP和CP
- AP
AP保证可用性,可能会出现临时数据不一致的情况,但会通过补偿数据来弥补,达到最终一致性。- CP
保证数据一致性,但是服务可能会临时不可用,但是基本可用。
二、Seata架构
2.1 Seata中的三个角色
- 事务协调者(TC)
维护全局和分支事务的状态,协调全局事务的提交与回滚。- 事务管理器(TM)
定义全局事务的范围,开启全局事务、提交和回滚事务。- 资源管理器(RM)
管理分支事务的资源,与TC交谈以注册和报告分支事务的状态,以驱动分支事务提交或回滚。
2.2 配置Seata
2.2.1 修改Seata-server下的conf的registry.conf
2.2.2 在nacos下配置seataServer.properties
#数据存储方式,db代表数据库
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
#事务、日志等配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
#客户端与服务端传输方式
transport.serialization=seata
transport.compressor=none
#关闭metrics功能,提高性能
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
2.2.2 创建seata表,执行sql
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for branch_table
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` tinyint(4) NULL DEFAULT NULL,
`client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of branch_table
-- ----------------------------
-- ----------------------------
-- Table structure for global_table
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`timeout` int(11) NULL DEFAULT NULL,
`begin_time` bigint(20) NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of global_table
-- ----------------------------
-- ----------------------------
-- Records of lock_table
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 1;
2.2.3 进入到bin目录执行命令seata-server.bat
2.3 微服务集成seata
2.3.1 引入seata相关依赖
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency> //1.4.2
2.3.2 配置yml,对应信息在之前的conf文件中
seata:
registry:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: ""
group: DEFAULT_GROUP
application: seata-tc-server
username: nacos
password: nacos
tx-service-group: seata-demo # 事务组名称
service:
vgroup-mapping:
seata-demo: SH
# data-source-proxy-mode: XA
# config:
# type: nacos
# nacos:
# server-addr: 127.0.0.1:8848
# username: nacos
# password: nacos
# group: SEATA_GROUP
# data-id: seataServer.properties
确认一个TC是要根据namespace、group、service(applicaitonname)、cluster。
三、 实践
3.1 XA模式(强一致)
3.2.1修改applicaiton.yml文件
seata:
data-source-proxy-mode: XA #数据源代理
3.2.2 给全局事务的入口方法添加@GlobalTransactionl注解
3.2.3 库存不足实现回滚
3.2 AT模式(最终一致)
AT模式的TM调用事务分支后,RM会先保存数据快照undo-log后执行sql并提交,然后报告事务的状态,如果TC判断全部提交,就可以删除log,如果失败,就可以基于log恢复数据,再删除log
3.2.1 AT模式的脏写问题
如果在第一个事务进入并提交,第二个事务进入也提交,并且成功。而此时第一个事务需要回滚,使得更新数据丢失。
AT模式采用全局锁来解决写隔离,通过线程idxid和表以及行数确定这行数据的锁被谁持有。需要在事务提交之前获取全局锁。
但是如果两个线程同时进来,一个需要db锁进行回滚,一个需要全局锁,造成死锁,通过获取全局锁设置时间间隔30ms,重试10次,获取不到就释放db锁进行数据库事务回滚,第一个事务就可以获取db锁就行数据恢复。
3.3 Seata实现AT模式
3.3.1 数据库导入
将lock_table导入TC服务关联的数据库,将undo_log表导入微服务关联的数据库。
DROP TABLE IF EXISTS
undo_log
;
CREATE TABLEundo_log
(
branch_id
bigint(20) NOT NULL COMMENT ‘branch transaction id’,
xid
varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘global transaction id’,
context
varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘undo_log context,such as serialization’,
rollback_info
longblob NOT NULL COMMENT ‘rollback info’,
log_status
int(11) NOT NULL COMMENT ‘0:normal status,1:defense status’,
log_created
datetime(6) NOT NULL COMMENT ‘create datetime’,
log_modified
datetime(6) NOT NULL COMMENT ‘modify datetime’,
UNIQUE INDEXux_undo_log
(xid
,branch_id
) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = ‘AT transaction mode undo table’ ROW_FORMAT = Compact;
DROP TABLE IF EXISTS
lock_table
;
CREATE TABLElock_table
(
row_key
varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
xid
varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
transaction_id
bigint(20) NULL DEFAULT NULL,
branch_id
bigint(20) NOT NULL,
resource_id
varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
table_name
varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
pk
varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
gmt_create
datetime NULL DEFAULT NULL,
gmt_modified
datetime NULL DEFAULT NULL,
PRIMARY KEY (row_key
) USING BTREE,
INDEXidx_branch_id
(branch_id
) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
3.3.2 yml配置的模式改为AT
3.3.3 AT和XT的区别
XT是在一阶段不提交,利用事务本身特性来进行隔离,AT是一阶段提交,利用全局锁来实现事务的隔离。
3.4 TCC模式
TCC模式与AT类似,每阶段都是独立事务。但是需要实现三个方法
- Try
资源的检测和预留- Confirm
完成资源的操作业务;要求Try成功,Confirm一定能成功- Cancel
预留资源的释放,为try的反向操作。
- 幂等性
如果Confirm或者Concel失败了,seata会帮我们做重试,但是我们要保证结果的幂等性。- 空回滚和业务悬挂
当有两个线程调用事务分支,第一个线程锁定资源,第二个事务超时了,这个业务出错了造成全局事务超时需要回滚,第一个事务回滚了,而第二个事务没有锁定资源就回回滚(会报错,然后一直重试),这时候不能进行回滚,就需要进行空回滚。
当执行了空回滚后,如果之后执行try,就永远不可能confirm和cancel,出现业务悬挂,应当阻止空回滚后的try操作。- 空回滚和业务悬挂问题可以通过记录当前的状态state,如果事务没有try锁定资源进行cancel空回滚,我们可以通过一张表记录状态,如果try锁定资源就增加一条数据,状态为try,并且锁定对应的资源freeze_money ,如果进行空回归,在try中查不到数据就直接退出。
- 幂等性可以对冻结资源进行判断,如果状态是CANCEL,那么就已经回滚过了,不需要在进行回滚,保证幂等性。
3.4.3 Seata实现TCC模式
@Override
@Transactional
public void deduct(String userId, int money) {
// 0.获取事务id
String xid = RootContext.getXID();
//解决业务悬挂
if(freezeMapper.selectById("id") != null){
return;
}
// 1.扣减可用余额
accountMapper.deduct(userId, money);
// 2.记录冻结金额,事务状态
AccountFreeze freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezeMoney(money);
freeze.setState(AccountFreeze.State.TRY);
freeze.setXid(xid);
freezeMapper.insert(freeze);
}
@Override
public boolean confirm(BusinessActionContext ctx) {
// 1.获取事务id
String xid = ctx.getXid();
// 2.根据id删除冻结记录
int count = freezeMapper.deleteById(xid);
return count == 1;
}
@Override
public boolean cancel(BusinessActionContext ctx) {
// 0.查询冻结记录
String xid = ctx.getXid();
AccountFreeze freeze = freezeMapper.selectById(xid);
String userId = ctx.getActionContext("userId").toString();
//判断freeze是否为null,如果为null,则证明try没有执行,需要空回滚
if (freeze == null) {
freeze = new AccountFreeze();
freeze.setUserId(userId);
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
freeze.setXid(xid);
freezeMapper.insert(freeze);
return true;
}
//幂等性
if(freeze.getState() == AccountFreeze.State.CANCEL)
return true;
// 1.恢复可用余额
accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
// 2.将冻结金额清零,状态改为CANCEL
freeze.setFreezeMoney(0);
freeze.setState(AccountFreeze.State.CANCEL);
int count = freezeMapper.updateById(freeze);
return count == 1;
}
4、四种模式的区别