Seata事务隔离
本文目标:帮助用户明白使用Seata AT模式时,该如何正确实现事务隔离,防止脏读脏写。
希望读者在阅读本文前,已阅读过seata官网中对AT模式的介绍,并且对数据库本地锁有所了解
(例如,两个事务同时在对同一条记录做update时,只有拿到record lock的事务才能更新成功,另一个事务在record lock未释放前只能等待,直到事务超时)
首先请看这样的一段代码,尽管看着“初级”,但持久层框架实际上帮我们做的主要事情也就这样。
@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);
        }
    }
}
从代理数据源说起
使用AT模式,最重要的事情便是代理数据源,那么用DataSourceProxy代理数据源有什么作用呢?
DataSourceProxy能帮助我们获得几个重要的代理对象
- 
通过 DataSourceProxy.getConnection()获得ConnectionProxy
- 
通过 ConnectionProxy.prepareStatement(...)获得StatementProxy
Seata的如何实现事务隔离,就藏在这2个Proxy中,我先概述下实现逻辑。
StatementProxy.executeXXX()的处理逻辑
- 
当调用 io.seata.rm.datasource.StatementProxy.executeXXX()会将sql交给io.seata.rm.datasource.exec.ExecuteTemplate.execute(...)处理。- ExecuteTemplate.execute(...)方法中,Seata根据不同dbType和sql语句类型使用不同的Executer,调用- io.seata.rm.datasource.exec.Executer类的- execute(Object... args)。
- 如果选了DML类型Executer,主要做了以下事情:
- 查询前镜像(select for update,因此此时获得本地锁)
- 执行业务sql
- 查询后镜像
- 准备undoLog
 
- 如果你的sql是select for update则会使用SelectForUpdateExecutor(Seata代理了select for update),代理后处理的逻辑是这样的:- 先执行 select for update(获取数据库本地锁)
- 如果处于@GlobalTransactionalor@GlobalLock,检查是否有全局锁
- 如果有全局锁,则未开启本地事务下会rollback本地事务,再重新争抢本地锁和全 局锁,以此类推,除非拿到全局锁
 
 
ConnectionProxy.commit()的处理逻辑
- 处于全局事务中(即,数据持久化方法带有@GlobalTransactional)- 注册分支事务,获取全局锁
- undoLog数据入库
- 让数据库commit本次事务
 
- 处于@GlobalLock中(即,数据持久化方法带有@GlobalLock)- 向tc查询是否有全局锁存在,如存在,则抛出异常
- 让数据库commit本次事务
 
- 除了以上情况(else分支)- 让数据库commit本次事务
 
@GlobalTransactional的作用
标识一个全局事务
@GlobalLock + select for update的作用
如果像updateA()方法带有@GlobalLock + select for update,Seata在处理时,会先获取数据库本地锁,然后查询该记录是否有全局锁存在,若有,则抛出LockConflictException。