基于LCN彻底解决分布式事务

一 . 理论知识

1. 数据库管理系统中事务(transaction)的四个特性:简称ACID(这种特性简称刚性事物)

原子性(Atomicity) 原子性是指事务是一个不可再分割的工作单元,事务中的操作要么都发生,要么都不发生。

一致性(Consistency)一致性是指在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏;这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。

隔离性(Isolation)多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。

持久性(Durability)持久性,意味着在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。(完成的事务是系统永久的部分,对系统的影响是永久性的,该修改即使出现致命的系统故障也将一直保持)

2. CAP理论(帽子原理)

由于对系统或者数据进行了拆分,我们的系统不再是单机系统,而是分布式系统,针对分布式系统的CAP原理包含如下三个元素:

C:Consistency 一致性:在分布式系统中的所有数据 备份,在同一时刻具有同样的值,所有节点在同一时刻读取的数据都是最新的数据副本

A:Availability 可用性:好的响应性能。完全的可用性指的是在任何故障模型下,服务都会在有限的时间内处理完成并进行响应

P: Partition tolerance 分区容忍性:尽管网络上有部分消息丢失,但系统仍然可继续工作

    CAP原理指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。因此在进行分布式架构设计时,必须做出取舍。而对于分布式数据系统,分区容忍性是基本要求,否则就失去了价值。因此设计分布式数据系统,就是在一致性和可用性之间取一个平衡。对于大多数web应用,其实并不需要强一致性,因此牺牲一致性而换取高可用性,是目前多数分布式数据库产品的方向。      当然,牺牲一致性,并不是完全不管数据的一致性,否则数据是混乱的,那么系统可用性再高分布式再好也没有了价值。牺牲一致性,只是不再要求关系型数据库中的强一致性,而是只要系统能达到最终一致性即可,考虑到客户体验,这个最终一致的时间窗口,要尽可能的对用户透明,也就是需要保障“用户感知到的一致性”。通常是通过数据的多份异步复制来实现系统的高可用和数据的最终一致性的,“用户感知到的一致性”的时间窗口则取决于数据复制到一致状态的时间。

3. Base理论

 BASE理论是指,Basically Available(基本可用)、Soft-state( 软状态/柔性事务)、Eventual Consistency(最终一致性)。是基于CAP定理演化而来,是对CAP中一致性和可用性权衡的结果。

核心思想:即使无法做到强一致性,但每个业务根据自身的特点,采用适当的方式来使系统达到最终一致性。

 ① 基本可用:指分布式系统在出现故障的时候,允许损失部分可用性,保证核心可用。但不等价于不可用。比如:搜索引擎0.5秒返回查询结果,但由于故障,2秒响应查询结果;网页访问过大时,部分用户提供降级服务等。

 ② 软状态:软状态是指允许系统存在中间状态,并且该中间状态不会影响系统整体可用性。即允许系统在不同节点间副本同步的时候存在延时。       

③ 最终一致性:系统中的所有数据副本经过一定时间后,最终能够达到一致的状态,不需要实时保证系统数据的强一致性。最终一致性是弱一致性的一种特殊情况。BASE理论面向的是大型高可用可扩展的分布式系统,通过牺牲强一致性来获得可用性。ACID是传统数据库常用的概念设计,追求强一致性模型。

4. 柔性事务和刚性事务

柔性事务满足BASE理论(基本可用,最终一致),刚性事务满足ACID理论。

本文主要围绕分布式事务当中的柔性事务的处理方式进行讨论。

柔性事务分为:①两段提交  ②补偿型  ③异步确保型  ④最大努力通知型

由于支付宝整个架构是SOA架构,因此传统单机环境下数据库的ACID事务满足了分布式环境下的业务需要,以上几种事务类似就是针对分布式环境下业务需要设定的。

5. 两段提交协议 -  2PC(Two-phaseCommit)

    

第一阶段: 准备阶段:协调者向参与者发起指令,参与者评估自己的状态,如果参与者评估指令可以完成,则会写redo或者undo日志,让后锁定资源,执行操作,但并不提交。

