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.