微服务seata 1.4.2 分布式事务AT模式示例

AT模式的优势,代码无侵入。

示例包括三个服务,订单服务,商品服务,账户服务。

创建订单的同时,需要扣减商品库存,并扣减账户余额,三个操作要么同时成功,要么同时失败。

1. 创建项目

在idea创建maven项目,parent项目seata-server-demo,在项目中创建订单服务模块(seata-oder-server)、商品服务模块(seata-product-server)、账户服务模块(seata-account-server)。

2. parent配置


        8
        8
        2.2.5.RELEASE
        2.3.11.RELEASE
        Hoxton.SR8
    

    
        
            
                org.springframework.boot
                spring-boot-dependencies
                ${spring-boot-version}
                pom
                import
            

            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud-version}
                pom
                import
            

            
                com.alibaba.cloud
                spring-cloud-alibaba-dependencies
                ${spring.cloud.alibaba.version}
                pom
                import
            
        
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            mysql
            mysql-connector-java
        

        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.2
        

        
        
            com.alibaba
            druid-spring-boot-starter
            1.2.6
        

        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-discovery
        

        
        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-nacos-config
        

        
            com.alibaba.cloud
            spring-cloud-starter-alibaba-seata
        
    

3. 账户服务模块

3.1 seata配置

# seata配置
seata:
  enabled: true
  # Seata 应用编号,默认为 ${spring.application.name}
  application-id: account-seata
  # Seata 事务组编号,用于 TC 集群名
  tx-service-group: seataserver-group
  # 关闭自动代理
  enable-auto-data-source-proxy: false
  enableAutoDataSourceProxy: true
  # 服务配置项
  service:
    # 虚拟组和分组的映射
    vgroup-mapping:
      seataserver-group: default
    # 分组和 Seata 服务的映射
    grouplist:
      default: 127.0.0.1:8091
  config:
    type: nacos
    nacos:
      serverAddr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace:
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      namespace:

3.2 功能代码(controller)

controller接口:

package com.platform.account.controller;

import com.platform.account.handler.AjaxResult;
import com.platform.account.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AccountController {

    @Autowired
    private AccountService accountService;

    /**
     * 扣减库存接口
     * @param userId
     * @param price
     */
    @PostMapping("/reduceBalance")
    AjaxResult reduceBalance(@RequestParam("userId") Long userId, @RequestParam("price")Double price)
    {
        int res = accountService.reduceBalance(userId, price);

        return AjaxResult.success(res);
    }

}

3.3 功能代码(service)

服务实现:

package com.platform.account.service.impl;

