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


Java极客 | 作者 / 铿然一叶 这是Java极客的第 97 篇原创文章


相关阅读:

萌新快速成长之路
JAVA编程思想(一)通过依赖注入增加扩展性
JAVA编程思想(二)如何面向接口编程
JAVA编程思想(三)去掉别扭的if,自注册策略模式优雅满足开闭原则
JAVA编程思想(四)Builder模式经典范式以及和工厂模式如何选?
Java编程思想(五)事件通知模式解耦过程
Java编程思想(六)事件通知模式解耦过程
Java编程思想(七)使用组合和继承的场景
JAVA基础(一)简单、透彻理解内部类和静态内部类
JAVA基础(二)内存优化-使用Java引用做缓存
JAVA基础(三)ClassLoader实现热加载
JAVA基础(四)枚举(enum)和常量定义,工厂类使用对比
JAVA基础(五)函数式接口-复用,解耦之利刃
HikariPool源码(二)设计思想借鉴
【极客源码】JetCache源码(一)开篇
【极客源码】JetCache源码(二)顶层视图
如何编写软件设计文档
Seata源码(一)初始化
Seata源码(二)事务基础对象
Seata源码(三)事务处理类结构和流程
Seata源码(四)全局锁GlobalLock
Seata源码(五)Seata数据库操作\


1. 概述

1.1 作用

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

1.2 日志结构

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

2. 核心类结构

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

3. 源码

3.1 UNDO日志准备入口

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

AbstractDMLBaseExecutor.java

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();
        T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
        // 模版方法,交给子类实现
        TableRecords afterImage = afterImage(beforeImage);
        // 准备undo日志,写入缓存
        prepareUndoLog(beforeImage, afterImage);
        return result;
    }

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

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

BaseTransactionalExecutor.java

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写入缓存:

java 复制代码
    public void appendUndoLog(SQLUndoLog sqlUndoLog) {
        context.appendUndoItem(sqlUndoLog);
    }

3.2 UNDO日志写入

3.2.1 ConnectionProxy.java

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

scss 复制代码
    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子类做数据压缩:

ini 复制代码
    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。

参数值和对应类:

参数值 编解码类
kryo KryoUndoLogParser
protostuff ProtostuffUndoLogParser
fastjson FastjsonUndoLogParser
fst FstUndoLogParser
jackson JacksonUndoLogParser

3.2.4 undo日志数据压缩

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

参数值 压缩类
zip ZipCompressor
sevenZ SevenZCompressor
deflater DeflaterCompressor
lz4 Lz4Compressor
gzip GzipCompressor
bZip2 BZip2Compressor

3.3 undo日志缓存清空

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

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

3.3.1 reset调用点

3.3.2 removeSavepoint调用点

3.3.3 ConnectionContext.java 清空undo缓存

java 复制代码
    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;
    }
java 复制代码
    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);
    }

end.


<--阅过留痕,左边点赞!

相关推荐
武昌库里写JAVA24 分钟前
39.剖析无处不在的数据结构
java·vue.js·spring boot·课程设计·宠物管理
Nelson_hehe3 小时前
Java基础第四章、面向对象
java·语法基础·面向对象程序设计
Thomas_YXQ3 小时前
Unity3D Lua集成技术指南
java·开发语言·驱动开发·junit·全文检索·lua·unity3d
ShiinaMashirol4 小时前
代码随想录打卡|Day27(合并区间、单调递增的数字、监控二叉树)
java·算法
东阳马生架构5 小时前
Nacos简介—3.Nacos的配置简介
java
北极的企鹅885 小时前
XML内容解析成实体类
xml·java·开发语言
oioihoii5 小时前
C++23 中 static_assert 和 if constexpr 的窄化布尔转换
java·jvm·c++23
聂 可 以6 小时前
调整IntelliJ IDEA当前文件所在目录(包路径)的显示位置
java·ide·intellij-idea
东阳马生架构6 小时前
Sentinel源码—7.参数限流和注解的实现一
java·sentinel
李白的粉6 小时前
基于springboot的在线教育系统
java·spring boot·毕业设计·课程设计·在线教育系统·源代码