(五)分布式事务

168 阅读25分钟

事务

要么都不做,要么做全套。

ACID

A:原子性(Atomicity)

一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。

事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

C:一致性(Consistency)

事务的一致性指的是在一个事务执行之前和执行之后数据库都必须处于一致性状态。

如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。

如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。

I:隔离性(Isolation)

指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。

由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。

D:持久性(Durability)

指的是只要事务成功结束,它对数据库所做的更新就必须保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

本地事务

基于单个服务单个数据库资源访问的事务,被称为本地事务

也称为数据库事务或者传统事务(相对分布式事务),执行模式就是常见的:

1. transaction begin
2. insert/delete/update
3. insert/delete/update
4. ...
5. transaction commit/rollback

特征

  1. 一次事务值连接一个支持事务的数据库(一般都是关系型数据库)
  2. 事务的执行结果保证ACID
  3. 会用到数据库锁。

分布式事务

分布式事务值事务的参与者、支持事务的服务器、资源的服务器以及事务管理器分别位于不同的分布式系统的不同节点上,并且属于不同的应用。

本质上来说:分布式事务就是为了保证不同数据库的数据一致性

场景

电商系统中的下单扣库存

电商系统中,订单系统库存系统是两个系统,一次下单的操作由两个系统协同完成

金融系统中的银行卡充值

在金融系统中通过银行卡向平台充值需要通过银行系统金融系统协同完成。

教育系统中下单选课业务

在线教育系统中,用户购买课程,下单支付成功后学生选课成功,此事务由订单系统选课系统协同完成。

SNS系统的消息发送

在社交系统中发送站内消息同时发送手机短信,一次消息发送由站内消息系统手机通信系统协同完成。

分布式系统

部署在不同节点上的系统通过网络交互来完成协同工作的系统。

CAP定理

CAP 定理,又被叫作布鲁尔定理。对于设计分布式系统(不仅仅是分布式事务)的架构师来说,CAP 就是入门理论。

分布式系统的最大难点,就是各个节点的状态如何同步。CAP 定理是这方面的基本定理,也是理解分布式系统的起点。

C (一致性Consistency):指数据在多个副本之间能够保持一致的特性(严格的一致性)在分布式系统中的所有数据备份,在同一时刻是否同样的值。(所有节点在同一时间具有相同的数据)

一致性(Consistency)是指多副本(Replications)问题中的数据一致性。可以分为强一致性与弱一致性。

① 强一致性

简言之,在任意时刻,所有节点中的数据是一样的。

例如,对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。

② 弱一致性

数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。

最终一致性就属于弱一致性。

A (可用性Availability):指系统提供的服务必须一直处于可用的状态,每次只要收到用户的请求,服务器就必须给出响应。在合理的时间内返回合理的响应(不是错误和超时的响应)

只有非故障节点才能满足业务正常;只有在合理的时间内,用户才能接受;只有返回合理的响应,用户才能接受。

P (网络分区容错性Partition tolerance):网络节点之间无法通信的情况下,节点被隔离,产生了网络分区, 整个系统仍然是可以保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

什么是分区?

在分布式系统中,不同的节点分布在不同的子网络中,由于一些特殊的原因,这些子节点之间出现了网络不通的状态,但他们的内部子网络是正常的。从而导致了整个系统的环境被切分成了若干个孤立的区域。这就是分区。

CAP原则的精髓就是要么AP,要么CP,要么AC,但是不存在CAP。

取舍策略

由于在分布式系统中,网络无法100%可靠,分区时必然纤细,随着网络节点出现问题,产生分区,这时候其他节点和出错节点数据必然不一致,这个时候就要选择:

  1. 选择停掉所有的服务,等网络节点修复后恢复数据,以此来保证一致性(PC)
  2. 还是继续提供服务,放弃强一致性,以此保证整体的可用性(PA)
组合分析结果
CA满足原子和可用,放弃分区容错。说白了,就是一个整体的应用。
CP满足原子和分区容错,也就是说,要放弃可用。当系统被分区,为了保证原子性,必须放弃可用性,让服务停用。
AP满足可用性和分区容错,当出现分区,同时为了保证可用性,必须让节点继续对外服务,这样必然导致失去原子性。

