Seata源码(六)Seata的undo日志操作

961 阅读4分钟

banner窄.png

铿然架构  |  作者  /  铿然一叶 这是铿然架构的第 97 篇原创文章

1. 概述

1.1 作用

undo日志用于AT模式下全局事务发生异常时,做数据回滚。

1.2 日志结构

image.png

属性描述
id主键
branch_id分支事务ID
xid全局事务ID
context上下文信息,存放编解码方式和数据压缩类型
rollback_infoundo日志内容
log_status日志状态
log_created日志创建时间
log_modified日志编辑时间

2. 核心类结构

image.png

序号类型
AbstractDMLBaseExecutor负责流程编排,流程包括生成before镜像、执行原生SQL、生成after镜像、缓存undo日志
ConnectionProxy继承AbstractConnectionProxy,提供核心方法实现,包括全局锁,事务,undo日志操作调用
ConnectionContext存储事务操作的关键信息,例如XID,Savepoint,undo日志缓存
UndoLogManagerFactory工厂类,根据SQL类型创建具体的UndoLogManager实现类
UndoLogManager提供Undo日志接口,子类根据不同数据库类型做不同实现
CompressorFactoryundo日志压缩工厂类,用于创建日志压缩实现类
Compressorundo日志压缩接口
UndoLogParserFactoryundo日志编解码工厂类,用于创建日志编解码工厂
UndoLogParserundo日志编解码接口

3. 源码

3.1 undo日志准备

3.1.1 入口

注意:看此方法名,不要误解只有非自动提交模式才写undo日志,实际自动提交方法也会调用此方法,因此不管是否自动提交,只要是AT模式都会写undo日志.

AbstractDMLBaseExecutor.java

    protected T executeAutoCommitFalse(Object[] args) throws Exception {
        if (!JdbcConstants.MYSQL.equalsIgnoreCase(getDbType()) && isMultiPk()) {
            throw new NotSupportYetException("multi pk only support mysql!");
        }
        // 模版方法,交给子类实现
        TableRecords beforeImage = beforeImage();
        // 执行业务SQL
        T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
        // 模版方法,交给子类实现
        TableRecords afterImage = afterImage(beforeImage);
        // 准备undo日志,写入缓存
        prepareUndoLog(beforeImage, afterImage);
        return result;
    }

3.1.2 undo镜像

undo镜像由对应的子类实现:

image.png

镜像核心逻辑如下:

语句类型镜像原理
insert记录插入数据的主键信息,undo时根据主键删除,仅有after镜像。
update记录更新前的数据和主键信息,updo时根据这些信息更新回去;同时要记录更新后的数据和主键信息,用于在undo时比较数据是否发生变化,如果没有变化才做undo操作。包括before和after镜像。
delete记录删除的数据信息,在undo时做插入操作,仅有before镜像。

3.1.3 缓存处理

缓存处理,commit提交时才真正写入数据库:

BaseTransactionalExecutor.java

    protected void prepareUndoLog(TableRecords beforeImage, TableRecords afterImage) throws SQLException {
        if (beforeImage.getRows().isEmpty() && afterImage.getRows().isEmpty()) {
            return;
        }
        if (SQLType.UPDATE == sqlRecognizer.getSQLType()) {
            if (beforeImage.getRows().size() != afterImage.getRows().size()) {
                throw new ShouldNeverHappenException("Before image size is not equaled to after image size, probably because you updated the primary keys.");
            }
        }
        ConnectionProxy connectionProxy = statementProxy.getConnectionProxy();

        TableRecords lockKeyRecords = sqlRecognizer.getSQLType() == SQLType.DELETE ? beforeImage : afterImage;
        String lockKeys = buildLockKey(lockKeyRecords);
        if (null != lockKeys) {
            connectionProxy.appendLockKey(lockKeys);

            SQLUndoLog sqlUndoLog = buildUndoItem(beforeImage, afterImage);
            // 缓存undo日志,commit提交时才真正写入
            connectionProxy.appendUndoLog(sqlUndoLog);
        }
    }

connectionProxy.java写入缓存:

    public void appendUndoLog(SQLUndoLog sqlUndoLog) {
        context.appendUndoItem(sqlUndoLog);
    }

3.2 undo日志写入

3.2.1 ConnectionProxy.java

commit全局事务分支才写undo日志(全局锁和本地事务分支不写入),调用UndoLogManagerFactory创建UndoLogManager的实现类来写入日志:

        private void processGlobalTransactionCommit() throws SQLException {
            try {
                register();
            } catch (TransactionException e) {
                recognizeLockKeyConflictException(e, context.buildLockKeys());
            }
            try {
                // 写入undo日志
                UndoLogManagerFactory.getUndoLogManager(this.getDbType()).flushUndoLogs(this);
                // 代理的Connection提交事务
                targetConnection.commit();
            } catch (Throwable ex) {
                LOGGER.error("process connectionProxy commit error: {}", ex.getMessage(), ex);
                report(false);
                throw new SQLException(ex);
            }
            if (IS_REPORT_SUCCESS_ENABLE) {
                report(true);
            }
            context.reset();
        }

