1、LCN源码下载
下载地址:https://github.com/codingapi/tx-lcn/releases
2、LCN转微服务
将下载源码中的该项目以微服务的形式集成入驻到你的微服务项目中
(1)替换配置文件
原配置文件
application.properties
#######################################txmanager-start#################################################
#服务端口
server.port=8899
#tx-manager不得修改
spring.application.name=tx-manager
spring.mvc.static-path-pattern=/**
spring.resources.static-locations=classpath:/static/
#######################################txmanager-end#################################################
#zookeeper地址
#spring.cloud.zookeeper.connect-string=127.0.0.1:2181
#spring.cloud.zookeeper.discovery.preferIpAddress = true
#eureka 地址
eureka.client.service-url.defaultZone=http://127.0.0.1:8761/eureka/
eureka.instance.prefer-ip-address=true
#######################################redis-start#################################################
#redis 配置文件,根据情况选择集群或者单机模式
##redis 集群环境配置
##redis cluster
#spring.redis.cluster.nodes=127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003
#spring.redis.cluster.commandTimeout=5000
##redis 单点环境配置
#redis
#redis主机地址
spring.redis.host=127.0.0.1
#redis主机端口
spring.redis.port=6379
#redis链接密码
spring.redis.password=
spring.redis.pool.maxActive=10
spring.redis.pool.maxWait=-1
spring.redis.pool.maxIdle=5
spring.redis.pool.minIdle=0
spring.redis.timeout=0
#####################################redis-end###################################################
#######################################LCN-start#################################################
#业务模块与TxManager之间通讯的最大等待时间(单位:秒)
#通讯时间是指:发起方与响应方之间完成一次的通讯时间。
#该字段代表的是Tx-Client模块与TxManager模块之间的最大通讯时间,超过该时间未响应本次请求失败。
tm.transaction.netty.delaytime = 5
#业务模块与TxManager之间通讯的心跳时间(单位:秒)
tm.transaction.netty.hearttime = 15
#存储到redis下的数据最大保存时间(单位:秒)
#该字段仅代表的事务模块数据的最大保存时间,补偿数据会永久保存。
tm.redis.savemaxtime=30
#socket server Socket对外服务端口
#TxManager的LCN协议的端口
tm.socket.port=9999
#最大socket连接数
#TxManager最大允许的建立连接数量
tm.socket.maxconnection=100
#事务自动补偿 (true:开启,false:关闭)
# 说明:
# 开启自动补偿以后,必须要配置 tm.compensate.notifyUrl 地址,仅当tm.compensate.notifyUrl 在请求补偿确认时返回success或者SUCCESS时,才会执行自动补偿,否则不会自动补偿。
# 关闭自动补偿,当出现数据时也会 tm.compensate.notifyUrl 地址。
# 当tm.compensate.notifyUrl 无效时,不影响TxManager运行,仅会影响自动补偿。
tm.compensate.auto=false
#事务补偿记录回调地址(rest api 地址,post json格式)
#请求补偿是在开启自动补偿时才会请求的地址。请求分为两种:1.补偿决策,2.补偿结果通知,可通过通过action参数区分compensate为补偿请求、notify为补偿通知。
#*注意当请求补偿决策时,需要补偿服务返回"SUCCESS"字符串以后才可以执行自动补偿。
#请求补偿结果通知则只需要接受通知即可。
#请求补偿的样例数据格式:
#{"groupId":"TtQxTwJP","action":"compensate","json":"{\"address\":\"133.133.5.100:8081\",\"className\":\"com.example.demo.service.impl.DemoServiceImpl\",\"currentTime\":1511356150413,\"data\":\"C5IBLWNvbS5leGFtcGxlLmRlbW8uc2VydmljZS5pbXBsLkRlbW9TZXJ2aWNlSW1wbAwSBHNhdmUbehBqYXZhLmxhbmcuT2JqZWN0GAAQARwjeg9qYXZhLmxhbmcuQ2xhc3MYABABJCo/cHVibGljIGludCBjb20uZXhhbXBsZS5kZW1vLnNlcnZpY2UuaW1wbC5EZW1vU2VydmljZUltcGwuc2F2ZSgp\",\"groupId\":\"TtQxTwJP\",\"methodStr\":\"public int com.example.demo.service.impl.DemoServiceImpl.save()\",\"model\":\"demo1\",\"state\":0,\"time\":36,\"txGroup\":{\"groupId\":\"TtQxTwJP\",\"hasOver\":1,\"isCompensate\":0,\"list\":[{\"address\":\"133.133.5.100:8899\",\"isCompensate\":0,\"isGroup\":0,\"kid\":\"wnlEJoSl\",\"methodStr\":\"public int com.example.demo.service.impl.DemoServiceImpl.save()\",\"model\":\"demo2\",\"modelIpAddress\":\"133.133.5.100:8082\",\"channelAddress\":\"/133.133.5.100:64153\",\"notify\":1,\"uniqueKey\":\"bc13881a5d2ab2ace89ae5d34d608447\"}],\"nowTime\":0,\"startTime\":1511356150379,\"state\":1},\"uniqueKey\":\"be6eea31e382f1f0878d07cef319e4d7\"}"}
#请求补偿的返回数据样例数据格式:
#SUCCESS
#请求补偿结果通知的样例数据格式:
#{"resState":true,"groupId":"TtQxTwJP","action":"notify"}
tm.compensate.notifyUrl=http://ip:port/path
#补偿失败,再次尝试间隔(秒),最大尝试次数3次,当超过3次即为补偿失败,失败的数据依旧还会存在TxManager下。
tm.compensate.tryTime=30
#各事务模块自动补偿的时间上限(毫秒)
#指的是模块执行自动超时的最大时间,该最大时间若过段会导致事务机制异常,该时间必须要模块之间通讯的最大超过时间。
#例如,若模块A与模块B,请求超时的最大时间是5秒,则建议改时间至少大于5秒。
tm.compensate.maxWaitTime=5000
#######################################LCN-end#################################################
logging.level.com.codingapi=debug
新配置文件
大家参考我的进行修改,注意redis配置因为我这边的微服务公共配置文件里有,所以这里就去掉了,大家要根据自己项目的实际情况去调整。
bootstrap.yml
spring:
application:
name: tx-manager
profiles:
active: dev
cloud:
config:
fail-fast: true
discovery:
service-id: ylapp-config-server
enabled: true
profile: ${spring.profiles.active}
label: ${spring.profiles.active}
#######################################LCN-start#################################################
#业务模块与TxManager之间通讯的最大等待时间(单位:秒)
#通讯时间是指:发起方与响应方之间完成一次的通讯时间。
#该字段代表的是Tx-Client模块与TxManager模块之间的最大通讯时间,超过该时间未响应本次请求失败。
tm:
transaction:
netty:
delaytime: 5
hearttime: 15 #业务模块与TxManager之间通讯的心跳时间(单位:秒)
redis:
savemaxtime: 30 #存储到redis下的数据最大保存时间(单位:秒) ,,,#该字段仅代表的事务模块数据的最大保存时间,补偿数据会永久保存。
socket:
port: 9998 #socket server Socket对外服务端口,,,#TxManager的LCN协议的端口
maxconnection: 100 #最大socket连接数,,,#TxManager最大允许的建立连接数量
compensate:
auto: false #事务自动补偿 (true:开启,false:关闭)
notifyUrl: http://ip:port/path #事务补偿记录回调地址(rest api 地址,post json格式)
tryTime: 30 #再次尝试间隔(秒),最大尝试次数3次,当超过3次即为补偿失败,失败的数据依旧还会存在TxManager下。
maxWaitTime: 5000 #各事务模块自动补偿的时间上限(毫秒)
#######################################LCN-end#################################################
---
spring:
profiles: dev
eureka:
instance:
prefer-ip-address: true
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 20
client:
serviceUrl:
defaultZone: http://ylapp:gip6666@192.168.50.250:1025/eureka,http://ylapp:gip6666@192.168.50.251:1025/eureka
registry-fetch-interval-seconds: 10
log:
path: ./logs
---
spring:
profiles: test
eureka:
instance:
prefer-ip-address: true
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 20
client:
serviceUrl:
defaultZone: http://ylapp:gip6666@192.168.50.235:1025/eureka,http://ylapp:gip6666@192.168.50.236:1025/eureka
registry-fetch-interval-seconds: 10
log:
path: /var/www/html/logs
(2)替换pom
同样我这边给出我的示例,大家参考,其实就是把你项目中其他微服务的pom拷贝过来做适当调整
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.***</groupId>
<artifactId>***-txmanager-service</artifactId>
<version>1.3.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>***-txmanager-service</name>
<description>***-txmanager-service</description>
<parent>
<groupId>com.github.***</groupId>
<artifactId>***-public</artifactId>
<version>1.3.0-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>com.github.***</groupId>
<artifactId>***-common</artifactId>
<version>1.3.0-SNAPSHOT</version>
</dependency>
<!--MySQL-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--七牛-->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>${qiniu.version}</version>
</dependency>
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>${kaptcha.version}</version>
</dependency>
<!--zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>com.github.1991wangliang</groupId>
<artifactId>lorne_core</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.12.Final</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<finalName>${project.name}</finalName>
</configuration>
</plugin>
</plugins>
</build>
</project>
(3)修改tx-client-4.1.0.jar
这里我直接贴上我修改后的,目的是为了支持多环境配置,怎样修改jar包里的class文件我就不详细教了,大家自己百度,有很多种方法。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.codingapi.tx.config;
import com.codingapi.tx.config.service.TxManagerTxUrlService;
import com.lorne.core.framework.utils.config.ConfigUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
@Component
public class ConfigReader {
private Logger logger = LoggerFactory.getLogger(ConfigReader.class);
private TxManagerTxUrlService txManagerTxUrlService;
@Autowired
private ApplicationContext spring;
public ConfigReader() {
}
public String getTxUrl() {
try {
this.txManagerTxUrlService = (TxManagerTxUrlService)this.spring.getBean(TxManagerTxUrlService.class);
} catch (Exception var4) {
this.logger.debug("load default txManagerTxUrlService ");
}
String activeProfile = this.spring.getEnvironment().getActiveProfiles()[0];
final String fileName = "tx-dev.properties";
if ("test".equals(activeProfile)) {
fileName = "tx-test.properties";
} else if ("ltest".equals(activeProfile)) {
fileName = "tx-ltest.properties";
} else if ("pre".equals(activeProfile)) {
fileName = "tx-pre.properties";
} else if ("prd".equals(activeProfile)) {
fileName = "tx-prd.properties";
}
if (this.txManagerTxUrlService == null) {
this.txManagerTxUrlService = new TxManagerTxUrlService() {
private final String configName = fileName;
private final String configKey = "url";
public String getTxUrl() {
return ConfigUtils.getString(this.configName, "url");
}
};
this.logger.debug("load default txManagerTxUrlService");
} else {
this.logger.debug("load txManagerTxUrlService");
}
return this.txManagerTxUrlService.getTxUrl();
}
}
(4)新增配置文件tx-dev.properties
因为凡是需要使用分布式事物的服务都需要该配置,所以我这边选择放在common服务下
url=http://192.168.50.250:7000/tx/manager/
(5)添加maven依赖
最外层父级pom.xml 添加
<properties>
<!-- LCN分布式事务版本 -->
<lcn.last.version>4.1.0</lcn.last.version>
</properties>
需要使用LCN的服务加
<dependencies>
<!-- lcn 依赖 start -->
<dependency>
<groupId>com.codingapi</groupId>
<artifactId>transaction-springcloud</artifactId>
<version>${lcn.last.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.codingapi</groupId>
<artifactId>tx-plugins-db</artifactId>
<version>${lcn.last.version}</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- lcn 依赖 end -->
</dependencies>
3、使用示例
这边都只贴出service层代码
事物发起方, A服务
/**
* @Author chenqi
* @Description 测试 lcn
* @Date 11:45 2019/8/7
* @Param []
* @return java.lang.Boolean
**/
@TxTransaction(isStart = true)
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean txManagerTest(){
//A服务插入数据
String time = DateUtils.findCurrentDateTime();
PartnerUser partnerUser = new PartnerUser();
partnerUser.setUserName("12111111111");
partnerUser.setPassword("12111111111");
partnerUser.setState(1);
partnerUser.setCreateTime(time);
partnerUser.setUpdateTime(time);
partnerUser.setFlagDel(CommonConstant.STATUS_NORMAL);
insert(partnerUser);
//调用B服务保存数据,使用springCloud-feign
appUserService.addTest();
//抛异常
throw new PartnerServiceException("测试回滚");
}
事物参与方, B服务
/**
* @Author chenqi
* @Description 新增用户测试 lcn
* @Date 11:46 2019/8/7
* @Param []
* @return void
**/
@TxTransaction
@Transactional(rollbackFor = Exception.class)
@Override
public void addTest(){
AppUser appUser = new AppUser();
appUser.setUserName("12111111111");
appUser.setPhone("12111111111");
appUser.setPassword("12111111111");
appUser.setName("test");
appUser.setCreateTime(new Date());
insert(appUser);
}
原理说明:
在上述示例中,A服务首先自身插入数据,然后调用B服务插入数据,最后在A服务抛出异常,
如果没有集成lcn分布式事物协调服务,虽然A服务的数据插入操作会回滚,但是B服务的数据是会正常插入到数据库的,从而造成了事物不一致的严重事故,但是集成了lcn之后,A服务在执行方法时会创建一个事务组,看如下日志:
08-07 13:56:29.773 INFO [com.codingapi.tx.framework.utils.SocketManager] - send-msg->
{"a":"cg","k":"GRDnKxRm","p":{"g":"GuzW5DVW"}}
08-07 13:56:29.842 INFO [c.c.tx.datasource.relational.LCNStartConnection] -
transaction is start-connection.
08-07 13:56:29.847 INFO [c.c.tx.datasource.relational.LCNStartConnection] -
lcn start connection init ok .
08-07 13:56:30.759 INFO [c.c.t.s.feign.TransactionRestTemplateInterceptor] -
LCN-SpringCloud TxGroup info -> groupId:GuzW5DVW
可以看到,会生成一个事务组id GuzW5DVW,当A服务调用B服务时,我们再看B服务的日志输出:
maxOutTime : 5000
transaction is wait for TxManager notify, groupId : GuzW5DVW
08-07 13:56:32.113 INFO [com.codingapi.tx.framework.utils.SocketManager] - send-msg->
{"a":"atg","k":"XI9Yk40D","p":{"s":0,"t":"SZc1UpSQ","ms":"public void
com.****.****.appuser.service.impl.AppUserServiceImpl.addTest()","g":"GuzW5DVW"}}
可以看到,B服务执行完了操作,但是事物状态是等待通知的状态,最后A服务抛出异常:
08-07 13:56:31.826 INFO [com.codingapi.tx.framework.utils.SocketManager] - send-msg->
{"a":"glb","k":"WU2p0cXS","p":{"g":"GuzW5DVW","k":"4aebbb3be0863f0b203fd82ec7076cab"}}
08-07 13:56:31.839 INFO [com.codingapi.tx.framework.utils.SocketManager] - send-msg->
{"a":"plb","k":"Pgd1A5ot","p":{"d":"a0e1839774470462c6294d2ef14ff916","g":"GuzW5DVW"
,"k":"4aebbb3be0863f0b203fd82ec7076cab"}}
08-07 13:56:32.161 INFO [c.c.tx.datasource.relational.LCNStartConnection] -
rollback label
08-07 13:56:32.178 INFO [com.codingapi.tx.framework.utils.SocketManager] - send-msg->
{"a":"ctg","k":"dggB9W2l","p":{"s":0,"g":"GuzW5DVW"}}
[com.****.****.common.util.exception.PartnerServiceException: 测试回滚]
08-07 13:56:32.433 INFO [c.p.y.common.bean.handler.GlobalExceptionHandler] -
保存***服务异常信息 ex=测试回滚
com.****.****.common.util.exception.PartnerServiceException: 测试回滚
at com.****.****.partner.service.impl.PartnerUserServiceImpl.txManagerTest(
PartnerUserServiceImpl.java:1828)
at com.****.****.partner.service.impl.PartnerUserServiceImpl$$FastClassBySpringCGLIB
$$388e1d9a.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
A服务抛出该异常后,A服务自身的数据库插入修改操作都会回滚了,那么B服务呢?看B服务如下日志:
08-07 13:56:32.398 INFO [c.c.tx.control.service.impl.ActionTServiceImpl] -
accept notify data ->{"a":"t","c":0,"t":"SZc1UpSQ","k":"KJDBQozR"}
lcn transaction over, res -> groupId:GuzW5DVW and state is rollback
08-07 13:56:32.403 INFO [c.c.tx.control.service.impl.ActionTServiceImpl] -
accept notify response res ->1
可以看到,B服务收到了来自lcn的通知,进行了事物回滚rollback
至此,实现了不同服务相互调用的事物一致性,从而解决了分布式事物的问题。
升级到三个服务调用
事物发起方 A服务
/**
* @Author chenqi
* @Description 测试 lcn
* @Date 11:45 2019/8/7
* @Param []
* @return java.lang.Boolean
**/
@TxTransaction(isStart = true)
@Transactional(rollbackFor = Exception.class)
@Override
public Boolean txManagerTest(){
//A服务插入数据
String time = DateUtils.findCurrentDateTime();
PartnerUser partnerUser = new PartnerUser();
partnerUser.setUserName("12111111111");
partnerUser.setPassword("12111111111");
partnerUser.setState(1);
partnerUser.setCreateTime(time);
partnerUser.setUpdateTime(time);
partnerUser.setFlagDel(CommonConstant.STATUS_NORMAL);
insert(partnerUser);
//调用B服务保存数据 使用springCloud-feign
appUserService.addTest();
//调用C服务保存数据 使用springCloud-feign
goodsService.addTest();
//抛异常
throw new PartnerServiceException("测试回滚");
}
经测试,B服务和C服务也是会同步回滚的,请看日志
A服务日志:
08-07 15:49:06.579 INFO [com.codingapi.tx.framework.utils.SocketManager] -
send-msg->{"a":"cg","k":"7lLz4YGg","p":{"g":"cczxxyiV"}}
08-07 15:49:06.665 INFO [c.c.tx.datasource.relational.LCNStartConnection] -
transaction is start-connection.
08-07 15:49:06.668 INFO [c.c.tx.datasource.relational.LCNStartConnection] -
lcn start connection init ok .
08-07 15:49:06.763 INFO [c.c.t.s.feign.TransactionRestTemplateInterceptor] -
LCN-SpringCloud TxGroup info -> groupId:cczxxyiV
08-07 15:49:06.771 INFO [c.c.r.loadbalancer.LcnZoneAwareLoadBalancerProxy] -
enter chooseServer method, key:null
08-07 15:49:06.797 INFO [com.codingapi.tx.framework.utils.SocketManager] -
send-msg->{"a":"plb","k":"9YlgHHbq","p":{"d":"a0e1839774470462c6294d2ef14ff916"
,"g":"cczxxyiV","k":"e5c4bd28f5d1161bcf7a7eeaccc3569e"}}
08-07 15:49:06.946 INFO [c.c.t.s.feign.TransactionRestTemplateInterceptor] -
LCN-SpringCloud TxGroup info -> groupId:cczxxyiV
08-07 15:49:06.947 INFO [c.c.r.loadbalancer.LcnZoneAwareLoadBalancerProxy] -
enter chooseServer method, key:null
08-07 15:49:06.960 INFO [com.codingapi.tx.framework.utils.SocketManager] -
send-msg->{"a":"plb","k":"bkFzaEM1","p":{"d":"4cd44af89aca600fc757d311a3825189"
,"g":"cczxxyiV","k":"5ced183dfe48f9d1268a36d688501cca"}}
08-07 15:49:08.610 INFO [c.c.tx.datasource.relational.LCNStartConnection] -
rollback label
08-07 15:49:08.621 INFO [com.codingapi.tx.framework.utils.SocketManager] -
send-msg->{"a":"ctg","k":"bgjlWoCA","p":{"s":0,"g":"cczxxyiV"}}
[com.****.****.common.util.exception.PartnerServiceException: 测试回滚]
08-07 15:49:08.720 INFO [c.p.y.common.bean.handler.GlobalExceptionHandler] -
保存***服务异常信息 ex=测试回滚
com.****.****.common.util.exception.PartnerServiceException: 测试回滚
at com.****.****.partner.service.impl.PartnerUserServiceImpl.txManagerTest(
PartnerUserServiceImpl.java:1835)
.....
B服务日志:
maxOutTime : 5000
transaction is wait for TxManager notify, groupId : cczxxyiV
08-07 15:49:06.914 INFO [com.codingapi.tx.framework.utils.SocketManager] -
send-msg->{"a":"atg","k":"vTbQz6xW","p":{"s":0,"t":"B6FG3S9b"
,"ms":"public void com.****.****.appuser.service.impl.AppUserServiceImpl.addTest()"
,"g":"cczxxyiV"}}
08-07 15:49:08.642 INFO [c.c.tx.control.service.impl.ActionTServiceImpl] -
accept notify data ->{"a":"t","c":0,"t":"B6FG3S9b","k":"mEJRboQ0"}
lcn transaction over, res -> groupId:cczxxyiV and state is rollback
08-07 15:49:08.654 INFO [c.c.tx.control.service.impl.ActionTServiceImpl] -
accept notify response res ->1
C服务日志:
maxOutTime : 5000
transaction is wait for TxManager notify, groupId : cczxxyiV
08-07 15:49:08.698 INFO [c.c.tx.control.service.impl.ActionTServiceImpl] -
accept notify data ->{"a":"t","c":0,"t":"YTT6A7Ht","k":"8hVTbHGo"}
lcn transaction over, res -> groupId:cczxxyiV and state is rollback
08-07 15:49:08.709 INFO [c.c.tx.control.service.impl.ActionTServiceImpl] -
accept notify response res ->1
至此,SpringCloud集成LCN4.1.0就完成了,使用示例这边只展示这两个场景,其他更多的场景大家可以自己去尝试写demo测试,有问题欢迎留言讨论!
如果该文章有帮助到您,就留言点个赞吧!您的支持与肯定是我持续更新最大的动力。