目录
案例准备
分布式事务
基本理论
CAP定理
BASE理论
Seata
部署TC服务
数据库准备
修改Nacos配置并导入信息
启动Seata
集成Seata
XA模式原理
Seata的XA实现
优点
缺点
实现
AT模式原理
AT模式的脏写问题
Seata的AT实现
XA与AT的区别
TCC模式原理
空回滚与业务悬挂问题
Seata的TCC实现
Saga模式原理
四种模式对比
高可用
案例资料下载地址:day02分布式事务
观察数据库
成功添加一个订单。
在分布式系统下,一个业务跨越多个服务或数据源,每个服务都是一个分支事务。要保证所有的分支事务最终状态保持一致,这样的事务就是分布式事务。
所谓CAP是指:
分布式系统无法同时满足这三个指标。当分区出现时系统的一致性和可用性无法同时满足。
一致性:用户访问分布式系统中的任意节点,得到的数据必须一致
可用性:用户访问集群中任意健康节点,必须得到相应,而不是超时或拒绝
分区容错:所谓分区,就是因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。而容错,就是在集群出现分区时,整个系统也要持续对外提供服务
BASE理论是对CAP的一种解决思路
而分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论:
比如ES集群就属于CP,当ES集群一个节点网络故障时,会被剔除,该节点的数据分片会被保存在其他节点上。保证了高一致性,低可用性。所以是CP。
Seata事务管理中存在三个角色:
Seata提供了四种不同的分布式事务解决方案:
下载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文件。
修改文件\seata\conf\application.yml
在Nacos创建配置文件
将路径\script\config-center\config.txt修改如下内容后全选粘贴到nacos中的配置内容处
双击启动bin目录下的seata-server.bat
访问地址http://localhost:7091/#/login
默认用户名与密码都为seata
nacos控制台可以看到seata的节点
引入依赖
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
io.seata
seata-spring-boot-starter
io.seata
seata-spring-boot-starter
1.4.2
配置文件添加如下内容
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的映射关系去寻找。
是强一致性的事务。基于数据库本身特性实现的
RM一阶段的工作:
TC二阶段的工作:
RM二阶段的工作:
修改配置文件。该配置作用是对数据源做代理,拦截所有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模式同样是分阶段提交事务模型。不过弥补了XA模型中资源锁定周期过长的缺陷。
阶段一RM工作:
阶段二提交时RM的工作:删除undo-log
阶段二回滚时RM的工作:根据undo-log恢复数据到更新前
出现脏写的情况是因为事务没有做到隔离,为了解决这个问题,引入了全局锁概念。TC记录当前正在操作某行数据的事务,该事务持有全局锁,具有执行权。主要记录的是事务id、事务操作的表名、以及该表的被修改的数据主键值。
需要注意的是。这里存在两个锁,一个是数据库的DB锁一个是Seata管理的全局锁。为了避免死锁问题,通常全局锁在等待300毫秒内还没有获取到锁就进行超时回滚。比DB锁超时时间要短很多。
由于全局锁只会对被Seata管理的事务生效,对普通事务不生效。因此,可能存在普通事务修改数据的可能,导致Seata管理的事务进行回滚时造成的脏写事件。对此,AT模式不光会保存修改前的数据快照(before-image),也会保存修改后的数据快照(after-image)。当进行回滚时,会对数据库当前的数据与修改后的数据库快照(after-image)进行对比。如果发现不一样,则说明在回滚之前期间有其他事务修改了数据。
将资料中的seata-at.sql文件以文本方式打开。里面有两张表的创建语句。将lock_table表(管理全局锁的表)创建语句在TC服务的数据库中执行。undo_log表(快照存放表,由RM管理)在微服务访问的数据库中创建。
修改配置文件中的事务模式为AT
seata:
data-source-proxy-mode: AT
重启服务后,发送一次库存不足的请求观察是否扣减余额和创建订单。
可以看到,执行了扣款操作,但是根据快照回滚并将快照信息删除。
XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源
XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚
XA模式强一致;AT模式最终一致
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
阶段一进行资源预留。阶段二不管是提交还是回滚,都是对自己预留部分的操作。不需要像XA模式加锁或是AT模式一样保存数据快照。在性能上比前两种模式更好一些。
TCC模式的优点:
TCC模式的缺点:
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚。
而对于已经空回滚的业务,如果后续阻塞的事务恢复,继续执行try,就永远不可能confire或cancel,这就是业务悬挂。应当阻止执行空回滚后的try操作,避免悬挂。
TCC实现资源冻结通常是新加一张表,被冻结的资源放在该表中,根据该表信息来执行Cancel
Try业务:
Confirm业务:根据xid删除account_freeze表的冻结记录
Cancel业务:
如何判断是否空回滚: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模式是Seata提供的长事务解决方案。也分为两个阶段
一阶段:直接提交本地事务
二阶段:成功什么也不用做,失败通过编写补偿业务进行回滚
由于TCC是通过预留资源实现业务提交或回滚,而Saga是直接对资源本身进行操作,因此不存在事务隔离性,有一定安全问题。
优点:
缺点:
通常不适用该模式,通常使用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中的配置信息
实现了动态切换集群