微服务之分布式事务Seata-第三章-实战(微服务集成seata)

版权声明

一、示例

在这里插入图片描述

1、导入示例

详细看最后一节【示例代码
在这里插入图片描述

2、数据表


SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for account_tbl
-- ----------------------------
DROP TABLE IF EXISTS `account_tbl`;
CREATE TABLE `account_tbl`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `money` int(11) UNSIGNED NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of account_tbl
-- ----------------------------
INSERT INTO `account_tbl` VALUES (1, 'user202003032042012', 1000);

-- ----------------------------
-- Table structure for order_tbl
-- ----------------------------
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `commodity_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `count` int(11) NULL DEFAULT 0,
  `money` int(11) NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Table structure for storage_tbl
-- ----------------------------
DROP TABLE IF EXISTS `storage_tbl`;
CREATE TABLE `storage_tbl`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commodity_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `count` int(11) UNSIGNED NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `commodity_code`(`commodity_code`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of storage_tbl
-- ----------------------------
INSERT INTO `storage_tbl` VALUES (1, '100202003032041', 10);

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `undo_log`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime(0) NOT NULL,
  `log_modified` datetime(0) NOT NULL,
  `ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

SET FOREIGN_KEY_CHECKS = 1;

在这里插入图片描述

账户表account_tbl

记录用户的账户余额
在这里插入图片描述

库存表storage_tbl

记录商品编号对应的库存
在这里插入图片描述

count int(11) UNSIGNED NULL DEFAULT 0
注意UNSIGNED表示字段不能为0
MySQL处理两个整数(INT)相减的时候,如果其中有一个是UNSIGNED INT类型的,那么结果就被当做是UNSIGNED的。
如果相减的结果是负数
在MySQL 5.5.5之前,结果变成了最大的整数(18446744073709551615)
从MySQL 5.5.5开始,这种情况会返回一个错误:BIGINT UNSIGNED value is out of range…

3、postman测试

在这里插入图片描述

二、代码解读

1、order-service

@RestController
@RequestMapping("order")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    public ResponseEntity<Long> createOrder(Order order){
        Long orderId = orderService.create(order);
        return ResponseEntity.status(HttpStatus.CREATED).body(orderId);
    }
}

创建订单之后,先扣减账户余额,接着扣减库存

Slf4j
@Service
public class OrderServiceImpl implements OrderService {

    private final AccountClient accountClient;
    private final StorageClient storageClient;
    private final OrderMapper orderMapper;

    public OrderServiceImpl(AccountClient accountClient, StorageClient storageClient, OrderMapper orderMapper) {
        this.accountClient = accountClient;
        this.storageClient = storageClient;
        this.orderMapper = orderMapper;
    }

    @Override
    @Transactional
    public Long create(Order order) {
        // 创建订单
        orderMapper.insert(order);
        try {
            // 扣用户余额
            accountClient.deduct(order.getUserId(), order.getMoney());
            // 扣库存
            storageClient.deduct(order.getCommodityCode(), order.getCount());

        } catch (FeignException e) {
            log.error("下单失败,原因:{}", e.contentUTF8(), e);
            throw new RuntimeException(e.contentUTF8(), e);
        }
        return order.getId();
    }
}

2、AccountClient

@RestController
@RequestMapping("account")
public class AccountController {

    @Autowired
    private AccountService accountService;

    @PutMapping("/{userId}/{money}")
    public ResponseEntity<Void> deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money){
        accountService.deduct(userId, money);
        return ResponseEntity.noContent().build();
    }
}
@FeignClient("account-service")
public interface AccountClient {

    @PutMapping("/account/{userId}/{money}")
    void deduct(@PathVariable("userId") String userId, @PathVariable("money") Integer money);
}

3、account-service

@Slf4j
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountMapper accountMapper;

    @Override
    @Transactional
    public void deduct(String userId, int money) {
        log.info("开始扣款");
        try {
            accountMapper.deduct(userId, money);
        } catch (Exception e) {
            throw new RuntimeException("扣款失败,可能是余额不足!", e);
        }
        log.info("扣款成功");
    }
}

4、storage-service

@RestController
@RequestMapping("storage")
public class StorageController {

    private final StorageService storageService;

    public StorageController(StorageService storageService) {
        this.storageService = storageService;
    }

