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 事务默认对 RuntimeException 和 Error 回滚,对受检异常不回滚。
如果业务方法可能抛受检异常,并且希望回滚,要写:
@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 不存请求状态,事务方法明确回滚规则,数据库事务里只放必须原子提交的数据库操作,这三条能避开很多隐蔽问题。




