应用事务管理混乱导致的一个坑

Spring 的事务传播属性

org.springframework.transaction.annotation.Propagation 定义了 Spring 的事务传播属性:

  • REQUIRED: 支持当前事务,如果不存在则新建一个。

  • REQUIRES_NEW: 创建新的事务,如果当前存在一个则 suppend 当前的。

  • SUPPORTS: 支持当前事务,如果不存在则以非事务方式执行。

  • MANDATORY: 支持当前事务,如果不存在则抛出异常。

  • NOT_SUPPORTED: 以非事务方式执行,如果当前存在一个事务则 suppend 当前的。

  • NEVER: 以非事务方式执行,如果存在事务则抛出异常。

  • NESTED: 如果当前存在一个事务则以嵌套事务的方式执行。

一个生产问题

一开始是 DBA 反馈数据库出现两种现象:

  1. 出现一些操作做完但会话一直还在等待客户端的提交动作。
  2. 偶尔出现大量的行锁,导致 JVM 线程互相等待而假死。

有个获取流水号的方法 systemService.getSerialNo 的事务传播属性是 NOT_SUPPORTED 的,这个方法通过类似这样的 update t_serialno set serial_no = :newSerialNo where serial_key = :key and serial_no = :oldSerialNo SQL 语句进行更新,更新返回的受影响行数等于 1 认为新的流水号是不重复的,更新不成功则重试。

行锁就出现在这些流水号的更新上。

有些遗留代码需要直接使用数据库连接对象,包装成 ConnectionWrapper(Connection) 对象,这个 ConnectionWrapper 的构造函数把传入的 Connection 对象设置为非自动提交。

出问题的方法体大概如下:

public void submit() {
    Connection connection = sqlSessionTemplate.getConnection();
    ConnectionWrapper wrapper = new ConnectionWrapper(Connection);
    // ...
    String serialNo = systemService.getSerialNo(,,,);
}

sqlSessionTemplate.getConnection() 获得的连接默认是跟当前线程绑定的,systemService.getSerialNo 在没有事务的方法里,会重用当前线程的连接,然而此处这个连接没有启动提交事务的能力(如果 submit 方法上有声明事务,则会 suppend 当前事务,然后用一个新连接去完成非事务的操作)。该方法会多次获取流水号,在并发压力大时就会出现互相等待。

后续的代码并没有把 连接 设置回 自动提交,检查发现是采用的 DBCP 连接池在连接返回给池时会自动把连接设置为自动提交。

之前一直没想明白:事务管理都是由 Spring 管理的,为什么会出现事务做完了又不提交?就算有的采用了遗留代码导致没有手工提交事务,那么后续用到这个连接的其他请求的事务也不会提交,这应该很快就能发现才对呀,现实中好像没有出现。

行锁一直等待也是因为没有设置语句执行的超时时间(DBCP 1.4 好像没参数可设),在连接池获取连接上一直等待是因为没有设置获取连接的最大等待时间。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.