第二阶段:如果每个参与者明确返回准备成功,则协调者向参与者发送提交指令,参与者释放锁定的资源,如何任何一个参与者明确返回准备失败,则协调者会发送中指指令,参与者取消已经变更的事务,释放锁定的资源。

两阶段提交方案应用非常广泛,几乎所有商业OLTP数据库都支持XA协议。但是两阶段提交方案锁定资源时间长,对性能影响很大,基本不适合解决微服务事务问题。

缺点:如果协调者宕机,参与者没有协调者指挥,则会一直阻塞。

6. 三段提交协议 -  3PC(Three-phaseCommit)

核心:在2pc的基础上增加了一个询问阶段(第一阶段),确认网络,避免阻塞,二三阶段就是上面的2pc

三阶段提交协议是两阶段提交协议的改进版本。它通过超时机制解决了阻塞的问题,并且把两个阶段增加为三个阶段:

询问阶段:协调者询问参与者是否可以完成指令,协调者只需回答是还是不是,而不需要做真正的操作,这个阶段超时导致中止

准备阶段:如果在询问阶段所有的参与者都返回可以执行操作,协调者向参与者发送预执行请求,然后参与者写redo和undo日志,执行操作,但是不提交操作;如果在询问阶段任何参与者返回不能执行操作的结果,则协调者向参与者发送中止请求,这里的逻辑与两阶段提交协议的的准备阶段是相似的,这个阶段超时导致成功

提交阶段:如果每个参与者在准备阶段返回准备成功,也就是预留资源和执行操作成功,协调者向参与者发起提交指令,参与者提交资源变更的事务,释放锁定的资源;如果任何一个参与者返回准备失败,也就是预留资源或者执行操作失败,协调者向参与者发起中止指令,参与者取消已经变更的事务,执行undo日志,释放锁定的资源,这里的逻辑与两阶段提交协议的提交阶段一致

二. LCN解决分布式事务

1. LCN框架简单介绍与案例分析

LCN并不生产事务,LCN只是本地事务的搬运工,兼容 dubbo、springcloud框架,支持各种关系型数据库,目前版本5.x,详情参考官方文档:https://www.txlcn.org/

场景分析:订单服务调用库存服务,调用完后报了个1/0的错,此时,库存事务已经提交,即库存已经减1了,而订单报错,事务会回滚,测试会造成分布式事务不同步问题。

解决办法:LCN解决分布式事务。

原理

① LCN客户端(发起方和参与方都必须注册到事务协调者tx-manager)建立一个长连接(优点:减少带宽传输,弊端:占内存)

② 订单服务(发起方)调用库存服务接口(参与方)之前会向tx-manager事务协调者创建一个事务的分组id

③ 订单服务(发起方)调用库存服务接口(参与方)的时候,会在请求头中存放该事务的分组id,给库存服务

④ 如果库存服务获取到请求头中有对应的事务分组id,库存服务业务逻辑代码执行完毕,会采用假关闭,不会提交该事务(实际等发起方代码执行完毕才会提交事务)

订单服务(发起方)调用库存服务接口(参与方)之后,如果订单服务(发起方)执行没有问题的下,订单服务(发起方)使用对应的事务分组id, 通知给Tx-manager事务协调者,然后Tx-manager事务协调者再根据该事务分组id,通知给所有的参与方提交事务。

事务分组id每次调用都不一样,参与方在被调用中,其它的发起方也可以调用该参与方(即- 个参与方可以同时被多个发起方调用)
如果订单调完库存,报1/0错,则订单事务本地回滚(@Transactional注解的作用),并发送一个通知(这个分组下面的所有协调者都要回滚)给Tx-manager事务协调者,Tx-manager收到消息后,再告诉这个分组id下面的所有参与者都要回滚。

