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.


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

相关推荐
abcnull11 分钟前
用javaparser做精准测试
java·ast·静态代码分析·精准测试·javaparser
叶小鸡17 分钟前
Java 篇-项目实战-苍穹外卖-笔记汇总
java·开发语言·笔记
AI人工智能+电脑小能手33 分钟前
【大白话说Java面试题】【Java基础篇】第22题:HashMap 和 HashSet 有哪些区别
java·开发语言·哈希算法·散列表·hash
juniperhan1 小时前
Flink 系列第21篇:Flink SQL 函数与 UDF 全解读:类型推导、开发要点与 Module 扩展
java·大数据·数据仓库·分布式·sql·flink
ID_180079054731 小时前
Python 实现亚马逊商品详情 API 数据准确性校验(极简可用 + JSON 参考)
java·python·json
c++之路1 小时前
C++23概述
java·c++·c++23
专注API从业者2 小时前
Open Claw 京东商品监控选品实战:一键抓取、实时监控、高效选品
java·服务器·数据库
摇滚侠2 小时前
DBeaver 导入数据库 导入 SQL 文件 MySQL 备份恢复
java·数据库·mysql
keep one's resolveY3 小时前
SpringBoot实现重试机制的四种方案
java·spring boot·后端
天空属于哈夫克33 小时前
企业微信API常见的错误和解决方案
java·数据库·企业微信