学习分布式事务方案、Seata 的 TC/TM/RM 角色与集成步骤,并整理登录模块的面试复习笔记。

本篇要点

  • 比较分布式事务的常见方案
  • 认识 Seata 的角色与配置
  • 复盘登录链路和面试问答

**学习提示:**文末“自我介绍”是笔记中的示例口径,不应直接当成个人履历;Seata 配置中的示例口令和密钥已替换为占位符。

分布式事务

1、分布式事务理论

1.1 本地事务

  • 在同一个数据库里面,操作多张表,保证同一个数据库多张表数据一致性,要么都成功,如果一个失败所有都失败,称为本地事务

  • 事务四个特性:原子性、一致性、隔离性、持久性

  • 如果不考虑事务隔离性,产生脏读、不可重复读、幻(虚)读,解决设置事务隔离级别

– MySQL默认隔离级别:可重复读

image-20251111090502365

  • 事务传播行为(经常问)

– 指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。

– REQUIRED:默认传播行为 如果当前有事务使用当前事务,如果没有创建新的事务

REQUIRES_NEW: 新建事务

image-20251111090844884

image-20251111090856522

1.2 分布式事务概念

  • 保证不同数据库中数据一致性。操作表在不同的数据库里面,保证不同数据库里面表操作,要么都成功,如果有一个失败所有都失败

  • CAP定理

在一个分布式系统中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。

  • C 一致性(Consistency):对某个指定的客户端来说,读操作保证能够返回最新的写操作结果
  • A 可用性(Availability):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)
  • P 分区容忍性(Partition Tolerance):当出现网络分区后(可能是丢包,也可能是连接中断,还可能是拥塞),系统能够继续“履行职责”

1.3 分布式事务解决方案

第一种 两阶段提交(2PC)

流程

(1)投票阶段(voting phase):参与者将操作结果通知协调者;

(2)提交阶段(commit phase):协调者收到参与者的通知后,协调者再向参与者发出通知,根据反馈情况决定各参与者是否要提交还是回滚;

特点

强一致性

第二种 补偿事务(TCC)

  • TCC事务其实主要包含两个阶段:Try阶段、Confirm/Cancel阶段。其中Try阶段完成业务检查并资源预留,确保在Confirm阶段资源可用,可以最大程度的确保confirm阶段能够执行成功。

image-20251111094731847

特点

最终一致性

第三种 本地消息表(数据库)

image-20251111104526813

  • 保证数据最终一致性

  • 需求:生成订单,扣减库存,有两个系统:订单系统 和 库存系统

1、在订单系统中,创建两个表:订单表 和 消息表,一个订单对应一个消息。添加订单时候,向订单表添加一条记录,同时向消息表添加记录 (消息数据:订单编号、购买数量等),消息状态:1未发送 2完成 3 失败

2、订单系统:每隔3s到消息表找到状态是1消息进行发送

3、库存系统接收到订单系统发送过来消息,根据消息内容扣减库存

4、库存扣减成功之后,调用订单系统接口通知订单扣减已经完成了,订单系统修改消息状态2

  • 库存扣减成功了,调用订单系统通知订单时候,突然网络出现问题,订单系统收不到通知,订单系统认为扣减库存失败了。订单系统重新发送消息,库存系统继续收到消息,不需要扣减,
  • 保证消息幂等性。库存再去通知订单,扣减成功了,比如网络又出错了,订单继续重新发送
  • 消息出错,重试最多可以是3次数,如果超过3次不再发送,执行人工处理,把消息状态修改3

2、Seata

Seata事务管理中三个重要角色

  • 1、TC (Transaction Coordinator) - **事务协调者:**全局事务决策者。

    2、TM (Transaction Manager) - **事务管理器:**全局事务发起者。

    3、RM (Resource Manager) - **资源管理器:**管理分支(本地)事务。

  • 总体概括就是:TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行

分布式事务在Seata中的执行流程

image-20251111140121223

1、由TM发起开启全局事务到TC服务器

2、TC服务器开启全局事务,会返回xid(本次全局事务的id)

