什么是二阶段提交
二阶段提交协议(Two-phase Commit Protocol,简称 2PC )是分布式事务的核心协议。在此协议中,一个事务管理器(Transaction Manager,简称 TM)协调 1 个或多个资源管理器(Resource Manager,简称 RM)的活动,所有资源管理器向事务管理器汇报自身活动状态,由事务管理器根据各资源管理器汇报的状态(完成准备或准备失败)来决定各资源管理器是“提交”事务还是进行“回滚”操作。
二阶段提交的具体流程如下:
-
应用程序向事务管理器提交请求,发起分布式事务;
-
在第一阶段,事务管理器联络所有资源管理器,通知它们准备提交事务;
-
各资源管理器返回完成准备(或准备失败)的消息给事务管理器(响应超时算作失败);
-
在第二阶段:
- 如果所有资源管理器均完成准备,则事务管理器会通知所有资源管理器执行事务提交;
- 如果任一资源管理器准备失败,则事务管理器会通知所有资源管理器进行事务回滚。
Lucene的二阶段提交
Lucene 定义了一个二阶段提交接口 TwoPhaseCommit 。Lucene 的写入的文档提交基于此接口实现。该接口提供了三个方法。
一阶段提交 prepareCommit
完成二阶段提交第一阶段的工作,它会尽可能多的完成更新工作,但又避免完成真实的提交。同时失败时可以轻松地利用 rollback 废弃掉当前阶段完成的所有工作。 实际上 Lucene 在一阶段提交时就已经将 segmnt 持久化到了磁盘,但通过修改文件名的方式巧妙的另 segment 在此时不生效。
提交前的校验
通过上锁避免多个线程同时 prepareCommit,同一个 segmentInfo 只需提交一次。这里只能避免不能多线程 prepareCommit 。为了实现整个提交流程都是串行,Lucene 通过校验 segmentInfo 是否为空来判断上次 prepareCommit 的 segmentInfo 是否已被最终提交,待提交的 segmentInfo 快照在提交成功或失败回滚后会被清空,因此不为空时意味着上一个线程仍在执行最后的提交。
synchronized(commitLock) {
ensureOpen(false);
if (infoStream.isEnabled("IW")) {
infoStream.message("IW", "prepareCommit: flush");
infoStream.message("IW", " index before flush " + segString());
}
if (tragedy != null) {
throw new IllegalStateException("this writer hit an unrecoverable error; cannot commit", tragedy);
}
if (pendingCommit != null) {
throw new IllegalStateException("prepareCommit was already called with no corresponding call to commit");
}
// 后续一阶段提交逻辑
}
flush生成待提交的段
在开始提交前再做一次 flush ,尽可能的让一次 commit 包含更多的段,最大程度让写入的文档快速持久化以确保数据安全。
关于 Lucene 的 flush 流程,在上一篇文章 从源码看 Lucene 的文档写入流程 的末节中已经阐述清楚,这里不再赘述。
组装待提交信息、更新文件计数
1.组装最终会被持久化到磁盘的信息。这里将用户触发 commit 前 set 的提交信息组装到了 segmentInfo 中。
2.拷贝一份 segmentInfo 的快照,到这里哪些 segmentInfo 会被持久化已经确定。
3.为新文件的引用初始化引用计数
// 组装用户提交信息
if (commitUserData != null) {
Map<String,String> userData = new HashMap<>();
for(Map.Entry<String,String> ent : commitUserData) {
userData.put(ent.getKey(), ent.getValue());
}
segmentInfos.setUserData(userData, false);
}
// flush和commit是可以同时进行的,为了避免commit过程中又有新的segmentInfo生成
// 拷贝一份快照确定需要被commit的segmentInfo
toCommit = segmentInfos.clone();
pendingCommitChangeCount = changeCount.get();
// 生成所有segmentInfo中所有文档的引用
filesToCommit = toCommit.files(false);
// 为以上文件初始化引用计数
// Lucene的segment删除策略是“引用计数”,即当一个segment不被引用时(如merge完后的旧segment等),就会被最终删除
// Lucene中任何被持久化的新段,初始计数都为1
deleter.incRef(filesToCommit);
segment持久化到磁盘
可能会有人好奇,不是一阶段提交吗,怎么segment就持久化到磁盘了?我们往下看。
private void write(Directory directory) throws IOException {
long nextGeneration = getNextPendingGeneration();
String segmentFileName = IndexFileNames.fileNameFromGeneration(IndexFileNames.PENDING_SEGMENTS,
"",
nextGeneration);
// io流、文件创建逻辑
}
从上面可以看出持久化的文件名格式是 pending_segments_N ,这里的N指的是 segment 递增编号。很明显,通过“pending”字眼我们可以可以确定这并不是最终的 segment 文件。
二阶段提交 commit
当所有 pending_segments_N 都保存无误后,开始二阶段提交。二阶段提交成功后,此次文档 commit 才真正完成。
重命名segment文件
这里将一阶段提交的 pending_segments_N 文件重命名为 segments_N 。即 segment 在硬盘内的最终形态。
final String finishCommit(Directory dir) throws IOException {
...
final String src = IndexFileNames.fileNameFromGeneration(IndexFileNames.PENDING_SEGMENTS, "", generation);
dest = IndexFileNames.fileNameFromGeneration(IndexFileNames.SEGMENTS, "", generation);
dir.rename(src, dest);
dir.syncMetaData();
...
return dest;
}
执行老segment删除策略
Lucene 提供了多种 segment 删除策略来适应不同的业务场景。
其定义了一个删除策略接口 IndexDeletionPolicy ,并实现了四种删除策略。
KeepOnlyLastCommitDeletionPolicy(默认)
只保留最新提交的 segment 文件。因此该种策略最节约硬盘和内存资源。
/**
* Deletes all commits except the most recent one.
*/
@Override
public void onCommit(List<? extends IndexCommit> commits) {
// note that commits.size() should normally be 2 (if not called by oninit above):
int size = commits.size();
for(int i=0;i<size-1;i++) {
commits.get(i).delete();
}
}
NoDeletionPolicy
不删除任何 segment 文件。此种策略需要大量的硬盘空间来存储历史的 segment ,但同时,用户可以将索引回滚到任意一次提交(有点像代码版本控制工具)。
@Override
public void onCommit(List<? extends IndexCommit> commits) {}
SnapshotDeletionPolicy
可在 KeepOnlyLastCommitDeletionPolicy 或 NoDeletionPolicy 策略的基础上,生成存储于内存的最新 segment 快照 lastCommit 。一般与 KeepOnlyLastCommitDeletionPolicy 搭配使用,可实现只持久化最新 segment 的同时,在内存中保留某一时刻的 segment 快照。
private final IndexDeletionPolicy primary;
@Override
public synchronized void onCommit(List<? extends IndexCommit> commits)
throws IOException {
primary.onCommit(wrapCommits(commits));
lastCommit = commits.get(commits.size() - 1);
}
PersistentSnapshotDeletionPolicy
与 SnapshotDeletionPolicy 策略几乎一致,区别是生成的 segment 快照将会持久化到硬盘。
快照文件名格式为:SNAPSHOTS_PREFIX + nextWriteGen
SNAPSHOTS_PREFIX 是固定前缀 snapshots_ ,nextWriteGen 是快照递增计数。
举例:snapshots_1、snapshots_2
synchronized private void persist() throws IOException {
String fileName = SNAPSHOTS_PREFIX + nextWriteGen;
IndexOutput out = dir.createOutput(fileName, IOContext.DEFAULT);
// 文件存储io逻辑
...
nextWriteGen++;
}
生成segment回滚位点
在这一步暂存了上一次提交的 segment 信息,以便本次提交失败后可以快速回滚。
List<SegmentCommitInfo> createBackupSegmentInfos() {
final List<SegmentCommitInfo> list = new ArrayList<>(size());
for(final SegmentCommitInfo info : SegmentInfo) {
assert info.info.getCodec() != null;
list.add(info.clone());
}
return list;
}
merge
文档的频繁 commit 会产生很多 segment 文件,为了提高文档的检索、写入速率,Lucene 会在合适的时机将 segment 合并。关于 merge 的细节篇幅较多,将在后续文章中展开。
public enum MergeTrigger {
/**
* Merge was triggered by a segment flush.
*/
SEGMENT_FLUSH,
/**
* Merge was triggered by a full flush. Full flushes
* can be caused by a commit, NRT reader reopen or a close call on the index writer.
*/
FULL_FLUSH,
/**
* Merge has been triggered explicitly by the user.
*/
EXPLICIT,
/**
* Merge was triggered by a successfully finished merge.
*/
MERGE_FINISHED,
/**
* Merge was triggered by a closing IndexWriter.
*/
CLOSING
}
失败回滚 rollback
当一二阶段提交任何一步中产生失败时,执行回滚策略。
- 尝试终止第二阶段提交最后可能产生的 merge 事件。
- 删除第一阶段提交过程中产生的临时文件 pending_segment_N 。
- 将 segmentInfo 回滚到上一次提交时设置的回滚位点。
最后
由此可见,当 Lucene 执行 commit 后,写入的文档才被持久化到了硬盘。Lucene 由于自身没有事务日志机制,因此只能通过二阶段提交来实现提交失败时回滚,使文档的提交更高效、安全。第一次提交已经确保了所有 segment 文件都持久化成功,第二次提交时仅需修改文件名即可,在尽可能简化代码的同时也保证了第二次提交的成功率。但在更上层的应用中,如 solr、Elasticsearch ...中,均有事务日志,当提交失败时,还能通过事务日志就行回滚。
参考文献
推荐阅读
招贤纳士
政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有 500 多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com
微信公众号
文章同步发布,政采云技术团队公众号,欢迎关注