转账这个场景,你大概率听过很多次:账户A给账户B转100块,数据库里要执行两条SQL,A的余额先减100,B的余额再加100。看起来平平无奇,但有一个经典问题悬在头顶——如果第一条SQL执行成功,第二条SQL在执行前数据库突然崩溃,A的钱已经扣了,B却没收到,这100块去哪里找?
所谓“事务”,就是用来解决这类由多个步骤组成、但必须保证整体生效的问题。很多同学能背出ACID,却说不清每个特性到底由什么机制兜底,也说不清Spring里@Transactional失效、隔离级别怎么调、分布式事务为什么那么难做。这篇文章我会把事务特性从概念、底层实现一路讲到工程落地,适合正在学习数据库原理的后端开发,也适合准备面试、想系统梳理事务体系的人。
1. 从“转账少了100块”说起:先搞清楚事务到底在保护什么
1.1 一个动作拆成多条SQL后,麻烦就来了
事务(Transaction)的定义很简单:一组数据库操作形成一个逻辑工作单元,要么全部成功,要么全部失败。
如果把转账放到代码里看,它大概是这个结构:
sql复制-- 伪SQL,一个转账动作被拆成两步
UPDATE account SET balance = balance - 100 WHERE account_id = 'A';
UPDATE account SET balance = balance + 100 WHERE account_id = 'B';
正常人看到这个设计,第一反应可能是:这不就是两句SQL吗,执行完不就行了?但在生产环境里,问题远比想象中多。
第一类问题是“执行一半”:第一句SQL执行成功,数据库宕机,或者第二句SQL因为字段长度、约束冲突报错。这时A少了100,B没多100,整个账本不平。第二类问题是“别人看到了中间态”:如果A扣完100后、B还没入账之前,刚好有人查了A和B的总资产,查出来的结果会少100块,这个数据对业务来说是脏的。第三类问题是“数据明明提交了却丢失”:假设两条SQL都执行完成,应用也收到了提交成功的响应,但如果提交后的数据没有真正落盘,一次断电可能就让这笔转账灰飞烟灭。
事务的ACID四个特性,本质上就是针对这三类问题设计的底线。
1.2 数据库怎么表达“这是同一个事务”
在MySQL这类关系型数据库里,开发者通过一组指令划出事务的范围,最常见的写法是:
sql复制START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE account_id = 'A';
UPDATE account SET balance = balance + 100 WHERE account_id = 'B';
COMMIT;
如果执行过程中发现某一步出错,可以用ROLLBACK回滚,让所有改动都还原到事务开始之前。
这里要注意一个细节:MySQL默认开启autocommit=1,也就是每一条SQL都会被当成一个独立事务自动提交。很多人刚开始没意识到这一点,多写两句SQL就以为自己在一个事务里,结果每句都自动提交了,一旦中间出错,前面的已提交语句根本撤不回来。
事务模式大致可以分成三类,理解它们能避免很多应用层的低级问题:
| 模式 | 触发方式 | 典型特点 |
|---|---|---|
| 自动提交事务 | 默认配置autocommit=1 |
每条SQL独立提交,无显式事务边界 |
| 显式事务 | START TRANSACTION / COMMIT / ROLLBACK |
程序员自己控制边界,适合多SQL业务 |
| 隐式事务 | 由框架或数据库会话机制自动管理 | Spring的@Transactional底层走的就是这种逻辑 |
不管哪种模式,底层落到数据库引擎时,事务都有一个唯一标识、一组操作日志、以及一套锁或版本控制机制。理解了事务的边界,我们才好在ACID的框架里继续拆解。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ACID四个特性逐个拆开:每一步背后都有真实事故
ACID是英文缩写:Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(持久性)。这四个词几乎出现在所有数据库教材里,但很多人背下来之后,并不知道它们各自对应什么样的代码事故、由什么机制兜底。