2. 核心代码(完整项目见博主码云地址:https://gitee.com/AkiraNicky/lcn-demo

① 单独建一个服务tx-manager,即事务协调者

    maven依赖(核心是txlcn-tm):

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tm</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<!-- druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
</dependency>
<!--mysql 依赖-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.1.1</version>
</dependency>
<!-- jdbc -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

    application.yml

spring.application.name=tx-manager
server.port=7970

#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/tx-manager?characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

#指定注册中心地址
eureka.client.serviceUrl.defaultZone=http://localhost:8080/eureka/
eureka.instance.instance-id=${spring.cloud.client.ip-address}:${server.port}
eureka.instance.prefer-ip-address=true

mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.use-generated-keys=true

# TxManager Host Ip
tx-lcn.manager.host=127.0.0.1
# TxClient连接请求端口1
tx-lcn.manager.port=8070
# 心跳检测时间(ms)
tx-lcn.manager.heart-time=15000
# 分布式事务执行总时间
tx-lcn.manager.dtx-time=30000
#参数延迟删除时间单位ms
tx-lcn.message.netty.attr-delay-time=10000
tx-lcn.manager.concurrent-level=128
# 开启日志
tx-lcn.logger.enabled=true
logging.level.com.codingapi=debug
#redis 主机
spring.redis.host=127.0.0.1
#redis 端口
spring.redis.port=6379

    启动类
 

package com.example;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.codingapi.txlcn.tm.config.EnableTransactionManagerServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
@EnableTransactionManagerServer
@EnableAutoConfiguration(exclude = {DruidDataSourceAutoConfigure.class})
public class AppTxManager{
    public static void main(String[] args) {
        SpringApplication.run(AppTxManager.class, args);
    }
}

② 订单服务和库存服务配置

    maven依赖

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tc</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-txmsg-netty</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>

    启动类分别加入注解:@EnableDistributedTransaction

    订单接口加入注解@LcnTransaction,库存接口加入注解@TxcTransaction

        @LcnTransaction
	@Transactional
	@GetMapping(value = "/addOrderAndStock")
	public ResponseBase addOrderAndStock(int i) throws Exception {
		OrderEntity orderEntity = new OrderEntity();
		orderEntity.setName("蚂蚁课堂永久会员充值");
		orderEntity.setOrderCreatetime(new Date());
		// 价格是300元
		orderEntity.setOrderMoney(300d);
		// 状态为 未支付
		orderEntity.setOrderState(0);
		Long commodityId = 30l;
		// 商品id
		orderEntity.setCommodityId(commodityId);
		// 1.先下单,创建订单
		int orderResult = orderMapper.addOrder(orderEntity);
		System.out.println("orderResult:" + orderResult);
		if (orderResult <= 0) {
			return setResultError("下单失败!");
		}
		// 2.下单成功后,调用库存服务
		ResponseBase inventoryReduction = stockFeign.inventoryReduction(commodityId);
		if (inventoryReduction.getRtnCode() != 200) {
			// 库存调用失败,回滚方法有两种(这种场景其实不属于分布式事务,分布式事务指订单掉完库存成功,订单后面的代码出现问题,则回滚库存):
			// ①.使用手动事务
			// TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
			// ②.获取将异常抛出给上一层,外面回滚
			throw new Exception("调用库存服务接口失败,开始回退订单事务代码");
		}
		int reuslt = 1 / i;
		return setResultSuccess("下单成功!");
	}
        @TxcTransaction
	@Transactional
	@RequestMapping("/inventoryReduction")
	public ResponseBase inventoryReduction(@RequestParam("commodityId") Long commodityId) {
		if (commodityId == null) {
			return setResultError("商品id不能为空!");
		}
		// 1.查询该商品id 是否存在
		StockEntity stockEntity = stockMapper.selectStock(commodityId);
		if (stockEntity == null) {
			return setResultError("商品id不存在!");
		}
		// 2.判断商品是否有超卖
		if (stockEntity.getStock() <= 0) {
			return setResultError("当前商品已经买完啦!");
		}
		// 3.减去库存1
		int updateStockResult = stockMapper.updateStock(commodityId);
		if (updateStockResult <= 0) {
			return setResultError("修改库存失败!");
		}
		return setResultSuccess("修改库存成功!");
	}

此时,分别启动 eureka,tx-manager,order,stock

  

浏览器访问:http://localhost:8010/addOrderAndStock?i=1,会发现下单成功,订单数据+1,库存总数-1;

浏览器访问:http://localhost:8010/addOrderAndStock?i=0,会发现下单失败,订单和库存事务都会回滚。

发布了45 篇原创文章 · 获赞 20 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/AkiraNicky/article/details/89681327