    /**
     * 扣减库存
     * @param code 商品编号
     * @param count 要扣减的数量
     * @return 无
     */
    @PutMapping("/{code}/{count}")
    public ResponseEntity<Void> deduct(@PathVariable("code") String code,@PathVariable("count") Integer count){
        storageService.deduct(code, count);
        return ResponseEntity.noContent().build();
    }
}
@Slf4j
@Service
public class StorageServiceImpl implements StorageService {

    @Autowired
    private StorageMapper storageMapper;

    @Transactional
    @Override
    public void deduct(String commodityCode, int count) {
        log.info("开始扣减库存");
        try {
            storageMapper.deduct(commodityCode, count);
        } catch (Exception e) {
            throw new RuntimeException("扣减库存失败,可能是库存不足!", e);
        }
        log.info("扣减库存成功");
    }
}

三、集成seata

注意mysql的版本不要太高,最好8.0.11否则报错java.lang.NoSuchMethodException: com.mysql.cj.conf.PropertySet.getBooleanReadableProperty(java.lang.String)

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.11</version>
</dependency>

1、引入依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
        <!--版本较低,1.3.0,因此排除-->
        <exclusion>
            <artifactId>seata-spring-boot-starter</artifactId>
            <groupId>io.seata</groupId>
        </exclusion>
    </exclusions>
</dependency>
<!--seata starter 采用1.4.2版本-->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>${seata.version}</version>
</dependency>

注意:如果cloud和阿里巴巴的版本是<spring-cloud.version>2021.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-alibaba.version>
则只需要引入

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

2、修改配置文件

需要修改application.yml文件,添加一些配置:
GZ:表示广州

seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    # 参考tc服务自己的registry.conf中的配置
    type: nacos
    nacos: # tc
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: DEFAULT_GROUP
      application: seata-server # tc服务在nacos中的服务名称
      cluster: GZ
  tx-service-group: seata-demo # 事务组,根据这个获取tc服务的cluster名称
  service:
    vgroup-mapping: # 事务组与TC服务cluster的映射关系
      seata-demo: GZ

由于阿里巴巴seata存在循环依赖,在application.yml 中使用以下配置,好像是SpringBoot2.6.0之后默认禁止的。

spring:
    main:
        allow-circular-references: true  #允许循环引用

在这里插入图片描述

3、启动

在服务控制台看到

05-17 17:25:13:346  INFO 16489 --- [eoutChecker_1_1] i.s.c.rpc.netty.TmNettyRemotingClient    : register TM success. client version:1.4.2, server version:1.4.2,channel:[id: 0xf95abb53, L:/192.168.0.55:63134 - R:/192.168.0.55:8091]

之后在seata控制台看到如下说明成功

在这里插入图片描述

四、实现XA模式

Seata的starter已经完成了XA模式的自动装配,实现非常简单,步骤如下:

1、修改application.yml文件(每个参与事务的微服务),开启XA模式:

seata:  
  data-source-proxy-mode: XA # 开启数据源代理的XA模式

2、给发起全局事务的入口方法添加@GlobalTransactional注解,

本例中是OrderServiceImpl中的create方法:

@Override@GlobalTransactional
public Long create(Order order) {    
    // 创建订单    
    orderMapper.insert(order);    
    // 扣余额 ...略 
    // 扣减库存 ...略    
    return order.getId();
}

3、重启服务并测试

如果扣减库存超过10则回顾
在这里插入图片描述

五、实现AT模式

AT模式是seata的默认模式。
AT模式的快照生成,回滚等动作都是有框架自动完成,没有任何代码侵入,因此实现非常简单。

1、导入数据库表

lock_table导入到TC服务关联的数据库:seata

create table lock_table
(
	row_key varchar(128) not null
		primary key,
	xid varchar(96) null,
	transaction_id bigint null,
	branch_id bigint not null,
	resource_id varchar(256) null,
	table_name varchar(32) null,
	pk varchar(36) null,
	gmt_create datetime null,
	gmt_modified datetime null
)charset=utf8;
create index idx_branch_id on lock_table (branch_id);

undo_log导入到微服务关联的业务数据库:seata_demo,在前面已经导入。

2、修改application.yml文件(每个参与事务的微服务),开启AT模式:

seata:  
  data-source-proxy-mode: AT # 开启数据源代理的AT模式

3、重启服务测试

account-service日志如下:
在这里插入图片描述
如果断点的话,可以看到undo_log数据,执行完之后会被清空
在这里插入图片描述

