目录
案例准备
分布式事务
基本理论
CAP定理
BASE理论
Seata
部署TC服务
数据库准备
修改Nacos配置并导入信息
启动Seata
集成Seata
XA模式原理
Seata的XA实现
优点
缺点
实现
AT模式原理
AT模式的脏写问题
Seata的AT实现
XA与AT的区别
TCC模式原理
空回滚与业务悬挂问题
Seata的TCC实现
Saga模式原理
四种模式对比
高可用
案例资料下载地址:day02分布式事务
案例准备
- 将资料中的seata-demo.sql文件加载到数据库中。
- seata-demo文件夹使用IDEA打开。
- 启动nacos与所有微服务
- 测试订单,发送POST请求
观察数据库
成功添加一个订单。
分布式事务
在分布式系统下,一个业务跨越多个服务或数据源,每个服务都是一个分支事务。要保证所有的分支事务最终状态保持一致,这样的事务就是分布式事务。
基本理论
CAP定理
所谓CAP是指:
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance(分区容错性)
分布式系统无法同时满足这三个指标。当分区出现时系统的一致性和可用性无法同时满足。
一致性:用户访问分布式系统中的任意节点,得到的数据必须一致
可用性:用户访问集群中任意健康节点,必须得到相应,而不是超时或拒绝
分区容错:所谓分区,就是因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。而容错,就是在集群出现分区时,整个系统也要持续对外提供服务
BASE理论
BASE理论是对CAP的一种解决思路
- Basically Available(基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用
- Soft state(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态
- Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
而分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论:
- AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
- CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
比如ES集群就属于CP,当ES集群一个节点网络故障时,会被剔除,该节点的数据分片会被保存在其他节点上。保证了高一致性,低可用性。所以是CP。
Seata
Seata事务管理中存在三个角色:
- TC(Transaction Coordinator)事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚
- TM(Transaction Manager)事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务
- RM(Resource Manager)资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
Seata提供了四种不同的分布式事务解决方案:
- XA模式:强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入
- TCC模式:最终一致的分阶段事务模式,有业务侵入
- AT模式:最终一致的分阶段事务模式,无业务侵入,也是Seata的默认模式
- SAGA模式:长事务模式,有业务侵入
部署TC服务
下载Seata:GitHub - seata/seata: :fire: Seata is an easy-to-use, high-performance, open source distributed transaction solution.
需要注意的是,最新版本与1.5.0之前的Seata配置方式不同,这里我采用的是1.7.1版本。资料中的版本也有对应的文本文件。
数据库准备
sql文件保存在该目录下\script\server\db。在数据库中执行sql文件。
修改Nacos配置并导入信息
修改文件\seata\conf\application.yml
在Nacos创建配置文件
将路径\script\config-center\config.txt修改如下内容后全选粘贴到nacos中的配置内容处
启动Seata
双击启动bin目录下的seata-server.bat
访问地址http://localhost:7091/#/login
默认用户名与密码都为seata
nacos控制台可以看到seata的节点
集成Seata
引入依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
配置文件添加如下内容
seata:
registry:
# nacos配置
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: DEFAULT_GROUP
namespace: seata-demo
username: nacos
password: nacos
tx-service-group: seata-demo #事务组名称
#如果seata是一个集群,那么在nacos寻找seata节点时,就要这么去配置
service:
vgroup-mapping: #事务组与cluster的映射关系
seata-demo: SH
seata客户端发现seata集群中的节点,需要tx-service-group中的值作为key,vgroup-mapping作为value的映射关系去寻找。
XA模式原理
是强一致性的事务。基于数据库本身特性实现的
Seata的XA实现
RM一阶段的工作:
- 注册分支事务到TC
- 执行分支业务sql但不提交
- 报告执行状态到TC
TC二阶段的工作:
- TC检测各分支事务执行状态
- 如果都成功,通知所有RM提交事务
- 如果有失败,通知所有RM回滚事务
RM二阶段的工作:
- 接收TC指令,提交或回滚事务
优点
- 强一致性
- 基于数据库做了一层封装,实现简单
缺点
- 如果关联的事务较多,且耗时较长,已经执行完毕的事务还需要等待其他事务完成,耗费资源更大,性能差。
- 依赖关系数据库实现,如果使用Redis则不适用
实现
修改配置文件。该配置作用是对数据源做代理,拦截所有sql请求,RM帮我们调用数据库的XA接口。
seata:
data-source-proxy-mode: XA
给全局事务的入口方法添加注解@GlobalTransactional注解(和Transactional注解添加位置一样)
@GlobalTransactional
public Long create(Order order) {
// 创建订单
orderMapper.insert(order);
try {
// 扣用户余额
accountClient.deduct(order.getUserId(), order.getMoney());
// 扣库存
storageClient.deduct(order.getCommodityCode(), order.getCount());
} catch (FeignException e) {
log.error("下单失败,原因:{}", e.contentUTF8(), e);
throw new RuntimeException(e.contentUTF8(), e);
}
return order.getId();
}
接下来测试一个一定失败的新增订单请求
查看数据库发现,库存没有减少,用户金额也没扣减,订单也没增加。观察控制台输出,是扣款之后再回滚
AT模式原理
AT模式同样是分阶段提交事务模型。不过弥补了XA模型中资源锁定周期过长的缺陷。
阶段一RM工作:
- 注册分支事务
- 记录undo-log(快照)
- 执行sql并提交
- 报告事务状态
阶段二提交时RM的工作:删除undo-log
阶段二回滚时RM的工作:根据undo-log恢复数据到更新前
AT模式的脏写问题
出现脏写的情况是因为事务没有做到隔离,为了解决这个问题,引入了全局锁概念。TC记录当前正在操作某行数据的事务,该事务持有全局锁,具有执行权。主要记录的是事务id、事务操作的表名、以及该表的被修改的数据主键值。
需要注意的是。这里存在两个锁,一个是数据库的DB锁一个是Seata管理的全局锁。为了避免死锁问题,通常全局锁在等待300毫秒内还没有获取到锁就进行超时回滚。比DB锁超时时间要短很多。
由于全局锁只会对被Seata管理的事务生效,对普通事务不生效。因此,可能存在普通事务修改数据的可能,导致Seata管理的事务进行回滚时造成的脏写事件。对此,AT模式不光会保存修改前的数据快照(before-image),也会保存修改后的数据快照(after-image)。当进行回滚时,会对数据库当前的数据与修改后的数据库快照(after-image)进行对比。如果发现不一样,则说明在回滚之前期间有其他事务修改了数据。
Seata的AT实现
将资料中的seata-at.sql文件以文本方式打开。里面有两张表的创建语句。将lock_table表(管理全局锁的表)创建语句在TC服务的数据库中执行。undo_log表(快照存放表,由RM管理)在微服务访问的数据库中创建。
修改配置文件中的事务模式为AT
seata:
data-source-proxy-mode: AT
重启服务后,发送一次库存不足的请求观察是否扣减余额和创建订单。
可以看到,执行了扣款操作,但是根据快照回滚并将快照信息删除。
XA与AT的区别
XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源
XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚
XA模式强一致;AT模式最终一致
TCC模式原理
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
- Try:资源的检测和预留
- Confirm:完成资源操作业务;要求Try 成功 Confirm 一定要能成功。
- Cancel:预留资源释放,可以理解为try的反向操作。
阶段一进行资源预留。阶段二不管是提交还是回滚,都是对自己预留部分的操作。不需要像XA模式加锁或是AT模式一样保存数据快照。在性能上比前两种模式更好一些。
TCC模式的优点:
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 相比AT模型,无需生成快照,无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC模式的缺点:
- 有代码侵入,需要人为编写try、Confirm和Cancel接口,麻烦
- 软状态,事务是最终一致
- 需要考虑Confirm和Cancel的失败情况,做好幂等处理
空回滚与业务悬挂问题
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚。
而对于已经空回滚的业务,如果后续阻塞的事务恢复,继续执行try,就永远不可能confire或cancel,这就是业务悬挂。应当阻止执行空回滚后的try操作,避免悬挂。
Seata的TCC实现
TCC实现资源冻结通常是新加一张表,被冻结的资源放在该表中,根据该表信息来执行Cancel
Try业务:
- 记录冻结金额和事务状态到account_freeze表
- 扣减account表可用金额
Confirm业务:根据xid删除account_freeze表的冻结记录
Cancel业务:
- 修改account_freeze表冻结金额为0,state为2
- 修改account表,恢复可用金额
如何判断是否空回滚:cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚。
如何避免业务悬挂:try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务
将资料中的account_freeze_tbl.sql文件在微服务访问的数据库执行。
编写Java业务代码
@LocalTCC
public interface AccountTCCService {
//该注解在哪个方法上就说明哪个方法为try方法,name值要和方法名一样
@TwoPhaseBusinessAction(name="deduct",commitMethod = "confirm",rollbackMethod = "cancel")
void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,//参数中的注解会被加载到BusinessActionContext中。
@BusinessActionContextParameter(paramName = "money") int money);
boolean confirm(BusinessActionContext ctx);
boolean cancel(BusinessActionContext ctx);
}
@Service
public class AccountTCCServiceImpl implements AccountTCCService {
@Autowired
private AccountMapper accountMapper;
@Autowired
private AccountFreezeMapper accountFreezeMapper;
@Override
public void deduct(String userId, int money) {
//获取全局事务ID
String xid = RootContext.getXID();
AccountFreeze accountFreeze = accountFreezeMapper.selectById(xid);
if (accountFreeze!=null){
//已经执行过Cancel,不去执行
return;
}
//扣减用户余额
accountMapper.deduct(userId, money);
//冻结表新增余额
accountFreeze = new AccountFreeze();
accountFreeze.setXid(xid);
accountFreeze.setUserId(userId);
accountFreeze.setFreezeMoney(money);
accountFreeze.setState(AccountFreeze.State.TRY);
accountFreezeMapper.insert(accountFreeze);
}
@Override
public boolean confirm(BusinessActionContext ctx) {
//提交事务,删除该表全局事务对应的数据就可以
String xid = ctx.getXid();
int count = accountFreezeMapper.deleteById(xid);
return count == 1;
}
@Override
public boolean cancel(BusinessActionContext ctx) {
//判断是否为空回滚
String xid = ctx.getXid();
String userId = ctx.getActionContext("userId").toString();
int money = (int) ctx.getActionContext("money");
AccountFreeze accountFreeze = accountFreezeMapper.selectById(xid);
if (accountFreeze == null) {
//是空回滚
accountFreeze = new AccountFreeze();
accountFreeze.setXid(xid);
accountFreeze.setUserId(userId);
accountFreeze.setFreezeMoney(money);
accountFreeze.setState(AccountFreeze.State.CANCEL);
accountFreezeMapper.insert(accountFreeze);
return true;
}
// 幂等处理
if (accountFreeze.getState() == AccountFreeze.State.CANCEL) {
return true;
}
//说明不是空回滚,恢复数据
accountMapper.refund(userId, money);
accountFreeze.setState(AccountFreeze.State.CANCEL);
accountFreeze.setFreezeMoney(0);
int count = accountFreezeMapper.updateById(accountFreeze);
return count == 1;
}
}
修改Controller代码,装配TCCService的bean对象。重启服务,再次发送一次库存不足请求
Saga模式原理
Saga模式是Seata提供的长事务解决方案。也分为两个阶段
一阶段:直接提交本地事务
二阶段:成功什么也不用做,失败通过编写补偿业务进行回滚
由于TCC是通过预留资源实现业务提交或回滚,而Saga是直接对资源本身进行操作,因此不存在事务隔离性,有一定安全问题。
优点:
- 事务参与者可以基于事件驱动实现异步调用,吞吐高
- 一阶段直接提交事务,无锁,性能好
- 不同编写TCC中的三个阶段
缺点:
- 软状态持续时间不确定,时效性差
- 没有锁,没有事务隔离,会存在脏写问题
通常不适用该模式,通常使用AT模式,使用TCC或XA做补充
四种模式对比
XA | AT | TCC | SAGA | |
一致性 | 强一致 | 弱一致 | 弱一致 | 最终一致 |
隔离性 | 完全隔离 | 基于全局锁隔离 | 基于资源预留隔离 | 无隔离 |
代码入侵 | 无 | 无 | 要编写三个接口 | 要编写状态机和补偿业务 |
性能 | 差 | 好 | 很好 | 很好 |
场景 | 对一致性、隔离性有高要求的业务 | 基于关系型数据库的大多数分布式场景都可以 | 对性能要求较高的事务。有非关系型数据库要参与的事务 | 业务流程长,业务流程多。参与者包含其他公司或遗留系统服务,无法提供TCC模式要求的三个接口 |
高可用
TC服务作为Seata的核心服务,一定要保证高可用和异地容灾。接下来我们对Seata进行集群配置
将原来seata文件复制一份作为第二个节点
修改第二份文件集群名称。
接着启动2号节点
seata-server.bat -p 8092
接下来,我们需要将tx-service-group与cluster的映射关系都配置到nacos配置中心,方便生产环境下实现热更新部署。
微服务读取nacos配置文件
seata:
config:
type: nacos
nacos:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
group: SEATA_GROUP
data-id: client.properties
启动微服务观察
所有服务都注册到SH集群节点上。
修改nacos中的配置信息
实现了动态切换集群