疯狂餐厅
86.88M · 2026-03-21
事务是数据库操作的基本单位,保证"要么全部成功,要么全部失败"。MySQL通过ACID特性确保数据一致性:
(图1:ACID关系图,展示四者如何共同保障数据安全)
Spring事务失效是高频痛点,以下场景占90%以上,按发生频率排序:
| 失效场景 | 错误代码示例 | 正确代码 | 原因 |
|---|---|---|---|
| 1. 自身调用 | @Transactional void method() { innerMethod(); } | @Transactional void method() { this.innerMethod(); } | Spring代理机制失效 |
| 2. 异常未被捕获 | @Transactional void method() { throw new RuntimeException(); } | @Transactional(rollbackFor = Exception.class) void method() { ... } | 默认只回滚RuntimeException |
| 3. 非public方法 | @Transactional private void method() { ... } | @Transactional public void method() { ... } | Spring代理仅对public方法生效 |
| 4. 事务嵌套 | @Transactional void outer() { inner(); } | @Transactional(propagation = Propagation.REQUIRES_NEW) void inner() | 事务传播机制未配置 |
| 5. 未开启事务管理 | @Service class Service { ... } | @EnableTransactionManagement | 缺少Spring事务配置 |
| 6. 事务方法调用外部服务 | @Transactional void method() { restTemplate.get(); } | @Transactional void method() { ... } | 外部调用阻塞事务 |
| 7. 事务方法未被Spring管理 | new Service().method(); | @Autowired Service service; service.method(); | 未通过Spring容器获取对象 |
| 8. 事务未提交 | @Transactional void method() { ... } | @Transactional void method() { ...; return; } | 方法执行完未提交 |
(图2:事务传播机制关系图,展示不同传播类型的行为差异)
| 传播类型 | 说明 | 适用场景 |
|---|---|---|
| REQUIRED | 默认值,存在事务则加入,不存在则新建 | 90%的业务场景 |
| REQUIRES_NEW | 每次都新建事务,外层事务回滚不影响内层 | 日志记录、异步通知等 |
| SUPPORTS | 存在事务则加入,不存在则不开启 | 仅查询类操作 |
| NOT_SUPPORTED | 不支持事务,存在则挂起 | 仅查询类操作 |
| NEVER | 不支持事务,存在则抛异常 | 仅查询类操作 |
(图3:MVCC工作原理图,展示ReadView、DB_TRX_ID、DB_ROLL_PTR等关键元素)
原理:通过版本链(DB_ROLL_PTR)实现非阻塞读
关键机制:ReadView(当前事务可见的版本范围)
隔离级别影响:
READ COMMITTED:每次查询生成新的ReadViewREPEATABLE READ:事务内只生成一次ReadView(图4:redo/undo log工作流程图,展示事务提交过程)
| 日志类型 | 作用 | 重要参数 |
|---|---|---|
| redo log | 保证事务持久性,崩溃恢复 | innodb_flush_log_at_trx_commit |
| undo log | 保证事务回滚,MVCC版本链 | innodb_undo_log_truncate |
核心原则:事务越小,锁持有时间越短,并发越高
实操建议:
SELECT、日志记录、HTTP调用INSERT/UPDATE/DELETE)放入事务案例:热点账户更新优化
-- 原始事务(持有锁时间500ms)
BEGIN;
UPDATE account SET balance = balance - 100 WHERE user_id = 1;
-- 复杂业务逻辑处理
COMMIT;
-- 优化后(锁时间降至50ms)
BEGIN;
INSERT INTO account_flow (user_id, amount) VALUES (1, -100);
COMMIT;
-- 异步处理
BEGIN;
UPDATE account SET balance = balance - 100 WHERE user_id = 1;
COMMIT;
| 隔离级别 | 读锁范围 | 幻读 | TPS(测试值) | 适用场景 |
|---|---|---|---|---|
| READ UNCOMMITTED | 无 | 100% | 8500 | 仅用于统计 |
| READ COMMITTED | 行锁 | 允许 | 6200 | 读多写少,高并发 |
| REPEATABLE READ | Next-Key | 阻止 | 4300 | 默认级别,大部分场景 |
| SERIALIZABLE | 表锁 | 阻止 | 1200 | 强一致性要求 |
WHERE条件走索引,避免全表扫描innodb_lock_wait_timeout=50(默认50秒)innodb_print_all_deadlocks-- 错误:未加锁,导致更新丢失
UPDATE goods SET stock = stock - 1 WHERE id = 1;
-- 正确:加行锁,解决更新丢失
BEGIN;
SELECT stock FROM goods WHERE id = 1 FOR UPDATE;
UPDATE goods SET stock = stock - 1 WHERE id = 1;
COMMIT;
SELECT * FROM information_schema.innodb_trx WHERE TIME > 60;答:默认是REPEATABLE READ。通过MVCC+Next-Key Lock保证一致性,避免幻读。在RR级别下,事务内首次查询生成ReadView,后续查询基于该ReadView,确保可重复读。
READ COMMITTED下仍可能有幻读?如何解决?答:RC级别下,每次查询都会生成新的ReadView,可能导致幻读。解决方法:
SELECT ... FOR UPDATE(行锁+Next-Key Lock)SERIALIZABLE(表锁,性能差)答:
innodb_print_all_deadlocksSHOW ENGINE INNODB STATUS查看死锁日志performance_schema.data_lock_waits分析锁等待链MySQL事务是数据库安全的基石,但用错会导致严重问题。核心原则:事务越小越好,隔离级别按需选择,索引优化是关键。
READ COMMITTED(高并发读场景)希望这篇文章能帮你彻底掌握MySQL事务。如果你觉得有用,欢迎关注我的公众号【SilkyStarter】或添加微信号:824414828,邀请你加入【silky-starter技术交流群】,获取更多Java技术干货,下期见!