import com.platform.account.domain.Account;
import com.platform.account.mapper.AccountMapper;
import com.platform.account.service.AccountService;
import io.seata.core.context.RootContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
public class AccountServiceImpl implements AccountService
{
    private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class);

    @Resource
    private AccountMapper accountMapper;

    /**
     * 扣减账户余额
     * 事务传播特性设置为 REQUIRES_NEW 开启新的事务 重要!!!!一定要使用REQUIRES_NEW
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public int reduceBalance(Long userId, Double price)
    {
        int result = 0;

        log.info("=============ACCOUNT START=================");
        log.info("当前 XID: {}", RootContext.getXID());

        Account account = accountMapper.selectById(userId);
        Double balance = account.getBalance();
        log.info("下单用户{}余额为 {},商品总价为{}", userId, balance, price);

        if (balance < price)
        {
            log.warn("用户 {} 余额不足,当前余额:{}", userId, balance);
            throw new RuntimeException("余额不足");
        }
        log.info("开始扣减用户 {} 余额", userId);
        double currentBalance = account.getBalance() - price;
        account.setBalance(currentBalance);
        result = accountMapper.updateById(account);
        log.info("扣减用户 {} 余额成功,扣减后用户账户余额为{}", userId, currentBalance);
        log.info("=============ACCOUNT END=================");

        return result;
    }

}

4. 商品服务模块

4.1 seata配置

# seata配置
seata:
  enabled: true
  # Seata 应用编号,默认为 ${spring.application.name}
  application-id: product-seata
  # Seata 事务组编号,用于 TC 集群名
  tx-service-group: seataserver-group
  # 关闭自动代理
  enable-auto-data-source-proxy: false
  enableAutoDataSourceProxy: true
  # 服务配置项
  service:
    # 虚拟组和分组的映射
    vgroup-mapping:
      seataserver-group: default
    # 分组和 Seata 服务的映射
    grouplist:
      default: 127.0.0.1:8091
  config:
    type: nacos
    nacos:
      serverAddr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace:
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      namespace:

4.2 功能代码(controller)

package com.platform.product.controller;

import com.platform.product.handler.AjaxResult;
import com.platform.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class ProductController {

    @Autowired
    private ProductService productService;

    @GetMapping("/reduceStock")
    AjaxResult reduceStock(@RequestParam("productId") Long productId, @RequestParam("amount")Integer amount)
    {
        Double remain = productService.reduceStock(productId, amount);
        return AjaxResult.success(remain);
    }

}

4.3 功能代码(service)

package com.platform.product.service.impl;

import com.platform.product.domain.Product;
import com.platform.product.mapper.ProductMapper;
import com.platform.product.service.ProductService;
import io.seata.core.context.RootContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
public class ProductServiceImpl implements ProductService
{
    private static final Logger log = LoggerFactory.getLogger(ProductServiceImpl.class);

    @Resource
    private ProductMapper productMapper;

    /**
     * 事务传播特性设置为 REQUIRES_NEW 开启新的事务 重要!!!!一定要使用REQUIRES_NEW
     */
    // @DS("product")
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @Override
    public Double reduceStock(Long productId, Integer amount)
    {
        log.info("=============PRODUCT START=================");
        log.info("当前 XID: {}", RootContext.getXID());

        // 检查库存
        Product product = productMapper.selectById(productId);
        Integer stock = product.getStock();
        log.info("商品编号为 {} 的库存为{},订单商品数量为{}", productId, stock, amount);

        if (stock < amount)
        {
            log.warn("商品编号为{} 库存不足,当前库存:{}", productId, stock);
            throw new RuntimeException("库存不足");
        }
        log.info("开始扣减商品编号为 {} 库存,单价商品价格为{}", productId, product.getPrice());
        // 扣减库存
        int currentStock = stock - amount;
        product.setStock(currentStock);
        productMapper.updateById(product);
        double totalPrice = product.getPrice() * amount;
        log.info("扣减商品编号为 {} 库存成功,扣减后库存为{}, {} 件商品总价为 {} ", productId, currentStock, amount, totalPrice);
        log.info("=============PRODUCT END=================");
        return totalPrice;
    }

}

5. 订单服务模块

5.1 pom.xml配置



        
        
            org.springframework.cloud
            spring-cloud-starter-openfeign
        

    

5.2 seata配置

# seata配置
seata:
  enabled: true
  # Seata 应用编号,默认为 ${spring.application.name}
  application-id: order-seata
  # Seata 事务组编号,用于 TC 集群名
  tx-service-group: seataserver-group
  # 服务配置项
  service:
    # 虚拟组和分组的映射
    vgroup-mapping:
      seataserver-group: default
    # 分组和 Seata 服务的映射
    grouplist:
      default: 127.0.0.1:8091
  config:
    type: nacos
    nacos:
      serverAddr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace:
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      namespace:

5.3 功能代码(controller)

controller代码:

package com.platform.order.controller;

import com.platform.order.dto.PlaceOrderRequest;
import com.platform.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController
{
    @Autowired
    private OrderService orderService;

    @PostMapping("/placeOrder")
    public String placeOrder(@Validated @RequestBody PlaceOrderRequest request)
    {
        orderService.placeOrder(request);
        return "下单成功";
    }

    @PostMapping("/test1")
    // @ApiOperation("测试商品库存不足-异常回滚")
    public String test1()
    {
        // 商品单价10元,库存20个,用户余额50元,模拟一次性购买22个。 期望异常回滚
        orderService.placeOrder(new PlaceOrderRequest(1L, 1L, 22));
        return "下单成功";
    }

    @PostMapping("/test2")
    // @ApiOperation("测试用户账户余额不足-异常回滚")
    public String test2()
    {
        // 商品单价10元,库存20个,用户余额50元,模拟一次性购买6个。 期望异常回滚
        orderService.placeOrder(new PlaceOrderRequest(1L, 1L, 6));
        return "下单成功";
    }

}

5.4 功能代码(service)

service实现代码:

package com.platform.order.service.impl;


