分布式事务|如何在微服务场景下使用TX-LCN做分布式事务

一、源起

使用SpringBoot+SpringCloud作微服务项目,我最头疼的莫过于事务的控制了,业务逻辑太复杂,不可能把一个接口中的数据库操作都放在一个事务中,服务之间的相互调用,怎么保证事务的一致性?

我看了https://yq.aliyun.com/articles/600584云栖的一篇文章,真的挺好,以至于我想直接接入阿里的FMT模型,不过貌似是收费的,哼!!我辈撸代码,收费呵呵哒(QTM)。

然后就找到了codingapi的官文(http://www.txlcn.org/zh-cn/docs/preface.html

二、话不多说,先梭一把

1.环境

    JDK1.8

    maven 3.3.9

    spring boot 2.2.1.RELEASE

    consul

    MySQL 5.6

    Mybatis

2.TX-LCN原理图,借鉴官网(侵删)

3.TxManager(TM) 

    tx-manager是事务的管理者,它不需要做任何操作 。

    创建spring boot项目,添加以下依赖

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tm</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

     application.properties中配置如下:

#配置来自codingapi官文
spring.application.name=tx-manager
server.port=7970
# JDBC 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/tx-manager?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
# 数据库方言
#spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
# 第一次运行可以设置为: create, 为TM创建持久化数据库表
#spring.jpa.hibernate.ddl-auto=validate

# TM监听IP. 默认为 127.0.0.1
tx-lcn.manager.host=127.0.0.1
# TM监听Socket端口. 默认为 ${server.port} - 100
tx-lcn.manager.port=8070
# 心跳检测时间(ms). 默认为 300000
tx-lcn.manager.heart-time=300000
# 分布式事务执行总时间(ms). 默认为36000
tx-lcn.manager.dtx-time=8000
# 参数延迟删除时间单位ms  默认为dtx-time值
tx-lcn.message.netty.attr-delay-time=${tx-lcn.manager.dtx-time}
# 事务处理并发等级. 默认为机器逻辑核心数5倍
tx-lcn.manager.concurrent-level=160
# TM后台登陆密码,默认值为codingapi
tx-lcn.manager.admin-key=codingapi
# 分布式事务锁超时时间 默认为-1,当-1时会用tx-lcn.manager.dtx-time的时间
tx-lcn.manager.dtx-lock-time=${tx-lcn.manager.dtx-time}
# 雪花算法的sequence位长度,默认为12位.
tx-lcn.manager.seq-len=12
# 异常回调开关。开启时请制定ex-url
tx-lcn.manager.ex-url-enabled=false
# 事务异常通知(任何http协议地址。未指定协议时,为TM提供内置功能接口)。默认是邮件通知
#tx-lcn.manager.ex-url=/provider/email-to/***@**.com

#redis
spring.redis.database=0
spring.redis.host=192.168.10.86
spring.redis.password=yunsign
spring.redis.port=6379
# 开启日志,默认为false
tx-lcn.logger.driver-class-name=${spring.datasource.driver-class-name}
tx-lcn.logger.jdbc-url=${spring.datasource.url}
tx-lcn.logger.username=${spring.datasource.username}
tx-lcn.logger.password=${spring.datasource.password}

    因为是本地测试环境,tx-manager不需要注册到consul。在启动类添加注解:@EnableTransactionManagerServer

4.事务的参与者

    简单做了一个 下单--支付--增加流水 的业务,分别由三个服务组成。

  1)  service-order(订单服务,事务的发起方)

    添加的依赖

<dependency>
	<groupId>com.codingapi.txlcn</groupId>
	<artifactId>txlcn-tc</artifactId>
	<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
	<groupId>com.codingapi.txlcn</groupId>
	<artifactId>txlcn-txmsg-netty</artifactId>
	<version>5.0.2.RELEASE</version>
</dependency>

    application.properties配置,官文上的几个其他配置删掉了,不需要配

spring.application.name=service-order
server.port=5002
server.tomcat.uri-encoding=UTF-8

##日志级别
logging.level.org.springframework.web=debug
logging.level.com.lcn.dao=debug

##数据库
spring.datasource.url=jdbc:mysql://localhost:3306/tx-service-order?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root

##mybatis
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.type-aliases-package=com.lcn.dao.entity
mybatis.configuration.use-generated-keys=true

# 是否启动LCN负载均衡策略(优化选项,开启与否,功能不受影响)
tx-lcn.ribbon.loadbalancer.dtx.enabled=true

# tx-manager 的配置地址,可以指定TM集群中的任何一个或多个地址
# tx-manager 下集群策略,每个TC都会从始至终<断线重连>与TM集群保持集群大小个连接。
# TM方,每有TM进入集群,会找到所有TC并通知其与新TM建立连接。
# TC方,启动时按配置与集群建立连接,成功后,会再与集群协商,查询集群大小并保持与所有TM的连接
tx-lcn.client.manager-address=127.0.0.1:8070
# 调用链长度等级,默认值为3(优化选项。系统中每个请求大致调用链平均长度,估算值。)
tx-lcn.client.chain-level=3

