在MySQL里,事务处理是一组要么全部执行成功、要么全部撤销回滚的数据库操作序列。它的核心价值在于当系统遭遇网络中断、程序异常或并发改写时,依然能让数据保持在一致状态。很多刚接触后端的同学以为只要把几条SQL丢进一个函数里就安全了,其实如果没有显式开启事务并且正确提交,MySQL默认每条语句都是独立自动提交的小事务,一旦出现中途报错,前面执行的更新根本不会被自动还原。

事务的ACID原理与底层机制
ACID是事务处理的理论基石,分别对应原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。原子性依靠undo log实现,当执行插入或更新时,MySQL会先写回滚日志,若事务失败则按日志反向补偿;一致性是目标状态,由应用层的业务约束与数据库约束共同维护;隔离性通过锁与多版本并发控制(MVCC)调节;持久性则由redo log与双写缓冲保障,即使宕机也能在重启后重放日志。
理解MVCC对用好事务非常关键。在可重复读级别下,每个事务开启时会拿到一个一致性视图(read view),普通查询读取的是快照版本而非最新行,这样写操作就不会阻塞读。但如果事务内执行了当前读(如select ... for update),就会穿透快照去拿最新数据并加锁,这时候更容易碰到锁等待。下面这段伪代码展示了开启事务后利用当前读防止超卖的常见写法:
begin; select stock from goods where id = 1 for update; -- 假设查到 stock = 10 update goods set stock = stock - 1 where id = 1; insert into orders(user_id, goods_id) values (1001, 1); commit;
从性能角度看,长事务会拖慢整个库。因为MVCC需要保留旧版本行,如果有一个事务几小时不提交,它看到的快照之前的垃圾数据都无法被purge线程清理,进而导致表空间膨胀。因此生产环境应当避免在一次事务里做远程HTTP调用或人工审核等待,把事务压缩到纯数据库操作内。
隔离级别差异与并发问题实战对比
MySQL支持四种标准隔离级别:读未提交(read uncommitted)、读已提交(read committed)、可重复读(repeatable read,默认)、串行化(serializable)。读未提交允许事务读到别人未提交的修改,会产生脏读;读已提交解决脏读但会出现不可重复读,即同一事务内两次查同一行结果不同;可重复读通过快照解决不可重复读,但标准定义里仍有幻读可能,不过InnoDB借助间隙锁在很大程度上消除了幻读;串行化则给所有读加共享锁,并发度最低。
我们可以通过一个对比表来看不同级别对业务的影响:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 | 最高 |
| 读已提交 | 无 | 可能 | 可能 | 较高 |
| 可重复读 | 无 | 无 | 极少 | 中等 |
| 串行化 | 无 | 无 | 无 | 最低 |
在电商库存扣减场景中,如果选用读已提交,事务A扣库存时事务B可能读到已提交但随后被回滚的版本(配合不当仍会乱),因此更稳妥的是可重复读加for update。但要注意间隙锁可能导致死锁,比如两个事务同时插入相邻主键区间并互相等待。此时可以统一业务插入顺序,或在应用层使用单线程队列串行化写请求来规避。
Spring与MySQL事务控制的整合避坑
在Java生态里,大家习惯用Spring的声明式事务@Transactional。但不少项目里这个方法明明标了注解,异常却被catch吞掉,导致事务没回滚。Spring默认只对RuntimeException和Error回滚,如果业务抛出受检异常(如IOException)且没配置rollbackFor,就会提交脏数据。此外自调用(同一个类里方法互调)会绕过代理,使注解失效。
下面给出一个正确的配置示例,显式指定回滚异常并控制超时:
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class, timeout = 3)
public void createOrder(Long userId, Long goodsId) throws Exception {
// 扣库存与写订单在同一事务
goodsMapper.decreaseStock(goodsId);
orderMapper.insert(userId, goodsId);
if (userId == null) {
throw new Exception("用户为空");
}
}
}
另一个常见陷阱是事务传播行为。比如方法A调用方法B,若B设成REQUIRES_NEW,它会挂起A的事务开新事务,那么B提交后A回滚并不影响B,这在写日志场景下有用,但若误用就会造成核心订单回滚了而扣款记录却留下了。因此设计业务时一定要画清哪些步骤必须同生共死,哪些可独立持久化,再匹配PROPAGATION属性。只有把MySQL原生事务机制与框架封装结合起来审视,才能写出真正安全的数据层代码。