Spring Cloud Seata分布式事务案例

wen java案例 2

本文目录导读:

Spring Cloud Seata分布式事务案例

  1. 案例场景设计
  2. 环境准备
  3. 核心代码实现
  4. Seata Server配置
  5. 事务配置
  6. 启动与测试
  7. 核心原理说明
  8. 常见问题及解决方案

我为您整理一个完整的Spring Cloud Alibaba Seata分布式事务实战案例,涵盖环境搭建、代码实现和核心原理。

案例场景设计

业务场景:电商下单服务

  • 订单服务:创建订单
  • 库存服务:扣减库存
  • 账户服务:扣减余额

三个服务需要通过分布式事务保证数据一致性。

环境准备

技术栈版本

Spring Boot: 2.3.12.RELEASE
Spring Cloud: Hoxton.SR12
Spring Cloud Alibaba: 2.2.5.RELEASE
Seata: 1.4.2
MySQL: 8.0
Nacos: 1.4.1

数据库初始化

-- 订单库
CREATE DATABASE seata_order;
USE seata_order;
CREATE TABLE t_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT,
    product_id BIGINT,
    count INT,
    money DECIMAL(10,2),
    status INT DEFAULT 0
);
-- 库存库
CREATE DATABASE seata_storage;
USE seata_storage;
CREATE TABLE t_storage (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_id BIGINT,
    total INT,
    used INT,
    residue INT
);
-- 账户库
CREATE DATABASE seata_account;
USE seata_account;
CREATE TABLE t_account (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT,
    total DECIMAL(10,2),
    used DECIMAL(10,2),
    residue DECIMAL(10,2)
);
-- 每个业务库都需要创建undo_log表
CREATE TABLE undo_log (
    id BIGINT NOT NULL AUTO_INCREMENT,
    branch_id BIGINT NOT NULL,
    xid VARCHAR(100) NOT NULL,
    context VARCHAR(128) NOT NULL,
    rollback_info LONGBLOB NOT NULL,
    log_status INT NOT NULL,
    log_created DATETIME NOT NULL,
    log_modified DATETIME NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY ux_undo_log (xid, branch_id)
);

核心代码实现

父POM配置

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.2.5.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

订单服务(Order Service)

pom.xml

<dependencies>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

application.yml

server:
  port: 8001
spring:
  application:
    name: seata-order-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    alibaba:
      seata:
        tx-service-group: my_test_tx_group
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
    username: root
    password: root
logging:
  level:
    io.seata: debug

OrderController.java

@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;
    @GetMapping("/order/create")
    public String createOrder(@RequestParam("userId") Long userId,
                              @RequestParam("productId") Long productId,
                              @RequestParam("count") Integer count) {
        orderService.createOrder(userId, productId, count);
        return "订单创建成功";
    }
}

OrderService.java

@Service
public class OrderService {
    @Autowired
    private OrderDao orderDao;
    @Autowired
    private StorageFeignClient storageFeignClient;
    @Autowired
    private AccountFeignClient accountFeignClient;
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(Long userId, Long productId, Integer count) {
        // 1. 创建订单
        BigDecimal money = new BigDecimal("10").multiply(new BigDecimal(count));
        Order order = new Order();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setCount(count);
        order.setMoney(money);
        order.setStatus(0);
        orderDao.createOrder(order);
        // 2. 扣减库存(远程调用)
        storageFeignClient.decreaseStock(productId, count);
        // 3. 扣减账户余额(远程调用)
        accountFeignClient.decreaseBalance(userId, money);
        // 4. 模拟异常测试回滚
        // 取消下面注释测试分布式事务回滚
        // int i = 1 / 0;
        // 5. 修改订单状态为已完成
        orderDao.updateOrderStatus(order.getId(), 1);
    }
}

StorageFeignClient.java

@FeignClient(name = "seata-storage-service", path = "/storage")
public interface StorageFeignClient {
    @GetMapping("/decrease")
    String decreaseStock(@RequestParam("productId") Long productId,
                         @RequestParam("count") Integer count);
}

AccountFeignClient.java

@FeignClient(name = "seata-account-service", path = "/account")
public interface AccountFeignClient {
    @GetMapping("/decrease")
    String decreaseBalance(@RequestParam("userId") Long userId,
                           @RequestParam("money") BigDecimal money);
}

库存服务(Storage Service)

StorageService.java