在分布式系统设计中AP的应用较多,即保证分区容忍性和可用性,牺牲数据的强一致性(写操作后立刻读取到最新数据),保证数据最终一致性(弱一致性)。比如:订单退款,今日退款成功,明日账户到账,只要在预定的用户可以接受的时间内退款事务走完即可。

CAP 理论中是忽略网络延迟,也就是当事务提交时,从节点 A 复制到节点 B 没有延迟,但是在现实中这个是明显不可能的,所以总会有一定的时间是不一致。

但是,有个特殊情况需要注意:但对于传统的项目就可能有所不同,拿银行的转账系统来说,涉及到金钱的对于数据一致性不能做出一丝的让步,C必须保证,出现网络故障的话,宁可停止服务,可以在A和P之间做取舍。

总而言之,没有最好的策略,好的系统应该是根据业务场景来进行架构设计的,只有适合的才是最好的。

分布式事务解决方案

  1. XA两段提交(低效率)分布式事务解决方案
  2. TCC三段提交(2段,高效率【不推荐,补偿代码】)
  3. 本地消息(MQ+Table)
  4. 事务消息(RocketMQ【alibaba】)
  5. Seata(alibaba)

1.基于XA协议的两段提交(2PC)

XA:分布式协议。大致分为两部分:事务管理器和本地资源管理器。本地资源管理器往往由数据库实现,比如Oracle,DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。

简介

二阶段提交2PC是指,在分布式系统里,为了保证所有节点在进行事务提交时保持一致性的一种算法。

背景

在分布式系统里,每个节点都可以知晓自己操作的成功或者失败,但是没有办法直到其他节点操作的成功或者失败。

当一个事务跨多个节点时,为了保持事务的原子性和一致性,需要引入一个协调者来同一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚。

思路

分为两个阶段

  1. 投票阶段:参与者将操作结果通知协调者
  2. 提交阶段:受到参与者的通知后,协调者在向参与者发出通知,根据反馈情况决定各个参与者是否要提交还是回滚。

缺陷

算法执行过程中,所有节点都处于阻塞阶段,所有节点所持有的资源(比如数据库数据,本地文件等)都处于封锁状态。

典型场景为:

  1. 某一个参与者发出通知之前,所有参与者以及协调者都处于阻塞状态。
  2. 在协调者发出通知之前,所有参与者都处于阻塞状态。

另外,如果有协调者或者某个参与者出现了崩溃,为了避免算法处于一个完全阻塞的状态,往往需要借助超时机制来将算法继续向前推进,故此时的算法效率比较低。

总的来说,2PC是一种比较保守的算法。

2PC效率很低,分布式事务很难做。

实际应用交互流程

第一阶段:在分布式事务的第一阶段,作为事务协调者的节点会首先向所有的参与者节点发送Prepare请求。

在接到Prepare请求之后,每一个参与者节点会各自执行与事务有关的数据更新,写入Undo Log和Redo Log。如果参与者执行成功,暂时不提交事务,而是向事务协调节点返回“完成”消息。

当事务协调者接到了所有参与者的返回消息,整个分布式事务将会进入第二阶段。

第二阶段:在2PC分布式事务的第二阶段,如果事务协调节点在之前所收到都是正向返回,那么它将会向所有事务参与者发出Commit请求。

接到Commit请求之后,事务参与者节点会各自进行本地的事务提交,并释放锁资源。当本地事务完成提交后,将会向事务协调者返回“完成”消息。

当事务协调者接收到所有事务参与者的“完成”反馈,整个分布式事务完成。

失败情况的处理流程

在2PC的第一阶段,如果某个事务参与者反馈失败消息,说明该节点的本地事务执行不成功,必须回滚。

于是在第二阶段,事务协调节点向所有的事务参与者发送Abort(中止)请求。接收到Abort请求之后,各个事务参与者节点需要在本地进行事务的回滚操作,回滚操作依照undo Log来进行。

有哪些不足

  1. 性能问题

    2PC遵循强一致性,在事务执行过程中,各个节点占用数据库资源,只有当所有节点准备完毕,事务协调者才会通知提交,参与者提交后释放资源。这样的过程有着显然的性能问题。

  2. 协调者单点故障问题

    2PC模型的核心,一旦事务协调者节点挂掉,参与者收不到提交或者回滚通知,参与者就会一致处于中间状态无法完成事务

  3. 丢失消息导致的不一致问题

    第二个阶段如果发生局部网络问题,一部分书屋参与者受到了提交消息,另一部分事务参与者没收到消息,就会导致节点之间数据不一致。

