Seata(原Fescar)是阿里巴巴今年1月刚开源的分布式事务框架,提供了AT、TCC、Saga和XA四种事务模式,致力于解决微服务架构下的分布式事务问题。开源一个多月,已经在社区引起了很大的反响,很多公司都在评估和试用。
最近在项目里深度使用了Seata,主要用的是AT模式,踩了不少坑,也总结了一些进阶技巧和最佳实践。官方文档和入门教程已经很多了,今天就不重复基础内容了,直接分享一些进阶的技巧和注意事项,这些是我在实际使用中发现的、很多人可能不知道的点,希望能帮助正在使用或准备使用Seata的朋友少走弯路。
先说明一下,本文基于Seata 0.5.x版本,AT模式为主,其他模式只简单提及。因为Seata还在快速迭代中,不同版本的API和配置可能会有差异,使用的时候请以官方文档为准。
一、AT模式的进阶技巧
AT模式是Seata最常用的模式,也是对业务代码侵入最小的模式。它通过二阶段提交的方式,自动生成回滚日志,实现分布式事务。大部分人用AT模式就是加个@GlobalTransactional注解就完事了,但其实AT模式有很多进阶的用法和注意事项。
1. 合理设置全局事务的超时时间
很多人不知道,Seata的全局事务是有超时时间的,默认是60秒。如果一个全局事务执行时间超过了超时时间,TC(事务协调器)会主动回滚这个事务,不管它是不是还在执行。
这个默认值在大部分场景下是够用的,但如果你的业务流程比较长,比如涉及到多个服务调用、有耗时的数据库操作、或者有外部接口调用,60秒可能不够,就会出现事务被意外回滚的问题。
解决方法是在@GlobalTransactional注解里设置timeoutMills参数,根据业务的实际情况设置合理的超时时间:
@GlobalTransactional(timeoutMills = 300000) // 5分钟超时
public void placeOrder(OrderDTO order) {
// 业务逻辑
}设置超时时间的时候要注意,不要设置得太长,否则如果事务真的卡住了,会长时间占用数据库连接和锁资源,影响系统性能。建议根据业务的最大预期执行时间,再留一些余量,设置一个合理的值。
2. 注意全局锁和本地锁的区别
Seata AT模式下,有两种锁:全局锁和本地锁。本地锁是数据库层面的行锁,在一阶段提交的时候就释放了;全局锁是Seata TC层面的锁,要等到整个全局事务提交或回滚之后才释放。
这个区别很重要,很多人搞不清楚,导致出现一些奇怪的问题。比如,一个全局事务修改了某条数据,一阶段提交之后,本地锁已经释放了,其他事务可以读取这条数据(读未提交隔离级别下),但不能修改这条数据,因为全局锁还没释放。如果其他事务试图修改这条数据,就会等待全局锁释放,等待超时之后就会报错。
理解了这个区别,就能解释很多现象。比如,为什么全局事务提交之后,其他事务才能修改同一条数据?为什么有时候会出现"获取全局锁超时"的错误?这些都是全局锁机制导致的。
在实际使用中,要注意尽量缩短全局事务的执行时间,减少全局锁的持有时间,避免影响其他事务的并发执行。同时,对于热点数据的修改,要特别注意,因为热点数据的全局锁竞争会比较激烈,容易导致超时和性能问题。
3. 合理使用@GlobalLock注解
除了@GlobalTransactional,Seata还提供了@GlobalLock注解,这个注解很多人不知道,也不知道什么时候用。
@GlobalLock的作用是:只获取全局锁,不开启全局事务。也就是说,被@GlobalLock修饰的方法,会检查和获取对应数据的全局锁,但不会注册全局事务,也不会生成回滚日志。
什么时候用@GlobalLock呢?主要用于那些不需要分布式事务,但需要和全局事务互斥的场景。比如,一个批量更新的任务,不需要和其他服务组成分布式事务,但它修改的数据可能正在被某个全局事务修改,这时候就需要用@GlobalLock来获取全局锁,避免和全局事务冲突。
举个例子:
@GlobalLock
public void batchUpdateStatus(List<Long> ids) {
// 批量更新,不需要分布式事务,但需要获取全局锁
orderMapper.batchUpdateStatus(ids);
}如果不用@GlobalLock,这个批量更新可能会和正在进行的全局事务冲突,导致更新失败或者数据不一致。用了@GlobalLock之后,它会等待全局锁释放,再执行更新,保证了数据的一致性。
4. 注意SQL的类型,避免全表扫描
Seata AT模式需要解析SQL,生成前后镜像,来实现回滚。这个过程对SQL的类型有一定的要求,如果SQL写得不好,可能会导致解析失败、性能问题,甚至数据不一致。
最常见的问题是update语句没有带主键或者唯一索引,导致Seata需要全表扫描来获取前后镜像,性能很差。比如:
-- 不好的写法:没有主键条件,全表扫描
UPDATE order SET status = 1 WHERE user_id = 123;
-- 好的写法:带主键条件
UPDATE order SET status = 1 WHERE id = 456;如果update语句的条件不是主键或唯一索引,Seata在生成前镜像的时候,需要根据条件查询所有匹配的行,如果匹配的行很多,就会有性能问题。而且如果条件太复杂,Seata的SQL解析器可能解析不了,导致报错。
所以在使用AT模式的时候,要尽量保证update和delete语句的条件是主键或唯一索引,避免全表扫描和复杂条件。如果确实需要批量更新,建议先查询出主键列表,再根据主键逐条更新,或者用@GlobalLock配合普通事务来处理。
5. 注意事务的隔离级别
Seata AT模式默认的隔离级别是读未提交(Read Uncommitted),也就是说,在一个全局事务的一阶段提交之后、二阶段完成之前,其他事务可以读到这个事务未提交的数据。如果这个全局事务最终回滚了,其他事务读到的就是脏数据。
这是Seata为了性能做的权衡,因为如果要实现读已提交,就需要在读取的时候检查全局锁,会影响并发性能。但对于一些对数据一致性要求很高的场景,读未提交可能会有问题。
如果需要读已提交的隔离级别,可以在查询的时候用SELECT ... FOR UPDATE,这样会获取全局锁,等待全局事务完成之后再读取,保证读到的是已提交的数据。但这样会降低并发性能,要根据业务场景权衡使用。
另外,Seata也在开发更高隔离级别的支持,后续版本可能会有更好的解决方案,关注官方动态即可。
二、TC(事务协调器)的部署和优化
TC是Seata的核心组件,负责全局事务的协调和管理。TC的部署和优化直接影响整个分布式事务系统的性能和稳定性。
1. TC的高可用部署
生产环境中,TC不能是单点,否则TC挂了,所有分布式事务都无法进行。Seata支持TC的集群部署,多个TC实例组成一个集群,通过注册中心(比如Nacos、Eureka)实现服务发现和负载均衡。
部署TC集群的时候,要注意以下几点:
- 多个TC实例要部署在不同的机器上,避免单机故障导致整个集群不可用。
- TC的事务日志存储(事务日志、锁信息等)要使用共享存储,比如数据库或者Redis,保证多个TC实例能共享数据。默认的文件存储不支持集群,因为每个实例的数据是独立的。
- 客户端(RM和TM)要配置TC的集群名称,通过注册中心发现TC实例,实现负载均衡和故障转移。不要硬编码TC的地址,否则TC实例变化的时候客户端无法感知。
2. TC的参数调优
TC有很多可配置的参数,合理调优能提升性能和稳定性。几个比较重要的参数:
- service.session.timeout:全局事务的默认超时时间,默认60秒。根据业务情况调整。
- service.session.check.interval:超时事务检查的间隔时间,默认1秒。可以适当调大,减少检查的频率,降低TC的负载。
- store.db.datasource:事务日志存储的数据源配置。如果用数据库存储,要配置合理的连接池大小,避免连接不够用。
- server.recovery.committing-retry-period:提交重试的间隔时间。如果二阶段提交失败,TC会重试,合理设置重试间隔,避免频繁重试影响性能。
这些参数的具体含义和默认值可以参考官方文档,根据自己的业务场景和压测结果来调整。不要盲目调优,先理解每个参数的作用,再根据实际情况调整。
3. TC的监控和告警
TC作为核心组件,一定要做好监控和告警。需要监控的指标包括:
- TC的存活状态和健康检查
- 全局事务的数量(提交数、回滚数、进行中数)
- 全局事务的平均执行时间和超时率
- TC的CPU、内存、磁盘使用率
- 事务日志存储的使用情况
- 锁等待和锁超时的数量
通过监控这些指标,可以及时发现TC的性能问题和故障,提前处理,避免影响业务。告警阈值要根据实际情况设置,不要太灵敏导致告警轰炸,也不要太迟钝导致故障发现不及时。
三、常见问题和排查技巧
使用Seata的过程中,会遇到各种各样的问题,这里分享一些常见问题的排查技巧。
1. 事务回滚了,但数据没回滚
这是最常见的问题之一。全局事务显示回滚了,但数据库里的数据还是修改后的状态,没有回滚。可能的原因有:
- 回滚日志丢失:如果TC的事务日志存储出了问题,或者RM的回滚日志表(undolog)被误删了,就会导致回滚的时候找不到回滚日志,无法回滚。要检查undolog表里是否有对应的回滚记录,TC的事务日志是否完整。
- 数据被其他事务修改了:如果全局事务一阶段提交之后,在二阶段回滚之前,数据被其他事务修改了,回滚的时候会发现前后镜像不一致,回滚失败。这种情况需要人工介入处理,Seata会把回滚失败的事务记录下来,需要人工排查和修复。
- 回滚SQL执行失败:如果回滚SQL因为某种原因执行失败(比如表结构变了、字段不存在了),也会导致回滚失败。要检查数据库的错误日志,看回滚SQL执行的时候有没有报错。
排查这个问题的步骤是:先看TC的日志,确认事务是不是真的回滚了;再看RM的日志,看回滚的时候有没有报错;然后检查undo_log表,看回滚日志是否存在;最后检查数据库的数据,看是不是被其他事务修改过。
2. 获取全局锁超时
这个问题也很常见,报错信息大概是"get global lock timeout"。意思是,一个事务试图修改某条数据,但这条数据的全局锁被其他全局事务持有了,等待超时之后就报错了。
可能的原因和解决方法:
- 全局事务执行时间太长:如果全局事务持有全局锁的时间太长,其他事务等待的时间就会变长,容易超时。要优化全局事务的执行时间,减少不必要的操作,把非事务性的操作移到事务外面。
- 热点数据竞争激烈:如果很多事务同时修改同一条热点数据,全局锁的竞争就会很激烈,容易超时。可以考虑对热点数据做分库分表,或者用队列串行化处理,减少并发冲突。
- TC的性能瓶颈:如果TC的负载太高,处理锁请求的速度变慢,也会导致锁等待超时。要监控TC的性能指标,必要的时候扩容TC集群。
- 全局锁的等待时间设置太短:Seata的全局锁等待超时时间默认是60秒,可以根据业务情况适当调大。但不要调得太大,否则会导致事务长时间等待,影响系统响应时间。
3. 事务悬挂和空回滚
这两个是TCC模式下常见的问题,AT模式下不太常见,但如果用了TCC模式,需要特别注意。
事务悬挂是指:二阶段的回滚请求比一阶段的try请求先到达,导致回滚执行了,但try还没执行,之后try执行了,资源被占用,无法释放。空回滚是指:一阶段的try没有执行,但二阶段的回滚执行了,导致回滚的时候没有资源可以回滚。
这两个问题的解决方法是在TCC的三个方法里做状态控制,用一张事务状态表来记录每个分支事务的状态,try之前检查状态,回滚和提交的时候也检查状态,避免悬挂和空回滚。具体的实现方式可以参考Seata的TCC模式文档和示例。
四、一些最佳实践
最后,总结一些使用Seata的最佳实践,供大家参考。
- 优先用AT模式,特殊场景用TCC或Saga。AT模式对业务代码侵入最小,开发效率最高,优先使用。对于一些特殊场景,比如涉及到非关系型数据库、或者需要自定义补偿逻辑的,可以用TCC模式。对于长事务、跨多个业务流程的,可以用Saga模式。
- 尽量缩短全局事务的执行时间。全局事务的执行时间越短,全局锁的持有时间就越短,并发性能就越好,出问题的概率也越低。把非事务性的操作(比如发送消息、写日志、调用非核心接口)移到全局事务外面,只把需要保证一致性的数据库操作放在事务里。
- 避免在全局事务里做远程调用的重试。如果在全局事务里调用远程服务,不要在业务代码里做重试,因为重试可能会导致重复执行,影响数据一致性。如果需要重试,可以用Seata的重试机制,或者把重试逻辑放在全局事务外面。
- 做好异常处理和事务回滚。全局事务里的异常要正确抛出,不要catch了异常不抛出,否则Seata感知不到异常,不会回滚事务。如果需要catch异常做处理,处理完之后要重新抛出,或者手动触发回滚。
- 做好压测和容量规划。在上线之前,一定要对Seata的性能做压测,了解TC和RM的性能瓶颈,做好容量规划。不要等到上线之后才发现性能不够,影响业务。
- 关注官方版本更新。Seata还在快速迭代中,每个版本都会修复很多bug,增加很多新功能。建议关注官方的版本更新,及时升级到稳定版本,避免已知的bug。
五、写在最后
Seata作为一个刚开源不久的分布式事务框架,虽然还不够成熟,还有一些bug和不完善的地方,但它的设计理念和功能完整性已经相当不错了,是目前微服务架构下解决分布式事务问题的一个很好的选择。
这篇文章分享了我在实际使用Seata过程中总结的一些进阶技巧和注意事项,可能不够全面,也可能有不对的地方,欢迎大家指正。分布式事务是一个复杂的话题,没有银弹,需要根据业务场景选择合适的方案和模式,也需要在实践中不断踩坑、不断总结、不断优化。
希望这篇文章能给正在使用或准备使用Seata的朋友一些帮助,也希望Seata能越来越好,成为分布式事务领域的标杆项目。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录