LCN是国产开源的分布式事务处理框架。LCN即:lock(锁定事务单元)、confirm(确认事务模块状态)、notify(通知事务)。
LCN的实现是基于3PC的算法,结合TCC的补偿机制。
LCN的核心步骤
核心步骤
1、创建事务组
是指在事务发起方开始执行业务代码之前先调用TxManager创建事务组对象,然后拿到事务标示GroupId的过程。
2、添加事务组
添加事务组是指参与方在执行完业务方法以后,将该模块的事务信息添加通知给TxManager的操作。
3、关闭事务组
是指在发起方执行完业务代码以后,将发起方执行结果状态通知给TxManager的动作。当执行完关闭事务组的方法以后, TxManager将根据事务组信息来通知相应的参与模块提交或回滚事务。
LCN正常执行序列图(来源于官方):
LCN异常执行序列图(来源于官方):
使用实例:
一、下载LCN工程
在LCN的github下载:https://github.com/codingapi/tx-lcn/
二、配置的LCN的tx-manager事务协调器(application.properties)
#######################################txmanager-start#################################################
#服务端口
server.port=7000
#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://user:zj123@localhost: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=ip
#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
这边配置文件简单的使用只需要改注册中心(这边我用的是eureka,配置eureka.client.service-url.defaultZone=)和redis ip(spring.redis.host)和端口(spring.redis.port)
启动LCN的tx-manager,访问:localhost:7000,若出现以下图片说明LCN事务管理器启动成功
三、事务的参与者(这边使用了持久层使用了Mybaits)
当A服务调用了B服务,B服务调用了C服务,则A服务是事务的发起者,B和C都是事务的参与者
1、pom.xml文件
org.springframework.boot
spring-boot-starter-parent
1.5.2.RELEASE
UTF-8
1.8
1.2.0
5.1.39
4.1.0
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-eureka
1.3.5.RELEASE
org.mybatis.spring.boot
mybatis-spring-boot-starter
${mybatis-spring-boot.version}
mysql
mysql-connector-java
com.codingapi
transaction-springcloud
${lcn.last.version}
org.slf4j
*
com.codingapi
tx-plugins-db
${lcn.last.version}
org.slf4j
*
2、application.yml
tm:
manager:
url: http://localhost:7000/tx/manager/
tm.manager.url是LCN的地址
这边就说明LCN需要加的配置,Mybaits和Eureka注册的配置的我就在这细说了,具有了可以看我之前的播客
https://blog.csdn.net/qq_25011427/article/details/83933519
3、在Spring加入TxManagerTxUrlServiceImpl
import com.codingapi.tx.config.service.TxManagerTxUrlService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* 添加从注册中心获取url;注意通过注解放入容器。
*/
@Service
public class TxManagerTxUrlServiceImpl implements TxManagerTxUrlService{
@Value("${tm.manager.url}")
private String url;
@Override
public String getTxUrl() {
return url;
}
}
4、参与者业务类使用@TxTransaction注解进行分布式的事务回滚(我这边直接为了简便就直接在Controller层使用@TxTransaction注解, 就没有遵循MVC架构,实际的项目中建议写到业务层中,遵循MVC架构)
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import com.codingapi.tx.annotation.ITxTransaction;
import com.codingapi.tx.annotation.TxTransaction;
import com.zhuojing.bean.User;
import com.zhuojing.dao.UserDao;
@RestController
public class UserController implements ITxTransaction{
@Autowired
private UserDao userDao;
@PostMapping("/saveUser")
@Transactional
@TxTransaction
public String saveUser(@RequestBody User user){
userDao.save(user);
return "SUCCESS";
}
}
5、启动类
@SpringBootApplication
@EnableEurekaClient
@EnableAutoConfiguration
public class ApplocationTest {
public static void main(String[] args) {
SpringApplication.run(ApplocationTest.class, args);
}
}
四、事务发起者
事务发起者这边使用了feign进行调用事务参与者,之前使用用来ribbon来调用发现分布式事务没有起作用,具体原因也没有找到,知道的大牛欢迎帮忙解答下。
1、pom.xml
org.springframework.boot
spring-boot-starter-parent
1.5.2.RELEASE
UTF-8
1.8
1.2.0
5.1.39
4.1.0
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-eureka
1.3.5.RELEASE
org.springframework.cloud
spring-cloud-starter-feign
1.3.5.RELEASE
com.codingapi
transaction-springcloud
${lcn.last.version}
org.slf4j
*
com.codingapi
tx-plugins-db
${lcn.last.version}
org.slf4j
*
2、application.yml文件
tm:
manager:
url: http://localhost:7000/tx/manager/
logging:
level:
com.codingapi: debug
3、在Spring容器中加入TxManagerTxUrlServiceImpl和TxManagerHttpRequestServiceImpl
import com.codingapi.tx.config.service.TxManagerTxUrlService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* 添加从注册中心获取url;注意通过注解放入容器。
*/
@Service
public class TxManagerTxUrlServiceImpl implements TxManagerTxUrlService{
@Value("${tm.manager.url}")
private String url;
@Override
public String getTxUrl() {
return url;
}
}
import com.codingapi.tx.netty.service.TxManagerHttpRequestService;
import com.lorne.core.framework.utils.http.HttpUtils;
import org.springframework.stereotype.Service;
/**
* 常见TxManagerHttpRequestService重写get、post方法;
*/
@Service
public class TxManagerHttpRequestServiceImpl implements TxManagerHttpRequestService{
@Override
public String httpGet(String url) {
System.out.println("httpGet-start");
String res = HttpUtils.get(url);
System.out.println("httpGet-end");
return res;
}
@Override
public String httpPost(String url, String params) {
System.out.println("httpPost-start");
String res = HttpUtils.post(url,params);
System.out.println("httpPost-end");
return res;
}
}
4、feign接口
@FeignClient(name="provider-user")
public interface UserFeignClient {
@RequestMapping(value="saveUser", method = RequestMethod.GET)
public String saveUser(@RequestBody User user);
}
5、事务发起者使用@TxTransaction(isStart = true)注解
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.codingapi.tx.annotation.TxTransaction;
import com.zhuojing.bean.User;
import com.zhuojing.feign.UserFeignClient;
@RestController
public class UserController {
@Autowired
private UserFeignClient userFeignClient;
@GetMapping("/txSaveUser")
@TxTransaction(isStart = true)
public String txSaveUser(){
User user = new User();
user.setUsername("aa");
user.setAge(11);
user.setBalance(11);
userFeignClient.saveUser(user);
int i = 1/0;
return "aaa";
}
}
当事务发起者出现异常时,会同时事务管理器让事务的参与者回滚,若事务参与者出现异常也会进行所有参与者和发起者的事务回滚
5、启动类
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class ApplicationCustomerFeignTest {
public static void main(String[] args) {
SpringApplication.run(ApplicationCustomerFeignTest.class, args);
}
}