Spring+Mybatis事務@Transactional註解timeout屬性作用過程原始碼淺層Debug

不爱吃蘑菇的大蘑菇發表於2024-08-06

結論:事務方法中呼叫的其他支援事務的方法執行超時才能夠觸發timeout

其他支援事務的方法恐怕目前接觸的就只有資料庫操作了。
一個事務方法A(如插入)在截至時間之前執行結束後,無論後續的非事務方法執行多久都不會觸發timeout,也就是這個事務方法A(如插入)會commit(如插入持久化)。
下面是一個很簡單的儲存業務

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    @Transactional(timeout = 10) // 10單位為秒
    public void insert(UserInfo userInfo) throws InterruptedException {
        int insert = userMapper.insert(userInfo);
        Thread.sleep(13000); // 各種耗時操作模擬,如IO。 13000單位為毫秒
    }

請求到來後,第一行方法userMapper.insert()執行之前,ResourceHolderSupport類會執行setTimeoutInMillis方法,結合timeout計算出一個事務deadline"死期",至於這個ResourceHolderSupport類怎麼來,順著下面Debug可以看到

流水賬預警

Debug進入userMapper.insert()方法

  1. 進入MybatisMapperProxy類invoke方法
  2. 進入method.invoke(this, args)方法
  3. 進入this.mapperMethod.execute(sqlSession, args)方法轉到MybatisMapperMethod類execute方法
  4. 進入sqlSession.insert(this.command.getName(), param)方法轉到SqlSessionTemplate類insert方法
  5. 進入this.sqlSessionProxy.insert(statement, parameter)方法轉到SqlSessionInterceptor類invoke方法
  6. 進入425行方法轉到DefaultSqlSession類update(statement, parameter)方法
  7. 進入最後返回的executor.update(ms, wrapCollection(parameter))方法轉到CachingExecutor類
  8. 進入最後返回的delegate.update(ms, parameterObject)方法轉到BaseExecutor類
  9. 進入最後返回的doUpdate(ms, parameter)方法轉到SimpleExecutor類prepareStatement(handler, ms.getStatementLog())方法

    這裡可以看到獲得transaction設定的timeout了
  10. 進入90行的transaction.getTimeout()方法轉到SpringManagedTransaction類
  11. 進入最後返回的holder.getTimeToLiveInSeconds()方法轉到ResourceHolderSupport類

    這裡已經可以看到有時間相關的內容了,往上滑動就能看到setTimeoutInSeconds方法setTimeoutInMillis,在這裡打一個斷點,如果重新請求,請求後就先到這個斷點設定時間,然後才進入userMapper.insert(userInfo)方法
  12. 進入getTimeToLiveInMillis()方法

    可以看到checkTransactionTimeout(timeToLive <= 0)方法了
  13. 進入checkTransactionTimeout

    可以看到最終timeout異常丟擲程式碼

也就是說,沒有支援事務的方法超時然後丟擲TransactionTimedOutException異常,就不會觸發設定的timeout進行回滾
上面定義的service中,insert顯然是10秒中內就可以執行完的,重新執行了傳送了一次請求

這個方法還有7.5秒能活呢
直接從checkTransactionTimeout()方法中出來,一路返回到SqlSessionTemplate類

下面的if判斷了是否有SqlSession事務,沒有就直接commit
最後執行Thread.Sleep方法睡13秒
雖然方法總體執行時間>10秒,但是並沒有觸發timeout
那定義一個這樣的方法行嗎?

    @Override
    @Transactional
    public void hold() throws InterruptedException {
        Thread.sleep(13000)  // 各種耗時操作模擬,如IO
    }

試過了,不行,不是加了一個@Transactional註解方法就支援事務了。
但是如果先執行Thread.sleep(13000)就可以觸發timeout,至於為什麼,前面的流水賬已經展示了timeout觸發的過程

怎麼設計一個資料庫操作+非事務操作如何支援事務?

看到網上一個帖子中一個博主用一個額外的輕量資料庫操作放在最後來實現
如果service裡的userMapper.insert操作有冪等性,最簡單可以這樣做

    @Override
    @Transactional(timeout = 10)
    public void insert(UserInfo userInfo) throws InterruptedException {
        int insert = userMapper.insert(userInfo);
        Thread.sleep(13000); // 各種耗時操作模擬,如IO
        int insert1 = userMapper.insert(userInfo); // 輕量化資料庫操作方法,用來觸發timeout來rollback
    }

這時伺服器就會報錯類似org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Tue Aug 06 15:02:39 CST 2024rollback第一行insert
但是要注意非事務方法,如果需要rollback需要自己進行實現。
具體怎麼合理的去實現就是另外一個話題了。

其他程式碼

程式碼都是隨手寫的,沒有遵守規範

@Data
public class UserInfo {

    @TableId(value = "u_id")
    private int uId;

    private String uName;

    private String uGender;
}
@Controller
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/add")
    public void addUser(@RequestBody UserInfo userInfo) throws InterruptedException {
        userService.insert(userInfo);
    }
}
public interface UserService {

    void insert(UserInfo userInfo) throws InterruptedException;

}
@Mapper
public interface UserMapper extends BaseMapper<UserInfo> {
}
DROP TABLE IF EXISTS `user_info`;
CREATE TABLE `user_info`  (
                         `u_id` int NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY ,
                         `u_name` varchar(20) NOT NULL COMMENT 'name',
                         `u_gender` enum('male','female') NOT NULL COMMENT 'gender'
);
// 請求json
{
    "uid": null,
    "uname":"打打怪",
    "ugender": "male"
}
// pom.xml版本

// spring版本
<spring-boot.version>2.7.6</spring-boot.version>

// myabtis-plus版本
<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.5.7</version>
</dependency>
<dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter-test</artifactId>
            <version>3.5.7</version>
</dependency>

// lombok
<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.30</version>
            <scope>provided</scope>
</dependency>

相關文章