LCN:Lock,Confirm,Notify
其实现分布式事务的原理是引入Transation Manager(简称TM),由TM来管理具体应用(TC Transaction Client)的事务确认及通知。
应用起来比较简单:1、搭建TM 2、在应用中使用@LcnTransaction 代替 @Transactional
上代码:
1、搭建TM
1.1) pom依赖
创建一个springboot工程lcn-tm 引入以下依赖:
<!-- tm manager -->
<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>
<dependency>
<groupId>com.codingapi.txlcn</groupId>
<artifactId>txlcn-txmsg-netty</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
引入这个版本的依赖需要springboot的版本是:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
1.2)配置。 TM需要mysql数据库及redis,所以需要事先准备mysql库和redis,目前lcn插件包不支持yaml,所以使用properties:
# TM事务管理器的服务端WEB访问端口。提供一个可视化的界面。端口自定义。
server.port=7970
# 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=root
# TM事务管理器,是依赖Redis使用分布式事务协调的。尤其是TCC和TXC两种事务模型。
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
# 为spring应用起名。
spring.application.name=lcm-tm
# TM事务管理器,提供的WEB管理平台的登录密码。无用户名。 默认是codingapi
tx-lcn.manager.admin-key=test
# 日志。如果需要TM记录日志。则开启,赋值为true,并提供后续的配置。
tx-lcn.logger.enabled=true
# 为日志功能,提供数据库连接。和之前配置的分布式事务管理依赖使用的数据源不同。
tx-lcn.logger.driver-class-name=com.mysql.cj.jdbc.Driver
tx-lcn.logger.jdbc-url=jdbc:mysql://localhost:3306/tx-manager?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
tx-lcn.logger.username=root
tx-lcn.logger.password=root
注意:
数据库这里名称必须是tx-manager,而且要在库中执行一个脚本:
CREATE DATABASE IF NOT EXISTS `tx-manager` DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
USE `tx-manager`;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_tx_exception
-- ----------------------------
DROP TABLE IF EXISTS `t_tx_exception`;
CREATE TABLE `t_tx_exception` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`group_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`unit_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`mod_id` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`transaction_state` tinyint(4) NULL DEFAULT NULL,
`registrar` tinyint(4) NULL DEFAULT NULL,
`ex_state` tinyint(4) NULL DEFAULT NULL COMMENT '0 待处理 1已处理',
`remark` varchar(10240) NULL DEFAULT NULL COMMENT '备注',
`create_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 967 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
1.3) 启动类上添加@EnableTransactionManagerServer 注解
至此,经过三部曲(pom、yaml/properties、启动类注解),TM搭建完毕。
启动TM,可以访问7970端口,因为配置的就是7970端口。访问之后就可以登录TM后台,需要输入用户名,配置文件中配置的是test。
2、TC应用端
既然是分布式事务,起码是有两个不同的应用的事务要在一起执行。所以在这里有两个服务,service-order,service-pay,
service-order接到请求,调用service-pay服务,然后插库,然后使用1/0制造错误,来验证service-pay的事务是不是也回滚了。
2.1)首先是service-order服务创建
2.1.1) 创建springboot工程,pom依赖。 tc端需要引入eureka客户端。我这里贴上所有我引入的依赖,跟着一篇有关的核心的主要是eureka和lcn的
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.kevin</groupId>
<artifactId>service-order</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>service-order</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<!-- mq -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-pool</artifactId>
<version>5.11.1</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<!-- 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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.1.2)yaml配置
server:
port: 6001
spring:
application:
name: service-order
activemq:
broker-url: tcp://127.0.0.1:61616
user: admin
password: admin
pool:
enabled: true
max-connections: 100
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/service-order?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: root
dbcp2:
initial-size: 5
min-idle: 5
max-idle: 5
max-wait-millis: 200
validation-query: select 1
test-while-idle: true
test-on-return: false
test-on-borrow: false
mybatis:
mapper-locations:
- classpath:mapper/*.xml
eureka:
client:
service-url:
defaultZone: http://eureka-7900:7900/eureka/
tx-lcn:
client:
manager-address: 127.0.0.1:8070
2.1.3)启动类上添加@EnableDistributedTransaction
@SpringBootApplication
@EnableJms
@EnableScheduling
@EnableDistributedTransaction
public class ServiceOrderApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceOrderApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
2.1.4) 创建controller,使用@LcnTransaction
import com.alibaba.fastjson.JSONObject;
import com.codingapi.txlcn.tc.annotation.LcnTransaction;
import com.kevin.dao.TblEventDao;
import com.kevin.entity.TblEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @author YangNa on 2021/4/12
*/
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private TblEventDao tblEventDao;
@RequestMapping("/add")
// @Transactional
@LcnTransaction
public String insertOrder(@RequestBody TblEvent tblEvent){
JSONObject data = new JSONObject();
data.put("id", tblEvent.getId());
data.put("type", tblEvent.getType());
data.put("content", tblEvent.getContent());
data.put("processor",tblEvent.getProcessor());
restTemplate.postForEntity("http://service-pay/pay/add", data, String.class);
tblEventDao.insert(tblEvent);
int i = 1/0;
return "添加成功";
}
}
TblEventDao:
@Repository
@Mapper
public interface TblEventDao {
int deleteByPrimaryKey(Integer id);
int insert(TblEvent record);
int insertSelective(TblEvent record);
TblEvent selectByPrimaryKey(Integer id);
int updateByPrimaryKeySelective(TblEvent record);
int updateByPrimaryKey(TblEvent record);
TblEvent:
@Data
public class TblEvent implements Serializable {
private Integer id;
private String type;
private String processor;
private String content;
private Date createTime;
private Date updateTime;
private static final long serialVersionUID = 1L;
}
2.2)然后是service-pay服务
2.2.1)pom和yaml跟order基本一样,只是数据库连pay的数据库。 启动类引入的注解跟order一样
2.2.2) controller
@RestController
@RequestMapping("pay")
public class PayController {
@Autowired
private TblEventDao tblEventDao;
@RequestMapping("add")
@LcnTransaction
// @Transactional
public String insertPay(@RequestBody TblEvent tblEvent){
tblEventDao.insert(tblEvent);
return "success";
}
}
TblEventDao和TblEvent跟 order一模一样。
至此TC完毕。
启动eureka服务端
启动TM
启动service-order,service-pay
使用postman访问:
查看数据库的话,可以看到order和pay的库中都没有执行数据。说明即便远程调用成功了,本地事务执行失败也会回滚远程事务。
LCN的控制台中,已注册的TC可以看到我们的应用