为什么事务特性是Java开发面试的必考题?
核心考点
- ACID四大特性:原子性、一致性、隔离性、持久性
- 隔离级别:Read Uncommitted / Read Committed / Repeatable Read / Serializable
- 传播行为:REQUIRED、REQUIRES_NEW、NESTED等7种模式
- 并发问题:脏读、不可重复读、幻读
工程关注点
- 事务失效的常见陷阱(如异步、自调用、异常捕获)
- 数据库锁机制与性能权衡
- 分布式事务方案对比(Seata / AT / TCC / Saga)
- Spring事务源码关键路径解析
延伸热点
- MySQL InnoDB:MVCC原理与Undo Log作用
- Java线程模型:ThreadLocal在事务传播中的应用
- 故障排查:死锁诊断、长事务监控、连接泄漏
- 最佳实践:事务边界设计、非阻塞事务、最终一致性
ACID特性:事务的四大基石
ACID是事务的缩写,代表原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。这四大特性共同构成事务的理论基础,缺一不可。
1. 原子性(Atomicity)
事务中的所有操作要么全部成功,要么全部失败回滚(Rollback)。这是通过数据库的Undo Log实现的——当事务回滚时,系统按日志逆序执行撤销操作。
BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT; -- 若任一步骤失败,整个事务回滚
注意:若在Java中手动提交(setAutoCommit(false)后未调用commit()),连接关闭时可能自动回滚——但依赖此行为属于反模式,应显式处理。
2. 一致性(Consistency)
事务执行前后,数据库必须从一个一致性状态转换到另一个一致性状态。一致性由原子性 + 隔离性 + 约束校验共同保障。
典型场景
- 外键约束:删除主表记录前需处理从表依赖
- 唯一索引:重复插入唯一字段会失败
- CHECK约束:如年龄不能为负
- 触发器:级联更新/删除逻辑
面试陷阱
误区:“只要回滚就保证一致性”
反例:若业务逻辑校验在事务外执行(如缓存预检失败),事务内操作仍会执行,导致不一致。
3. 隔离性(Isolation)
多个事务并发执行时,彼此操作应相互隔离。隔离级别由低到高:Read Uncommitted → Read Committed → Repeatable Read → Serializable。
读未提交(Read Uncommitted)
最低隔离级别,允许读取未提交数据,导致脏读(Dirty Read)。
结果:T2读到“脏数据”500,但实际应为原值(如1000)。
读已提交(Read Committed)
Oracle默认级别,仅允许读取已提交数据,解决脏读,但存在不可重复读(Non-repeatable Read)。
关键点:同一事务内多次查询结果不一致。
可重复读(Repeatable Read)
MySQL InnoDB默认级别,通过MVCC(多版本并发控制)和Next-Key Lock保证同一事务内多次读取结果一致,解决不可重复读,但存在幻读(Phantom Read)。
-- 场景:T1先查询id>10的记录,T2插入id=15 T1: SELECT * FROM orders WHERE id > 10; -- 返回2条 T2: INSERT INTO orders (id, ...) VALUES (15, ...); COMMIT; T1: SELECT * FROM orders WHERE id > 10; -- 仍返回2条(MVCC快照) T1: INSERT INTO orders (id, ...) VALUES (15, ...); -- 报错:唯一键冲突 T1: SELECT * FROM orders WHERE id > 10 FOR UPDATE; -- 会阻塞,Next-Key Lock生效
幻读本质:范围查询时新插入的行。InnoDB通过Next-Key Lock(间隙锁+记录锁)部分解决幻读。
序列化(Serializable)
最高隔离级别,强制事务串行执行,通过加读写锁实现,完全避免并发问题,但性能极低。
适用场景:金融核心系统、数据迁移等对一致性要求极高的操作。
4. 持久性(Durability)
事务提交后,对数据的修改是永久的。通过Redo Log(重做日志)实现——数据先写入Redo Log Buffer,再刷入磁盘(fsync)。
注意:若Redo Log未刷盘(如突然断电),可能丢失数据。可通过配置innodb_flush_log_at_trx_commit=1确保强持久性(默认值)。
隔离级别深度对比与实战建议
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能影响 | 适用场景 |
|---|---|---|---|---|---|
| Read Uncommitted | ✅ 允许 | ✅ 允许 | ✅ 允许 | 低 | 几乎不用(仅测试) |
| Read Committed | ❌ 禁止 | ✅ 允许 | ✅ 允许 | 中 | Oracle、报表查询 |
| Repeatable Read | ❌ 禁止 | ❌ 禁止 | 部分允许 | 高 | MySQL默认、交易系统 |
| Serializable | ❌ 禁止 | ❌ 禁止 | ❌ 禁止 | 极高 | 金融核心、数据一致性强场景 |
面试加分点:MySQL 8.0已废弃SET TRANSACTION ISOLATION LEVEL语法,改用SET SESSION TRANSACTION ISOLATION LEVEL。
隔离级别设置方式
-- MySQL全局设置 SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 会话级设置(当前连接) SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; -- Spring Boot配置(application.yml) spring: datasource: hikari: connection-init-sql: SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
长事务危害与监控
长事务会持有锁时间过长,导致:
• 其他事务阻塞堆积(SHOW PROCESSLIST中State: Waiting for table lock)
• Undo Log积累,影响性能
• 主从延迟增大(binlog堆积)
-- 查看长事务(MySQL 5.7+) SELECT id, user, host, db, command, time, state, info FROM information_schema.processlist WHERE time > 60 -- 超过60秒 AND command != 'Sleep'; -- 查看当前事务(MySQL 8.0) SELECT * FROM performance_schema.data_locks WHERE lock_duration = 'TRANSACTION';
建议:通过APM工具(如SkyWalking)监控事务耗时,设置告警阈值(如>500ms)。
Spring事务传播机制:开发者最易忽略的陷阱
当多个事务方法相互调用时,传播行为决定如何处理当前事务上下文。Spring提供7种传播行为:
| 传播行为 | 描述 | 典型场景 | 风险提示 |
|---|---|---|---|
| REQUIRED | 有事务则加入,无则新建 | 默认值,90%业务场景 | 嵌套调用时,子方法回滚导致父方法回滚 |
| REQUIRES_NEW | 挂起当前事务,新建独立事务 | 日志记录、异步通知 | 父事务回滚不影响子事务;子事务异常可能导致数据不一致 |
| NESTED | 若当前有事务,则在嵌套事务中执行 | 需要部分回滚的业务 | 依赖JDBC Savepoint,MySQL需支持 |
| NOT_SUPPORTED | 以非事务方式执行,挂起当前事务 | 只读操作、查询 | 可能读到未提交数据(需注意隔离性) |
| NEVER | 以非事务方式执行,若当前有事务则抛异常 | 禁止事务的操作(如某些外部接口) | 误用导致系统崩溃 |
| MANDATORY | 必须在已有事务中执行,否则抛异常 | 强制事务边界校验 | 开发阶段易忽略 |
| SUPPORTS | 有事务则加入,无则非事务执行 | 查询操作(性能优化) | 数据一致性不可控 |
NESTED与REQUIRES_NEW的本质区别
@Service public class UserService { @Transactional(propagation = Propagation.REQUIRED) public void methodA() { // 主事务开始 methodB(); // REQUIRES_NEW:新建独立事务 // methodB()失败回滚,但methodA()继续执行 throw new RuntimeException("main fail"); -- methodA回滚,methodB提交的数据不回滚 } @Transactional(propagation = Propagation.NESTED) public void methodC() { // 嵌套事务:有savepoint throw new RuntimeException("nested fail"); -- methodC失败回滚到savepoint,methodA可继续执行 } }
注意:NESTED要求数据库支持Savepoint(如MySQL InnoDB),且必须使用@Transactional(propagation = Propagation.NESTED),默认不生效!
常见事务失效场景
解决方案:通过AopContext.currentProxy()或拆分Service类规避自调用;使用@Transactional(rollbackFor = Exception.class)。
高频面试问题与真实案例解析
Q1:MySQL中InnoDB的MVCC如何解决不可重复读?
核心机制:
• Read View:事务启动时生成快照,记录当前活跃事务ID列表
• Undo Log:存储历史版本,形成版本链
• 版本链查询:从最新版本开始,根据Read View判断可见性
-- 版本链结构(简化) record(version=3, trx_id=102, prev_version=version=2) record(version=2, trx_id=101, prev_version=version=1) record(version=1, trx_id=100, prev_version=null) -- T1启动(trx_id=103),Read View=[100,101,102] SELECT * FROM t WHERE id=1; → 查version=3:trx_id=102 ∈ Read View → 不可见 → 查version=2:trx_id=101 ∈ Read View → 不可见 → 查version=1:trx_id=100 ∉ Read View(且100已提交)→ 可见 结果:返回version=1的数据
注意:MVCC仅解决读一致性,写操作仍需加锁(如Next-Key Lock)防幻读。
Q2:如何排查死锁?
SET GLOBAL innodb_print_all_deadlocks = ON;SHOW ENGINE INNODB STATUS\G;• WAITING FOR LOCK
• HOLDS THE LOCK
• TRANSACTION ID
• LOCK WAIT
*** (1) TRANSACTION: TRANSACTION 12345, ACTIVE 0 sec starting index read mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row(s) locked ... *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 123 page no 456 n bits 72 index `PRIMARY` of table `test`.`user` lock_mode X locks rec but not gap Record lock, heap no 3 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 80000001; asc ;; 1: len 6; hex 000000003000; asc 0 ;; ... *** (2) TRANSACTION: TRANSACTION 12346, ACTIVE 0 sec fetching rows ... *** (2) HOLDS THE LOCK: RECORD LOCKS space id 123 page no 456 n bits 72 index `PRIMARY` of table `test`.`user` lock_mode X locks rec but not gap Record lock, heap no 4 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 80000002; asc ;; ... *** WE ROLL BACK TRANSACTION (2)
关键点:分析两个事务的锁请求与持有关系,优化SQL顺序或添加索引避免死锁。
Q3:如何避免长事务?
开发规范
- 事务内避免调用外部接口(HTTP/RPC)
- 批量操作拆分为小批次(如每次100条)
- 使用
SELECT ... FOR UPDATE时加WHERE条件 - 避免在事务中执行耗时计算
监控方案
- 数据库层:监控
Information Schema.INNODB_TRX中trx_started时间 - 应用层:用AOP拦截
@Transactional,记录耗时 - 告警阈值:事务执行时间 > 500ms
分布式事务:从单机到微服务的挑战
微服务架构下,业务拆分为多个服务,事务需跨越数据库甚至网络。常见方案对比:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 2PC(两阶段提交) | 协调者统一提交/回滚 | 强一致性 | 性能差、单点故障 | 数据库分布式事务(如XA) |
| Seata AT模式 | 自动补偿(Undo Log) | 无侵入、易集成 | 需全局锁,高并发易冲突 | 数据库同构、低并发场景 |
| TCC(Try-Confirm-Cancel) | 业务层实现三阶段 | 高性能、灵活 | 代码侵入大、幂等性难保证 | 金融核心系统(如转账) |
| Saga模式 | 顺序执行+补偿 | 长事务友好 | 需设计补偿逻辑 | 电商订单(创建→支付→发货) |
| 消息队列(最终一致性) | 可靠消息+本地表 | 解耦、高可用 | 非强一致 | 通知类业务(短信、日志) |
Seata AT模式核心流程
@GlobalTransactional注解启动TC(Transaction Coordinator)关键点:Undo Log需包含主键+修改字段,用于生成反向SQL(如UPDATE → DELETE/INSERT)。
最终一致性实践:可靠消息+本地事务表
-- 1. 创建消息表(与业务表同库) CREATE TABLE message_queue ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_id VARCHAR(64) NOT NULL, msg_type VARCHAR(32) NOT NULL, payload JSON NOT NULL, status TINYINT DEFAULT 0, -- 0:待发送, 1:已发送, 2:已消费 create_time DATETIME DEFAULT NOW(), update_time DATETIME DEFAULT NOW() ON UPDATE NOW() ); -- 2. 业务逻辑(本地事务) @Transactional public void placeOrder(Order order) { // 1. 创建订单 orderMapper.insert(order); // 2. 插入消息(与订单同一事务) Message msg = new Message(order.getId(), "ORDER_CREATED", order); messageMapper.insert(msg); -- status=0 } -- 3. 消息服务轮询发送(幂等) SELECT * FROM message_queue WHERE status = 0 AND create_time < NOW() - INTERVAL 1 MINUTE LIMIT 100; -- 4. 发送成功后更新状态 UPDATE message_queue SET status = 1, update_time = NOW() WHERE id = ? AND status = 0; -- 乐观锁防重复发送
优点:本地事务保证消息不丢失,消费端幂等处理保证最终一致性。
高频真题实战:附参考回答与评分要点
参考回答
事务的四大特性是ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。隔离级别从低到高分为:
• Read Uncommitted:允许脏读,几乎不用
• Read Committed:解决脏读,Oracle默认,存在不可重复读
• Repeatable Read:解决不可重复读,MySQL默认,通过MVCC+Next-Key Lock部分解决幻读
• Serializable:串行化执行,完全避免并发问题,性能最低
MySQL InnoDB默认是Repeatable Read,它通过MVCC保证同一事务内多次读取结果一致,同时用Next-Key Lock(记录锁+间隙锁)防止其他事务在范围内插入新数据,从而减少幻读。
加分点:提到InnoDB对幻读的优化(如当前读时加锁);对比MySQL与Oracle默认级别的差异。
参考回答
Spring事务失效常见原因有:
① 自调用:同类中方法A调用B(B有@Transactional),因Spring AOP代理机制,直接调用绕过代理,事务不生效;
② 异常被catch捕获:默认只回滚RuntimeException,checked异常需指定rollbackFor;
③ 异步方法:@Async新线程丢失事务上下文;
④ 非public方法:AOP仅拦截public方法。
解决方案:
• 用AopContext.currentProxy()调用;
• 拆分Service类;
• 显式指定@Transactional(rollbackFor = Exception.class);
• 异步场景改用消息队列解耦。
加分点:结合源码说明(TransactionInterceptor.invoke);提到Spring Boot 2.x对自调用的改进(proxyMode=PROXY)。
参考回答
分布式事务方案需权衡一致性与性能:
• 强一致:2PC(XA协议),适合数据库分布式事务,但性能差、有单点风险;
• 最终一致:Seata AT(自动补偿)、TCC(业务三阶段)、Saga(顺序+补偿)、可靠消息+本地事务表。
实际项目中:
• 金融转账:用TCC(Try冻结资金→Confirm扣款→Cancel解冻);
• 订单创建:用可靠消息(本地事务表+消息服务),确保订单与通知最终一致;
• 高并发场景:避免2PC,改用Saga或消息队列。
加分点:提到Seata AT的Undo Log结构;说明TCC的幂等性设计(防重试)。
延伸问题:你们系统如何保障事务数据一致性?
回答框架:
① 单机场景:用Spring事务+隔离级别配置;
② 分布式场景:核心链路用Seata,非核心用消息队列;
③ 监控:事务耗时告警、死锁日志分析;
④ 补偿:人工干预平台+自动重试机制;
⑤ 测试:压测时模拟网络延迟、服务宕机验证事务回滚。
网友们还关心:Java事务与周边技术的交叉问题
① 事务与线程池的关系
线程池执行异步任务时,事务上下文不会自动传递。若需在异步线程中使用事务:
• 方案1:手动传播XID(通过ThreadLocal)
• 方案2:改用@Async配合@Transaction(需配置ProxyType.TARGET_CLASS)
• 方案3:避免在异步中操作事务数据(推荐)
② Redis事务 vs 数据库事务
Redis事务
MULTI/EXEC:命令队列执行- 不支持回滚(失败后继续执行后续命令)
- 无隔离性(命令执行时无锁)
- 适用:批量操作、非关键数据
数据库事务
- ACID强保障
- 支持回滚(Undo Log)
- 隔离级别可控
- 适用:金融、订单等核心场景
③ 事务与缓存一致性
经典问题:先更新DB,再删缓存?还是先删缓存,再更新DB?
• 推荐:先更新DB → 成功后删缓存(避免缓存穿透)
• 进阶:延时双删(删缓存→sleep→再删一次,防脏数据)
• 高阶:监听Binlog(Canal)实现异步同步