事务管理

本示例演示如何通过 Seata 实现分布式 Dubbo 服务的事务管理,保证数据一致性。

Seata 是什么

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

一、示例架构说明

用户采购商品业务,整个业务包含3个微服务:

  • 库存服务: 扣减给定商品的库存数量。
  • 订单服务: 根据采购请求生成订单。
  • 账户服务: 用户账户金额扣减。

image.png

StorageService

  1. public interface StorageService {
  2. /**
  3. * 扣除存储数量
  4. */
  5. void deduct(String commodityCode, int count);
  6. }

OrderService

  1. public interface OrderService {
  2. /**
  3. * 创建订单
  4. */
  5. Order create(String userId, String commodityCode, int orderCount);
  6. }

AccountService

  1. public interface AccountService {
  2. /**
  3. * 从用户账户中借出
  4. */
  5. void debit(String userId, int money);
  6. }

二、主要的业务逻辑

BusinessService

  1. public class BusinessServiceImpl implements BusinessService {
  2. private StorageService storageService;
  3. private OrderService orderService;
  4. /**
  5. * 采购
  6. */
  7. public void purchase(String userId, String commodityCode, int orderCount) {
  8. // 扣除存储数量
  9. storageService.deduct(commodityCode, orderCount);
  10. // 创建订单
  11. orderService.create(userId, commodityCode, orderCount);
  12. }
  13. }

StorageService

  1. public class StorageServiceImpl implements StorageService {
  2. private JdbcTemplate jdbcTemplate;
  3. @Override
  4. public void deduct(String commodityCode, int count) {
  5. // 修改数据库:扣减存储数量
  6. jdbcTemplate.update("update storage_tbl set count = count - ? where commodity_code = ?",
  7. new Object[]{count, commodityCode});
  8. }
  9. }

OrderService

  1. public class OrderServiceImpl implements OrderService {
  2. private AccountService accountService;
  3. private JdbcTemplate jdbcTemplate;
  4. public Order create(String userId, String commodityCode, int orderCount) {
  5. // 计算金额
  6. int orderMoney = calculate(commodityCode, orderCount);
  7. // 用户账户中扣减金额
  8. accountService.debit(userId, orderMoney);
  9. // 修改数据库:新建订单
  10. final Order order = new Order();
  11. order.userId = userId;
  12. order.commodityCode = commodityCode;
  13. order.count = orderCount;
  14. order.money = orderMoney;
  15. KeyHolder keyHolder = new GeneratedKeyHolder();
  16. jdbcTemplate.update(con -> {
  17. PreparedStatement pst = con.prepareStatement(
  18. "insert into order_tbl (user_id, commodity_code, count, money) values (?, ?, ?, ?)",
  19. PreparedStatement.RETURN_GENERATED_KEYS);
  20. pst.setObject(1, order.userId);
  21. pst.setObject(2, order.commodityCode);
  22. pst.setObject(3, order.count);
  23. pst.setObject(4, order.money);
  24. return pst;
  25. }, keyHolder);
  26. order.id = keyHolder.getKey().longValue();
  27. return order;
  28. }
  29. }

AccountService

  1. public class AccountServiceImpl implements AccountService {
  2. private JdbcTemplate jdbcTemplate;
  3. @Override
  4. public void debit(String userId, int money) {
  5. // 修改数据库:用户账户中扣减金额
  6. jdbcTemplate.update("update account_tbl set money = money - ? where user_id = ?", new Object[]{money, userId});
  7. }
  8. }

三、快速启动示例

Step 1: 下载源码

  1. git clone -b master https://github.com/apache/dubbo-samples.git
  2. cd ./dubbo-samples-transaction/

Step 2: 通过 docker-compose 启动 Seata-Server 和 MySQL 等

在此示例中,我们使用 docker-compose 快速拉起 seata-server 和 mysql 等服务。

  1. cd src/main/resources/docker
  2. docker-compose up

Step 3: 构建用例

执行 maven 命令,打包 demo 工程

  1. mvn clean package

Step 4: 启动 AccountService

  1. java -classpath ./target/dubbo-samples-transaction-1.0-SNAPSHOT.jar org.apache.dubbo.samples.starter.DubboAccountServiceStarter

