学习笔记—微服务—技术栈实践(11)—基于Seata的分布式事务

分布式事务一致性

  在微服务架构中,系统被拆分为多个独立的服务,每个服务拥有自己的数据库。这种架构在带来灵活性和可扩展性的同时,也引入了新的问题,其中之一就是分布式事务。分布式事务是指一个业务操作需要跨多个服务和数据库才能完成,这时需要确保所有服务和数据库的操作要么全部成功,要么全部失败,以保持数据的一致性。

分布式事务所存在的挑战

  分布式系统中的事务管理要比传统单体应用复杂得多。

  首先,复杂网络和不可靠是一个问题。在分布式系统中,各个服务间通过网络通信,网络的不可靠性增加了事务的复杂性。

  第二,数据一致性是一个问题。各个服务可能有不同的数据源和操作,如何保证所有服务中的数据保持一致是分布式事务的关键问题。

  第三,隔离性又是一个问题。多个事务并发执行时,如何避免事务之间的冲突和数据不一致?

  最后,还有一个问题是可用性,如何在保证事务一致性的同时,不影响系统的高可用性?

分布式事务常见解决方案

  因此,分布式事务有多种常见的实现方式,包括:

两阶段提交协议(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的用户名密码进入。由此,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 =
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值