分布式事务产生的原因:当系统从单体应用按照领域,做高内聚、低耦合的拆分后,一个单体应用变成了多个分布式子系统。分布式子系统相互之间的协作。由于不在一个进程内,不是一个数据库连接,当服务编排中,各子系统协同完成业务时,需要保证所有协作子系统要么都成功,要么都失败,这即满足在分布式环境下,多个系统的操作原子性的分布式事务。大型分布式应用由于数据的快速扩张,以及数据的高可用,通常需要跨网络线性的伸缩。那么在很多时候,需要考虑的不仅仅是数据的一致性,本文仅讨论在特殊的、必须实现分布式事务的场景下,分布式事务的实现理论及LCN框架。
分布式事务常见算法:
2PC: two-phase commit,2阶段提交。即将分布式事务分成2个阶段:prepare、commit。事务协调者向所有参与发起prepare,所有参与者应答。当所有参与者应答yes,协调者向所有参与者发起commit,否则向所有参与者发起rollback。这要求所有参与者都实现prepare、commit和rollback接口。
2PC算法中,参与方在等待协调者提交事务通知时是阻塞状态,协调者单点的话,问题会很严重。但即使协调者存在集群的情况下,避免了严重的阻塞问题。但是2PC算法本身还存在出现数据不一致的可能。比如:协调者集群接收到所有参与者的yes应答,则通知所有参与者commit;但是当通知发送时,某个参与者由于网络原因,不能正常接收并处理事务提交,而其他参与者都已经成功commit;此时,所有参与者的操作就不再满足原子性,导致数据出现不一致。
3PC: three-phase commit,3阶段提交。即将分布式事务分成3个阶段:canCommit、preCommit、doCommit。在2阶段算法的基础上,进一步细分,同时引入了参与方超时等算法。通过preCommit阶段后,默认认为是可以提交事务的,那么当参与方等待超时后,会默认提交事务。(毕竟前面已经确认过,所有系统都是可以提交事务的。canCommit类似2阶段的prepare阶段)。有效解决了2PC中阻塞的问题。但是类似2PC,也存在数据不一致的可能性。比如:当第三阶段,协调者发送终端指令,但是某一个参与方因网络问题未收到,执行了commit;数据就不一致了。
TCC:Try/Confirm/Cancel;事务协调者(一般就是事务发起方)调用所有参与者的try;任一失败,则调用所有的cancel;如都成功,则一起执行confirm;TCC的各方法作用:try中,处理预订资源(比如A账户100元余额;try后变成:50余额,50冻结额;);在confirm中,根据try中预订的资源(冻结额)处理业务;在Cancel中,处理还原数据。(实际使用中,要根据业务场景设计Try、Confirm、Cancel代码;并且要考虑幂等性问题)
TCC的特点:
1,实际上每个参与方的try、confirm、cancel方法,每个方法都是在独立的本地事务中,每个方法执行就是一个完整的本地事务。
2,try只有一次,不管成功还是失败,决定后面是执行confirm还是执行cancel;
3,所有参与方的confirm方法(或者cancel)在执行时,必须成功,如果不成功,则一直重试。(所以需要幂等性设计)。
TCC的实现复杂度比较高,实际使通过对业务实现进行设计,以满足特殊场景的需要。避免了数据库层面的过高性能消耗。在必要的时候,TCC是个不错的选择方案。
LCN是国产开源的分布式事务处理框架。LCN即:lock(锁定事务单元)、confirm(确认事务模块状态)、notify(通知事务)。
LCN的实现是基于3PC的算法,结合TCC的补偿机制。
LCN正常执行序列图(来源于官方):
LCN异常执行序列图(来源于官方):
在LCN的github下载:https://github.com/codingapi/tx-lcn/
修改其属性文件: (修改下载事务协调服务器的端口、接入的服务注册中心、使用的redis库等的集群或单点配置)
#######################################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://eurekaserver1:8081/eureka/,http://eurekaserver2:8082/eureka/,http://eurekaserver3:8083/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=192.168.6.211
#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;(注意,配置中的redis等必须正常启动)
启动成功后,检查tx-manager协调者,见下图:
假定:事务参与方已经是正常运行的服务提供者。样例中的数据库是mysql,连接池采用druid;
<properties>
<lcn.last.version>4.1.0lcn.last.version>
properties>
<dependency>
<groupId>com.codingapigroupId>
<artifactId>transaction-springcloudartifactId>
<version>${lcn.last.version}version>
<exclusions>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>*artifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>com.codingapigroupId>
<artifactId>tx-plugins-dbartifactId>
<version>${lcn.last.version}version>
<exclusions>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>*artifactId>
exclusion>
exclusions>
dependency>
tm:
manager:
url: http://127.0.0.1:8899/tx/manager/
package com.mark.springcloud.service.impl;
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;
}
}
package com.mark.springcloud.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.codingapi.tx.annotation.ITxTransaction;
import com.mark.springcloud.dao.DeptDao;
import com.mark.springcloud.entities.Dept;
import com.mark.springcloud.service.DeptService;
/**
* 注意需要实现 ITxTransaction;
*/
@Service
public class DeptServiceImpl implements DeptService, ITxTransaction {
@Autowired
private DeptDao dao;
//注意需要开启事务
@Override
@Transactional
public boolean add(Dept dept) {
boolean rtnValue = dao.addDept(dept);
return rtnValue;
}
}
启动spring boot应用。
正常情况下,一个服务一般即可能是事务的发起方也是事务的参与方。(3.4.1-3.4.3在测试事务发起方、参与方都是同样配置。所以直接略过,只描述发起方特有代码)
package com.mark.springcloud.controller;
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;
}
}
package com.mark.springcloud.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.codingapi.tx.annotation.TxTransaction;
import com.mark.springcloud.entities.Dept;
import com.mark.springcloud.service.DeptClientService;
@RestController
public class DeptController_Consumer
{
@Autowired
private DeptClientService service;
//@TxTransaction(isStart = true)注解修饰该方法为事务发起方,开启事务组。
@TxTransaction(isStart = true)
@RequestMapping(value = "/consumer/dept/add")
public Object add(Dept dept)
{
Object rtnObj = this.service.add(dept);
int x = (int)(Math.random()*10);
//事务发起方随机数小于5时,抛出异常,则事务参与方事务会回滚。否则正常执行,事务参与方事务正常提交。
if (x < 5) {
int m = 1/0;
}
return rtnObj;
}
}
启动spring boot 应用。
调用事务发起方服务,事务正常受事务协调者控制,当发起方和参与方都正常执行无异常时,事务正常提交,否则回滚。
Spring Cloud 集成LCN进行分布式事务控制使用简单,整个原理也很清晰。但是必须意识到,分布式事务在微服务环境下不可避免的有较大开销,请尽量采用通过消息队列实现的最终一致性设计。