@Transactional事务是真的好用吗

事务管理在系统开发中举足轻重,Spring提供了精妙细腻的事务管理机制,主要分为编程式事务和声明式事务两大架构。

关于事务的根本概念,包括事务的本质、数据库中的事务特性以及Spring事务的ACID属性、隔离级别、传播规则和行为方式等,本文将不做深入探讨。我也相信读者对此有一定的了解。

笔者将以简洁方式阐述声明式事务和编程式事务的概念,随后探讨笔者不推崇使用声明式事务的理由。

编程式事务

借助底层API,如PlatformTransactionManager、TransactionDefinition和TransactionTemplate等核心接口,开发者能够以编程的方式精准地进行事务管理。

在编程式事务模式中,开发者需在代码中手动管理事务的启动、提交和回滚等操作。

伪代码:

java 复制代码
public void test() {
    TransactionDefinition definition = new DefaultTransactionDefinition();
    TransactionStatus status = transactionManager.getTransaction(definition);

    try {
        // 执行事务操作
        // 提交事务
        transactionManager.commit(status);
    } catch (DataAccessException e) {
        // 回滚事务
        transactionManager.rollback(status);
        throw e;
    }
}

如以上代码,开发者可以通过API自己控制事务。

声明式事务

声明式事务管理方式允许开发者在配置的指引下进行事务管理,无需直接操作底层API进行硬编码。开发者可以通过注解或基于配置的XML来便捷地管理事务。

java 复制代码
@Transactional
public void test() {
    // 执行事务操作  
}

如上使用@Transactional注解即可为test方法添加事务控制。

当然,以上代码只是简化的版本,实际使用事务还需要进行一些配置。这里不展开详细说明。

这两种事务管理方式各有优缺点,所适用的场景也各有不同。为什么有人会拒绝使用声明式事务呢?

声明式事务的优点

通过上面的例子,我们很容易看出声明式事务的优点:它帮助我们节省大量代码,自动处理事务启动、提交和回滚等操作,使开发人员摆脱繁琐的事务管理工作。

声明式事务管理是通过AOP实现的,其本质是在目标方法执行前后进行拦截。在执行方法之前创建或加入一个事务,在方法执行结束后根据情况选择提交或回滚事务。

这种方式不会对代码造成侵入性,方法内只需编写业务逻辑即可。

然而,是否声明式事务就一定完美无缺呢?未必如此。

声明式事务的粒度问题

首先,声明式事务存在一个限制,即其最小作用粒度应为方法级别

换言之,若想向某段代码块添加事务,就需要将该代码块独立出来作为一个独立方法。

然而,正是由于这个粒度问题,我个人并不赞成过度使用声明式事务。注意是不建议过度使用,是过度使用

首先,由于声明式事务通常是通过注解或配置实现的,这可能导致一个问题,即开发者有可能忽略了该事务。

事务被忽略会带来什么问题呢?

首先,如果开发者未注意到某个方法被包裹在事务中,就可能在方法内执行诸如RPC远程调用、消息发送、缓存更新、文件写入等操作。

我们知道,这些操作本身无法回滚,这会导致数据不一致。

  • 例如,RPC调用成功但本地事务回滚,此时RPC调用无法回滚。
  • 其次,在事务中存在远程调用将延长整个事务周期。若这种操作过于频繁,可能导致数据库连接池耗尽。
  • 有时,即使没有远程操作,某些人可能会不经意地进行一些内存操作或运算。或者在分库分表情况下,可能会意外执行跨库操作。

相比之下,如果使用编程式事务,业务代码将清晰表示何处启动、提交和回滚事务。这样,修改代码时,开发人员将被迫考虑所添加代码是否应该处于事务内。

有人或许会认为已经存在声明式事务,但是开发人员未留意,这该责怪谁。

尽管如此说,我们仍希望通过一些机制或规范,减少此类问题发生的可能性。

例如,建议大家使用编程式事务而非声明式事务。我在多年的工作中多次遇到开发者未留意声明式事务而导致故障。

因为有时,声明式事务确实不够显著。

声明式事务若用错易失效

除了事务粒度问题外,声明式事务还存在另一主要问题,即使看似简化了大量代码,一旦使用不当,便很容易导致事务失效。

以下几种情景可能导致声明式事务失效:

  1. 将 @Transactional 应用于非公有(non-public)方法
  2. @Transactional 注解中的 propagation 属性设置错误
  3. @Transactional 注解中的 rollbackFor 属性设置错误
  4. 同一类中的方法调用会使 @Transactional 失效
  5. 异常被捕获导致 @Transactional 失效
  6. 数据库引擎不支持事务

详情可参考文章: Spring事务失效的12种场景总结 对于上述问题,若使用编程式事务,则很多情况是可以避免的。

经历过声明式事务失效问题

我们团队不止一次遭遇声明式事务失效的情况。或许您也曾有此经历,我是深受其害的一位。

由于Spring事务基于AOP实现,在编码中,我们可能涉及多个切面,这些切面各自处理不同事务,相互影响。

在之前的一个项目中,我曾发现我们的Service层事务全部失效,一旦SQL操作失败未能回滚。我们追查后才发现,是因为一位同事添加了一个切面,其中实施了异常统一捕获,导致事务切面无法捕获异常,从而无法回滚事务。

此类问题不仅一次发生,而且难以察觉。

许多人可能会辩解,毕竟问题源于自身能力不足,对事务理解还不够透彻,因此出现误用。

然而,我依旧坚持认为,我们确实无法期望每个人都具备高超技能,也不可能要求所有开发者都能百分之百避免错误。我们能做的,是尽力通过机制或规范,减少或降低此类问题的发生几率。

实际上,若对阿里巴巴发布的Java开发手册有过深入研读,便会发现其中很多规约非常珍贵,有些内容可能不易理解,甚至显得有些生硬。然而,这些规范实则由无数开发者在实战中摸爬滚打后总结而来。其实有些东西都是实践出真知。

关于@Transactional的用法,规约中也有提到过,只不过规约中的观点没有我这么鲜明。

文章最后

最后,本文观点或许不会得到所有人的认同,很多人可能会称:Spring官方推崇无侵入的声明式事务,你又有何资格质疑。

老实说,初入职场的那几年,我也钟情于声明式事务,认为其简洁、"优雅"。觉得那些热衷于编程式事务的前辈多此一举,缺乏工匠精神。

然而,随着线上遇到几次问题后的反思,我们发现,有时候你的代码确实优雅无瑕。

然而,这种优雅也常伴随一些副作用,并且前辈们也无法指责我,因为我的做法确实无可指摘...

因此,有些事情,只能在切身体会后才能领悟。

当然,本文并非要求每个人完全放弃声明式事务,只是提议在未来使用事务时,考虑本文所提观点,然后自行做出选择。

如有问题,欢迎加微信交流:w714771310,备注- 技术交流 。或微信搜索【码上遇见你】。

免费的Chat GPT可微信搜索【AI贝塔】进行体验,无限使用。

好了,本章节到此告一段落。希望对你有所帮助,祝学习顺利。

相关推荐
空中湖1 小时前
Spring AI Agent 编排:ReAct 模式 + 多 Agent 协作实战
人工智能·spring·react.js
止语Lab1 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活1 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困1 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn1 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530141 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908401 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金1 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛2 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构
IT_陈寒2 小时前
Vue的响应式让我加班到凌晨3点,原来问题出在这
前端·人工智能·后端