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.