# 开启日志,默认为false
tx-lcn.logger.enabled=true
tx-lcn.logger.driver-class-name=${spring.datasource.driver-class-name}
tx-lcn.logger.jdbc-url=${spring.datasource.url}
tx-lcn.logger.username=${spring.datasource.username}
tx-lcn.logger.password=${spring.datasource.password}

    bootstrap.properties配置,基于consul的服务注册与发现

spring.cloud.consul.host=192.168.10.82
spring.cloud.consul.discovery.prefer-ip-address=true
spring.cloud.consul.discovery.instance-id=${spring.application.name}:${spring.cloud.client.ip-address}
spring.cloud.consul.discovery.service-name=${spring.application.name}
spring.cloud.consul.discovery.hostname=${spring.cloud.client.ip-address}
spring.cloud.consul.discovery.port=${server.port}
spring.cloud.consul.discovery.health-check-interval=30s

     service-order的项目结构如下:

 

   2)service-account(账户服务),service-pay (支付服务)(事务的参与方)

    这两个项目的配置与 service-order 都一样,我这里每个服务都有一个独立的数据库,是为了测试LCN的效果,是不是要分库在这里没区别。

5.CODING

    先创建一个待支付的订单,然后拿订单号去支付,服务之间的调用采用 feign 的方式,尝试在支付过程的任何位置抛出异常,看看数据库的结果是否一致。

    1)service-order | OrderController.java  (LCN模式)

/**
     * 支付订单,事务发起方
     * @param userId
     * @param orderNo
     * @param ex 异常标识
     * @return
     */
    @LcnTransaction
    @GetMapping("payorder")
    public String payOrder(@RequestParam("userId") Integer userId,
                           @RequestParam("orderNo") String orderNo,
                           @RequestParam(value = "ex", required = false) String ex) {
        Order order = orderMapper.getByOrderNo(orderNo);
        //调用支付
        String payResult = payService.pay(orderNo, order.getPrice(), null);
        if (Objects.isNull(payResult)) {
            throw new IllegalStateException("pay failed");
        }
        //记录流水
        String recordResult = accountFlowService.recordFlow(userId, order.getPrice());
        if (Objects.isNull(recordResult)) {
            throw new IllegalStateException("record failed");
        }
        //更新订单
        orderMapper.updateOrderPayStatus(order.getId(), 1);
        if (Objects.nonNull(ex)) {
            throw new IllegalStateException("by exFlag");
        }
        return payResult + " > " + recordResult + " > " + "ok-service-order";
    }

    2)service-pay | PayController.java  (TXC模式)

//    @LcnTransaction(propagation = DTXPropagation.SUPPORTS)
    @TxcTransaction(propagation = DTXPropagation.SUPPORTS)
    @GetMapping("pay")
    public String pay(@RequestParam("orderNo") String orderNo, 
                      @RequestParam("amount") BigDecimal amount,
                      @RequestParam(value = "ex", required = false) String ex) {
        Pay pay = new Pay()
                .setPayAmount(amount)
                .setOrderNo(orderNo);
        payMapper.insert(pay);
        return "ok-pay-service";
    }

    3)service-account | AccountFlowController.java  (TCC模式)

    TCC模式需要自己写业务执行成功的 commit 和失败的 cancel 方法,这种模式对业务的侵入最大,在业务逻辑非常复杂的时候是很烦的,另外两种不需要。

@Slf4j
@RestController
public class AccountFlowController {

    @Autowired
    private AccountFlowMapper accountFlowMapper;

    private ConcurrentHashMap<String, Integer> ids = new ConcurrentHashMap<>();

    @GetMapping("recoredflow")
    @TccTransaction(propagation = DTXPropagation.SUPPORTS)
    @Transactional
    public String recordFlow(@RequestParam("userId") Integer userId, 
                             @RequestParam("amount") BigDecimal changeAmount) {
        AccountFlow accountFlow = new AccountFlow()
                .setFlowDirection(0)
                .setChangeAmount(changeAmount)
                .setEvent("购买钢笔")
                .setUserId(userId)
                .setGroupId(TracingContext.tracing().groupId());
        accountFlowMapper.insert(accountFlow);
        ids.put(TracingContext.tracing().groupId(), accountFlow.getId());
        return "ok-service-account";
    }

    public void confirmRecordFlow(Integer userId, BigDecimal changeAmount) {
        log.info("tcc-confirm-{}", TracingContext.tracing().groupId());
        ids.remove(TracingContext.tracing().groupId());
    }

    public void cancelRecordFlow(Integer userId, BigDecimal changeAmount) {
        log.info("tcc-cancel-{}", TracingContext.tracing().groupId());
        Integer flowId = ids.get(TracingContext.tracing().groupId());
        accountFlowMapper.deleteById(flowId);
    }
}

6.测试

    先启动 TM ,再启动其他三个项目,访问 http://localhost:5002/payorder?orderNo=1578986829457&userId=1&ex=true ,在所有业务都执行完毕的情况下抛出异常,另外两个项目的事务也正常回滚。说明此次的分布式事务LCN模式学习成功!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值