解密seata全局锁(一)

从哪开始分析呢?

seata的基本原理可以参照2PC, 而本地事务方面的实现是通过代理方式实现扩展, 如: 在执行提交前后生成image等

StatementProxy

  • 熟悉JDBC的同学应该都知道Statement的作用吧, seata实现Statement功能的代理类就是StatementProxy

  • 通过观察StatementProxy里面的方法可以发现所有的操作基本都是委派给ExecuteTemplate实现的

    @Override
        public int executeUpdate(String sql) throws SQLException {
            this.targetSQL = sql;
            return ExecuteTemplate.execute(this, (statement, args) -> statement.executeUpdate((String) args[0]), sql);
        }
    

ExecuteTemplate

  • 继续追踪execute()方法, 上面的内容主要是做一些执行前的判断, 还有就是选择合适的Executor, 如UpdateExecutor

  • 我们只关注较为核心的executor.execute(args)

        public static <T, S extends Statement> T execute(List<SQLRecognizer> sqlRecognizers,
                                                         StatementProxy<S> statementProxy,
                                                         StatementCallback<T, S> statementCallback,
                                                         Object... args) throws SQLException {
    
            ...
            T rs;
            try {
                // 核心代码
                rs = executor.execute(args);
            } catch (Throwable ex) {
                if (!(ex instanceof SQLException)) {
                    // Turn other exception into SQLException
                    ex = new SQLException(ex);
                }
                throw (SQLException) ex;
            }
            return rs;
        }
    
  • 继续则可以一直追踪到io.seata.rm.datasource.exec.AbstractDMLBaseExecutor#doExecute

    @Override
        public T doExecute(Object... args) throws Throwable {
            AbstractConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
            // 通过判断是否自动提交分别执行方法, 这里我们只关注executeAutoCommitFalse, 感兴趣的同学查看executeAutoCommitTrue原理一探究竟
            if (connectionProxy.getAutoCommit()) {
                return executeAutoCommitTrue(args);
            } else {
                return executeAutoCommitFalse(args);
            }
        }
    

executeAutoCommitFalse

executeAutoCommitFalse主要做了这么几件事

  • 判断只有mysql支持复合主键
  • 执行语句之前生成beforeImage
  • 执行语句
  • 执行语句之后生成afterImage
  • 根据beforeImage和afterImage生成UndoLog
    protected T executeAutoCommitFalse(Object[] args) throws Exception {
        if (!JdbcConstants.MYSQL.equalsIgnoreCase(getDbType()) && getTableMeta().getPrimaryKeyOnlyName().size() > 1)
        {
            throw new NotSupportYetException("multi pk only support mysql!");
        }
        TableRecords beforeImage = beforeImage();
        T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
        TableRecords afterImage = afterImage(beforeImage);
        prepareUndoLog(beforeImage, afterImage);
        return result;
    }
  • 我们先来大体看一下beforeImage到底是在干什么

    @Override
        protected TableRecords beforeImage() throws SQLException {
            ArrayList<List<Object>> paramAppenderList = new ArrayList<>();
            // 获取数据表元数据信息
            TableMeta tmeta = getTableMeta();
            // 构建一个select语句, tmeta里有表名, paramAppenderList用来存储参数
            String selectSQL = buildBeforeImageSQL(tmeta, paramAppenderList);
            // 核心操作
            return buildTableRecords(tmeta, selectSQL, paramAppenderList);
        }
    
        protected TableRecords buildTableRecords(TableMeta tableMeta, String selectSQL, ArrayList<List<Object>> paramAppenderList) throws SQLException {
            	...
                // 执行获取的select语句, 将结果集rs传入, 接下来方法执行的就是取出结果集的操作, 这里不再深追
                return TableRecords.buildRecords(tableMeta, rs);
            } finally {
                IOUtil.close(rs);
            }
        }
    
  • afterImage原理相同, 也就是说beforeImageafterImage的作用就是把执行操作之前的结果集和执行操作之后的结果集查询出来

  • 接下来我们看prepareUndoLog方法

        protected void prepareUndoLog(TableRecords beforeImage, TableRecords afterImage) throws SQLException {
            if (beforeImage.getRows().isEmpty() && afterImage.getRows().isEmpty()) {
                return;
            }
    
            ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();
    
            TableRecords lockKeyRecords = sqlRecognizer.getSQLType() == SQLType.DELETE ? beforeImage : afterImage;
            // 构建全局锁的key
            String lockKeys = buildLockKey(lockKeyRecords);
            connectionProxy.appendLockKey(lockKeys);
    
            SQLUndoLog sqlUndoLog = buildUndoItem(beforeImage, afterImage);
            connectionProxy.appendUndoLog(sqlUndoLog);
        }
    
  • buildLockKey()方法

    • 注意注释中的return, 说明了构建的全局锁的key到底长什么样, 具体构建过程先省略了, 其实就是把记录中包含的所有主键按照规律拼接字符串
    /**
         * build lockKey
         *
         * @param rowsIncludingPK the records
         * @return the string as local key. the local key example(multi pk): "t_user:1_a,2_b"
         */
        protected String buildLockKey(TableRecords rowsIncludingPK) {
            ...
        }
    
  • buildUndoItem()方法

    /**
         * build a SQLUndoLog
         *
         * @param beforeImage the before image
         * @param afterImage  the after image
         * @return sql undo log
         */
        protected SQLUndoLog buildUndoItem(TableRecords beforeImage, TableRecords afterImage) {
            // 获取sql类型和表名, 创建sqlundoLog对象
            SQLType sqlType = sqlRecognizer.getSQLType();
            String tableName = sqlRecognizer.getTableName();
    
            SQLUndoLog sqlUndoLog = new SQLUndoLog();
            sqlUndoLog.setSqlType(sqlType);
            sqlUndoLog.setTableName(tableName);
            sqlUndoLog.setBeforeImage(beforeImage);
            sqlUndoLog.setAfterImage(afterImage);
            return sqlUndoLog;
        }
    

总结

通过上面的分析我们知道了:

  1. beforeImage和afterImage是在执行sql之前和之后查询的结果集
  2. 全局锁的key是由结果集中的主键按照一定规律拼接而成
  3. undoLog由beforeImage和afterImage组成
  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 12
    评论
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值