分布式事务讲解 - TX-LCN分布式事务框架(含LCN、TCC、TXC三种模式)

TX-LCN框架原理

  • 框架主要由事务参与者(TxClient)、事务管理者(TxManager)两部分构成,事务控制的原理图如下:
  • 由图中我们就能理解TX-LCN框架的原理了,第四步可以作为阶段分割线,第四步上面所有操作就像是2PC的第一阶段,第四步及下面所有操作就像是2PC的第二阶段,如果大家忘记了2PC的话,可以看一下我写的前一篇博客【分布式事务讲解 - 2PC、3PC】,这个时序图其实就是框架源码流程的翻译,源码中也用了很多巧妙的手法来实现整体事务的工作流程,这些我会在下面介绍旗下三种模式的时候给大家一起讲解。

LCN

原理及主要特点

  • LCN模式使用了代理技术,把本地事务的数据库连接维持住不释放,通过TxManager来统一控制事务。
  • 在本地事务中,对事务进行的commit/rollback/close操作都是假操作,大家再远那种可以看到代理后的数据库连接中的commit/rollback/close方法都是空方法,限制本地事务进行除执行SQL以外的其他操作。
  • 可以说在本地事务执行SQL之后就会一直保持数据库连接,直到TxManager发来提交或者回滚操作才会使本地事务释放数据库连接,这样增加了数据库连接的占用时长。
  • 最后一阶段的TxManager发出的提交或者回滚命令是通过Netty发送的,会循环所有TxClient并发送指令。

代码实现

  • 不光是对LCN这个模式的实现,对于所有框架的实现无非主要就三个步骤:
  • 引依赖
  • 写配置
  • 加注解
实现场景
  • 用户买东西下单,下完单之后保存【单据记录】以及【支付金额】,只要有一个操作执行失败,那就下单失败,否则,整体流程执行成功,下单成功。
创建数据库及表(三个数据库,两个表)
-- 用户维护日志及异常信息,可按需创建表
CREATE DATABASE tx-manager;

CREATE DATABASE lcn-pay;

CREATE TABLE `tbl_pay` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pay_name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;


CREATE DATABASE lcn-order;

