前言
众所周知,分布式事务是个复杂的问题,有很多种不同的思路和方法。
目前,只要是微服务架构做的分布式系统,就绕不开分布式事务这个话题。当然,并不是使用了分布式事务解决方案服务就稳定,高大上,不使用分布式事务服务也并不一定不好。目前我们公司就没有使用分布式事务,一样撑起了稳定,灵活的系统。分布式事务的选择与否没有对错,在于真实项目中权衡利弊后的抉择。
分布式事务发展来源
最开始我们的应用是单体服务,在使用得当的情况下,spring的事务可以很好的帮我们解决相关事务问题。简单分析spring的事务:当某个代理方法使用 @Transactiona
并被AOP切面切到以后。spring会给aop切面中使用ThreadLocal管理同一个DBConnection。由这一个connection替我们管理事务:即整个方法执行结束(活异常)则一起提交或者回滚事务。此时我们的应用如下图:
或者这样
但是随着项目流量的增长,同一个DB连接肯定承接不住各类服务数据,因此,在实际的项目中,我们大多数的应用与DB关系是这样的:
我们知道,spring事务是在一个jvm下通过同一个db连接控制的事务,但是当有多台服务器的时候,不同的服务器有自己的jvm,即使远程调用,db-connection肯定不是同一个。由此,我们需要一个第三者去协调每个服务的数据连接。便产生了我们常说的分布式事务问题。
准备知识点
ACID
原子性(Atomic) 一致性(Consistency) 隔离性(Isolation) 持久性(Durability)
隔离级别
Read Uncommitted(读未提交) Read Committed(读已提交) Repeated Read(重复读) Serialization(串行化)
CAP定律
一致性(C) 在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本) 可用性(A) 在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性) 分区容错性(P) 以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
- BASE理论
BASE是Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结, 是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
基本可用
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性—-注意,这绝不等价于系统不可用。比如:
(1)响应时间上的损失。正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒
(2)系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面
软状态
软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
最终一致性
最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
开源的分布式事务框架
目前世面上用于开源的分布式事务问题的框架还不少,比如seata,lcn,使用事务消息rocketmq等等。而每种框架下面支持的模式又有很多种(可以想想为什么每种框架都支持好几种模式)。今天我们详细解读一下seata的无代码侵入式的AT模式。
首先通过一个简单的sata例子认识一下
============user模块=============
/**
* @author liuliang
* @date 2021/7/9 2:31 下午
*/
@Slf4j
@Service
public class UserService extends ServiceImpl<UserMapper, UserInfo> {
@Resource
private OrderFeign orderFeign;
@GlobalTransactional
public void buyProduct() {
UserInfo userInfo = this.getById(1L);
userInfo.setAmount(userInfo.getAmount() - 1);
//扣减自己的余额
this.updateById(userInfo);
//创建订单
orderFeign.test();
log.info("购买结束");
}
}
user模块调用order模块
/**
* @author liuliang
* @date 2021/7/9 2:58 下午
*/
@FeignClient(name = "mm-order")
public interface OrderFeign {
@GetMapping("/order/test")
void test();
}
order模块
/**
* @author liuliang
* @date 2021/7/9 3:05 下午
*/
@Slf4j
@Service
public class OrderService extends ServiceImpl<OrderMapper, OrderInfo> {
public void test() {
OrderInfo orderInfo = new OrderInfo();
orderInfo.setNum(1);
orderInfo.setPrice(100L);
orderInfo.setSkuName("小视频");
this.save(orderInfo);
log.info("小视频数据已保存");
// throw new RuntimeException("111");
}
}
可以看到,当user模块加入了seata的@GlobalTransactional
注解后,当order模块异常,user会回滚,属于两个不同的jvm的不同代理连接居然能在同一个事务里面!接下来我们分析一下seata的原理
- seata框架
可以看到,seata有三个组件:
1. TM(Transaction Manager)事务管理者
2. RM(Resource Manager) 数据源管理者
3. TC(Transaction Coordinator)事务协调器
通常,一个分布式事务的流程可以分析为以下步骤:
1. TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
2. XID 在微服务调用链路的上下文中传播。
3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
4. TM 向 TC 发起针对 XID 的全局提交或回滚决议。
5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求。
在具体分析seata执行流程之前,先将简单的demo分别演示:
user调用order前异常
user调用order时异常
user调用order后异常(order处打断点)
user调用order后正常
几种情况。
接下来我们通过问题的形式,来逐步解开疑惑:
1.undo_log是什么,是什么时候产生的?又是什么时候消失?t1回滚时,本地锁被t2持有,全局锁被t1持有,此时会死锁吗?
io.seata.rm.datasource.undo.mysql.MySQLUndoLogManager#insertUndoLogWithNormal
io.seata.tm.api.TransactionalTemplate#execute
io.seata.core.rpc.netty.RmMessageListener#onMessage
经过阅读源码发现,seata在数据源代理层,在本地事物提交之前,向undo_log表插入修改前和修改后的记录。
在切面最后,如果事务正常,则触发全局提交,全局提交时,tm通知tc,tc查到各个分支事务,通知删除undo_log数据
如果事务异常,则出发全局回滚,全局回滚是,tm通知tc,tc查到各个分支事务,通知各个分支回滚到执行前的数据,然后删除undlog
不会死锁,t2获取全局锁的时间可配置,默认6s,超时自动放弃获取全局锁并释放本地锁
2.会存在undo_log与本地事物不一致的情况吗?
不会,本地事物里,业务逻辑和undo_log在同一个事务里面
3.如果一条数据在执行的过程中被另一个事务修改了,那数据就错乱码了吗?(或者说隔离性)
写/读隔离,解释全局锁,本地锁
4.为什么seata或者说lcn会有多种模式去处理分布式事务?
能力边界,seata会需要原生数据库支持事务,并且有主键
总结下seata的AT 模式:
Seata 的 AT 模式基于本地事务的特性,通过拦截并解析 SQL 的方式,记录自定义的回滚日志,从而打破 XA 协议阻塞性的制约,在一致性、性能、易用性 3 个方面取得平衡:在达到确定一致性(非最终一致)的前提下,即保障较高的性能,又能完全不侵入业务。
在绝大部分应用场景下,Seata 的 AT 模式都能很好地发挥作用,把应用的分布式事务支持成本降到极低的水平。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。