3、各个微服务模块分支事务,在TC服务器进行注册(注册到全局事务里面)

4、各个微服务模块执行业务SQL,把各个模块执行结果报告给TC服务器

5、TC会进行分支事务状态的统计,如果各个分支都成功,TC决定都提交,如果各个分支有任何一个失败的,TC决定都回滚

Seata四种不同的分布式事务模式

  • XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
  • AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
  • TCC模式:最终一致的分阶段事务模式,有业务侵入
  • SAGA模式:长事务模式,有业务侵入

1、XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入

首先,注册各个分支到TC里面

第二,执行各个分支事务sql语句,但是sql语句只是执行不提交

第三,各个分支把执行sql语句结果告诉TC,最终由TC决定这些分支要么都提交,要么都不提交

image-20241119144722778

2、AT模式:默认模式,最终一致的分阶段事务模式,无业务侵入

首先,注册各个分支到TC里面

第二,执行各个分支sql语句,执行之前记录当前状态到undolog里面,执行sql语句并提交

第三,各个分支把执行结果告诉TC,最终由TC决定这些分支要么都提交,要么都回滚,回滚使用undolog

整合Seata

第一步 部署TC服务

1、创建TC服务使用数据库和表

image-20251111141556138

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
-- 1. 执行语句创建名为 seata 的数据库
CREATE DATABASE seata DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;

-- 2.执行脚本完成 Seata 表结构的创建
use seata;

-- the table to store GlobalSession data
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_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

-- 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 = utf8mb4;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
2、在Nacos配置中心创建Seata服务核心配置文件

image-20251111143202713

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
#Transport configuration, for client and server
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none

#Transaction routing rules configuration, only for the client
service.vgroupMapping.default_tx_group=default
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false

#Transaction rule configuration, only for the client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
#For TCC transaction mode
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h

#Log rule configuration, for client and server
log.exceptionRate=100

#Transaction storage configuration, only for the server. The file, DB, and redis configuration values are optional.
store.mode=db
store.lock.mode=db
store.session.mode=db
#Used for password encryption
store.publicKey=

#If `store.mode,store.lock.mode,store.session.mode` are not equal to `file`, you can remove the configuration block.
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100

#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.200.130:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=<SEATA_DB_PASSWORD>
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000

#These configurations are required if the `store mode` is `redis`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `redis`, you can remove the configuration block.
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.sentinel.masterName=
store.redis.sentinel.sentinelHosts=
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100

#Transaction rule configuration, only for the server
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.distributedLockExpireTime=10000
server.xaerNotaRetryTimeout=60000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
server.enableParallelRequestHandle=false

#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
  • **store.mode=db **存储模式选择为数据库
  • 192.168.200.130 MySQL主机地址
  • store.db.user=root 数据库用户名
  • store.db.password= 数据库密码
3、下载Seata服务(windows版本),解压

image-20251111142748081

4、修改解压文件夹里面配置文件

image-20251111142900641

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
server:
port: 7091

spring:
application:
name: seata-server

logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
extend:
logstash-appender:
destination: 127.0.0.1:4560
kafka-appender:
bootstrap-servers: 127.0.0.1:9092
topic: logback_to_logstash

console:
user:
username: seata
password: <SEATA_DB_PASSWORD>
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: 192.168.200.130:8848
namespace:
username:
password:
context-path:
data-id: seataServer.properties
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos:
application: seata-server
server-addr: 192.168.200.130:8848
namespace:
cluster: default
username:
password:
context-path:
store:
# support: file 、 db 、 redis
mode: file
# server:
# service-port: 8091 #If not configured, the default is '${server.port} + 1000'
security:
secretKey: <SEATA_SECRET_KEY>
tokenValidityInMilliseconds: 1800000
ignore:
urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login
5、启动Seata服务

image-20251111143321125

image-20251111145109741

第二步 注册各个分支事务到TC

1、注册的微服务模块添加依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--Seata依赖  -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>