import com.platform.order.domain.Order;
import com.platform.order.dto.PlaceOrderRequest;
import com.platform.order.entity.AjaxResult;
import com.platform.order.feign.service.AccountFeignService;
import com.platform.order.feign.service.ProductFeignService;
import com.platform.order.mapper.OrderMapper;
import com.platform.order.service.OrderService;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Service
public class OrderServiceImpl implements OrderService
{
    private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);

    @Resource
    private OrderMapper orderMapper;

    @Autowired
    private AccountFeignService accountService;

    @Autowired
    private ProductFeignService productService;

    // @DS("order") // 每一层都需要使用多数据源注解切换所选择的数据库
    @Override
    @GlobalTransactional // 重点 第一个开启事务的需要添加seata全局事务注解
    public void placeOrder(PlaceOrderRequest request)
    {
        log.info("=============ORDER START=================");
        Long userId = request.getUserId();
        Long productId = request.getProductId();
        Integer amount = request.getAmount();
//        log.info("收到下单请求,用户:{}, 商品:{},数量:{}", userId, productId, amount);
//
//        log.info("当前 XID: {}", RootContext.getXID());
//
        Order order = new Order(userId, productId, 0, amount);
//
//        orderMapper.insert(order);
//        log.info("订单一阶段生成,等待扣库存付款中");
        // 扣减库存并计算总价

        commitOrder(request);

        AjaxResult result = productService.reduceStock(productId, amount);

        Double totalPrice = 0d;

        if(0 == (Integer) result.get(AjaxResult.CODE_TAG))
        {
            totalPrice = (Double)result.get("data");
        }
        else
        {
            throw new RuntimeException("扣减库存Failed!");
        }

        // 扣减余额
        result = accountService.reduceBalance(userId, totalPrice);

        // success
        if(0 == (Integer) result.get(AjaxResult.CODE_TAG))
        {
            order.setStatus(1);
            order.setTotalPrice(totalPrice);
            orderMapper.updateById(order);
            log.info("订单已成功下单");
        }
        else {
            throw new RuntimeException("扣减Account Failed!");
        }

        log.info("=============ORDER END=================");
    }

    @Transactional
    public void commitOrder(PlaceOrderRequest request)
    {
        log.info("=============ORDER START=================");
        Long userId = request.getUserId();
        Long productId = request.getProductId();
        Integer amount = request.getAmount();
        log.info("收到下单请求,用户:{}, 商品:{},数量:{}", userId, productId, amount);

        log.info("当前 XID: {}", RootContext.getXID());

        Order order = new Order(userId, productId, 0, amount);

        orderMapper.insert(order);
        log.info("订单一阶段生成,等待扣库存付款中");
    }

}

6. 数据库脚本

这里以单个数据库为例。如果多个数据库,则需要分表在每个库中创建undo_log表。