2.代码补偿事务

简称TCC,作用是解决阔服务调用场景下的事务问题。

TCC是try(尝试)-->Confirm(确认)--->Cancel(取消)的简称

TCC两段提交和XA两端提交的区别

XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。

TCC是业务层面的分布式事务,最终一致性,不会一致持有资源的锁。

3.本地消息表

最终一致性。

思路

源于ebay,后来通过支付宝等公司的布道,在业内广泛使用,基本的涉及思想是将远程分布式事务拆分成为一系列的本地事务。如果不考虑性能以及设计优雅,借助关系型数据库中的表即可实现。

该方案保证的是最终一致性.

优点:一种非常经典的实现,避免了分布式事务,实现了最终一致性。

缺点:消息表会耦合到业务系统中,如果没有封装号的解决方案,会有很多杂活需要处理。

4.MQ事务消息

RocketMQ支持事务消息,支持的方式类似于采用的二阶段提交。

注:市面上一些主流的MQ都不支持事务消息,比如RabbitMQ和Kafka都不支持。

以阿里的 RocketMQ 中间件为例,其思路大致为:

  1. RocketMQ提供了类似于X/Open XA的分布式事务功能,通过MQ的事务消息能达到分布式事务的最终一致。
  2. 发送方在业务执行开始前会向消息服务器中投递“半消息”,就是暂时不会真正投递的消息,当发送方(生产者)将消息成功发送给MQ服务端并且没有将该消息的二次确认结果返回,此时消息状态是“暂时不可投递”,状态位置,该状态下的消息就是半消息。
  3. 如果出现网络闪断、生产者应用重启等原因导致事务消息二次确认丢失,MQ服务端会通过扫描发现某条消息长期处于“半消息”状态,MQ服务端会主动向生产者查询该消息的最终状态是处于Commit(消息提交)还是Rollback(消息回滚),这个过程称为消息回查

总体而言,RocketMQ事务消息分为两条主线:

  1. 定时任务发送流程:发送half message(半消息),执行本地事务,发送事务执行结果
  2. 定时任务回查流程:MQ服务器回查本地事务,发送事务执行结果

流程

  1. Producer向MQ服务器发送消息,MQ Server将消息状态标记为Prepared(预备状态),此时这条消息无法消费。
  2. MQ服务器收到消息并持久化成功之后,会像Producer确认首次消息发送成功,此时消息处于half message(半消息)状态,并没有发送给对应的Consumer。
  3. Producer开始执行本地事务逻辑,通过本地数据库事务控制。
  4. 根据事务执行结果,Producer向MQ服务器提交二次确认(commit或者rollback)。MQ Server受到Commit状态则将半消息标记为可投递,Consumer最终收到该消息;如果MQ Server收到Rollback状态就删除半消息,Consumer将不会收到该消息。
  5. 在断网或者应用重启的情况下,二次确认未成功的发给MQ Server,MQ Server会主动向Producer启动消息回查。
  6. Producer根据事务执行结果,对消息回查返回对应的结果。
  7. MQ Server根据返回结果,决定继续投递消息或者丢弃消息(重复第4步消息)。

1-4是事务消息的发送过程;5-6为事务消息的回查过程。

优点:实现了最终一致性,不需要依赖本地数据库事务。

缺点:目前主流MQ中只有RocketMQ支持事务消息。

5.Seata

seata.io/zh-cn/

Seata是案例开源的一个分布式事务框架,能够让大家在操作分布式事务时,像操作本地事务一样简单,一个注解搞定分布式事务。

设计初衷:

  • 对业务无入侵:就是减少技术架构上的微服务化所带来的分布式事务问题对业务的侵入。
  • 高性能:减少分布式事务解决所带来的性能消耗。

包含两种实现方案:

  • AT模式:主要关注DB访问的数据一致性,当然也包括多服务下的多DB数据访问一致性2PC-改进。
  • MT模式:本质上是一种TCC方案,主要关注业务拆分,在按照业务横向扩展资料时,解决微服务间调用的一致性问题。

Seata全称:Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架。

AT模式

Transaction Coordinator(TC):事务协调器,维护全局事务的运行状态,负责协调并决定全局事务的提交或回滚。