<!--使用2.0.0的seata的版本-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.0</version>
</dependency>
2、在微服务模块配置文件添加内容
1
2
spring.cloud.nacos.config.shared-configs[0].data-id=seata-common.yaml
spring.cloud.nacos.config.shared-configs[0].refresh=true
3、nacos配置中心创建seata-common.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
seata:
# 配置seata-server在nacos注册中心上的信息
registry:
type: nacos
nacos:
namespace:
application: seata-server
server-addr: 192.168.200.130:8848
# 配置事务组的名称,需要和seata服务端的配置保持一致
tx-service-group: default_tx_group
service:
vgroup-mapping:
default_tx_group: default
data-source-proxy-mode: XA # 配置事务管理模式为xa模式

第三步 在业务代码添加注解

1
2
3
4
5
6
7
8
9
10
11
12
@GlobalTransactional
@Override
public void saveUser(UserInfo userInfo) {
//添加用户信息
this.save(userInfo);
//int i = 1/0;

//初始化账户
//方法一:远程调用 在service-account创建初始化接口
// 在service-user进行远程调用实现
userAccountFeignClient.initUser(userInfo.getId());
}

自我介绍

– 简单介绍

  • 我叫张三,我今年28岁,我来自哪里,我有三年JAVA AI开发经验

– 列出你熟悉技术

  • 我熟悉 SpringBoot、SpringCloud常用框架的使用
  • 我还熟悉数据库的使用,比如MySQL/Redis等,另外还对MySQL优化有一定了解
  • 除此之外,我还对RabbitMQ、JVM、JUC也有一定使用经验

– 介绍最近项目和核心点

  • 我最近正在开发一个阅读类的项目,我在项目里面负责登录、订单模块,在这个项目中,redis、rabbitmq、多线程、设计模式等都有实际应用场景,另外我还参与了项目的重构优化

登录模块

流程

  • 前端发起登录请求,请求微信服务器返回code值
  • 前端拿着code请求后端登录接口
  • 后端登录接口根据code + 公众号id + 秘钥请求微信登录接口,返回openid
  • 根据openid判断是否第一次登录,如果第一次,添加用户信息并且初始化账号
  • 创建token,把token作为redis的key,value是用户信息,返回token给前端
  • 前端把token存储到cookie里面,每次请求携带token值,因为cookie不能跨域,每次请求把token放到请求里面进行传递
  • 后端每次从请求头获取到token,根据token查redis,如果可以查询到登录,否则不是登录

细节和技术

1、远程调用实现方式

  • OpenFeign :底层原理
  • HttpClient

2、添加用户信息 并且初始化账号

  • 方案一:异步mq方式

– 如果第一次登录,首先添加用户信息到用户表

– 之后发送mq消息,在账户模块接收消息,初始化账户信息

– mq消息失败:确认模式、返回模式、消费端确认,redis持久化重试

  • 方案二:远程调用实现

– – 如果第一次登录,首先添加用户信息到用户表

– 之后远程调用账户模块接口,初始化账户信息

– 保证不同数据库数据一致性,使用Seata实现分布式事务

– Seata机制:组成、流程、模式等

3、生成的token按照什么规则生成的

  • 我们项目方案:

– 我们项目生成唯一的值,把值作为redis的key,value是用户信息

– 使用redis存储用户信息

  • 其他方案:

– 我们按照一定规则生成token,比如token包含userId + userName,对token进行加密

– 不需要redis存储

– 这种方案不好设置token过期时间

– 按照一定规则生成token,token加密,并且设置token过期时间,有工具:jwt

4、cookie问题

cookie不能跨域,每次请求时候把token放到请求头里面传递

**请求头有哪些?**语言、编码、类型 User-Agent请求浏览器类型

其他问题

1、redis过期问题

  • 登录成功之后,判断当前redis数据还有多长时间过期
  • 比如约定过期时间小于60s,对数据续期

2、多终端登录问题

  • 强制绑定手机号

  • 强制做人脸识别认证(调用腾讯云人脸识别接口)

  • 在不同端使用同一个用户登录,不同端操作数据同步?

方案一:不能多端登录

方案二:定时刷新,每隔10s

任务

1、准备自我介绍

2、登录模块问题

3、订单模块问题

4、Seata总结概念