Spring Cloud LCN 分布式事务

1,分布式事务

分布式事务产生的原因:当系统从单体应用按照领域,做高内聚、低耦合的拆分后,一个单体应用变成了多个分布式子系统。分布式子系统相互之间的协作。由于不在一个进程内,不是一个数据库连接,当服务编排中,各子系统协同完成业务时,需要保证所有协作子系统要么都成功,要么都失败,这即满足在分布式环境下,多个系统的操作原子性的分布式事务。大型分布式应用由于数据的快速扩张,以及数据的高可用,通常需要跨网络线性的伸缩。那么在很多时候,需要考虑的不仅仅是数据的一致性,本文仅讨论在特殊的、必须实现分布式事务的场景下,分布式事务的实现理论及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是个不错的选择方案。

2,LCN

2.1 LCN是什么

LCN是国产开源的分布式事务处理框架。LCN即:lock(锁定事务单元)、confirm(确认事务模块状态)、notify(通知事务)。

LCN的实现是基于3PC的算法,结合TCC的补偿机制。

LCN正常执行序列图(来源于官方):

Spring Cloud LCN 分布式事务_第1张图片

LCN异常执行序列图(来源于官方):

Spring Cloud LCN 分布式事务_第2张图片

3,Spring Cloud 整合LCN

3.1,下载LCN工程;

在LCN的github下载:https://github.com/codingapi/tx-lcn/

3.2,配置tx-manager事务协调器

修改其属性文件: (修改下载事务协调服务器的端口、接入的服务注册中心、使用的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

3.3 启动事务协调者

启动事务协调者,让事务协调者注入进入eureka;(注意,配置中的redis等必须正常启动)

启动成功后,检查tx-manager协调者,见下图:

Spring Cloud LCN 分布式事务_第3张图片

3.4 事务参与方配置

假定:事务参与方已经是正常运行的服务提供者。样例中的数据库是mysql,连接池采用druid;

3.4.1 pom文件引入LCN db插件和springcloud支持:

    <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>

3.4.2 yml添加配置

tm:
  manager:
    url: http://127.0.0.1:8899/tx/manager/

3.4.3 添加TxManagerTxUrlService到spring中

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;
    }
}

3.4.4 事务参与方服务:

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;
    }
}

3.4.5 启动事务参与方

启动spring boot应用。

3.5 事务发起方配置

正常情况下,一个服务一般即可能是事务的发起方也是事务的参与方。(3.4.1-3.4.3在测试事务发起方、参与方都是同样配置。所以直接略过,只描述发起方特有代码)

3.5.1 参照样例,实现TxManagerHttpRequestService

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;
    }
}

3.5.2 事务发起方服务处理

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;
    }
}

3.5.3 启动事务发起方

启动spring boot 应用。

4,测试事务

调用事务发起方服务,事务正常受事务协调者控制,当发起方和参与方都正常执行无异常时,事务正常提交,否则回滚。

4,总结

Spring Cloud 集成LCN进行分布式事务控制使用简单,整个原理也很清晰。但是必须意识到,分布式事务在微服务环境下不可避免的有较大开销,请尽量采用通过消息队列实现的最终一致性设计。

你可能感兴趣的:(IT研发技术类)