数据库事务与 ACID:转账这种操作为什么要包事务

举报
柠檬-夏日清爽 发表于 2026/09/16 10:02:36 2026/09/16
【摘要】 转账、下单扣库存这类"多个步骤必须一起成功或一起失败"的操作,离不开数据库事务。本文用转账例子讲清事务的 ACID 含义,给出 Spring @Transactional 的实际用法,对比四种隔离级别(读未提交/读已提交/可重复读/串行化)能避免哪些并发问题并配可复现的 SQL 现象,说明事务传播行为怎么选,以及长事务、吞异常等高频坑,最后点出在 GaussDB 这类云数据库上的注意点。

“从 A 账户扣 100,给 B 账户加 100”——这看起来是一句话,落到数据库里其实是两步 SQL。如果扣完 A 之后程序崩了,B 没加上,钱就凭空没了。事务要解决的正是这种"要么都做、要么都不做"的问题。这篇文章用转账例子把事务的 ACID、实际写法、隔离级别讲清楚,顺便说清并发下会出什么幺蛾子。

一、事务到底是什么

事务(Transaction)是把一组操作打包成一个不可分割的整体。它必须满足 ACID 四个特性:

特性 含义 转账里怎么体现
原子性 A 要么全成功,要么全回滚 扣 A 和加 B 不能只做一半
一致性 C 数据始终满足约束 两人总额前后不变
隔离性 I 并发事务互不影响 别人看不到中间状态
持久性 D 提交后永久生效 提交后即使宕机也不丢

二、没有事务会出什么事

-- 危险:两步之间没有事务保护
UPDATE account SET balance = balance - 100 WHERE name = 'A'; -- 扣款
-- 假设这里程序崩溃 / 断电
UPDATE account SET balance = balance + 100 WHERE name = 'B'; -- 加款没执行

结果就是 A 少了 100,B 没多,100 块消失了。包上事务后,崩溃会自动回滚到扣款前的状态,钱不会丢。

三、代码里怎么用:Spring @Transactional

在 Java/Spring 里,最常用注解声明事务:

@Transactional
public void transfer(String from, String to, BigDecimal amount) {
    accountMapper.deduct(from, amount);   // 扣款
    accountMapper.add(to, amount);        // 加款
    // 任意一步抛异常,整个方法自动回滚
}

@Transactional 会在方法开始开事务、正常返回提交、抛异常回滚。前提是数据库引擎支持事务(如 InnoDB),且异常要真正往外抛,被 Spring 捕获才能触发回滚。需要部分回滚还能用保存点:

Object sp = status.createSavepoint(); // 设保存点
// ... 出错时
status.rollbackToSavepoint(sp);       // 只回滚到保存点,前面已做的保留

四、隔离级别:并发时怎么互不干扰

多个事务同时跑,会冒出三类经典问题:

  • 脏读:读到了别人还没提交的修改(他可能回滚,你读到的就是脏的)。
  • 不可重复读:同一事务内两次读同一行,结果不一样(被别人改并提交)。
  • 幻读:同一事务内两次查同一条件,行数变了(别人插入/删除了符合的行)。

四种隔离级别逐一解决:

隔离级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 避免 可能 可能
可重复读(MySQL 默认) 避免 避免 基本避免
串行化 避免 避免 避免

可复现的现象:在"读已提交"下开两个事务,T1 改了数据但没提交,T2 读不到(避免了脏读);但 T1 提交后 T2 在同事务内再读,值变了(出现了不可重复读)。升到"可重复读",T2 两次读到的值就一致了。

隔离越严,一致性越好,但并发性能越低。绝大多数业务用"读已提交"或"可重复读"就够,只有强一致要求才上串行化。

五、事务传播:方法互相调用时怎么办

一个事务方法里调用另一个事务方法,行为由传播行为决定。最常用的两种:

  • REQUIRED(默认):有事务就加入,没有就新建——大家共用同一个事务。
  • REQUIRES_NEW:挂起当前事务,自己开一个新的——内外互不影响,内层提交外层崩了也不回滚内层。

选错传播行为,可能出现"内层回滚把外层一起带崩"或"本该回滚却没回滚"的坑,调用链复杂时要特别留意。

六、几个容易踩的坑

第一,异常被吞了事务不回滚。catch 住异常又不往外抛,Spring 认为成功了就提交了,错误数据就落库了。

第二,自调用失效。this.method() 内部调用带 @Transactional 的方法,代理不生效,事务不会开启——要通过注入的 Bean 调。

第三,事务里别做远程调用。事务持有数据库连接和锁,期间去调第三方接口,连接被占很久,容易拖垮连接池。长耗时操作放事务外。

第四,长事务是隐形杀手。一个事务里循环处理上万条数据,锁持有时间长,别人全堵在等它,还容易触发锁超时。该分批就分批。

第五,隔离级别不是越高越好。串行化在大并发下会大量锁等待,下单这类场景要权衡。

七、在云数据库上的注意点

以 GaussDB (for MySQL) 这类云数据库为例,事务行为和 MySQL 基本一致,但有几件事要留意:它通常在底层做了高可用和强一致,提交后的持久性更有保障;但跨分片/读写分离场景下,从库延迟可能影响"读已提交"的体感,读写强一致的需求要走主库。长事务还会拖慢主备同步,线上尤其要避免。

小结

事务的核心价值就一句话:把"多步操作"变成"要么全成、要么全败"的原子动作,转账、扣库存、下单这类场景离了它就会出资损。理解 ACID 后,重点落到两件事:代码上用对 @Transactional(别吞异常、别自调用、别在事务里调远程),并发上按业务选对隔离级别(多数用读已提交/可重复读即可)。事务不是越严越好,而是在"正确"和"性能"之间取到刚好够用的那档,长事务则是无论什么数据库都要躲开的雷。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。