• 一、seata全局锁


    所有文章

    https://www.cnblogs.com/lay2017/p/12485081.html

    正文

    seata的at模式主要实现逻辑是数据源代理,而数据源代理将基于如MySQL和Oracle等关系事务型数据库实现,基于数据库的隔离级别为read committed。换而言之,本地事务的支持是seata实现at模式的必要条件,这也将限制seata的at模式的使用场景。

    官方文档给出了非常好的图来说明at模式下,全局锁与隔离相关的逻辑:https://seata.io/zh-cn/docs/dev/mode/at-mode.html

    写隔离

    首先,我们理解一下写隔离的流程

    分支事务1-开始
    | 
    V 获取 本地锁
    | 
    V 获取 全局锁    分支事务2-开始
    |               |
    V 释放 本地锁     V 获取 本地锁
    |               |
    V 释放 全局锁     V 获取 全局锁
                    |
                    V 释放 本地锁
                    |
                    V 释放 全局锁

    如上所示,一个分布式事务的锁获取流程是这样的

    1)先获取到本地锁,这样你已经可以修改本地数据了,只是还不能本地事务提交

    2)而后,能否提交就是看能否获得全局锁

    3)获得了全局锁,意味着可以修改了,那么提交本地事务,释放本地锁

    4)当分布式事务提交,释放全局锁。这样就可以让其它事务获取全局锁,并提交它们对本地数据的修改了。

    可以看到,这里有两个关键点

    1)本地锁获取之前,不会去争抢全局锁

    2)全局锁获取之前,不会提交本地锁

    这就意味着,数据的修改将被互斥开来。也就不会造成写入脏数据。全局锁可以让分布式修改中的写数据隔离。

    beforeImage校验全局锁

    在将StatementProxy的时候我们提到过,在执行业务sql之前。会生成一个前置数据镜像,也就是beforeImage方法。

    那么,beforeImage方法中将会生成一个 select [字段] from [表] where [条件] for update 这样的sql来查询镜像数据。seata的数据源代理将会对select for update这样的语句进行代理,代理中将会检验一下全局锁是否冲突,如下所示

    while (true) {
        try {
            // 执行sql
            rs = statementCallback.execute(statementProxy.getTargetStatement(), args);
    
            // 构建数据行
            TableRecords selectPKRows = buildTableRecords(getTableMeta(), selectPKSQL, paramAppenderList);
            // 构建锁KEY
            String lockKeys = buildLockKey(selectPKRows);
            if (StringUtils.isNullOrEmpty(lockKeys)) {
                break;
            }
    
            if (RootContext.inGlobalTransaction()) {
                // 校验全局锁
                statementProxy.getConnectionProxy().checkLock(lockKeys);
            } else if (RootContext.requireGlobalLock()) {
                statementProxy.getConnectionProxy().appendLockKey(lockKeys);
            } else {
                throw new RuntimeException("Unknown situation!");
            }
            break;
        } catch (LockConflictException lce) {
            if (sp != null) {
                conn.rollback(sp);
            } else {
                conn.rollback();
            }
            // 锁冲突,重试
            lockRetryController.sleep(lce);
        }
    }

    如代码所示,select for update会做一次全局锁校验(checkLock会去调用Server端)。如果出现锁冲突,那么不断进行重试。

    这样依赖,select for update所在的本地事务只要等待全局锁释放,由于已经占了本地锁,所以可以顺利获取全局锁。而后,进行入update等业务操作,然后提交顺利提交本地事务,

    seata也表明,默认的事务隔离级别是read uncommitted。那么要实现read committed的话,就可以使用select for update来实现,逻辑和这里是一样的,其实就是通过占用本地锁,然后重试等待全局锁来达到读写隔离的目的。

    分支事务register占用锁

    在看分支事务register的时候,我们只是简单地扫了扫BranchSession的创建,然后添加到GlobalSession中。

    这其中忽略了一个要点,就是在branch的register过程,会进行全局锁的获取操作。客户端会讲tablename和数据行的primary key给构造成lock key传输到Server端。而Server端将会根据这个lock key来判断是否能够占用全局锁

    我们看看seata关于database方式的实现,跟进LockStoreDataBaseDao的acquireLock(List<LockDO> lockDOs)方法

    方法很长,删减以后逻辑其实很简单。就是构造一个checkLock的sql,查查看是否已经有相关的数据。如果没有则进行doAcquireLock占用操作,占用操作也很简单就是进行数据插入。

    前面checkLock提到的调用Server端的校验,其实也就是构造并执行一下checkLock看看有没数据而已

    @Override
    public boolean acquireLock(List<LockDO> lockDOs) {
        // ...
        try {
            // ...
    
            // 获取checkLock的sql语句
            String checkLockSQL = LockStoreSqls.getCheckLockableSql(lockTable, sj.toString(), dbType);
            ps = conn.prepareStatement(checkLockSQL);
            // ...
            // 查询是否有占用的数据
            rs = ps.executeQuery();
            String currentXID = lockDOs.get(0).getXid();
            while (rs.next()) {
                String dbXID = rs.getString(ServerTableColumnsName.LOCK_TABLE_XID);
                if (!StringUtils.equals(dbXID, currentXID)) {
                    canLock &= false;
                    break;
                }
                // ...
            }
    
            if (!canLock) {
                conn.rollback();
                return false;
            }
    
            // ...
    
            if (unrepeatedLockDOs.size() == 1) {
                LockDO lockDO = unrepeatedLockDOs.get(0);
                // 进行占用操作
                if (!doAcquireLock(conn, lockDO)) {
                    // ...
                }
            } else {
                // 进行占用操作
                if (!doAcquireLocks(conn, unrepeatedLockDOs)) {
                    // ...
                }
            }
            conn.commit();
            return true;
        } catch (SQLException e) {
            // ...
        } finally {
            // ...
        }
    }

    那么checkLockSql和doAcquireLock分开两个步骤是否会有并发问题呢?

    理论上不会有的,正如我们前面一直提到的,要先获取本地锁,再来查询获取全局锁。所以,当本地锁还没有获取的时候,不会去获取全局锁。也就不需要考虑并发问题

    如果占用全局锁失败怎么办呢?客户端会进行锁冲突的判断,然后进行重试操作。

    分支事务释放全局锁

    而分支事务在从GlobalSession中remove的时候会去unlock全局锁,如下GlobalSession中的代码

    @Override
    public void removeBranch(BranchSession branchSession) throws TransactionException {
        for (SessionLifecycleListener lifecycleListener : lifecycleListeners) {
            lifecycleListener.onRemoveBranch(this, branchSession);
        }
        branchSession.unlock();
        remove(branchSession);
    }

    从unlock一路跟进LockStoreDataBaseDao的unlock(String xid, Long branchId)会看看

    @Override
    public boolean unLock(String xid, Long branchId) {
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = logStoreDataSource.getConnection();
            conn.setAutoCommit(true);
            // 批量删除的sql构造并执行
            String batchDeleteSQL = LockStoreSqls.getBatchDeleteLockSqlByBranch(lockTable, dbType);
            ps = conn.prepareStatement(batchDeleteSQL);
            ps.setString(1, xid);
            ps.setLong(2, branchId);
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new StoreException(e);
        } finally {
            IOUtil.close(ps, conn);
        }
        return true;
    }

    其实就是去删除之前doAcquireLock方法insert进去的数据,就算解锁了

    @GlobalLock

    有的方法它可能并不需要@GlobalTransactional的事务管理,但是我们又希望它对数据的修改能够加入到seata机制当中。那么这时候就需要@GlobalLock了。

    加上了@GlobalLock,在事务提交的时候就回去checkLock校验一下全局锁。

    private void processLocalCommitWithGlobalLocks() throws SQLException {
        // 全局锁校验
        checkLock(context.buildLockKeys());
        try {
            // 提交本地事务
            targetConnection.commit();
        } catch (Throwable ex) {
            throw new SQLException(ex);
        }
        context.reset();
    }

    可以看到,在本地事务提交之前会调用checkLock校验全局锁,和之前在事务中的写隔离一样的逻辑。也一样的,如果出现锁冲突的话进行重试操作

    总结

    本文简单看了几个全局锁的场景,可以感觉到只要遵循本地锁、全局锁的获取和释放的逻辑顺序,将数据读写的操作纳入seata的管理里面就可以基本做到维持数据一致性。

  • 相关阅读:
    iScroll.js 用法参考
    行内元素和块级元素
    struct和typedef struct彻底明白了
    C/C++语法知识:typedef struct 用法详解
    不是技术牛人,如何拿到国内IT巨头的Offer (转载)
    笔试客观题-----每天收集一点点
    <C++Primer>第四版 阅读笔记 第一部分 “基本语言”
    <C++Primer>第四版 阅读笔记 第四部分 “面向对象编程与泛型编程”
    <C++Primer>第四版 阅读笔记 第三部分 “类和数据抽象”
    <C++Primer>第四版 阅读笔记 第二部分 “容器和算法”
  • 原文地址:https://www.cnblogs.com/lay2017/p/12528071.html
Copyright © 2020-2023  润新知