微服务实战(十三)初探微服务分布式事务 - Seata AT - Go语言中文社区

微服务实战(十三)初探微服务分布式事务 - Seata AT


本章主要内容

本章我们主要了解一下分布式事务的概念、目前市面上的解决方案、以及在微服务中如何实现分布式事务。

什么是事务,什么是ACID

首先,提到分布式事务,咱们得明白什么是事务(Transaction),百科的链接放这里咯,事务应该具有4个属性:原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)。这四个属性通常称为ACID特性

举例:我们把这件事看做一个事务:“张三有300元,李四有500元,张三转账100给李四,李四又转回50给张三,最后张三有250元,李四有550元” 。   

原始状态--张三有300元,李四有500元

操作--张三转账100给李四,李四又转回50给张三

结果--张三有250元,李四有550元

原子性 atomicity: 一个事务是一个不可分割的工作单位,事务中包括的操作要么都做,要么都不做。即“张三转账100给李四,李四又转回50给张三”这套操作,要么全做,要么不做。

一致性(consistency)务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。实际上跟原子性是同一回事,只不过从不同角度来看,一致性是从结果的角度出发,即如果“操作”发生,那么“结果”就是“张三有250元,李四有550元”,而如果不发生,则“结果”是 “张三有300元,李四有500元”,在这件事上,不允许出现其他“结果”。

隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。   在我们这个事务中,假如在做“张三转账100给李四,李四又转回50给张三” 这个过程中,突然有个王五给张三转了100元,那么就干扰了我们这个事务的结果,在数据库中,通过锁机制来让有资源冲突的事务不能并行,即王五的转账必须等我们当前这个事务执行完有结果后才能开始。

持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。 按照字面就很好理解了,我们对数据进行改变后,就保存了,除非有其他正常操作来改变这个值,不然这个值就永久不变了。

什么是分布式事务

 

  1. 刚性事务:遵循ACID原则,强一致性。
  2. 柔性事务:遵循BASE理论,最终一致性;与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。

分布式事务是指在分布式的环境下实现事务(目前主要是讲柔性事务),那什么是分布式环境呢?即跨服务器、跨数据库的环境,比如之前提到的示例,非分布式事务(本地事务)可以看成整个示例在同一个数据库里执行,而分布式环境下,可能不同用户的余额按照规则被放到不同的数据库里,可能交易服务器和账户服务器也不在同一个服务器中。

本地事务的实现逻辑是这样的: 库A   开启事务-->库A "张三转账100给李四" -->库A  提交/回滚事务

而到了分布式环境:(以XA方案举例说明分布式事务)

库A  开启事务-->库A 张三扣减100  -->库B 开启事务 --> 库B 李四增加100 

如果整个事务执行成功,  库A 提交事务  & 库B 提交事务

如果整个事务执行失败,  库A 回滚事务  & 库B 回滚事务

分布式事务解决方案

常见分布式事务解决方案
分布式事务的四种模式:AT、TCC、Saga、XA

AT 模式是一种无侵入的分布式事务解决方案,在单机数据库事务上扩展出对于分布式的支持,它的实现主要通过对执行SQL进行解析,把要操作的数据保存操作前操作后两个版本(每个库中),最终根据整个事务的成败,来确定最后的数据(每个库中)应该是怎样。

TCC模式是一种逻辑上的分布式事务,不单依附在数据库事务上,它需要每个事务参与者实现  Try、Confirm 和 Cancel 三个操作,即自行实现事务的准备、提交、回滚操作,然后TCC来从整体角度去统一调用每个事务参与者的 这三个 实现,从而达成分布式的事务。

Saga模式 是一种补偿协议,在 Saga 模式下,分布式事务内有多个参与者,每一个参与者都是一个冲正补偿服务,需要用户根据业务场景实现其正向操作和逆向回滚操作。Saga 模式适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁、长流程情况下可以保证性能。

XA模式是分布式强一致性的解决方案,但性能低而使用较少。需要有一个[全局]协调器,每一个数据库事务完成后,进行第一阶段预提交,并通知协调器,把结果给协调器。协调器等所有分支事务操作完成、都预提交后,进行第二步;第二步:协调器通知每个数据库进行逐个commit/rollback。

Seata 分布式事务框架

Seata 是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。

Seata 支持 4 种分布式事务解决方案,分别是 AT 模式、TCC 模式、Saga 模式和 XA 模式。

github  : 

 https://github.com/seata/seata

Seata 分布式事务实践和开源详解 :

https://www.sofastack.tech/blog/seata-distributed-transaction-deep-dive

 

SpringBoot 、Seata 的分布式事务(AT模式)

https://github.com/seata/seata-samples/tree/master/springboot-mybatis

官方已经提供了demo,我们直接跑起来,后面我再介绍如何接入SpringCloud+Feign的环境中。

工程搭建

demo中提供了完整的SQL脚本

 

我们用它创建3个数据库,每个数据库中有一个业务表和 undo_log 表

 

配置好每个工程的数据库链接信息:

 

启动seata服务端

从 https://github.com/seata/seata/releases,下载服务器软件包,将其解压缩。