Transaction Manager(TM):事务管理者,控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或者全局回滚的决议。

Resource Manager(RM):资源管理器,负责本地事务的注册,本地事务状态的汇报(投票),并且负责本地事务的提交和回滚。

XID:一个全局事务的唯一标识。

其中,TM是一个分布式事务的发起者和终结者,TC负责维护分布式事务的运行状态,而RM则负责本地事务的运行。

执行流程
  1. TM向TC申请开启了一个全局事务,全局事务创建陈工并且生成了一个全局唯一的XID。
  2. XID在微服务调用链路的上下文中传播。
  3. RM向TC注册分支事务,接着执行这个分支事务并提交(重点:RM在第一阶段就已经执行了本地事务的提交/回滚),最后将执行结果汇报给TC。
  4. TM根据TC中所有的分支事务的执行情况,发起全局提起或者回滚决议。
  5. TC调度XID下管辖的全部分支事务完成提交或者回滚请求。

Seata中有三大模块,分别是TM,RM和TC。其中TM和RM是作为Seata的客户端和业务系统集成在一起,TC作为Seata的服务器独立部署。

MT模式

本质上是一种TCC方案,业务逻辑需要被拆分为Prepare/Commit/Rollback三个部分,形成一个MT分支,加入全局事务。

MT模式一方面是AT模式的补充,另外,更重要的价值在于,通过MT模式可以把众多非事务性资源纳入全局事务的管理中。

Seata案例

1.需求分析

完成一个案例:用户下单的时候记录下单日志,完成订单添加,完成用户账户扣款,完成商品库存削减功能,然后在任何一个微服务中制造异常,测试分布式事务。

也就是说我们需要四个数据库:business,item,order,user。

2.搭建工程

父工程fescar-parent:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
    </parent>
    <packaging>pom</packaging>
    <!--跳过测试-->
    <properties>
        <skipTests>true</skipTests>
    </properties>
    <!--依赖包-->
    <dependencies>
        <!--测试包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>
        <!--鉴权-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <!--web起步依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- redis 使用
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency> -->
        <!--eureka-client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--微信支付-->
        <dependency>
            <groupId>com.github.wxpay</groupId>
            <artifactId>wxpay-sdk</artifactId>
            <version>0.0.3</version>
        </dependency>
        <!--httpclient支持,微信支付-->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>
        <!--通用mapper起步依赖,MyBatis通用Mapper封装,基于MyBatis动态SQL实现,可以实现对数据库的操作,不需要编写SQL语句-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.0.4</version>
        </dependency>
        <!--MySQL数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis分页插件,用于解决数据库分页实现  PageHelper.start(当前页,每页显示的条数)-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.3</version>
        </dependency>
    </dependencies>
<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

公共工程fescar-api:将所有数据库对应的pojo/Feign抽取出一个公共工程fescar-api。是fescar-parent的子工程。

商品微服务fescar-item:用于实现库存削减。

<parent>
        <artifactId>fescar-parent</artifactId>
        <groupId>com.dyy</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>fescar-item</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.dyy</groupId>
            <artifactId>fescar-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

dao

public interface ItemInfoMapper extends Mapper<ItemInfo> {
}

Service:创建库存递减方法:

public interface ItemInfoService {
    /**
     * 库存递减
     * @param id
     * @param count
     */
    void decrCount(int id, int count);
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ItemInfoServiceImpl implements ItemInfoService {
    @Autowired
    private ItemInfoMapper itemInfoMapper;
    /***
     * 库存递减
     * @param id
     * @param count
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void decrCount(int id, int count) {
        //查询商品信息
        ItemInfo itemInfo = itemInfoMapper.selectByPrimaryKey(id);
        itemInfo.setCount(itemInfo.getCount()-count);
        int dcount = itemInfoMapper.updateByPrimaryKeySelective(itemInfo);
        System.out.println("库存递减受影响行数:"+dcount);
    }
}

Controller

@RestController
@RequestMapping("/itemInfo")
@CrossOrigin
public class ItemInfoController {
    @Autowired
    private ItemInfoService itemInfoService;
    /**
     * 库存递减
     * @param id
     * @param count
     * @return
     */
    @PostMapping(value = "/decrCount")
    public String decrCount(@RequestParam(value = "id") int id, @RequestParam(value = "count") int count){
        //库存递减
        itemInfoService.decrCount(id,count);
        return "success";
    }
}

启动类

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.dyy.feign"})
@MapperScan(basePackages = {"com.dyy.dao"})
public class ItemApplication {
public static void main(String[] args) {
        SpringApplication.run(ItemApplication.class,args);
    }
}


application.yml

server:
  port: 18082
spring:
  application:
    name: item
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/fescar-item?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  main:
    allow-bean-definition-overriding: true
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka/
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true
#hystrix 配置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000
          strategy: SEMAPHORE

用户微服务fescar-user:引入公共工程依赖。

<dependencies>
        <dependency>
            <groupId>com.dyy</groupId>
            <artifactId>fescar-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

Dao

public interface UserInfoMapper extends Mapper<UserInfo> {
}

Service

public interface UserInfoService {
    /***
     * 账户金额递减
     * @param username
     * @param money
     */
    void decrMoney(String username, int money);
}

UserInfoServiceImpl,可以在这里设置一个异常。

@Service
public class UserInfoServiceImpl implements UserInfoService {
    @Autowired
    private UserInfoMapper userInfoMapper;
    /***
     * 账户金额递减
     * @param username
     * @param money
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void decrMoney(String username, int money) {
        UserInfo userInfo = userInfoMapper.selectByPrimaryKey(username);
        userInfo.setMoney(userInfo.getMoney()-money);
        int count = userInfoMapper.updateByPrimaryKeySelective(userInfo);
        System.out.println("添加用户受影响行数:"+count);
      //  int q=10/0;
    }
}

Controller

@RestController
@RequestMapping("/userInfo")
@CrossOrigin
public class UserInfoController {
    @Autowired
    private UserInfoService userInfoService;
    /***
     * 账户余额递减
     * @param username
     * @param money
     */
    @PostMapping(value = "/add")
    public String decrMoney(@RequestParam(value = "username") String username, 
                                                @RequestParam(value = "money") int money){
        userInfoService.decrMoney(username,money);
        return "success";
    }
}

启动类

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.dyy.feign"})
@MapperScan(basePackages = {"com.dyy.dao"})
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class,args);
    }
}

application.yml

server:
  port: 18084
spring:
  application:
    name: user
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/fescar-user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  main:
    allow-bean-definition-overriding: true
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true
#hystrix 配置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000
          strategy: SEMAPHORE

订单微服务fescar-order:实现调用商品微服务递减库存

<parent>
        <artifactId>fescar-parent</artifactId>
        <groupId>com.dyy</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>fescar-order</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.dyy</groupId>
            <artifactId>fescar-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

Dao:创建Mapper

public interface OrderInfoMapper extends Mapper<OrderInfo> {
}

Service

public interface OrderInfoService {
    /***
     * 添加订单
     * @param username
     * @param id
     * @param count
     */
    void add(String username, int id, int count);
}

@Service
public class OrderInfoServiceImpl implements OrderInfoService {
    @Autowired
    private OrderInfoMapper orderInfoMapper;
    @Autowired
    private ItemInfoFeign itemInfoFeign;
    /***
     * 添加订单
     * @param username
     * @param id
     * @param count
     */
    @Transactional
    @Override
    public void add(String username, int id, int count) {
        //添加订单
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setMessage("生成订单");
        orderInfo.setMoney(10);
        int icount = orderInfoMapper.insertSelective(orderInfo);
        System.out.println("添加订单受影响函数:"+icount);
        //递减库存
        itemInfoFeign.decrCount(id,count);
    }
}

Controller

@RestController
@RequestMapping("/orderInfo")
@CrossOrigin
public class OrderInfoController {
    @Autowired
    private OrderInfoService orderInfoService;
    /**
     * 增加订单
     * @param username
     * @param id
     * @param count
     */
    @PostMapping(value = "/add")
    public String add(@RequestParam(value = "name") String username, @RequestParam(value = "id") int id, @RequestParam(value = "count") int count){
        //添加订单
        orderInfoService.add(username,id,count);
        return "success";
    }
}

启动类

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.dyy.feign"})
@MapperScan(basePackages = {"com.dyy.dao"})
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class,args);
    }
}

application.yml

server:
  port: 18083