六、实现TCC模式

不需要生成快照,不需要全局锁,不依赖数据库事务,而是通过Try、Confirm和Cancel接口实现冻结
示例需求如下:

  • 修改account-service,编写try、confirm、cancel逻辑
  • try业务:添加冻结金额,扣减可用金额
  • confirm业务:三处冻结金额
  • cancel业务:删除冻结金额,回复可用金额
  • 保证confirm、cancel接口的幂等性
  • 允许空回滚
  • 拒绝业务悬挂

1、流程

在这里插入图片描述

2、新增数据表

DROP TABLE IF EXISTS `account_freeze_tbl`;
CREATE TABLE `account_freeze_tbl`  (
 `xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '事务id',  
 `user_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户id',
 `freeze_money` int(11) UNSIGNED NULL DEFAULT 0 COMMENT '冻结金额',
 `state` int(1) NULL DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
 PRIMARY KEY (`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 ROW_FORMAT = COMPACT;

3、声明 TCC接口

在这里插入图片描述

4、申明AccountTCCService接口

@LocalTCC
public interface AccountTCCService {

    /**
     * deduct try的接口
     *
     * @param userId             the account
     * @param money             the money
     * @return the boolean
     */
    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
    void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                           @BusinessActionContextParameter(paramName = "money") int money);

    /**
     * confirm boolean. 提交事务的接口
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean confirm(BusinessActionContext actionContext);

    /**
     * cancel boolean. 回滚接口
     *
     * @param actionContext the action context
     * @return the boolean
     */
    boolean cancel(BusinessActionContext actionContext);

}

5、实现接口

@Service
@Slf4j
public class AccountTCCServiceImpl implements AccountTCCService {

    @Autowired
    private AccountMapper accountMapper;
    @Autowired
    private AccountFreezeMapper freezeMapper;

    @Override
    @Transactional
    public void deduct(String userId, int money) {
        // 0.获取事务id
        String xid = RootContext.getXID();

        //1.判断freeze中是否有冻结数据,有回滚记录则,会造成业务悬挂
        AccountFreeze oldFreeze = freezeMapper.selectById(xid);
        if (oldFreeze!=null){
            return;
        }

        // 1.扣减可用余额
        accountMapper.deduct(userId, money);
        // 2.记录冻结金额,事务状态
        AccountFreeze freeze = new AccountFreeze();
        freeze.setUserId(userId);
        freeze.setFreezeMoney(money);
        freeze.setState(AccountFreeze.State.TRY);
        freeze.setXid(xid);
        freezeMapper.insert(freeze);
    }

    @Override
    public boolean confirm(BusinessActionContext ctx) {
        // 1.获取事务id
        String xid = ctx.getXid();
        // 2.根据id删除冻结记录,删除是天然幂等性
        int count = freezeMapper.deleteById(xid);
        return count == 1;
    }

    @Override
    public boolean cancel(BusinessActionContext ctx) {
        // 0.查询冻结记录c
        String xid = ctx.getXid();
        AccountFreeze freeze = freezeMapper.selectById(xid);

        //1.判断空回滚,根据freeze是否为null
        String userId = ctx.getActionContext("userId").toString();
        if (freeze==null){
            //try没执行,需要回滚,并记录回滚
            freeze = new AccountFreeze();
            freeze.setUserId(userId);
            freeze.setFreezeMoney(0);
            freeze.setState(AccountFreeze.State.CANCEL);
            freeze.setXid(xid);
            freezeMapper.insert(freeze);
            return true;
        }
        //2.幂等性判断,防止重复操作
        if (freeze.getState()==AccountFreeze.State.CANCEL){
            //说明已经回滚过了
            return true;
        }

        // 1.恢复可用余额
        accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
        // 2.将冻结金额清零,状态改为CANCEL
        freeze.setFreezeMoney(0);
        freeze.setState(AccountFreeze.State.CANCEL);
        int count = freezeMapper.updateById(freeze);
        return count == 1;
    }
}

6、效果

当库存申请10时,扣减失败,account-service日志如下
在这里插入图片描述

七、示例代码

1、代码仓库

seata-demo

  • base分支:未集成Seata
  • XA分支:实现XA模式
  • AT分支:实现AT模式
  • TCC分支:实现TCC模式

2、数据库表

在这里插入图片描述

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值