跳转到内容

事务使用规范

事务边界、隔离级别、分布式事务


【Requirement】事务必须短小精悍;禁止在事务内执行远程调用、耗时计算或非数据库操作。

  1. 事务内只包含必要的数据库操作
  2. 事务开始到提交的时间越短越好(建议 < 1 秒)
  3. 查询操作(只读)不应放入事务
  4. 远程调用(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 失效,必须规避:

  1. 同类内部方法调用(this.method()),AOP 不生效
  2. 方法非 public
  3. 异常被 catch 吞掉未抛出
  4. 抛出的异常不在 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);
}