CREATE TABLE `tbl_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
创建工程(四个)
事务管理系统(lcn-tm)
引依赖
<!-- txManager所需依赖 -->
<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tm</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tc</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<!-- 用于txManager通知txClient -->
<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-txmsg-netty</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<!--tm-->
写配置
# TM事务管理器的服务端WEB访问端口。提供一个可视化的界面。端口自定义。
server.port=3000

# TM事务管理器,需要访问数据库,实现分布式事务状态记录。
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/tx-manager?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=wk3515134

# 为spring应用起名。
spring.application.name=tx-lcn-transaction-manager

# TM事务管理器,提供的WEB管理平台的登录密码。无用户名。 默认是codingapi
tx-lcn.manager.admin-key=wk3515134
# 日志。如果需要TM记录日志。则开启,赋值为true,会自动为tx-manager数据库添加t_logger表。
tx-lcn.logger.enabled=true
加注解
  • 在lcn-tm工程启动类添加注解
@EnableTransactionManagerServer
Eureka注册及发现系统(eureka)
引依赖
<dependency>
   <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
写配置
spring:
  application:
    name: cloud-eureka
eureka:
  instance:
    # 在hosts文件中修改内容
    hostname: euk-server-one.com
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      # 5 24
      defaultZone: http://euk-server-one.com:8080/eureka/
加注解
  • 在eureka工程启动类添加注解
@EnableEurekaServer
订单系统(lcn-order)
引依赖
<!-- lcn -->
<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>

<!-- json-lib -->
<dependency>
    <groupId>net.sf.json-lib</groupId>
    <artifactId>json-lib</artifactId>
    <version>2.4</version>
    <classifier>jdk15</classifier>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>

<!-- mysql:MyBatis相关依赖 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>

<!-- mysql:mysql驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- mysql:阿里巴巴数据库连接池 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.12</version>
</dependency>
写配置
server:
  port: 1000
# 服务名称
spring:
  application:
    name: lcn-order
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/lcn-order?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: wk3515134
    dbcp2:
      initial-size: 5
      min-idle: 5
      max-total: 5
      max-wait-millis: 200
      validation-query: SELECT 1
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false

mybatis:
  mapper-locations:
  - classpath:mapper/*.xml

eureka:
  client:
    service-url:
      # 注册进eureka
      defaultZone: http://euk-server-one.com:8080/eureka/

# 事务处理器地址,需要先启动lcn-tm系统并登录,端口就在下图所示位置
tx-lcn:
  client:
    manager-address: 127.0.0.1:3100
加注解
@EnableDistributedTransaction
启动类定义RestTemplate的bean(用于调用lcn-pay系统方法)
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
    return new RestTemplate();
}
添加测试Controller类(OrderController)
import com.codingapi.txlcn.tc.annotation.LcnTransaction;
import com.hepai.lcnorder.dao.TblOrderDao;
import com.hepai.lcnorder.entity.TblOrder;
import net.sf.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class OrderController {
    @Autowired
    private TblOrderDao tblOrderDao;
    @Autowired
    private RestTemplate restTemplate;
    @PostMapping("/add-order")
    @Transactional(rollbackFor = Exception.class) //一定要加,LCN源码会判断这个注解
    @LcnTransaction // 注解必须加,使用LCN模式,加在有调用分布式系统的方法之上即可
    public String add(@RequestBody TblOrder bean){
        JSONObject date = new JSONObject();
        date.put("payName",bean.getOrderName()+"pay");
		// 调用支付系统指定方法
        restTemplate.postForEntity("http://lcn-pay/add-pay",date,String.class);
//        int i = 1/0;
		// 插入sql,Service、Mapper以及Entity大家自己写吧,按照普通业务逻辑写就好
        tblOrderDao.insert(bean);
        return "新增订单成功";
    }
}
支付系统(lcn-pay)
引依赖(与lcn-order系统依赖相同)
<!-- lcn -->
<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>
<!-- json-lib -->
<dependency>
    <groupId>net.sf.json-lib</groupId>
    <artifactId>json-lib</artifactId>
    <version>2.4</version>
    <classifier>jdk15</classifier>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <version>2.2.2.RELEASE</version>
</dependency>

<!-- mysql:MyBatis相关依赖 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>

<!-- mysql:mysql驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- mysql:阿里巴巴数据库连接池 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.12</version>
</dependency>
写配置
server:
  port: 2000
# 服务名
spring:
  application:
    name: lcn-pay
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/lcn-pay?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: wk3515134
    dbcp2:
      initial-size: 5
      min-idle: 5
      max-total: 5
      max-wait-millis: 200
      validation-query: SELECT 1
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
mybatis:
  mapper-locations:
  - classpath:mapper/*.xml
eureka:
  client:
    service-url:
      defaultZone: http://euk-server-one.com:8080/eureka/
# 事务处理器地址,需要先启动lcn-tm系统并登录,端口就在下图所示位置
tx-lcn:
  client:
    manager-address: 127.0.0.1:3100
加注解
  • 在lcn-pay工程启动类添加注解
添加测试Controller类(PayController)
import com.codingapi.txlcn.tc.annotation.LcnTransaction;
import com.hepai.lcnpay.dao.TblPayDao;
import com.hepai.lcnpay.entity.TblPay;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PayController {
    @Autowired
    private TblPayDao tblPayDao;
    @PostMapping("/add-pay")
    @Transactional(rollbackFor = Exception.class)
    @LcnTransaction
    public String addPay(@RequestBody TblPay bean){
        tblPayDao.insert(bean);
        return "新增支付成功";

    }
}
执行测试
测试事务执行成功
  • 使用Postman调用【http://localhost:1000/add-order】,填写相应参数,执行结果:
  • 查看数据库数据:
测试事务执行失败
  • 修改OderController类方法,添加异常
public String add(@RequestBody TblOrder bean){
   JSONObject date = new JSONObject();
   date.put("payName",bean.getOrderName()+"pay");
   restTemplate.postForEntity("http://lcn-pay/add-pay",date,String.class);
// 添加异常
   int i = 1/0;
   tblOrderDao.insert(bean);
   return "新增订单成功";
}
  • 使用Postman调用【http://localhost:1000/add-order】,填写相应参数,执行结果:
  • 查看数据库数据:
  • 可见,数据库数据没有变化,还是刚才事务执行成功插入的数据,所以,分布式事务生效。

TCC

原理及主要特点

代码实现(以LCN测试代码为基础添加TCC代码)

订单系统(lcn-order)
新增测试Controller类(OrderTccController)
import com.codingapi.txlcn.tc.annotation.TccTransaction;
import com.hepai.lcnorder.dao.TblOrderDao;
import com.hepai.lcnorder.entity.TblOrder;
import net.sf.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

@RestController
public class OrderTccController {
    @Autowired
    private TblOrderDao tblOrderDao;
    @Autowired
    private RestTemplate restTemplate;
    @PostMapping("/add-order-tcc")
    @Transactional(rollbackFor = Exception.class)
    @TccTransaction  // 使用框架的TCC模式实现分布式事务
    public String add(@RequestBody TblOrder bean){
        JSONObject date = new JSONObject();
        date.put("payName",bean.getOrderName()+"pay");
        restTemplate.postForEntity("http://lcn-pay/add-pay-tcc",date,String.class);
        tblOrderDao.insert(bean);
        Integer id = bean.getId();
        maps.put("a",id);
//        int i = 1/0;
        return "新增订单成功1";
    }
    public String confirmAdd(TblOrder bean){
        System.out.println("order confirm ");
        return "新增订单成功2";
    }
    private static Map<String,Integer> maps = new HashMap<>();
    public String cancelAdd(TblOrder bean){
        Integer a = maps.get("a");
        System.out.println("a:"+a);
        // 取消方法执行sql逆操作
        tblOrderDao.deleteByPrimaryKey(a);
        System.out.println("order cancel ");
        return "新增订单失败";
    }
}
  • 注意:
  • cancelAdd()和confirmAdd()不用加注解,因为框架源码中表明如果没有发现【提交方法】和【取消方法】的注解,那就默认把加了【@TccTransaction】注解的方法名第一个字母大写并加前缀【cancel】和【confirm】来分别作为【提交方法】和【取消方法】的方法名。
支付系统(lcn-pay)
新增测试Controller类(PayTccController)
import com.codingapi.txlcn.tc.annotation.TccTransaction;
import com.hepai.lcnpay.dao.TblPayDao;
import com.hepai.lcnpay.entity.TblPay;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;

@RestController
public class PayTccController {
    @Autowired
    private TblPayDao tblPayDao;
    @PostMapping("/add-pay-tcc")
    @Transactional(rollbackFor = Exception.class)
    @TccTransaction
    public String addPay(@RequestBody TblPay bean){
        tblPayDao.insert(bean);
        Integer id = bean.getId();
        maps.put("a",id);
        int i = 1/0;
        return "新增支付成功1";
    }
    public String confirmAddPay(TblPay bean){
        System.out.println("pay confirm");
        return "新增支付成功2";

    }
    private static Map<String,Integer> maps = new HashMap<>();
    /**
     * 逆sql
     * @param bean
     * @return
     */
    public String cancelAddPay(TblPay bean){
        Integer a = maps.get("a");
        System.out.println("a:"+a);
        System.out.println("pay cancel");
        tblPayDao.deleteByPrimaryKey(a);
        return "支付失败";
    }
}
执行测试
测试事务执行成功
  • 使用Postman调用【http://localhost:1000/add-order-tcc】,填写相应参数,执行结果:
  • 查看数据库数据:
测试事务执行失败
  • 修改OderTccController类方法,添加异常
public String add(@RequestBody TblOrder bean){
   JSONObject date = new JSONObject();
    date.put("payName",bean.getOrderName()+"pay");
    restTemplate.postForEntity("http://lcn-pay/add-pay-tcc",date,String.class);
    tblOrderDao.insert(bean);
    Integer id = bean.getId();
    maps.put("a",id);
    // 添加异常
    int i = 1/0;
    return "新增订单成功1";
}
  • 使用Postman调用【http://localhost:1000/add-order-tcc】,填写相应参数,执行结果:
  • 查看数据库数据:
  • 可见,数据库数据没有变化,还是刚才事务执行成功插入的数据,所以,分布式事务生效。

TXC

原理及主要特点

  • TXC模式的工作流程中执行SQL前要先查询影响的数据,如果数据量大的情况下,效率会极低。
  • TXC模式只能支持SQL方式的事务模块,因为第一条,它必须要先查询影响数据,所以有要执行的SQL是大前提。
  • 不会占用数据库的连接资源。
  • 我个人认为,对于第一条,如果是有大量的数据,并且涉及到的影响数据表有成百上千个,那这种方式是最不建议的,这种模式对查询的效率要求极高。

总结

  • 在TX-LCN框架中,在参与者本地支持事务的分布式事务情况下,建议使用LCN模式,参与者本地有不支持事务的情况下,只能用TCC。