@Service
public class StorageService {
    @Autowired
    private StorageDao storageDao;
    public void decreaseStock(Long productId, Integer count) {
        // 检查库存并扣减
        Storage storage = storageDao.findByProductId(productId);
        if (storage == null) {
            throw new RuntimeException("商品不存在");
        }
        if (storage.getResidue() < count) {
            throw new RuntimeException("库存不足");
        }
        storageDao.decreaseStock(productId, count);
    }
}

StorageController.java

@RestController
@RequestMapping("/storage")
public class StorageController {
    @Autowired
    private StorageService storageService;
    @GetMapping("/decrease")
    public String decreaseStock(@RequestParam("productId") Long productId,
                                @RequestParam("count") Integer count) {
        storageService.decreaseStock(productId, count);
        return "扣减库存成功";
    }
}

账户服务(Account Service)

AccountService.java

@Service
public class AccountService {
    @Autowired
    private AccountDao accountDao;
    public void decreaseBalance(Long userId, BigDecimal money) {
        // 检查余额并扣减
        Account account = accountDao.findByUserId(userId);
        if (account == null) {
            throw new RuntimeException("用户不存在");
        }
        if (account.getResidue().compareTo(money) < 0) {
            throw new RuntimeException("余额不足");
        }
        accountDao.decreaseBalance(userId, money);
    }
}

AccountController.java

@RestController
@RequestMapping("/account")
public class AccountController {
    @Autowired
    private AccountService accountService;
    @GetMapping("/decrease")
    public String decreaseBalance(@RequestParam("userId") Long userId,
                                  @RequestParam("money") BigDecimal money) {
        accountService.decreaseBalance(userId, money);
        return "扣减余额成功";
    }
}

Seata Server配置

registry.conf

registry {
  type = "nacos"
  nacos {
    application = "seata-server"
    serverAddr = "localhost:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}
config {
  type = "nacos"
  nacos {
    serverAddr = "localhost:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
  }
}

file.conf

store {
  mode = "db"
  db {
    datasource = "druid"
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://localhost:3306/seata_server?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC"
    user = "root"
    password = "root"
    minConn = 5
    maxConn = 20
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
  }
}

事务配置

每个服务添加配置类

@Configuration
public class DataSourceProxyConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return new DruidDataSource();
    }
    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }
}

Seata配置(每个服务的bootstrap.yml)

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group
  config:
    type: nacos
    nacos:
      serverAddr: localhost:8848
      group: SEATA_GROUP
  registry:
    type: nacos
    nacos:
      serverAddr: localhost:8848
      group: SEATA_GROUP

启动与测试

启动顺序

启动Nacos
2. 启动Seata Server(执行seata-server.sh)
3. 启动数据库服务
4. 启动三个业务服务
5. 初始化测试数据

测试数据初始化

-- 库存表
INSERT INTO t_storage VALUES (1, 1, 100, 0, 100);
-- 账户表
INSERT INTO t_account VALUES (1, 1, 1000.00, 0, 1000.00);

测试接口

# 正常下单
GET http://localhost:8001/order/create?userId=1&productId=1&count=2
# 异常场景测试(在OrderService中取消注释异常代码)
# 观察库存和余额是否回滚

核心原理说明

AT模式工作原理

一阶段:
   - 业务SQL执行前获取全局锁
   - 执行业务SQL
   - 生成undo_log快照
2. 二阶段提交:
   - 删除undo_log
   - 释放全局锁
3. 二阶段回滚:
   - 根据undo_log回滚数据
   - 释放全局锁

事务传播机制

  • @GlobalTransactional 开启全局事务
  • 所有参与方通过同一XID关联
  • Seata通过拦截器自动处理分支事务

常见问题及解决方案

数据源代理问题

// 必须使用DataSourceProxy包装
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
    return new DataSourceProxy(dataSource);
}

事务超时设置

seata:
  service:
    vgroup-mapping:
      my_test_tx_group: default
    enable-degrade: false
    disable-global-transaction: false
  client:
    tm:
      commit-retry-count: 5
      rollback-retry-count: 5

性能优化建议

seata:
  client:
    rm:
      report-success-enable: false
      table-meta-check-enable: true
      report-retry-count: 5
      async-commit-buffer-limit: 10000

这个案例完整展示了如何使用Seata实现分布式事务,需要注意的是,在实际生产环境中,还需要考虑性能优化、高可用部署、监控告警等方面的问题。

抱歉,评论功能暂时关闭!