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.


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

相关推荐
m0_5719575834 分钟前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功3 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2343 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨3 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟4 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity5 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天5 小时前
java的threadlocal为何内存泄漏
java
caridle5 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^6 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋36 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx