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.


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

相关推荐
是梦终空6 分钟前
JAVA毕业设计176—基于Java+Springboot+vue3的交通旅游订票管理系统(源代码+数据库)
java·spring boot·vue·毕业设计·课程设计·源代码·交通订票
落落落sss16 分钟前
sharding-jdbc分库分表
android·java·开发语言·数据库·servlet·oracle
码爸19 分钟前
flink doris批量sink
java·前端·flink
Monodye1 小时前
【Java】网络编程:TCP_IP协议详解(IP协议数据报文及如何解决IPv4不够的状况)
java·网络·数据结构·算法·系统架构
一丝晨光1 小时前
逻辑运算符
java·c++·python·kotlin·c#·c·逻辑运算符
无名指的等待7121 小时前
SpringBoot中使用ElasticSearch
java·spring boot·后端
Tatakai252 小时前
Mybatis Plus分页查询返回total为0问题
java·spring·bug·mybatis
武子康2 小时前
大数据-133 - ClickHouse 基础概述 全面了解
java·大数据·分布式·clickhouse·flink·spark
.生产的驴2 小时前
SpringBoot 消息队列RabbitMQ 消费者确认机制 失败重试机制
java·spring boot·分布式·后端·rabbitmq·java-rabbitmq
Code哈哈笑2 小时前
【C++ 学习】多态的基础和原理(10)
java·c++·学习