linux
sh seata-server.sh -p 8091 -h 127.0.0.1 -m file

windows
直接运行 seata-server.bat

Usage: sh seata-server.sh(for linux and mac) or cmd seata-server.bat(for windows) [options]
  Options:
    --host, -h
      The host to bind.
      Default: 0.0.0.0
    --port, -p
      The port to listen.
      Default: 8091
    --storeMode, -m
      log store mode : file、db
      Default: file
    --help

e.g.

启动demo中的微服务

分别启动 (运行main方法)

/sbm-account-service/src/main/java/io/seata/samples/account/SpringbootMybatisAccountApplication.java

/sbm-business-service/src/main/java/io/seata/samples/business/SpringbootMybatisBusinessApplication.java

/sbm-order-service/src/main/java/io/seata/samples/order/SpringbootMybatisOrderApplication.java

/sbm-storage-service/src/main/java/io/seata/samples/storage/SpringbootMybatisStorageApplication.java

 

业务分析

首先,sbm-business-service工程中,提供下单的总接口。

接口上使用seata的 @GlobalTransactional 注解,来定义此方法内需要实现分布式事务。

package io.seata.samples.business.service;

import io.seata.core.context.RootContext;
import io.seata.samples.business.client.OrderClient;
import io.seata.samples.business.client.StorageClient;
import io.seata.spring.annotation.GlobalTransactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class BusinessService {

    private static final Logger LOGGER = LoggerFactory.getLogger(BusinessService.class);

    @Autowired
    private StorageClient storageClient;
    @Autowired
    private OrderClient orderClient;

    /**
     * 减库存,下订单
     *
     * @param userId
     * @param commodityCode
     * @param orderCount
     */
    @GlobalTransactional
    public void purchase(String userId, String commodityCode, int orderCount) {
        LOGGER.info("purchase begin ... xid: " + RootContext.getXID());
        storageClient.deduct(commodityCode, orderCount);
        orderClient.create(userId, commodityCode, orderCount);
    }
}

在purchase方法里,先后调用了 

//1、通过restTemplate远程调用 sbm-storage-service 微服务中的 deduct 方法

storageClient.deduct(commodityCode, orderCount);

//2、通过restTemplate远程调用 sbm-order-service 为服务中的create方法

orderClient.create(userId, commodityCode, orderCount);

 

而在 sbm-order-service 中的 create 方法,又远程调用了sbm-account-service 的 debit 接口

@Service
public class OrderService {

    @Autowired
    private AccountClient accountClient;
    @Autowired
    private OrderMapper orderMapper;

    public void create(String userId, String commodityCode, Integer count) {
        BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
        Order order = new Order();
        order.setUserId(userId);
        order.setCommodityCode(commodityCode);
        order.setCount(count);
        order.setMoney(orderMoney);

        orderMapper.insert(order);

        accountClient.debit(userId, orderMoney);

    }
}

 

代码就不全贴出来了,整个事务是这样的:

sbm-business-service

-->purchase开始【分布式事务开始】

---->sbm-storage-service  

-------->deduct【事务1-扣减库存,数据库:seata_storage

---->sbm-order-service

-------->create 【事务2-插入订单数据,数据库:seata_order】

------------>sbm-account-service

---------------->debit【事务3-扣减用户余额,数据库:seata_account】

-->purchase执行完毕【分布式事务结束】

 

先看一下当前数据库中的数据:

seata_account. account_tbl

用户余额表里 ,用户1001余额9984,  用户1002余额10000

seata_order.order_tbl

订单记录表,commodity_code是商品编号,count是库存,money为订单金额

seata_storage.storage_tbl

库存表,commodity_code是商品编号,count是库存

 

我们在浏览器中调用 http://localhost:8084/api/business/purchase/commit

 

然后看一下数据,按照执行顺序:

库存被扣减1

订单新增了一条

用户1001 余额被扣减了5

 

这是整个分布式事务最终被提交的情况。

接下来我们看一下分布式事务回滚的情况:

在sbm-account-service 中有一个埋点,当要扣减用户1002的余额的时候,会抛出一个异常。

@Service
public class AccountService {

    private static final String ERROR_USER_ID = "1002";

    @Autowired
    private AccountMapper accountMapper;

    public void debit(String userId, BigDecimal num) {
        Account account = accountMapper.selectByUserId(userId);
        account.setMoney(account.getMoney().subtract(num));
        accountMapper.updateById(account);

        if (ERROR_USER_ID.equals(userId)) {
            throw new RuntimeException("account branch exception");
        }
    }

}

 

当我们调用 http://localhost:8084/api/business/purchase/rollback 这个接口时,里面的用户ID就会传递 1002 来模拟账户服务中出现异常的情况。

我们的 sbm-business-service 服务中,也是抛出了 异常。

然后我们检查一下数据,可以发现虽然  扣减库存 ,插入订单数据 执行成功,但是由于 余额扣减 异常,所以整个事务就回滚了。

 

 

 

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/u011177064/article/details/104515463
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-06-29 21:07:02
  • 阅读 ( 2053 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