Java 数据库事务实战
本文整理 Java 后端开发中数据库事务的核心干货:ACID、隔离级别、JDBC/MyBatis 与 Spring 协作、@Transactional 用法、传播行为、常见失效与测试要点。适合已会用 Spring Boot + MyBatis 的读者。
一、事务是什么?
事务(Transaction) = 一组逻辑上不可分割的数据库操作,要么全部提交(commit),要么全部回滚(rollback)。
经典例子:转账扣款 + 加款必须同时成功或同时失败。
userMapper.deduct(fromId, amount); // 第 1 步成功
// 若此处异常或宕机,第 2 步未执行 → 数据不一致
userMapper.add(toId, amount);
有事务时,两步绑定在同一工作单元:全部 commit 或全部 rollback。
| 层次 | 作用 |
|---|---|
| 数据库 | InnoDB 等引擎提供 BEGIN / COMMIT / ROLLBACK |
| JDBC | 通过 Connection 控制自动提交 |
| Spring | PlatformTransactionManager 统一管理;MyBatis 复用同一条连接 |
日常在 Service 层用 @Transactional 划定事务边界即可,底层仍由数据库完成真正的提交与回滚。
二、ACID 与隔离级别
2.1 ACID
| 特性 | 含义 |
|---|---|
| 原子性 | 全部生效或全部撤销 |
| 一致性 | 事务前后满足业务规则与约束(常由原子性 + 隔离性 + 代码 + 约束共同保证) |
| 隔离性 | 并发事务互不影响的程度,由隔离级别控制 |
| 持久性 | 提交后结果持久保存 |
2.2 并发问题
| 问题 | 说明 |
|---|---|
| 脏读 | 读到别的事务未提交的数据 |
| 不可重复读 | 同一事务内两次读同一行,结果不同 |
| 幻读 | 同一事务内两次条件查询,行数不同 |
2.3 四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 备注 |
|---|---|---|---|---|
READ_UNCOMMITTED | 可能 | 可能 | 可能 | 几乎不用 |
READ_COMMITTED | 不会 | 可能 | 可能 | Oracle/PG 默认 |
REPEATABLE_READ | 不会 | 不会 | 可能* | MySQL InnoDB 默认 |
SERIALIZABLE | 不会 | 不会 | 不会 | 性能最差 |
\* InnoDB 在 REPEATABLE_READ 下用 MVCC + 间隙锁大幅减轻幻读,面试需结合引擎说明。
实践:多数业务用数据库默认级别即可;遇并发问题再调高或加锁,勿随意用 SERIALIZABLE。
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateOrder() { ... }
三、JDBC 与 MyBatis
3.1 JDBC 要点(MyBatis 底层同样是 JDBC)
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
try {
// 多条 SQL 共用 conn ...
conn.commit();
} catch (Exception e) {
conn.rollback();
throw e;
} finally {
conn.setAutoCommit(true);
conn.close();
}
- 默认
autoCommit=true:每条 SQL 立即提交,无跨语句事务。 - 同一事务内多条 SQL 必须用同一个
Connection。 - Spring 在方法入口取连接、绑定线程,结束时统一提交/回滚。
3.2 Spring + MyBatis
整合后事务由 Spring 管,MyBatis 只执行 SQL;同一 @Transactional 方法内,Mapper 共用线程上的连接。
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
orderMapper.insertLog(...);
}
}
原生 MyBatis(无 Spring)用 sqlSessionFactory.openSession(false) + commit/rollback;整合 Spring 后改由 Spring 管,勿在 Service 里手动 commit。MyBatis-Plus 机制相同。
执行流程:@Transactional 开启 → 连接绑定线程 → Mapper 复用该连接 → 正常结束 commit / 异常 rollback。
四、Spring 事务原理
@Transactional = AOP 代理 + 事务管理器:
调用方 → 代理 → 开启事务 → 执行业务 → 提交/回滚
Spring Boot 检测到 DataSource 时会自动配置 DataSourceTransactionManager,一般无需手写 @EnableTransactionManagement。
自调用失效(高频):同类内 this.methodB() 不经过代理,methodB 上的 @Transactional 不生效。
| 做法 | 说明 |
|---|---|
拆到另一个 @Service 注入调用 | 推荐 |
AopContext.currentProxy() 自调用 | 需 exposeProxy = true |
TransactionTemplate | 见下文 |
五、@Transactional 用法
5.1 基本示例
@Service
public class UserService {
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
userMapper.deduct(fromId, amount);
userMapper.add(toId, amount);
logMapper.insert(new Log("转账成功"));
}
}
5.2 常用属性
| 属性 | 说明 |
|---|---|
propagation | 传播行为,默认 REQUIRED |
isolation | 隔离级别,默认跟数据源 |
timeout | 超时秒数 |
readOnly | 只读优化;有写操作勿设 true |
rollbackFor / noRollbackFor | 控制哪些异常回滚 |
5.3 回滚规则
- 默认回滚:
RuntimeException、Error - 默认不回滚:受检异常 → 需
rollbackFor = Exception.class
@Transactional(rollbackFor = Exception.class)
public void importData() throws IOException { ... }
- 吞异常不回滚:
@Transactional
public void bad() {
try {
mapper.insert(...);
} catch (Exception e) {
log.error("失败", e); // 只打日志不抛出 → 事务仍提交
}
}
应重新 throw,或 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。
类上 @Transactional(readOnly = true) 可设默认只读,写方法用 @Transactional(readOnly = false) 覆盖。
5.4 注解加在哪?
| 建议 | 原因 |
|---|---|
Service 实现类 的 public 方法 | 业务层管事务;Controller 保持轻薄 |
| 加在实现类而非接口 | 更稳妥 |
勿加 private / Controller | 代理拦截不到 |
注解属性速查见《Spring 注解开发指南》第五章。
六、传播行为
解决:事务方法 A 调用事务方法 B 时,事务如何衔接?
| 取值 | 说明 | 使用频率 |
|---|---|---|
REQUIRED(默认) | 有则加入,无则新建 | 最常用 |
REQUIRES_NEW | 总是新建;挂起外层,内外互不影响 | 日志/审计需独立提交时 |
NESTED | 嵌套 Savepoint,子回滚不一定拖垮外层 | 较少 |
SUPPORTS | 有事务则加入,无则以非事务执行 | 少见 |
NOT_SUPPORTED | 非事务执行,挂起当前事务 | 少见 |
MANDATORY | 必须在已有事务中,否则抛异常 | 少见 |
NEVER | 必须无事务,否则抛异常 | 少见 |
REQUIRED:下单 + 扣库存在同一事务,任一步失败全部回滚。
REQUIRES_NEW:主业务回滚,但操作日志仍要保留 → 日志方法单独开事务:
@Transactional
public void pay() {
orderMapper.updatePaid(...);
logService.saveLog("支付"); // REQUIRES_NEW,已单独提交
throw new RuntimeException(); // pay 回滚,日志不回滚
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(String msg) { ... }
NESTED vs REQUIRES_NEW(了解即可)
| 对比 | NESTED | REQUIRES_NEW |
|---|---|---|
| 与外层 | 嵌套在外层内(Savepoint) | 完全独立新事务 |
| 外层回滚 | 子事务一起回滚 | 子事务可能已提交,不受影响 |
| 使用频率 | 较少,需驱动支持 Savepoint | 日志/审计等独立提交 |
日常 REQUIRED + REQUIRES_NEW 够用;SUPPORTS、NOT_SUPPORTED、MANDATORY、NEVER 仅特殊场景需要。
七、编程式事务(了解)
注解不好表达或需规避自调用时,用 TransactionTemplate:
transactionTemplate.execute(status -> {
reportMapper.insertHeader(...);
reportMapper.insertDetails(...);
if (needRollback) status.setRollbackOnly();
return null;
});
日常 CRUD 仍优先 @Transactional。
八、失效排查
| 现象 | 原因 | 处理 |
|---|---|---|
| 完全不生效 | 非 public;类非 Spring Bean | 检查 @Service 与注解位置 |
| 部分不生效 | 同类 this 自调用 | 拆 Service 或 TransactionTemplate |
| 异常不回滚 | 受检异常;catch 吞异常 | rollbackFor 或重新抛出 |
| 写不进去 | readOnly=true | 写方法设 readOnly=false |
| 跨库不一致 | 默认只管一个数据源 | 避免跨库强一致,或 Seata 等 |
开发环境可开 DEBUG:
logging:
level:
org.springframework.transaction: DEBUG
多数据源 / 分布式:默认 @Transactional 只管单库单服务;跨库用最终一致性(消息、对账)或 Seata 等,勿与本地事务混为一谈。
九、单元测试
测试类上加 @Transactional,方法结束后自动回滚,不污染数据库:
@SpringBootTest
@Transactional
class UserServiceTest {
@Test
void transfer() {
userService.transfer(1L, 2L, new BigDecimal("10"));
// 断言后自动 rollback
}
}
验证事务是否回滚:在事务方法末尾故意抛异常,查库确认无脏数据;或配合 @Sql 准备数据。
更多写法见《JUnit 单元测试实战》。
十、上线 checklist
- 写操作在 Service 的
@Transactional内? - 避免 同类自调用?
- 受检异常配了
rollbackFor?catch未吞异常? - 写操作未误设
readOnly=true? - 需独立保留的日志/审计用了
REQUIRES_NEW? - 多条 Mapper 操作在同一事务方法内?
速记
| 主题 | 要点 |
|---|---|
| 基础 | 一组操作全成功或全失败;ACID + 隔离级别 |
| JDBC | setAutoCommit(false),同一 Connection |
| Spring | @Transactional = AOP 代理;防自调用 |
| 传播 | 默认 REQUIRED;独立提交 REQUIRES_NEW |
| 避坑 | public、Service 层、勿吞异常、rollbackFor |
延伸阅读
- 《Spring 注解开发指南》:
@Transactional注解速查 - 《MyBatis 持久层开发》《MyBatis-Plus 单表开发与增强》:持久层与事务协作
- 《JUnit 单元测试实战》:测试回滚