CREATE TABLE p_order
(
    id               INT(11) NOT NULL AUTO_INCREMENT,
    user_id          INT(11) DEFAULT NULL,
    product_id       INT(11) DEFAULT NULL,
    amount           INT(11) DEFAULT NULL,
    total_price      DOUBLE       DEFAULT NULL,
    status           VARCHAR(100) DEFAULT NULL,
    add_time         DATETIME     DEFAULT CURRENT_TIMESTAMP,
    last_update_time DATETIME     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;


CREATE TABLE product
(
    id               INT(11) NOT NULL AUTO_INCREMENT,
    price            DOUBLE   DEFAULT NULL,
    stock            INT(11) DEFAULT NULL,
    last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;


INSERT INTO product (id, price, stock) VALUES (1, 10, 20);


CREATE TABLE account
(
    id               INT(11) NOT NULL AUTO_INCREMENT,
    balance          DOUBLE   DEFAULT NULL,
    last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;

INSERT INTO account (id, balance) VALUES (1, 50);

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,
    PRIMARY KEY (id),
    UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;

undo_log表,是Seata AT模式必须创建的表,主要用于分支事务的回滚。 

7. 功能测试

测试发送数据的方式:

1. Success test.
Post: http://localhost:9202/order/placeOrder
Content-Type/application/json
{
    "userId": 1,
    "productId": 1,
    "amount": 1
}

2. Fail Test库存不足
Post: http://localhost:9202/order/placeOrder
Content-Type/application/json
{
    "userId": 1,
    "productId": 1,
    "amount": 22
}

3. Fail Test用户余额不足
Post: http://localhost:9202/order/placeOrder
Content-Type/application/json
{
    "userId": 1,
    "productId": 1,
    "amount": 6
}

根据数据库表中的数据多少,决定测试的场景参数。

7.1 下单成功

微服务seata 1.4.2 分布式事务AT模式示例_第1张图片

 7.2 扣减库存失败微服务seata 1.4.2 分布式事务AT模式示例_第2张图片

7.3 账户余额不足微服务seata 1.4.2 分布式事务AT模式示例_第3张图片

  

8. 遇到的问题

在执行回滚时,出现无法回滚的错误提示:

2022-03-24 14:35:29.998  INFO 172484 --- [h_RMROLE_1_9_16] i.seata.rm.datasource.DataSourceManager  : branchRollback failed. branchType:[AT], xid:[192.168.17.85:8091:9205601971902693387], branchId:[9205601971902693392], resourceId:[jdbc:mysql://localhost:3306/ftcsp], applicationData:[null]. reason:[Branch session rollback failed and try again later xid = 192.168.17.85:8091:9205601971902693387 branchId = 9205601971902693392 com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (byte[])"{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"192.168.17.85:8091:9205601971902693387","branchId":9205601971902693392,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"UPDATE","tableName":"product","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"product","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.dataso"[truncated 1605 bytes]; line: 1, column: 940] (through reference chain: io.seata.rm.datasource.undo.BranchUndoLog["sqlUndoLogs"]->java.util.ArrayList[0]->io.seata.rm.datasource.undo.SQLUndoLog["beforeImage"]->io.seata.rm.datasource.sql.struct.TableRecords["rows"]->java.util.ArrayList[0]->io.seata.rm.datasource.sql.struct.Row["fields"]->java.util.ArrayList[3]->io.seata.rm.datasource.sql.struct.Field["value"])]
2022-03-24 14:35:29.999  INFO 172484 --- [h_RMROLE_1_9_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_RollbackFailed_Retryable
2022-03-24 14:35:30.986  INFO 172484 --- [_RMROLE_1_10_16] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:xid=192.168.17.85:8091:9205601971902693387,branchId=9205601971902693392,branchType=AT,resourceId=jdbc:mysql://localhost:3306/ftcsp,applicationData=null
2022-03-24 14:35:30.986  INFO 172484 --- [_RMROLE_1_10_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 192.168.17.85:8091:9205601971902693387 9205601971902693392 jdbc:mysql://localhost:3306/ftcsp
2022-03-24 14:35:30.992 ERROR 172484 --- [_RMROLE_1_10_16] i.s.r.d.u.parser.JacksonUndoLogParser    : json decode exception, Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (byte[])"{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"192.168.17.85:8091:9205601971902693387","branchId":9205601971902693392,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"UPDATE","tableName":"product","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"product","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.dataso"[truncated 1605 bytes]; line: 1, column: 940] (through reference chain: io.seata.rm.datasource.undo.BranchUndoLog["sqlUndoLogs"]->java.util.ArrayList[0]->io.seata.rm.datasource.undo.SQLUndoLog["beforeImage"]->io.seata.rm.datasource.sql.struct.TableRecords["rows"]->java.util.ArrayList[0]->io.seata.rm.datasource.sql.struct.Row["fields"]->java.util.ArrayList[3]->io.seata.rm.datasource.sql.struct.Field["value"])

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (byte[])"{"@class":"io.seata.rm.datasource.undo.BranchUndoLog","xid":"192.168.17.85:8091:9205601971902693387","branchId":9205601971902693392,"sqlUndoLogs":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.undo.SQLUndoLog","sqlType":"UPDATE","tableName":"product","beforeImage":{"@class":"io.seata.rm.datasource.sql.struct.TableRecords","tableName":"product","rows":["java.util.ArrayList",[{"@class":"io.seata.rm.datasource.sql.struct.Row","fields":["java.util.ArrayList",[{"@class":"io.seata.rm.dataso"[truncated 1605 bytes]; line: 1, column: 940] (through reference chain: io.seata.rm.datasource.undo.BranchUndoLog["sqlUndoLogs"]->java.util.ArrayList[0]->io.seata.rm.datasource.undo.SQLUndoLog["beforeImage"]->io.seata.rm.datasource.sql.struct.TableRecords["rows"]->java.util.ArrayList[0]->io.seata.rm.datasource.sql.struct.Row["fields"]->java.util.ArrayList[3]->io.seata.rm.datasource.sql.struct.Field["value"])
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67) ~[jackson-databind-2.11.4.jar:2.11.4]
	at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1615) ~[jackson-databind-2.11.4.jar:2.11.4]
	at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400) ~[jackson-databind-2.11.4.jar:2.11.4]

回滚时,出现回滚日志无法解析到实体类,此时需要将mysql表中需要回滚的业务表的datetime数据类型,修改为timestamp类型。原因是seata 1.4.2 在MySQL8.0上对datetime类型的转换存在bug

参考代码下载地址

你可能感兴趣的:(分布式事务,微服务,seata,分布式事务)