spring:
  application:
    name: order
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/fescar-order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  main:
    allow-bean-definition-overriding: true
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true
#hystrix 配置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000
          strategy: SEMAPHORE

业务微服务:fescar-business 实现分布式事务控制,下单入口从这里开始

<dependencies>
        <dependency>
            <groupId>com.dyy</groupId>
            <artifactId>fescar-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

Dao

public interface LogInfoMapper extends Mapper<LogInfo> {
}

Service

public interface BusinessService {
    /**
     * 下单
     * @param username
     * @param id
     * @param count
     */
    void add(String username, int id, int count);
}

@Service
public class BusinessServiceImpl implements BusinessService {
    @Autowired
    private OrderInfoFeign orderInfoFeign;
    @Autowired
    private UserInfoFeign userInfoFeign;
    @Autowired
    private LogInfoMapper logInfoMapper;
    /***
     * ①
     * 下单
     * @GlobalTransactional:全局事务入口
     * @param username
     * @param id
     * @param count
     */
    @Override
    public void add(String username, int id, int count) {
        //添加订单日志
        LogInfo logInfo = new LogInfo();
        logInfo.setContent("添加订单数据---"+new Date());
        logInfo.setCreatetime(new Date());
        int logcount = logInfoMapper.insertSelective(logInfo);
        System.out.println("添加日志受影响行数:"+logcount);
        //添加订单
        orderInfoFeign.add(username,id,count);
        //用户账户余额递减
        userInfoFeign.decrMoney(username,10);
    }
}

Controller

@RestController
@RequestMapping(value = "/business")
public class BusinessController {
    @Autowired
    private BusinessService businessService;
    /***
     * 购买商品分布式事务测试
     */
    @RequestMapping(value = "/addorder")
    public String order(){
        String username="zhangsan";
        int id=1;
        int count=5;
        //下单
        businessService.add(username,id,count);
        return "success";
    }
}

启动类

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.dyy.feign"})
@MapperScan(basePackages = {"com.dyy.dao"})
public class BusinessApplication {
    public static void main(String[] args) {
        SpringApplication.run(BusinessApplication.class,args);
    }
}

yml

server:
  port: 18081
spring:
  application:
    name: business
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/fescar-business?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
  main:
    allow-bean-definition-overriding: true
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka
  instance:
    prefer-ip-address: true
feign:
  hystrix:
    enabled: true
#读取超时设置
ribbon:
  ReadTimeout: 30000
#hystrix 配置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000
          strategy: SEMAPHORE

3.启动测试

此时一个业务失败,其他业务是不会回滚的。

加入Seata

在fescar-api项目中引入依赖,这样可以传递给其他微服务项目使用。

<!--Seata依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-seata</artifactId>
    <version>2.1.0.RELEASE</version>
</dependency>

在 fescar-api 工程的resource 文件夹下面,新建 registry.conf 配置文件, TC 存储RM注册信息的文件配置

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "file"
nacos {
    serverAddr = "localhost"
    namespace = "public"
    cluster = "default"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}