3.2.2 AbstractUndoLogManager.java

具体写入逻辑,根据配置创建UndoLogParser子类做编解码,创建Compressor子类做数据压缩:

        public void flushUndoLogs(ConnectionProxy cp) throws SQLException {
            ConnectionContext connectionContext = cp.getContext();
            if (!connectionContext.hasUndoLog()) {
                return;
            }

            String xid = connectionContext.getXid();
            long branchId = connectionContext.getBranchId();

            BranchUndoLog branchUndoLog = new BranchUndoLog();
            branchUndoLog.setXid(xid);
            branchUndoLog.setBranchId(branchId);
            branchUndoLog.setSqlUndoLogs(connectionContext.getUndoItems());

            UndoLogParser parser = UndoLogParserFactory.getInstance();
            byte[] undoLogContent = parser.encode(branchUndoLog);

            CompressorType compressorType = CompressorType.NONE;
            if (needCompress(undoLogContent)) {
                compressorType = ROLLBACK_INFO_COMPRESS_TYPE;
                undoLogContent = CompressorFactory.getCompressor(compressorType.getCode()).compress(undoLogContent);
            }

            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Flushing UNDO LOG: {}", new String(undoLogContent, Constants.DEFAULT_CHARSET));
            }

            // 此方法由具体的Mysql,Oracle,Postgresql实现类处理
            insertUndoLogWithNormal(xid, branchId, buildContext(parser.getName(), compressorType), undoLogContent, cp.getTargetConnection());
        }

3.2.3 undo日志数据编解码

参数client.undo.logSerialization配置使用的编解码类,默认值jackson。

参数值和对应类:

参数值编解码类
kryoKryoUndoLogParser
protostuffProtostuffUndoLogParser
fastjsonFastjsonUndoLogParser
fstFstUndoLogParser
jacksonJacksonUndoLogParser

3.2.4 undo日志数据压缩

参数client.undo.compress.type配置使用的数据压缩类,默认值zip。

参数值压缩类
zipZipCompressor
sevenZSevenZCompressor
deflaterDeflaterCompressor
lz4Lz4Compressor
gzipGzipCompressor
bZip2BZip2Compressor

3.3 undo日志缓存清空

1.数据库操作完成或者发生异常后需要清空undo日志缓存

2.undo日志缓存存储在ConnectionContext类中,调用其reset方法和removeSavepoint方法清空undo缓存

3.3.1 reset调用点

image.png

3.3.2 removeSavepoint调用点

image.png

3.3.3 ConnectionContext.java 清空undo缓存

    public void reset() {
        this.reset(null);
    }

    void reset(String xid) {
        this.xid = xid;
        branchId = null;
        this.isGlobalLockRequire = false;
        savepoints.clear();
        lockKeysBuffer.clear();
        // 清空undo日志
        sqlUndoItemsBuffer.clear();
        this.autoCommitChanged = false;
    }
    public void removeSavepoint(Savepoint savepoint) {
        List<Savepoint> afterSavepoints = getAfterSavepoints(savepoint);

        if (null == savepoint) {
            // 清空undo日志
            sqlUndoItemsBuffer.clear();
            lockKeysBuffer.clear();
        } else {

            for (Savepoint sp : afterSavepoints) {
                // 清空undo日志
                sqlUndoItemsBuffer.remove(sp);
                lockKeysBuffer.remove(sp);
            }
        }

        savepoints.removeAll(afterSavepoints);
        currentSavepoint = savepoints.size() == 0 ? DEFAULT_SAVEPOINT : savepoints.get(savepoints.size() - 1);
    }

其他阅读:

萌新快速成长之路
如何编写软件设计文档
JAVA编程思想(一)通过依赖注入增加扩展性
JAVA编程思想(二)如何面向接口编程
JAVA编程思想(三)去掉别扭的if,自注册策略模式优雅满足开闭原则
JAVA编程思想(四)Builder模式经典范式以及和工厂模式如何选?
Java编程思想(七)使用组合和继承的场景
JAVA基础(一)简单、透彻理解内部类和静态内部类
JAVA基础(二)内存优化-使用Java引用做缓存
JAVA基础(三)ClassLoader实现热加载
JAVA基础(四)枚举(enum)和常量定义,工厂类使用对比
JAVA基础(五)函数式接口-复用,解耦之利刃