Java 数据库事务实战

本文整理 Java 后端开发中数据库事务的核心干货:ACID、隔离级别、JDBC/MyBatis 与 Spring 协作、@Transactional 用法、传播行为、常见失效与测试要点。适合已会用 Spring Boot + MyBatis 的读者。


一、事务是什么?

事务(Transaction) = 一组逻辑上不可分割的数据库操作,要么全部提交(commit),要么全部回滚(rollback)

经典例子:转账扣款 + 加款必须同时成功或同时失败。

Java
userMapper.deduct(fromId, amount);   // 第 1 步成功
// 若此处异常或宕机,第 2 步未执行 → 数据不一致
userMapper.add(toId, amount);

有事务时,两步绑定在同一工作单元:全部 commit 或全部 rollback

层次作用
数据库InnoDB 等引擎提供 BEGIN / COMMIT / ROLLBACK
JDBC通过 Connection 控制自动提交
SpringPlatformTransactionManager 统一管理;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

Java
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateOrder() { ... }

三、JDBC 与 MyBatis

3.1 JDBC 要点(MyBatis 底层同样是 JDBC)

Java
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 共用线程上的连接。

Java
@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 代理 + 事务管理器:

Text
调用方 → 代理 → 开启事务 → 执行业务 → 提交/回滚

Spring Boot 检测到 DataSource 时会自动配置 DataSourceTransactionManager,一般无需手写 @EnableTransactionManagement

自调用失效(高频):同类内 this.methodB() 不经过代理,methodB 上的 @Transactional 不生效。

做法说明
拆到另一个 @Service 注入调用推荐
AopContext.currentProxy() 自调用exposeProxy = true
TransactionTemplate见下文

五、@Transactional 用法

5.1 基本示例

Java
@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 回滚规则

  • 默认回滚RuntimeExceptionError
  • 默认不回滚:受检异常 → 需 rollbackFor = Exception.class
Java
@Transactional(rollbackFor = Exception.class)
public void importData() throws IOException { ... }
  • 吞异常不回滚
Java
@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:主业务回滚,但操作日志仍要保留 → 日志方法单独开事务:

Java
@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(了解即可)

对比NESTEDREQUIRES_NEW
与外层嵌套在外层内(Savepoint)完全独立新事务
外层回滚子事务一起回滚子事务可能已提交,不受影响
使用频率较少,需驱动支持 Savepoint日志/审计等独立提交

日常 REQUIRED + REQUIRES_NEW 够用;SUPPORTSNOT_SUPPORTEDMANDATORYNEVER 仅特殊场景需要。


七、编程式事务(了解)

注解不好表达或需规避自调用时,用 TransactionTemplate

Java
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:

YAML
logging:
  level:
    org.springframework.transaction: DEBUG

多数据源 / 分布式:默认 @Transactional 只管单库单服务;跨库用最终一致性(消息、对账)或 Seata 等,勿与本地事务混为一谈。


九、单元测试

测试类上加 @Transactional,方法结束后自动回滚,不污染数据库:

Java
@SpringBootTest
@Transactional
class UserServiceTest {
    @Test
    void transfer() {
        userService.transfer(1L, 2L, new BigDecimal("10"));
        // 断言后自动 rollback
    }
}

验证事务是否回滚:在事务方法末尾故意抛异常,查库确认无脏数据;或配合 @Sql 准备数据。

更多写法见《JUnit 单元测试实战》。


十、上线 checklist

  1. 写操作在 Service@Transactional 内?
  2. 避免 同类自调用
  3. 受检异常配了 rollbackForcatch 未吞异常?
  4. 写操作未误设 readOnly=true
  5. 需独立保留的日志/审计用了 REQUIRES_NEW
  6. 多条 Mapper 操作在同一事务方法内?

速记

主题要点
基础一组操作全成功或全失败;ACID + 隔离级别
JDBCsetAutoCommit(false),同一 Connection
Spring@Transactional = AOP 代理;防自调用
传播默认 REQUIRED;独立提交 REQUIRES_NEW
避坑public、Service 层、勿吞异常、rollbackFor

延伸阅读

  • 《Spring 注解开发指南》:@Transactional 注解速查
  • 《MyBatis 持久层开发》《MyBatis-Plus 单表开发与增强》:持久层与事务协作
  • 《JUnit 单元测试实战》:测试回滚