Spring MVC 和事务边界怎么理解:Controller 单例、rollbackFor 和事务回调

3次阅读
没有评论

Spring MVC 和事务问题经常分散在不同地方:Controller 参数绑定、单例线程安全、@Transactional 不回滚、事务里发消息、rollback-only。把它们放到一起看,其实都在讲同一件事:Web 请求进入后,状态、线程和数据库事务的边界要清楚。

Controller 默认是单例

Spring MVC 的 @Controller 默认是 singleton。也就是说,一个 Controller 实例会被多个请求线程共享。

错误示例:

@Controller
public class HomeController {
    private int count;

    @GetMapping("/count")
    @ResponseBody
    public int count() {
        return ++count;
    }
}

count 是共享字段,并发访问时结果不可预测。Controller 里不要放请求级可变状态,状态应该放在方法局部变量、请求对象、数据库、缓存或明确的上下文里。

如果使用 ThreadLocal,一定要在请求结束时 remove。应用服务器线程会复用,不清理就可能把上个请求的数据带到下个请求。

参数绑定看三类

Spring MVC 常见绑定注解:

  • @RequestParam:query 或表单参数。
  • @PathVariable:路径变量。
  • @RequestBody:JSON 请求体。

接口入参越复杂,越需要明确校验。后端不能把未校验的请求体直接传到业务层。推荐把请求 DTO、校验注解和业务模型分开,让 Controller 负责协议边界,Service 负责业务语义。

@Transactional 默认只回滚运行时异常

Spring 事务默认对 RuntimeExceptionError 回滚,对受检异常不回滚。

如果业务方法可能抛受检异常,并且希望回滚,要写:

@Transactional(rollbackFor = Exception.class)
public void createOrder() throws Exception {
    // ...
}

更好的方式是按业务异常精确指定 rollbackFor,避免所有异常都混在一起。

事务注解建议放在实现类或具体方法上,不要只放接口上。这样在代理方式变化时更稳定,也方便阅读。

rollback-only 是事务已被标记回滚

经典异常:

Transaction rolled back because it has been marked as rollback-only

常见场景是:事务方法 A 调用事务方法 B,B 抛异常后被 A 捕获吞掉,但底层事务已经被标记为 rollback-only。A 最后想提交时,Spring 发现这个物理事务只能回滚,于是报错。

处理方式:

  • 不要吞掉应该导致回滚的异常。
  • 如果 B 必须独立提交或回滚,考虑 REQUIRES_NEW
  • 把事务边界拆清楚,不要让一个大事务包住过多动作。

事务里不要塞慢 IO

反例:

@Transactional
public void createOrder(Order order) {
    saveOrder(order);
    sendRpc();
    sendMessage();
}

RPC、MQ、HTTP 调用不属于数据库 ACID 的一部分,却会占着数据库连接和锁。高并发下,这类大事务很容易把连接池拖死。

更稳的方式是:

  • 数据库写入在事务内完成。
  • 事务外触发后续动作。
  • 要保证一致性时,用事务消息、outbox 表或任务表异步补偿。

TransactionSynchronization 可以注册事务完成后的回调,但不要在回调里做重 IO。回调仍处在事务收尾边界,抛异常和连接占用都容易制造新的复杂度。

最后抓住一句话

Spring MVC 负责请求边界,Service 负责事务边界。Controller 不存请求状态,事务方法明确回滚规则,数据库事务里只放必须原子提交的数据库操作,这三条能避开很多隐蔽问题。

正文完
 0
bdspAdmin
版权声明:本站原创文章,由 bdspAdmin 于2026-07-01发表,共计1566字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)