社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
本章我们主要了解一下分布式事务的概念、目前市面上的解决方案、以及在微服务中如何实现分布式事务。
首先,提到分布式事务,咱们得明白什么是事务(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),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。 按照字面就很好理解了,我们对数据进行改变后,就保存了,除非有其他正常操作来改变这个值,不然这个值就永久不变了。
分布式事务是指在分布式的环境下实现事务(目前主要是讲柔性事务),那什么是分布式环境呢?即跨服务器、跨数据库的环境,比如之前提到的示例,非分布式事务(本地事务)可以看成整个示例在同一个数据库里执行,而分布式环境下,可能不同用户的余额按照规则被放到不同的数据库里,可能交易服务器和账户服务器也不在同一个服务器中。
本地事务的实现逻辑是这样的: 库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 是 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
https://github.com/seata/seata-samples/tree/master/springboot-mybatis
官方已经提供了demo,我们直接跑起来,后面我再介绍如何接入SpringCloud+Feign的环境中。
demo中提供了完整的SQL脚本
我们用它创建3个数据库,每个数据库中有一个业务表和 undo_log 表
配置好每个工程的数据库链接信息:
从 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.
分别启动 (运行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 服务中,也是抛出了 异常。
然后我们检查一下数据,可以发现虽然 扣减库存 ,插入订单数据 执行成功,但是由于 余额扣减 异常,所以整个事务就回滚了。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!