Seata Transaction Isolation
This article aims to help users understand how to correctly implement transaction isolation when using Seata AT mode to prevent dirty reads and writes.
It is expected that readers have already read the introduction to the AT mode on the Seata official website and have an understanding of local database locks.
(For example, when two transactions are simultaneously updating the same record, only the transaction that holds the record lock can update successfully, while the other transaction must wait until the record lock is released, or until the transaction times out)
First, take a look at this piece of code. Although it looks "basic," the main thing that the persistence layer framework actually does for us is just that.
@Service
public class StorageService {
@Autowired
private DataSource dataSource;
@GlobalTransactional
public void batchUpdate() throws SQLException {
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false);
String sql = "update storage_tbl set count = ?" +
" where id = ? and commodity_code = ?";
preparedStatement = connection.prepareStatement(sql);
preparedStatement.setInt(1, 100);
preparedStatement.setLong(2, 1);
preparedStatement.setString(3, "2001");
preparedStatement.executeUpdate();
connection.commit();
} catch (Exception e) {
throw e;
} finally {
IOutils.close(preparedStatement);
IOutils.close(connection);
}
}
}
Starting with the Proxy Data Source
When using the AT mode, the most important thing is the proxy data source. So what is the purpose of using DataSourceProxy to proxy the data source?
DataSourceProxy can help us obtain several important proxy objects
-
Obtain
ConnectionProxythroughDataSourceProxy.getConnection() -
Obtain
StatementProxythroughConnectionProxy.prepareStatement(...)
Seata's implementation of transaction isolation is hidden in these 2 proxies, let me outline the implementation logic first.
Processing logic of StatementProxy.executeXXX()
-
When calling
io.seata.rm.datasource.StatementProxy.executeXXX(), the SQL is passed toio.seata.rm.datasource.exec.ExecuteTemplate.execute(...)to process.- In the
ExecuteTemplate.execute(...)method, Seata uses different Executers based on differentdbTypeand SQL statement types, and calls theexecute(Object... args)method of theio.seata.rm.datasource.exec.Executerclass. - If a DML type Executer is chosen, the following main actions are performed:
- Pre-query image (select for update, obtaining local lock at this time)
- Execute business SQL
- Post-query image
- Prepare undoLog
- If your SQL is
select for update, thenSelectForUpdateExecutorwill be used (Seata proxiesselect for update), and the logic for post-processing after proxying is as follows:- First, execute select for update (obtain the database's local lock)
- If in
@GlobalTransactionalor@GlobalLock, check if there is a global lock - If there is a global lock, under the condition of not starting a local transaction, rollback the local transaction, then re-acquire the local lock and global lock, and so on, unless the global lock is obtained.
- In the
Handling logic of ConnectionProxy.commit()
- In a global transaction (i.e., the data persistence method has
@GlobalTransactional)- Register branch transaction, obtain global lock
- UndoLog data persistence
- Let the database commit the current transaction
- In
@GlobalLock(i.e., the data persistence method has@GlobalLock)- Query the TC for the existence of a global lock, and if it exists, throw an exception
- Let the database commit the current transaction
- For other cases (the
elsebranch)- Let the database commit the current transaction
Purpose of @GlobalTransactional
Identifies a global transaction
Purpose of @GlobalLock + select for update
If a method like updateA() has @GlobalLock + select for update, Seata, in processing, will first obtain a database local lock, then query if there is a global lock for that record, and if there is, it will throw a LockConflictException.