一、 分布式事务理论基础
1.1 分布式事务问题

导入学习资料里的项目代码:


执行学习资料里的.sql文件:





启动nacos和seata-demo项目中所有的微服务。

下面看一下业务逻辑:




使用postman进行测试:

http://localhost:8082/order?userId=user202103032042012&commodityCode=100202003032041&money=200&count=2
调用成功后:



再下购买10个商品的单:




说明,这些微服务的状态不一样。

库存服务出错,账户服务并不知道,账户服务执行完扣款操作后提交事务。
分布式事务:在分布式系统下,一个业务跨多个服务或数据源,每个服务都是一个分支事务,要保证所有分支事务最终状态一致。
1.2 理论基础
1.2.1 CAP定理
分布式系统有三个指标:
(1)一致性 Consistency
(2)可用性 Availability
(3)分区容错性 Partition tolerance
其中,分区 Partition:因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。
容错 Tolerance:在集群出现分区时,整个系统也要持续对外提供服务。

分布式系统无法同时满足这三个指标,这个结论就是CAP定理。比如,上图中node03与node01,node02间因网络故障无法进行数据同步,此时为了保障分区容错性和一致性,node03不再向外提供服务,这就违背了可用性。
1.2.2 BASE理论
Base理论是对CAP的一种解决思路,包含三个思想:
Basically Available(基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
Soft State(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态。
Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论:
(1)AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
(2)CP模式:各个子事务执行后相互等待,同时提交,同时回滚,达到强一致。但事务等待过程中,处于弱可用状态(锁定资源,导致无法访问)。
可以看出,上面两种模式,各个子事务之间需要通讯,因此需要一个事务协调者来协调每一个事务的参与者(子系统事务,称为分支事务)。有关联的各个分支事务在一起称为全局事务。

二、Seata
2.1 Seata的架构
首先来回顾一下事物的ACID特性:




2.2 部署seata的TC服务
(一)下载seata-server包
地址为:http://seata.io/zh-cn/blog/download.html
学习资料里也有:


(二)修改配置文件registry.conf



registry {
# 注册中心类型 file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
application = "seata-tc-server"
serverAddr = "127.0.0.1:8848"
group = "DEFAULT_GROUP"
namespace = ""
cluster = "SH"
username = "nacos"
password = "nacos"
}
}
config {
# 配置中心file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
(三)在nacos中添加配置文件seataServer.properties
seataServer.properties就是(二)中在registry.conf中指定的dataId。


# 数据存储方式,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=123
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
(四)创建数据库表
在(三)中指定的数据库地址创建。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 分支事务表
-- ----------------------------
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;
-- ----------------------------
-- 全局事务表
-- ----------------------------
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;
SET FOREIGN_KEY_CHECKS = 1;

(五)启动TC服务



2.3 微服务集成seata
以storage-service服务为例,其他服务进行相同配置即可。
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>
2.3.2 微服务注册到seata(修改配置文件)


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: # 事务组与cluster的映射关系
seata-demo: SH

再对order-service和account-service做相同操作。
三、Seata实践
3.1 XA模式
3.1.1 XA模式原理
把分布式事务定义为两个阶段,第一阶段为准备阶段,事务协调者(TC)向RM(XA标准中RM由数据库实现)发起准备请求,告知RM可以准备执行,执行完后不提交,将执行结果告知事务协调者(已就绪)。当事务协调者收到RM都成功执行的通知,第二阶段事务协调者告知RM可以提交了,RM提交事务,并将结果告知事务协调者。

当有RM告知事务协调者执行失败,则在第二阶段事务协调者告知执行成功的RM进行事务的回滚。

XA模式由数据库本身就可以实现。
3.1.2 seata的XA模式
Seata里多了TM的概念。

第一阶段:
1.1 TM先向TC注册全局事务
1.2 TM作为分布式事务的入口,去调用微服务
1.3 RM向TC注册分支事务
1.4 微服务执行业务sql(执行完不提交)
1.5 微服务执行完业务sql向TC报告事务状态
第二阶段:
2.1 TM 向TC提交/回滚全局事务
2.2 TC检查分支事务状态
2.3 TC向RM发起提交/回滚请求
XA模式优点:
具有强一致性;
容易实现(mysql数据库都支持),并且没有代码侵入。
缺点:
性能较差,一阶段的等待所有RM执行成功时占用数据库锁;
依赖关系型数据库实现事务。
3.1.3 代码实现XA模式

上图中,使用@GlobalTransactional注解后,方法中调用了其他微服务的地方会被注册为分支事务。
注意:第一个开启XA模式是每一个微服务项目都要添加的,第二部添加注解只有调用者需要添加。
(一)给每个微服务开启XA模式

data-source-proxy-mode: XA


(二)在服务调用者中加GlobalTransactional注解

测试:

需确保seata-server、nacos服务正常启动。

测试前数据库状态:








查询数据库可以看到没变化。


3.2 AT模式(常用)
3.2.1 AT模式原理


但是AT模式存在脏写问题。

解决方法是利用全局锁机制:

注意:AT模式的全局锁和XA模式的等待阶段的数据库锁不一样,数据库锁锁住之后,对该数据的增删改查都不能进行;AT模式的全局锁只管理seata管理的微服务之间的,此时其他服务来修改该数据是可以的(当然这也会出现脏写问题,即隔离不完全)。

注意:AT模式的快照有两份,更新前快照和更新后快照,在做回滚时会用更新后快照与当前状态进行对比,若不一样seata则不回滚,而是记录异常,人工介入。

AT模式中的快照生成、回滚等动作都是由框架自动完成,没有任何代码侵入,因此实现非常简单。
3.2.2 代码实现

注意:第一步建立表是因为快照和全局锁都是保存在数据库表里的。
(一)执行seata-atsql脚本
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_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 INDEX `ux_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;
-- ----------------------------
-- Records of undo_log
-- ----------------------------
-- ----------------------------
-- Table structure for lock_table
-- ----------------------------
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_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,
INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
SET FOREIGN_KEY_CHECKS = 1;
注意:lock_table表保存在“2.2 部署seata的TC服务”中seata-server连接的nacos的配置文件seataServer.properties中的数据库地址。undo_log表存在微服务(这里是order-service、account-service、storage-service的配置文件)连接的数据库地址。
(二)修改微服务的数据模式

重启服务进行测试:

观察数据库发现没变化,事务生效。
3.3 TCC模式
3.3.1 TCC模式原理


TCC模式与AT模式很像,事务执行完就提交,释放数据库资源,性能好;但TCC模式不需要生成快照,不需要加锁实现事务隔离,性能最强;不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库。
TCC的缺点:
有代码侵入,需要人为编写try、Comfirm和Cancel接口;
软状态,事务是最终一致;
需要考虑Confirm和Cancel的失败情况,最好幂等处理。

3.3.2 代码实现

注意:try业务是第一阶段执行的,第二阶段TC根据各服务执行结果决定RM执行confirm或者cancel。
保证confirm、cancel接口的幂等性;
允许空回滚;

拒绝业务悬挂。


CREATE TABLE `account_freeze_tbl`(
`xid` varchar(128) NOT NULL,
`user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
`freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
`state` int(1) DEFAULT NULL COMMENT '事务状态,0-try,i-confirm,2-cancel',
PRIMARY KEY(`xid`) USING BTREE
)ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;


注意:
上中的try方法(prepare)中的参数param加了注解@BusinessActionContextParameter,该参数都可以从confirm方法和cancel方法中的context参数中获取。
具体步骤如下:
(一)account-service中新建一个接口和实现类

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
@LocalTCC
public interface AccountTCCService {
@TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "money")int money);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}

import cn.itcast.account.entity.AccountFreeze;
import cn.itcast.account.mapper.AccountFreezeMapper;
import cn.itcast.account.mapper.AccountMapper;
import cn.itcast.account.service.AccountTCCService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper freezeMapper;
@Override
@Transactional
public void deduct(String userId, int money) {
// 0.获取事务id
String xid = RootContext.getXID();
//1.避免业务悬挂,判断freeze中是否有冻结记录,如果有,一定是CANCEL执行过,要拒绝业务
AccountFreeze oldFreeze = freezeMapper.selectById(xid);
if(oldFreeze != null){
//CANCEL执行过,拒绝业务
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();
String userId = ctx.getActionContext("userId").toString();
AccountFreeze freeze = freezeMapper.selectById(xid);
//1.空回滚的判断,判断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);
}
//2.判断幂等
if(freeze.getState()==AccountFreeze.State.CANCEL){
//已经处理过一次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;
}
}

3.4 Saga模式
3.4.1 Saga模式原理

该模式事务之间没有隔离性,可能会出现脏写。

该模式使用于长事务,比如银行转账等。
该模式使用较少。

四、seata高可用

TC服务使用多地、多节点(集群模式)。
将具有依赖关系的微服务定义为一个事务组(tx-service-group值相同)

将配置文件中的集群放置到nacos配置文件中。


这样,当一个事务组的服务挂了之后,可以通过修改配置文件进行热更新(改为其他集群),这样不需要重启服务。
2131

被折叠的 条评论
为什么被折叠?



