数据库事务与 ACID:转账这种操作为什么要包事务
“从 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(别吞异常、别自调用、别在事务里调远程),并发上按业务选对隔离级别(多数用读已提交/可重复读即可)。事务不是越严越好,而是在"正确"和"性能"之间取到刚好够用的那档,长事务则是无论什么数据库都要躲开的雷。
- 点赞
- 收藏
- 关注作者
评论(0)