Step 5: 启动 OrderService

  1. java -classpath ./target/dubbo-samples-transaction-1.0-SNAPSHOT.jar org.apache.dubbo.samples.starter.DubboOrderServiceStarter

Step 6: 启动 StorageService

  1. java -classpath ./target/dubbo-samples-transaction-1.0-SNAPSHOT.jar org.apache.dubbo.samples.starter.DubboStorageServiceStarter

Step 7: 启动 BusinessService

  1. java -classpath ./target/dubbo-samples-transaction-1.0-SNAPSHOT.jar org.apache.dubbo.samples.starter.DubboBusinessTester

四、示例核心流程

image.png

Step 1: 修改业务代码

此处仅仅需要一行注解 @GlobalTransactional 写在业务发起方的方法上:

  1. @GlobalTransactional
  2. public void purchase(String userId, String commodityCode, int orderCount) {
  3. ......
  4. }

Step 2: 安装数据库

  • 要求: MySQL (InnoDB 存储引擎)。

提示: 事实上例子中3个微服务需要3个独立的数据库,但为了方便我们使用同一物理库并配置3个逻辑连接串。

更改以下xml文件中的数据库url、username和password

dubbo-account-service.xml dubbo-order-service.xml dubbo-storage-service.xml

  1. <property name="url" value="jdbc:mysql://x.x.x.x:3306/xxx" />
  2. <property name="username" value="xxx" />
  3. <property name="password" value="xxx" />

Step 3: 为 Seata 创建 undo_log 表

UNDO_LOG 此表用于 Seata 的AT模式。

  1. -- 注意当 Seata 版本升级至 0.3.0+ 将由之前的普通索引变更为唯一索引。
  2. CREATE TABLE `undo_log` (
  3. `id` bigint(20) NOT NULL AUTO_INCREMENT,
  4. `branch_id` bigint(20) NOT NULL,
  5. `xid` varchar(100) NOT NULL,
  6. `context` varchar(128) NOT NULL,
  7. `rollback_info` longblob NOT NULL,
  8. `log_status` int(11) NOT NULL,
  9. `log_created` datetime NOT NULL,
  10. `log_modified` datetime NOT NULL,
  11. `ext` varchar(100) DEFAULT NULL,
  12. PRIMARY KEY (`id`),
  13. UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
  14. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

Step 4: 创建相关业务表

  1. DROP TABLE IF EXISTS `storage_tbl`;
  2. CREATE TABLE `storage_tbl` (
  3. `id` int(11) NOT NULL AUTO_INCREMENT,
  4. `commodity_code` varchar(255) DEFAULT NULL,
  5. `count` int(11) DEFAULT 0,
  6. PRIMARY KEY (`id`),
  7. UNIQUE KEY (`commodity_code`)
  8. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  9. DROP TABLE IF EXISTS `order_tbl`;
  10. CREATE TABLE `order_tbl` (
  11. `id` int(11) NOT NULL AUTO_INCREMENT,
  12. `user_id` varchar(255) DEFAULT NULL,
  13. `commodity_code` varchar(255) DEFAULT NULL,
  14. `count` int(11) DEFAULT 0,
  15. `money` int(11) DEFAULT 0,
  16. PRIMARY KEY (`id`)
  17. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  18. DROP TABLE IF EXISTS `account_tbl`;
  19. CREATE TABLE `account_tbl` (
  20. `id` int(11) NOT NULL AUTO_INCREMENT,
  21. `user_id` varchar(255) DEFAULT NULL,
  22. `money` int(11) DEFAULT 0,
  23. PRIMARY KEY (`id`)
  24. ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Step 5: 启动 Seata-Server 服务

  1. Usage: sh seata-server.sh(for linux and mac) or cmd seata-server.bat(for windows) [options]
  2. Options:
  3. --host, -h
  4. The host to bind.
  5. Default: 0.0.0.0
  6. --port, -p
  7. The port to listen.
  8. Default: 8091
  9. --storeMode, -m
  10. log store mode : filedb
  11. Default: file
  12. --help
  13. e.g.
  14. sh seata-server.sh -p 8091 -h 127.0.0.1 -m file

最后修改 December 16, 2022: Fix check (#1736) (97972c1)