config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"
nacos {
    serverAddr = "localhost"
    namespace = "public"
    cluster = "default"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    app.id = "seata-server"
    apollo.meta = "http://192.168.1.204:8801"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

在 fescar-api 工程的resource 文件夹下面,新建 file.conf 配置文件,各大微服务的RM和TC之间的通信配置

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  #thread factory for netty
  thread-factory {
    boss-thread-prefix = "NettyBoss"
    worker-thread-prefix = "NettyServerNIOWorker"
    server-executor-thread-prefix = "NettyServerBizHandler"
    share-boss-worker = false
    client-selector-thread-prefix = "NettyClientSelector"
    client-selector-thread-size = 1
    client-worker-thread-prefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    boss-thread-size = 1
    #auto default pin or 8
    worker-thread-size = 8
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #vgroup->rgroup
  vgroup_mapping.my_test_tx_group = "default"
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}
client {
  #TC通知提交最大的数据缓存大小
  async.commit.buffer.limit = 10000
  lock {
    retry.internal = 10
    retry.times = 30
  }
  report.retry.count = 5
}
## transaction log store
store {
  ## store mode: file、db
  mode = "db"
## file store
  file {
    dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    max-branch-session-size = 16384
    # globe session size , if exceeded throws exceptions
    max-global-session-size = 512
    # file buffer size , if exceeded allocate new buffer
    file-write-buffer-cache-size = 16384
    # when recover batch read size
    session.reload.read_size = 100
    # async, sync
    flush-disk-mode = async
  }
## database store
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "mysql"
    password = "mysql"
    min-conn = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
}
lock {
  ## the lock store mode: local、remote
  mode = "remote"
local {
    ## store locks in user's database
  }
remote {
    ## store locks in the seata's server
  }
}
recovery {
  committing-retry-delay = 30
  asyn-committing-retry-delay = 30
  rollbacking-retry-delay = 30
  timeout-retry-delay = 30
}
transaction {
  undo.data.validation = true
  undo.log.serialization = "jackson"
}
## metrics settings
metrics {
  enabled = false
  registry-type = "compact"
  # multi exporters use comma divided
  exporter-list = "prometheus"
  exporter-prometheus-port = 9898
}

修改 fescar-business , fescar-item , fescar-order fescar-user 各大微服务的 application.yml 配置,配置通信指定的组名( my_test_tx_group 在 file.conf 中有):

  cloud:
    alibaba:
      seata:
        tx-service-group: my_test_tx_group

在 fescar-api 工程下面新建配置类

@Configuration
public class DataSourceProxyConfig {
    /**
     * 普通数据源
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return new DruidDataSource();
    }
    /**
     * 代理数据源绑定DataSourceProxy ---> undo_log的操作
     * @param dataSource
     * @return
     */
    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }
    /**
     * mybatis--->手动指定sqlSessionFactory所使用的代理数据源
     * @param dataSourceProxy
     * @return
     * @throws Exception
     */
    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        // 换成代理数据源
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        return sqlSessionFactoryBean.getObject();
    }
}

在数据库中创建seata数据库及库中的表加 undo_log 表

DROP TABLE undo_log;
CREATE TABLE `undo_log`(
    `id`            BIGINT(20)   NOT NULL AUTO_INCREMENT,
    `branch_id`     BIGINT(20)   NOT NULL,
    `xid`           VARCHAR(100) NOT NULL,
    `context`       VARCHAR(128) NOT NULL,
    `rollback_info` LONGBLOB     NOT NULL,
    `log_status`    INT(11)      NOT NULL,
    `log_created`   DATETIME     NOT NULL,
    `log_modified`  DATETIME     NOT NULL,
    `ext`           VARCHAR(100) DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8

修改registry.conf、file.conf和微服务修改的一样,但是含义不一样

注意:这里修改的registry.conf和file.conf是给全局事务seata使用的控制全局事务的global_branch的

​ 而微服务中的修改的registry.conf和file.conf各个微服务中的控制本地事务的branch_table的。

启动 seata-server

双击 D:\seata-server-0.8.0\bin\seata-server.bat

在入口方法上添加**@GlobalTransactional开启事务**

在每个微服务[fescar-item、fescar-order、fescar-user]的service层加@Transactional注解

Seata原理

分布式事务-1.png

执行流程

  1. TM开启分布式事务(TM向TC注册全局事务记录)
  2. 换业务场景,编排数据库,服务等事务内资源(RM向TC汇报资源准备状态)
  3. TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务)
  4. TC汇总事务信息,决定分布式事务是提交还是回滚
  5. TC通知所有RM提交/回滚资源,事务二阶段结束。

AT模式如何做到对业务的无侵入

一阶段加载

在第一阶段,Seata会拦截“业务SQL”,执行以下步骤:

  1. 解析SQL语义,找到“业务SQL”要更新的业务数据,在业务数据被更新前,保存成”before image“
  2. 执行”业务SQL“更新业务数据,在业务数据更新之后
  3. 将其保存成”after image“,最后生成行锁。

以上操作都在一个数据库事务内完成,保证了一阶段操作的原子性。

二阶段提交

二阶段如果顺利提交的话,因为业务SQL在第一阶段已经提交到数据库,所以Seata框架只需要将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

二阶段回滚

二阶段如果是回滚的话,Seata就需要回滚一阶段执行的”业务SQL“,还原业务数据。

回滚方式就是用”before image“还原业务数据;但在还原之前要首先校验脏写,对比”数据库当前业务数据“和”after image“,如果两份数据完全一致,就说明没有脏写,可以还原业务处理,如果不一致就说明有脏写,出现脏写需要转人工处理。