事务使用规范
事务边界、隔离级别、分布式事务
【Requirement】事务必须短小精悍;禁止在事务内执行远程调用、耗时计算或非数据库操作。
- 事务内只包含必要的数据库操作
- 事务开始到提交的时间越短越好(建议 < 1 秒)
- 查询操作(只读)不应放入事务
- 远程调用(RPC/HTTP)、消息发送、缓存操作必须放在事务外
// 正确:事务内只有数据库操作,远程调用在事务外
public void createOrder(OrderDTO dto) {
// 1. 事务外:参数校验、远程调用
User user = userService.getUser(dto.getUserId());
// 2. 开启事务:只做数据库操作
transactionTemplate.execute(status -> {
orderMapper.insert(order);
orderItemMapper.batchInsert(items);
return null;
});
// 3. 事务后:发消息、清缓存
mqProducer.sendOrderCreatedEvent(order.getId());
redisTemplate.delete("order:" + order.getId());
}反模式:
// 错误:事务内包含远程调用,长事务
@Transactional
public void createOrder(OrderDTO dto) {
orderMapper.insert(order);
orderItemMapper.batchInsert(items);
// 远程调用在事务内,慢则长时间持有锁
InventoryClient.deductStock(dto.getItems());
mqProducer.sendEvent(order.getId());
}- 优先方法级事务,避免类级
@Transactional - 只读查询方法加
@Transactional(readOnly = true) - 禁止在 Controller 层开启事务,事务应在 Service 层
// 正确:方法级事务
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) { ... }
@Transactional(readOnly = true)
public OrderDTO getOrder(Long id) { ... }
}
// 错误:类级事务,所有方法都开启事务
@Transactional
@Service
public class OrderService { ... }【Standard】默认使用数据库默认隔离级别(MySQL 为 REPEATABLE READ);读多写少场景可降级为 READ COMMITTED 提升并发。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 适用场景 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 | 极少使用,统计场景 |
| READ COMMITTED | ❌ | ✅ 可能 | ✅ 可能 | 读多写少,互联网业务 |
| REPEATABLE READ(MySQL 默认) | ❌ | ❌ | ✅ 可能(MySQL 已解决) | 通用场景 |
| SERIALIZABLE | ❌ | ❌ | ❌ | 强一致性,极低并发 |
- 【Default】使用数据库默认级别,不随意修改
- 【Caution】避免使用 SERIALIZABLE,性能极差
- 读操作占比高、对一致性要求不极致的场景,可设为
READ COMMITTED
// 指定隔离级别
@Transactional(isolation = Isolation.READ_COMMITTED)
public void someMethod() { ... }MySQL InnoDB 在 REPEATABLE READ 下通过 MVCC + Next-Key Lock 解决了幻读问题,因此互联网场景下使用默认级别即可。
【Standard】默认使用 REQUIRED;嵌套场景使用 REQUIRES_NEW 或 NESTED;禁止在同类方法中通过 this 调用导致事务失效。
| 传播行为 | 说明 | 使用场景 |
|---|---|---|
| REQUIRED(默认) | 有事务则加入,无则新建 | 常规业务 |
| REQUIRES_NEW | 挂起当前事务,新建独立事务 | 日志记录、独立操作 |
| NESTED | 嵌套事务,外层回滚内层也回滚 | 子操作可独立回滚 |
| SUPPORTS | 有事务则加入,无则非事务 | 查询方法 |
| NOT_SUPPORTED | 挂起事务,非事务执行 | 不需要事务的操作 |
| MANDATORY | 必须在事务中调用,否则抛异常 | 强制要求事务 |
| NEVER | 必须非事务调用,否则抛异常 | 禁止事务 |
【Caution】以下场景会导致 @Transactional 失效,必须规避:
- 同类内部方法调用(
this.method()),AOP 不生效 - 方法非
public - 异常被
catch吞掉未抛出 - 抛出的异常不在
rollbackFor范围内(默认只回滚 RuntimeException)
// 错误1:同类内部调用,事务失效
@Service
public class OrderService {
public void createOrder() {
this.doCreate(); // this 调用,AOP 不拦截
}
@Transactional
public void doCreate() { ... }
}
// 正确:注入自身或拆分到不同类
@Service
public class OrderService {
@Autowired
private OrderService self; // 注入自身代理
public void createOrder() {
self.doCreate(); // 通过代理调用
}
@Transactional
public void doCreate() { ... }
}【Requirement】@Transactional 必须显式指定 rollbackFor = Exception.class。
// 正确:捕获所有异常回滚
@Transactional(rollbackFor = Exception.class)
public void createOrder() { ... }
// 错误:默认只回滚 RuntimeException,checked 异常不回滚
@Transactional
public void createOrder() throws IOException { ... }【Caution】优先使用最终一致性方案(本地消息表 / TCC / Saga),禁止使用 2PC(XA)作为互联网业务的默认方案。
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 本地消息表 | 最终一致 | 高 | 低 | 异步解耦、订单-库存 |
| TCC | 最终一致 | 中 | 高 | 资金、库存扣减 |
| Saga | 最终一致 | 高 | 中 | 长流程业务 |
| 2PC (XA) | 强一致 | 低 | 中 | 金融核心、跨库 |
| 最大努力通知 | 最终一致 | 高 | 低 | 通知类业务 |
核心思路:业务操作 + 消息记录在同一本地事务中,异步发送消息。
-- 本地消息表
CREATE TABLE msg_local_message (
id BIGINT PRIMARY KEY,
message_id VARCHAR(64) NOT NULL COMMENT '消息唯一ID',
business_type VARCHAR(32) NOT NULL COMMENT '业务类型',
payload TEXT NOT NULL COMMENT '消息内容',
status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0-待发送 1-已发送 2-失败',
retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE KEY uk_message_id (message_id)
) COMMENT='本地消息表';// 业务操作 + 消息写入同一事务
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
orderMapper.insert(order);
// 写入本地消息表
localMessageMapper.insert(buildMessage(order));
}
// 定时任务扫描待发送消息,发送到 MQ
@Scheduled(fixedDelay = 5000)
public void sendPendingMessages() {
List<LocalMessage> msgs = localMessageMapper.selectPending(100);
for (LocalMessage msg : msgs) {
try {
mqProducer.send(msg.getBusinessType(), msg.getPayload());
localMessageMapper.markSent(msg.getId());
} catch (Exception e) {
localMessageMapper.incrementRetry(msg.getId());
}
}
}适用于强一致的资金扣减场景,分为 Try / Confirm / Cancel 三个阶段:
public interface InventoryTccService {
// Try:冻结库存
boolean tryDeduct(String businessId, Long productId, Integer count);
// Confirm:扣减冻结库存
boolean confirmDeduct(String businessId);
// Cancel:释放冻结库存
boolean cancelDeduct(String businessId);
}【Caution】TCC 三个方法必须保证幂等,且 Cancel 必须能处理 Try 未执行的情况。
【Caution】以下事务反模式必须严格禁止。
// 错误:事务内包含循环远程调用,事务持续数十秒
@Transactional
public void batchProcess(List<Long> ids) {
for (Long id : ids) {
Order order = orderMapper.selectById(id);
remoteService.notify(order); // 远程调用在事务内
orderMapper.update(order);
}
}
// 正确:循环外开启事务,远程调用移出
public void batchProcess(List<Long> ids) {
for (Long id : ids) {
Order order = orderMapper.selectById(id);
transactionTemplate.execute(s -> {
orderMapper.update(order);
return null;
});
remoteService.notify(order); // 事务外调用
}
}一次事务操作大量数据,导致锁表、回滚慢:
// 错误:一次更新 10 万条
@Transactional
public void batchUpdate() {
orderMapper.updateAllStatus(); // 全表更新,长事务锁表
}
// 正确:分批更新
public void batchUpdate() {
int batchSize = 500;
Long lastId = 0L;
while (true) {
List<Long> ids = orderMapper.selectIds(lastId, batchSize);
if (ids.isEmpty()) break;
transactionTemplate.execute(s -> {
orderMapper.batchUpdateStatus(ids);
return null;
});
lastId = ids.get(ids.size() - 1);
}
}// 错误:REQUIRES_NEW 导致外层事务异常时内层已提交
@Transactional
public void outer() {
orderMapper.insert(order);
innerService.inner(); // REQUIRES_NEW,独立提交
throw new RuntimeException(); // 外层回滚,但内层已提交
}// 错误:查询方法开启读写事务,占用资源
@Transactional
public OrderDTO getOrder(Long id) {
return orderMapper.selectById(id);
}
// 正确:只读事务
@Transactional(readOnly = true)
public OrderDTO getOrder(Long id) {
return orderMapper.selectById(id);
}