MyBatis Multi-Threaded Transactions

Time: Column:Backend & Servers views:185

The Premise of Multi-Threaded Transactions: The Same Connection, The Same Transaction

Previously, we worked with JPA multi-threaded transactions. Now, let's look at MyBatis multi-threaded transactions.

Scenario: Multi-threaded data insertion, where if one insert operation fails, all operations should be rolled back.

Normally, transactions are handled using @Transactional, but in a multi-threaded environment, using @Transactional doesn’t work. This is because, just like with JPA multi-threaded transactions, they don't share the same connection or transaction. Therefore, if one thread fails, the rollback won't affect the other threads.

Example Code Where @Transactional Doesn't Work in Multi-Threaded Context

Mapper Code (for inserting data):

public interface ThreadMapper {

    @Insert("insert into user(id,name,age) values(#{id},#{name},#{age})")
    void insertUser(User user);

}

Service Code:

@Service
public class ThreadService {

    @Autowired
    private ThreadMapper threadMapper;

    // Normal thread without exception
    @Transactional
    public void testTransactional(){
        User user = new User();
        user.setId("888");
        user.setName("zhangsan");
        user.setAge("888");

        threadMapper.insertUser(user);
    }

    // Thread simulating an exception
    @Transactional
    public void testTransactional1(){
        User user = new User();
        user.setId("666");
        user.setName("lisi");
        user.setAge("666");

        threadMapper.insertUser(user);
        System.out.println(1/0);  // Intentionally causing an exception
    }
}

Testing the Multi-Threaded Transactions

import com.wzw.thread.ThreadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
@RequestMapping("/thread")
public class ThreadController {

    @Autowired
    private ThreadService threadService;

    @RequestMapping("/test1")
    @Transactional
    public void threadTrans() throws InterruptedException {
        ExecutorService service = Executors.newFixedThreadPool(10);

        // Ensure all threads finish before proceeding
        CountDownLatch countDownLatch = new CountDownLatch(2);

        Thread thread = new Thread(() -> {
            try {
                threadService.testTransactional();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
            }
        });

        Thread thread1 = new Thread(() -> {
            try {
                threadService.testTransactional1();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
            }
        });

        service.submit(thread);
        service.submit(thread1);

        countDownLatch.await();

        System.out.println("Main thread execution complete.");
    }
}

Expected Outcome

In the test, one thread should insert zhangsan correctly, and the other should fail due to an exception.

However, in reality, the transaction does not work as expected. One thread fails, but the other continues and successfully inserts lisi. This is because multi-threaded transactions do not work with @Transactional in this context.

Using SqlSession for Manual Transaction Management

To address this issue, we can handle transactions manually using SqlSession. Here's how we do it.

Normal Scenario Without Exceptions:

@RequestMapping("/test2")
public void test2() throws Exception {
    SqlSession sqlSession = threadService.getSqlSession();
    Connection connection = sqlSession.getConnection();

    try {
        // Manually manage transaction
        connection.setAutoCommit(false);
        // Get the mapper
        ThreadMapper mapper = sqlSession.getMapper(ThreadMapper.class);

        ExecutorService service = Executors.newFixedThreadPool(10);

        Future<Integer> future = service.submit(() -> {
            User user = new User();
            user.setId("888");
            user.setName("zhangsan");
            user.setAge("888");
            mapper.insertUser(user);

            return 1;
        });

        Future<Integer> future1 = service.submit(() -> {
            User user = new User();
            user.setId("666");
            user.setName("lisi");
            user.setAge("666");
            mapper.insertUser(user);

            return 1;
        });

        System.out.println(future.get());
        System.out.println(future1.get());

        connection.commit();

        System.out.println("Thread execution complete.");

    } catch (Exception e) {
        // If an exception occurs, roll back the transaction
        connection.rollback();
        System.err.println("An error occurred.");
    }
}

Scenario with Exception to Test Rollback:

Future<Integer> future1 = service.submit(() -> {
    User user = new User();
    user.setId("666");
    user.setName("lisi");
    user.setAge("666");
    mapper.insertUser(user);
    System.out.println(1/0);  // Intentional exception to trigger rollback
    return 1;
});

In the case of a normal execution, both threads will complete successfully. However, if one thread encounters an exception (due to the System.out.println(1/0);), both operations will roll back as expected.

Conclusion

By using SqlSession to manually manage transactions, we ensure that multi-threaded transactions work as expected, i.e., if one thread fails, all threads will be rolled back, preserving the consistency of the transaction across all threads.