前言
LCN并不生产事务,LCN只是本地事务的协调工
一、分布式事务
1、什么是分布式事务
分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
2、分布式事务产生的原因
从上面本地事务来看,我们可以看为两块,一个是service产生多个节点,另一个是resource产生多个节点。
①service多个节点的情况
随着互联网快速发展,微服务,SOA等服务架构模式正在被大规模的使用,举个简单的例子,一个公司之内,用户的资产可能分为好多个部分,比如余额,积分,优惠券等等。在公司内部有可能积分功能由一个微服务团队维护,优惠券又是另外的团队维护
这样的话就无法保证积分扣减了之后,优惠券能否扣减成功。
②resource多个节点
同样的,互联网发展得太快了,我们的Mysql一般来说装千万级的数据就得进行分库分表,对于一个支付宝的转账业务来说,你给的朋友转钱,有可能你的数据库是在北京,而你的朋友的钱是存在上海,所以我们依然无法保证他们能同时成功。
二、LCN的原理图
在上图中,微服务A,微服务B,TxManager事务协调器,都需要去Eureka中去注册服务。Eureka是用于TxManager与其他服务之间的相互服务发现。redis是用于存放我们事务组的信息以及补偿的信息。然后微服务A与微服务B他们都需要去配置上我们TxClient的包架构(代码的包架构),来支持我们的LCN框架以及他们的数据库。
三、LCN的执行步骤
模拟场景演示
若存在事务发起方、参与方A、参与方B。调用关系图如下
那么他们正常执行业务的时序图为:
若参与方B出现异常,那么他们的业务时序图为:
若他们的调用关系是这样的情况
此时发生参与方B出现异常时他们的时序图为:
四、LCN的事务协调机制
如图,假设服务已经执行到关闭事务组的过程了,那么接下来作为一个模块执行通知给TxManager,然后告诉它本次事务已经完成。那么如图中的TxManager下一个动作就是通过事务组的id,获取到本次事务组的事务信息,然后查看一下对应有哪几个模块参与,然后如果是有A,B,C三个模块,那么对应的对三个模块做通知、提交、回滚。
那么提交的时候是提交给谁呢?
是提交给了我们的TxClient模块。然后TxClient模块下有一个连接池,就是框架自定义的一个连接池(如图DB连接池);这个连接池其实就是在没有通知事务之前一直占有着这次事务的连接资源,就是没有释放。但是他在切面里面执行了close方法。在执行close方法的时候,其实是“假关闭”,也就是没有真正的关闭,实际的资源是没有释放的。这个资源是掌握在LCN 的连接池里的。
然后当TxManager通知提交或事务回滚的时候呢?
TxManager会通知我们的TxClient端。然后TxClient会去执行相应的提交或回滚。提交或回滚之后再去关闭连接。这就是LCN的事务协调机制。说白了就是代理DataSource的机制;相当于是拦截了一下连接池,控制了连接池的事务提交。
五、补偿机制
为什么需要事务补偿?
事务补偿是指在执行某个业务方法时,本应该执行成功的操作却因为服务器挂机或者网络抖动等问题导致事务没有正常提交,此种场景就需要通过补偿来完成事务,从而达到事务的一致性。
补偿机制的触发条件?
当执行关闭事务组步骤时,若发起方接受到失败的状态后将会把该次事务识别为待补偿事务,然后发起方将该次事务数据异步通知给TxManager。TxManager接受到补偿事务以后先通知补偿回调地址,然后再根据是否开启自动补偿事务状态来补偿或保存该次切面事务数据。
补偿事务机制?
LCN的补偿事务原理是模拟上次失败事务的请求,然后传递给TxClient模块然后再次执行该次请求事务。
简单的说:lcn事务补偿是在在服务挂机和网络抖动情况下;服务挂机是指在完成三个核心步骤的时候
尤其也只有最后一步关闭事务组时,去执行关闭事务组的时候,比如本次事务是要提交的。txManager接收到提交的请求再去通知的时候发现通知不到了(讲解:由于LCN并不生产事务,LCN只是本地事务的协调工,所以最后真正的事务提交还是通过txManager去通知各个服务提供者去提交各自的事务,通知服务提供者。如果通知不到的话,那就返回标志给发起方 注意:是通知服务提供方,而不是通知服务发起方
)。
(通知不到也就两种原因服务挂了和网络出问题)在这种情况下TxManager会做一个标示;然后返回给发起方。告诉他本次事务有存在没有通知到的情况。
那么如果是接收到这个信息之后呢,发起方就会做一个标示,标示本次事务是需要补偿事务的。这就是事务补偿机制。
LCN是怎么去实现事务补偿呢?
首先他会根据发起方拿到的TxManager的标示,判断是否需要做事务补偿,如果需要,他会首先在本地去记录一下日志 ,然后再把本次切面的信息,也就是发起方本次事务切面的信息
以及事务组的id信息提交给TxManager;然后TxManager接收到这次数据之后,他会查询到本次事务组的整个信息,获取到本次事务组信息之后呢,他会把这些信息一块保存到redis下,也就是做为补偿数据一块存下来。
在存下来的时候,他会先执行一次叫做回调接口的请求;这个回调接口其实是指的是回调给第三方服务的一个地址,也就是我们自己的服务地址。这里是作为一次通知用的,我们来看下4.0的界面
当补偿发生之后,TxManager记录完数据以后会通知这个回调接口地址如图:
告诉你有补偿信息存在。这个地方我们就可以做一些通知,例如邮件、短信提醒功能,通知给相应的人员。让他们知道现在我们的服务里存在了补偿,要及时处理。
当然你也可以开启自动补偿功能(如上图),当开启自动补偿之后的话,每当触发补偿机制的时候,就会通知上面的补偿回调接口(讲解:回调接口这个地方我们就可以做一些通知,例如邮件、短信提醒功能,通知给相应的人员。让他们知道现在我们的服务里存在了补偿,要及时处理。
),通知完之后,他会把本次事务去补偿一下。他是如何执行的呢?上面说到他会把切面的信息上传上来,他会把切面的信息上传给发起方,传递给发起方以后让发起方重复执行本次事务。但是有一个差异性的地方是在于,他在去做模块提交的时候呢,会根据历史的提交数据(补偿数据)做一个逆向的操作,也就是说,必须如下图来说:
梳理一下流程:当关闭事务组之后,txManager会陆续通知各个服务参与方(注:是通知参与方而不是通知发起方)去提交各自的事务,先通知Demo2提交,然后再通知Demo3提交,最后如果都提交成功,那么txManager就会给发起方Demo1发送一个成功标志,然后Demo1自己再提交一下即可完成整个事务组事务的提交了。
如果在中间某一步出了异常,比如我们这三个模块Demo1、Demo2、Demo3.那么现在如果通知Demo2提交成功,然后在通知Demo3提交的时候由于网络抖动的原因导致txManager通知Demo3失败了。然后这一次作为补偿通知(注:这个时候就触发了补偿机制
)给了发起方Demo1,发起方调用TxManager,告诉Demo1这一次需要补偿
那么TxManager发信息给Demo1,Demo1执行补偿的时候呢,首先启动方(也就是“发起方”
)事务是要回滚的;那么Demo2、Demo3是否回滚取决于上次的事务请求。上面说Demo2提交成功了,也就是说他要回滚(讲解:因为我们要进行重试机制嘛,所以必须要清除之前所做的一切操作
),Demo3失败,他是要补偿的,是要提交的。那么在TxManager内部怎么去实现这一点呢?下面说说它的原理
原理如下:还是跟以前一样,就是说其实这次是与之前的执行事务流程是相同的;唯一不同的是在于执行关闭,就是在这次补偿的事务过程中的关闭事务的时候。
TxManager会判断历史数据,会根据历史数据(补偿数据)做差异性的通知,是判断谁要需要提交,谁需要回滚。这就是补偿机制的原理。
本文参考自;https://blog.csdn.net/gududedabai/article/details/83012487#4、补偿机制
六、如何使用LCN?
第一步:
先在lcn官网【http://www.txlcn.org/】 找到GitHub 地址【https://github.com/codingapi/tx-lcn】,拷下所有的源码
第二步:
解压下载的zip,放置在一个目录下,用IDEA打开【注意打开父层项目】
问题:
git上的源码是1.5.4版本的Spring Boot,若使用2.0版本,项目启动会报错,主要原因是2.0版本移除了context.embedded
包,导致EmbeddedServletContainerInitializedEvent.java
对象找不到(该对象主要和Listener配合使用,“通过监听,在刷新上下文后将要发布的事件,用于获取A的本地端口运行服务器。”TxClient主要从该Event中获取服务器端口号。)
导入完整的jar包,然后下面就要开始更改源码中不支持spring boot 2.X的部分
第三步:
修改transaction-springcloud 项目下com.codingapi.tx.springcloud.listener包中的ServerListener.java
源码更改为:
@Component
public class ServerListener implements ApplicationListener<ApplicationEvent> {
private Logger logger = LoggerFactory.getLogger(ServerListener.class);
private int serverPort;
@Value("${server.port}")
private String port;
@Autowired
private InitService initService;
@Override
public void onApplicationEvent(ApplicationEvent event) {
// logger.info("onApplicationEvent -> onApplicationEvent. "+event.getEmbeddedServletContainer());
// this.serverPort = event.getEmbeddedServletContainer().getPort();
//TODO Spring boot 2.0.0没有EmbeddedServletContainerInitializedEvent 此处写死;modify by young
this.serverPort = Integer.parseInt(port);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 若连接不上txmanager start()方法将阻塞
initService.start();
}
});
thread.setName("TxInit-thread");
thread.start();
}
public int getPort() {
return this.serverPort;
}
public void setServerPort(int serverPort) {
this.serverPort = serverPort;
}
}
@Component注解会自动扫描配置文件中的server.port值;
第四步:
修改tx-manager项目下com.codingapi.tm.listener包中的ApplicationStartListener.java
@Component
public class ApplicationStartListener implements ApplicationListener<ApplicationEvent> {
@Override
public void onApplicationEvent(ApplicationEvent event) {
//TODO Spring boot 2.0.0没有EmbeddedServletContainerInitializedEvent 此处写死;modify by young
// int serverPort = event.getEmbeddedServletContainer().getPort();
String ip = getIp();
Constants.address = ip+":48888";//写死端口号,反正TxManager端口也是配置文件配好的(●′ω`●)
}
private String getIp(){
String host = null;
try {
host = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
return host;
}
}
第五步:
改写tx-manager项目下com.codingapi.tm.manager.service.impl包中MicroServiceImpl.java类的getState()方法
@Override
public TxState getState() {
TxState state = new TxState();
String ipAddress = "";
//TODO Spring boot 2.0.0没有discoveryClient.getLocalServiceInstance() 用InetAddress获取host;modify by young
//String ipAddress = discoveryClient.getLocalServiceInstance().getHost();
try {
ipAddress = InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
e.printStackTrace();
}
if (!isIp(ipAddress)) {
ipAddress = "127.0.0.1";
}
state.setIp(ipAddress);
state.setPort(Constants.socketPort);
state.setMaxConnection(SocketManager.getInstance().getMaxConnection());
state.setNowConnection(SocketManager.getInstance().getNowConnection());
state.setRedisSaveMaxTime(configReader.getRedisSaveMaxTime());
state.setTransactionNettyDelayTime(configReader.getTransactionNettyDelayTime());
state.setTransactionNettyHeartTime(configReader.getTransactionNettyHeartTime());
state.setNotifyUrl(configReader.getCompensateNotifyUrl());
state.setCompensate(configReader.isCompensateAuto());
state.setCompensateTryTime(configReader.getCompensateTryTime());
state.setCompensateMaxWaitTime(configReader.getCompensateMaxWaitTime());
state.setSlbList(getServices());
return state;
}
注:有些服务器这个方法获取不到ip,注册到tx.manager的ip为127.0.0.1
@Value("${spring.cloud.client.ip-address}")
private String ipAddress;
public TxState getState() {
TxState state = new TxState();
// String ipAddress = "";
//TODO Spring boot 2.0.0没有discoveryClient.getLocalServiceInstance() 用InetAddress获取host;modify by young
//String ipAddress = discoveryClient.getLocalServiceInstance().getHost();
// try {
// ipAddress = InetAddress.getLocalHost().getHostAddress();
// } catch (Exception e) {
// e.printStackTrace();
// }
if (!isIp(ipAddress)) {
ipAddress = "127.0.0.1";
}
...
}
第七步:
改写tx-client项目下TransactionServerFactoryServiceImpl.java第60行;
//分布式事务已经开启,业务进行中 **/
if (info.getTxTransactionLocal() != null || StringUtils.isNotEmpty(info.getTxGroupId())) {
//检查socket通讯是否正常 (第一次执行时启动txRunningTransactionServer的业务处理控制,然后嵌套调用其他事务的业务方法时都并到txInServiceTransactionServer业务处理下)
if (SocketManager.getInstance().isNetState()) {
if (info.getTxTransactionLocal() != null) {
return txDefaultTransactionServer;
} else {
// if(transactionControl.isNoTransactionOperation() // 表示整个应用没有获取过DB连接
// || info.getTransaction().readOnly()) { //无事务业务的操作
// return txRunningNoTransactionServer;
// }else {
// return txRunningTransactionServer;
// }
if(!transactionControl.isNoTransactionOperation()) { //TODO 有事务业务的操作 modify by young
return txRunningTransactionServer;
}else {
return txRunningNoTransactionServer;
}
}
} else {
logger.warn("tx-manager not connected.");
return txDefaultTransactionServer;
}
}
//分布式事务处理逻辑*结束***********/
重新maven deploy打出jar包,上传自己的maven服务器;
就此Spring Boot2.0引入LCN分布式事务正常启动。
优化
git项目上引用tx.manager.url(txmanager服务器路径)需要在各自的微服务中添加TxManagerHttpRequestService.java和TxManagerTxUrlService.java的实现类;
可将两个方法继承到springcloud源码包中
(1)添加TxManagerConfiguration.java
@Configuration
@ConditionalOnProperty(value = "tx.manager.url")
public class TxManagerConfiguration {
@Bean
@RefreshScope
@ConfigurationProperties(prefix = "tx.manager")
public TxManagerProperity txManagerProperity(){
return new TxManagerProperity();
};
@Bean
public TxManagerTxUrlService txManagerTxUrlService(){
return new TxManagerTxUrlServiceImpl(txManagerProperity());
}
@Bean
public TxManagerHttpRequestService txManagerHttpRequestService(){
return new TxManagerHttpRequestServiceImpl();
}
}
(2) 添加TxManagerProperity .java
public class TxManagerProperity {
private String url;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
(3)修改TxManagerTxUrlServiceImpl.java
public class TxManagerTxUrlServiceImpl implements TxManagerTxUrlService {
private TxManagerProperity property;
public TxManagerTxUrlServiceImpl(TxManagerProperity property) {
this.property = property;
}
@Override
public String getTxUrl() {
return property.getUrl();
}
}
未完待续:
原文参考自;https://www.jianshu.com/p/453741e0f28f