分布式事务一致性
在微服务架构中,系统被拆分为多个独立的服务,每个服务拥有自己的数据库。这种架构在带来灵活性和可扩展性的同时,也引入了新的问题,其中之一就是分布式事务。分布式事务是指一个业务操作需要跨多个服务和数据库才能完成,这时需要确保所有服务和数据库的操作要么全部成功,要么全部失败,以保持数据的一致性。
分布式事务所存在的挑战
分布式系统中的事务管理要比传统单体应用复杂得多。
首先,复杂网络和不可靠是一个问题。在分布式系统中,各个服务间通过网络通信,网络的不可靠性增加了事务的复杂性。
第二,数据一致性是一个问题。各个服务可能有不同的数据源和操作,如何保证所有服务中的数据保持一致是分布式事务的关键问题。
第三,隔离性又是一个问题。多个事务并发执行时,如何避免事务之间的冲突和数据不一致?
最后,还有一个问题是可用性,如何在保证事务一致性的同时,不影响系统的高可用性?
分布式事务常见解决方案
因此,分布式事务有多种常见的实现方式,包括:
两阶段提交协议(2PC)
第一阶段:协调者向所有参与者发出“准备”请求,各个参与者执行本地事务并锁定相关资源,但不提交。
第二阶段:如果所有参与者都准备好了,协调者发出“提交”请求,所有参与者提交本地事务;否则,发出“回滚”请求,所有参与者回滚本地事务。
缺点:2PC 的问题在于性能开销大,参与者锁定资源的时间较长,且存在单点故障风险(协调者故障)。
三阶段提交协议(3PC)
相比 2PC,3PC 引入了超时机制和中间阶段,进一步减少了单点故障问题,但依然有性能和复杂性问题。
TCC(Try-Confirm-Cancel)模式
Try 阶段:尝试执行业务操作并预留必要的资源。
Confirm 阶段:在所有服务的 Try 成功后,确认执行业务操作,真正提交事务。
Cancel 阶段:如果 Try 阶段失败,则执行补偿操作,释放预留资源。
优点:TCC 适合对事务有严格控制的业务场景,灵活性较高,但需要开发人员手动编写补偿逻辑。
除此之外,还有如本地消息表保证最终一致性、saga模式(把长事务分解为多个小事务,小事务执行后才会触发下一个小事务。如果某个事务失败,会调用相应的补偿事务来回滚已完成的事务。)等等。
Seata
Seata 是阿里巴巴开源的一款分布式事务解决方案,它旨在解决微服务架构下的分布式事务问题。Seata 提供了一种简单、有效的分布式事务解决方案,主要包括 AT(Automatic Transaction)、TCC、Saga 和 XA 四种模式,覆盖了从简单到复杂的分布式事务场景。
Seata的架构
Seata 的核心架构包括以下三个组件:
TM(Transaction Manager,事务管理器):负责全局事务的开始、提交和回滚。
RM(Resource Manager,资源管理器):负责管理本地事务和资源,与 TC 协调提交或回滚。
TC(Transaction Coordinator,事务协调器):全局事务的协调者,维护事务状态,并驱动事务的提交或回滚。
在 Seata 中,AT 模式是其核心模式之一。它将 2PC 的复杂性隐藏起来,开发者只需要编写普通的业务逻辑,Seata 自动将 SQL 的操作转化为两阶段提交流程。
Seata 的 四种 模式
AT 模式类似于 2PC,它会自动生成回滚日志来支持分布式事务。适用于基于数据库的分布式事务场景,开发者不需要手动编写复杂的事务控制逻辑,系统会自动生成回滚日志并进行事务恢复。
TCC 是一种灵活的分布式事务模式,开发者需要为每个操作定义 Try、Confirm 和 Cancel 方法,用于分别执行事务尝试、确认和回滚。提供了高灵活性,适用于复杂的业务逻辑,但需要开发者编写大量自定义代码。
Seata 提供 Saga 模式的支持,适用于长事务场景,主要通过补偿机制来处理事务失败后的回滚。
XA是一种分布式事务协议,由 X/Open 标准定义,Seata 通过支持 XA 模式实现数据库级别的分布式事务。该模式适用于需要强一致性的场景,但性能相对较差,资源锁定时间长。
Seata的部署
首先,需要下载并启动seata,建议采用docker方式进行简单的部署。
docker pull seataio/seata-server
docker run -d --name seata-server -p 8091:8091 seataio/seata-server
docker logs seata-server
███████╗███████╗ █████╗ ████████╗ █████╗
██╔════╝██╔════╝██╔══██╗╚══██╔══╝██╔══██╗
███████╗█████╗ ███████║ ██║ ███████║
╚════██║██╔══╝ ██╔══██║ ██║ ██╔══██║
███████║███████╗██║ ██║ ██║ ██║ ██║
╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
21:18:36.340 INFO --- [ main] [ta.config.ConfigurationFactory] [ load] [] : load Configuration from :Spring Configuration
21:18:36.355 INFO --- [ main] [ta.config.ConfigurationFactory] [ buildConfiguration] [] : load Configuration from :Spring Configuration
21:18:36.381 INFO --- [ main] [seata.server.ServerApplication] [ logStarting] [] : Starting ServerApplication using Java 1.8.0_342 on 7583cdde42ef with PID 1 (/seata-server/classes started by root in /seata-server)
21:18:36.382 INFO --- [ main] [seata.server.ServerApplication] [ogStartupProfileInfo] [] : No active profile set, falling back to 1 default profile: "default"
21:18:38.043 INFO --- [ main] [mbedded.tomcat.TomcatWebServer] [ initialize] [] : Tomcat initialized with port(s): 7091 (http)
21:18:38.088 INFO --- [ main] [oyote.http11.Http11NioProtocol] [ log] [] : Initializing ProtocolHandler ["http-nio-7091"]
21:18:38.089 INFO --- [ main] [.catalina.core.StandardService] [ log] [] : Starting service [Tomcat]
21:18:38.089 INFO --- [ main] [e.catalina.core.StandardEngine] [ log] [] : Starting Servlet engine: [Apache Tomcat/9.0.62]
21:18:38.233 INFO --- [ main] [rBase.[Tomcat].[localhost].[/]] [ log] [] : Initializing Spring embedded WebApplicationContext
21:18:38.233 INFO --- [ main] [letWebServerApplicationContext] [ebApplicationContext] [] : Root WebApplicationContext: initialization completed in 1792 ms
21:18:39.043 INFO --- [ main] [vlet.WelcomePageHandlerMapping] [ <init>] [] : Adding welcome page: class path resource [static/index.html]
21:18:39.472 INFO --- [ main] [oyote.http11.Http11NioProtocol] [ log] [] : Starting ProtocolHandler ["http-nio-7091"]
21:18:39.502 INFO --- [ main] [mbedded.tomcat.TomcatWebServer] [ start] [] : Tomcat started on port(s): 7091 (http) with context path ''
21:18:39.511 INFO --- [ main] [seata.server.ServerApplication] [ logStarted] [] : Started ServerApplication in 3.941 seconds (JVM running for 4.541)
21:18:39.846 INFO --- [ main] [a.server.session.SessionHolder] [ init] [] : use session store mode: file
21:18:39.868 INFO --- [ main] [rver.lock.LockerManagerFactory] [ init] [] : use lock store mode: file
21:18:39.988 INFO --- [ main] [rpc.netty.NettyServerBootstrap] [ start] [] : Server started, service listen port: 8091
21:18:40.013 INFO --- [ main] [io.seata.server.ServerRunner ] [ run] [] :
you can visit seata console UI on http://127.0.0.1:7091.
log path: /root/logs/seata.
21:18:40.013 INFO --- [ main] [io.seata.server.ServerRunner ] [ run] [] : seata server started in 500 millSeconds
OpenJDK 64-Bit Server VM warning: Cannot open file /root/logs/seata/seata_gc.log due to No such file or directory
接下来,对其进行配置:
# 创建配置文件目录
mkdir -p /home/docker_home/seata/seata-data
# 将容器内的默认配置文件拷贝出来
docker cp seata-server:/seata-server/resources /home/docker_home/seata/seata-data
# 删除容器
docker rm -f seata-server
而后,考虑到seata作为阿里巴巴开源的分布式解决方案,考虑直接将seata注册并配置到nacos上,首先按先下载官方的config.txt:https://github.com/apache/incubator-seata/tree/develop/script/config-center
这里,采用MySQL方式进行配置如下:
#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=
#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://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root
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
将上述配置文件config.txt导入nacos,命名seata-server.properties。配置格式类型选择为properties。
在上面cp出来的文件中,找到resources/application.yml根据nacos的实际配置信息进行修改。
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: 192.168.186.1:8848
username: nacos
password: nacos
namespace: 3bc00f76-05de-4fa5-bdbe-06c57ad5c31c
data-id: seata-server.properties
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos:
application: seata-server
server-addr: 192.168.186.1:8848
username: nacos
password: nacos
namespace: 3bc00f76-05de-4fa5-bdbe-06c57ad5c31c
而后去往https://github.com/apache/incubator-seata/tree/develop/script/server/db
下载mysql所需要的数据表。创建seata数据库,并导入sql
+------------------+
| Tables_in_seata |
+------------------+
| branch_table |
| distributed_lock |
| global_table |
| lock_table |
+------------------+
4 rows in set (0.00 sec)
global_table:全局事务表,每当有一个全局事务发起后,就会在该表中记录全局事务的ID
branch_table:分支事务表,记录每一个分支事务的ID,分支事务操作的哪个数据库等信息
lock_table:全局锁
distributed_lock:分布式锁
而后,重启seata:
docker run --name seata-server -d -p 8091:8091 -p 7091:7091 -v /home/docker_home/seata/seata-data/resources:/seata-server/resources seataio/seata-server
打开seata界面localhost:7091
输入seata seata的用户名密码进入。由此,seata基于nacos和mysql的配置方式便部署完成了。另外,需要格外注意得是,每个参与分布式事务的数据库都需要加一张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;
seata的使用
seata配置完成后,接下来以AT模式和TCC模式为例分别展示seata对于分布式事务的处理。
示例的构造
于此,采用一个示例。即商品扣库存+用户账户扣款。
首先,先构造三张数据表,分别是product、account、order,即商品表、账户表和订单表。
CREATE TABLE product (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50),
stock INT
);
CREATE TABLE account (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
balance DECIMAL(10, 2)
);
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT,
product_id BIGINT,
status VARCHAR(20)
);
在此基础上,设计实现三个简单的服务,即ProductService,用于扣减商品库存;AccountService,用于扣减用户账户余额;OrderService,用于订单并处理整个事务。
参见代码https://github.com/gagaducko/learning_demos/tree/main/seata-demo
其中,对于ProductService来说,扣减库存如下:
public void reduceStock(Long productId, int amount) {
String sql = "UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?";
int updatedRows = jdbcTemplate.update(sql, amount, productId, amount);
if (updatedRows == 0) {
throw new RuntimeException("库存不足");
}
}
对于AccountService来说,扣减账户余额如下:
public void debit(Long userId, BigDecimal amount) {
String sql = "UPDATE account SET balance = balance - ? WHERE user_id =