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
ConnectionProxy
throughDataSourceProxy.getConnection()
-
Obtain
StatementProxy
throughConnectionProxy.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 differentdbType
and SQL statement types, and calls theexecute(Object... args)
method of theio.seata.rm.datasource.exec.Executer
class. - 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
, thenSelectForUpdateExecutor
will 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
@GlobalTransactional
or@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
else
branch)- 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.
Let's first give an example of dirty write, and then see how Seata prevents dirty write
Let's assume your business code is like this:
updateAll()
is used to update records in both table A and B,updateA()
andupdateB()
are used to update records in table A and B respectivelyupdateAll()
has already been annotated with@GlobalTransactional
class YourBussinessService {
DbServiceA serviceA;
DbServiceB serviceB;
@GlobalTransactional
public boolean updateAll(DTO dto) {
serviceA.update(dto.getA());
serviceB.update(dto.getB());
}
public boolean updateA(DTO dto) {
serviceA.update(dto.getA());
}
}
class DbServiceA {
@Transactional
public boolean update(A a) {
}
}
|