分布式事务2PC两阶段提交原理、XA规范与生产落地实践

1. 为什么需要分布式事务

1.1 从一次转账超时说起

很多同学第一次接触分布式事务,都是被线上问题逼出来的。我印象很深的一个案例:一个交易系统做用户余额扣减和积分发放,两个操作分别落在两个独立的数据库实例上。某天网络抖动,扣款成功了,积分发放超时失败,用户账户里钱少了但积分没到账,客服电话被打爆。这就是典型的分布式场景下数据不一致问题。

单机数据库时代,我们靠本地事务的ACID就能搞定一切。一个BEGIN到COMMIT之间,要么全成功,要么全回滚,数据库引擎帮我们兜底。可一旦业务拆分到多个服务、多个数据库、多个存储系统,本地事务就鞭长莫及了。每个服务只能管好自己的那一亩三分地,跨服务的数据一致性就成了一个必须正面硬刚的架构难题。

分布式事务要解决的核心问题就一句话:在多个独立节点参与的情况下,如何保证所有节点的操作要么全部生效,要么全部不生效。听起来简单,做起来极难,因为网络会超时、节点会宕机、消息会丢失、时钟还不同步。

1.2 2PC在分布式事务家族中的位置

分布式事务的解决方案有很多流派,常见的有2PC(两阶段提交)、3PC(三阶段提交)、TCC(Try-Confirm-Cancel)、Saga、本地消息表、事务消息等。其中2PC是出现最早、理论最扎实、也是很多其他方案思想源头的一种。

2PC的全称是Two-Phase Commit,中文叫两阶段提交。它把一个分布式事务的提交过程拆成两个阶段:准备阶段(Prepare Phase)和提交阶段(Commit Phase)。整个流程由一个协调者(Coordinator)统一指挥,多个参与者(Participant)配合执行。

为什么先讲2PC?因为它是理解分布式事务的“地基”。你搞懂了2PC的投票机制、阻塞问题、单点故障,再看3PC怎么改进、TCC怎么变通、Saga怎么补偿,思路会清晰很多。很多团队在选型时直接跳过2PC,但面试和架构评审里,2PC永远是绕不开的必答题。

1.3 适合哪些读者和场景

这篇内容适合三类人:一是正在做微服务拆分、被跨库一致性问题困扰的后端开发;二是准备系统学习分布式事务、想搞懂底层原理的技术爱好者;三是需要在架构评审中讲清楚方案选型理由的技术负责人。

需要提前说明的是,2PC不是银弹,它有明确的适用边界。它更适合对一致性要求极高、参与方数量可控、事务执行时间较短的场景,比如跨库的账户扣减、库存锁定等。如果你的业务涉及十几个微服务、每个操作耗时几百毫秒,硬上2PC大概率会把系统拖垮。具体原因后面会详细拆解。

2. 2PC的核心原理拆解

2.1 两个阶段到底在干什么

先打个生活化的比方。假设你和几个朋友合伙买一批设备,你是牵头人(协调者),朋友们是出资方(参与者)。流程是这样的:

第一阶段,你挨个问每个朋友:“钱准备好了吗?准备好就告诉我,但先别打款。”每个朋友检查自己账户,回复“准备好了”或者“钱不够”。这就是准备阶段,也叫投票阶段。

第二阶段,如果所有人都回复“准备好了”,你就通知大家“统一打款”;只要有一个人说“钱不够”,你就通知所有人“取消,别打款了”。这就是提交阶段,也叫执行阶段。

对应到技术层面,准备阶段协调者向所有参与者发送prepare请求,参与者执行事务操作(但不提交),把undo和redo日志写好,锁定相关资源,然后回复Yes或No。提交阶段协调者根据所有参与者的回复决定全局提交还是全局回滚,并把决定发给所有参与者执行。

这里有个关键点:准备阶段参与者已经执行了操作,只是没有提交。这意味着资源已经被占住了,其他事务想动这些数据就得等着。这是2PC阻塞问题的根源。

2.2 协调者与参与者的角色分工

协调者通常是一个独立的事务管理器组件,也可以是发起全局事务的那个服务。它的职责包括:开启全局事务、向所有参与者广播prepare、收集投票结果、做出全局决策、广播commit或rollback、处理超时和异常。

参与者的职责相对简单:接收prepare请求、执行本地事务操作、记录日志、锁定资源、返回投票结果、等待最终指令、执行提交或回滚。

角色分工看似清晰,但实际落地时有个坑:协调者本身也可能宕机。如果协调者在发出prepare之后、做出决策之前挂了,所有参与者就会一直卡在准备阶段,资源锁着不放,整个系统可能陷入停滞。这就是经典的协调者单点故障问题。

2.3 投票机制与全局决策逻辑

投票机制是2PC的核心。协调者的决策规则非常严格:

  • 所有参与者都投Yes,全局提交。
  • 任意一个参与者投No,全局回滚。
  • 任意一个参与者超时未回复,协调者通常按No处理,触发全局回滚。

为什么超时按失败处理?因为协调者无法区分参与者是“正在处理”还是“已经挂了”。为了保证一致性,宁可回滚也不能冒险提交。这个选择牺牲了可用性,换来了强一致性,符合CAP理论中偏向CP的取舍。

全局决策一旦做出,就必须持久化到协调者的日志里。哪怕协调者随后宕机重启,也能从日志里恢复决策,继续通知参与者。这一点非常关键,否则参与者可能永远等不到最终指令。

2.4 与3PC的关键差异

3PC在2PC基础上加了一个阶段,把准备阶段拆成CanCommit和PreCommit,再加上最后的DoCommit。同时引入了超时机制:参与者在等待PreCommit或DoCommit超时后,可以自行做出决策,而不是无限期阻塞。

3PC的改进点在于降低了阻塞范围,但代价是逻辑更复杂,而且在网络分区场景下仍可能出现不一致。实际生产中3PC用得并不多,更多是作为理论演进的一个中间形态。理解2PC之后再去看3PC,你会发现它并没有从根本上解决协调者单点问题和网络分区问题,只是缓解了阻塞。

3. 2PC的实操落地要点

3.1 数据库层面的XA规范

说到2PC的落地,就绕不开XA规范。XA是X/Open组织提出的分布式事务处理标准,定义了事务管理器(TM)和资源管理器(RM)之间的接口。主流关系型数据库基本都支持XA,比如MySQL、PostgreSQL、Oracle等。

在MySQL中,XA事务的语法大致是这样的:

XA START 'transaction_id'; -- 执行具体的SQL操作 UPDATE account SET balance = balance - 100 WHERE user_id = 1; XA END 'transaction_id'; XA PREPARE 'transaction_id'; -- 此时事务处于准备状态,等待最终指令 XA COMMIT 'transaction_id'; -- 或者 XA ROLLBACK 'transaction_id';

XA START开启一个XA事务,XA END表示SQL执行完毕,XA PREPARE进入准备阶段,XA COMMIT或XA ROLLBACK是最终提交或回滚。协调者需要为每个参与者维护一个全局事务ID,确保各分支事务能正确关联。

注意:MySQL的XA事务在准备阶段会持有行锁,直到最终提交或回滚才释放。如果协调者迟迟不发指令,锁会一直占着,其他事务只能排队等待。

3.2 一个可运行的Java示例框架

下面用Java伪代码展示一个简化的2PC协调者实现思路,帮助理解流程。实际生产不会自己手写协调者,而是用Seata、Atomikos这类框架,但手写一遍对理解原理极有帮助。

public class TwoPhaseCommitCoordinator { private List<Participant> participants; private String globalTxId; public boolean executeTransaction() { // 第一阶段:准备 List<Boolean> votes = new ArrayList<>(); for (Participant p : participants) { try { boolean ok = p.prepare(globalTxId); votes.add(ok); } catch (Exception e) { votes.add(false); } } // 决策 boolean allYes = votes.stream().allMatch(v -> v); // 第二阶段:提交或回滚 if (allYes) { for (Participant p : participants) { p.commit(globalTxId); } return true; } else { for (Participant p : participants) { p.rollback(globalTxId); } return false; } } }

参与者接口大致定义如下:

public interface Participant { boolean prepare(String txId); void commit(String txId); void rollback(String txId); }

这段代码省略了日志持久化、超时处理、重试机制等关键细节,但核心投票和决策逻辑已经体现出来了。真实框架里,prepare的结果要落盘,commit和rollback要支持幂等重试,协调者状态要能恢复。

3.3 超时与重试的处理策略

超时处理是2PC落地最容易出问题的地方。协调者发出prepare后,必须设置合理的超时时间。超时太短,正常慢查询会被误判为失败;超时太长,故障时资源锁定时间过久。

我的经验是:超时时间设置为参与者平均响应时间的3到5倍。比如参与者平时处理一个prepare请求平均50毫秒,超时可以设200到300毫秒。同时要配合重试,但重试次数不宜过多,一般2到3次即可,避免雪崩。

重试必须保证幂等。参与者收到重复的commit指令时,如果本地事务已经提交,直接返回成功即可,不能重复执行。这要求参与者在本地记录事务状态,通常用一张事务日志表来维护。

3.4 日志与恢复机制

协调者和参与者都需要持久化事务日志。协调者的日志记录全局事务ID、参与者列表、当前阶段、最终决策。参与者的日志记录分支事务ID、所属全局事务ID、本地操作内容、当前状态。

恢复流程是这样的:协调者重启后,扫描日志中处于“已决策但未完成通知”的事务,重新向参与者发送commit或rollback。参与者重启后,扫描日志中处于“已准备但未收到最终指令”的事务,主动向协调者查询决策。

提示:日志表要定期清理,否则会无限膨胀。一般保留最近7到30天的记录,确认所有事务都已终结后归档或删除。

4. 常见问题与排查技巧

4.1 参与者长时间锁定资源怎么办

这是2PC最常被吐槽的问题。准备阶段参与者已经执行了操作并加锁,如果协调者迟迟不做决策,锁就一直不释放。在高并发场景下,这可能导致大量请求超时。

排查思路:先看协调者日志,确认是否卡在收集投票阶段。如果某个参与者一直没返回,检查该参与者的数据库连接池是否耗尽、是否有慢SQL、是否发生了死锁。如果协调者本身挂了,需要尽快恢复协调者,让它从日志中恢复决策并通知参与者。

缓解手段包括:设置prepare阶段的锁等待超时、限制单个全局事务的参与者数量、对耗时长的操作做拆分。但根本上,2PC不适合长事务,这是它的基因决定的。

4.2 协调者宕机后的数据一致性

协调者宕机分两种情况。一种是在发出prepare之前挂了,此时参与者还没执行任何操作,直接回滚即可,没有一致性问题。另一种是在发出prepare之后、做出决策之前挂了,参与者处于准备状态,资源锁定,等待指令。

第二种情况最麻烦。如果协调者没有持久化决策日志,重启后无法判断该提交还是回滚。这时候只能人工介入,或者依赖参与者之间的协商机制。所以协调者必须把决策日志落盘,这是2PC可靠性的生命线。

4.3 网络分区导致的脑裂问题

网络分区时,协调者和部分参与者失去联系,可能做出错误的全局决策。比如协调者认为某个参与者挂了,决定回滚,但实际上该参与者只是网络不通,本地事务已经准备好。等网络恢复后,参与者收到回滚指令,数据就出现了不一致。

2PC本身无法完美解决网络分区问题,这是它的理论局限。实际生产中,通常会结合超时、重试、人工补偿等手段来降低风险。如果业务对分区容忍度极低,可能需要考虑TCC或Saga这类柔性事务方案。

4.4 常见问题速查表

问题现象可能原因排查方向解决建议
参与者锁等待超时协调者未发最终指令检查协调者日志和状态恢复协调者,重发指令
全局事务长时间未结束某个参与者响应慢查看参与者慢SQL和连接池优化SQL,调整超时时间
数据不一致协调者决策未持久化检查协调者日志表补全日志机制,人工修复
重复提交重试未做幂等检查参与者事务状态表增加幂等校验
协调者重启后状态丢失日志未落盘检查日志持久化配置开启同步刷盘

4.5 几个踩过的坑

第一个坑:把2PC用在跨多个微服务的业务编排上。微服务之间的调用链路长,每个服务内部还可能调用其他服务,参与者数量一多,2PC的协调开销和阻塞时间呈指数级上升。后来改成只在数据库层面用XA,服务之间用事务消息,效果好很多。

第二个坑:忽略参与者的幂等设计。网络抖动导致协调者重发commit指令,参与者重复执行,数据被改了两次。后来在参与者侧加了一张事务状态表,每次执行前先查状态,已完成的直接返回。

第三个坑:超时时间设置过短。高峰期数据库响应变慢,prepare请求频繁超时,协调者误判为失败触发回滚,导致大量正常事务被回滚。后来把超时时间从100毫秒调整到500毫秒,并增加了动态调整机制。

5. 2PC的适用边界与替代思路

5.1 什么时候该用2PC

2PC最适合的场景有几个特征:参与方数量少(一般不超过5个)、事务执行时间短(毫秒级)、对强一致性要求极高、参与方都支持XA协议。比如跨两个数据库的账户转账、跨库的订单和库存扣减。

如果参与方超过10个,或者单个操作耗时超过1秒,或者参与方包含不支持XA的存储(比如Redis、MongoDB),2PC就不是好选择。硬上只会让系统变得脆弱。

5.2 与TCC、Saga的对比

TCC把操作拆成Try、Confirm、Cancel三个阶段,Try阶段做资源预留,Confirm做确认,Cancel做补偿。相比2PC,TCC不依赖数据库的XA支持,锁的粒度更细,但业务侵入性大,需要为每个操作写三个方法。

Saga把长事务拆成一串本地事务,每个本地事务都有对应的补偿操作。Saga适合长流程业务,比如订单履约、旅行预订,但补偿逻辑复杂,且只能保证最终一致性。

方案一致性业务侵入适用场景
2PC强一致低短事务、少参与方
TCC最终一致高资源预留型业务
Saga最终一致中长流程业务

5.3 生产环境的选型建议

我的建议是:能用本地事务就不用分布式事务,能用柔性事务就不用强一致事务。很多业务场景其实可以接受最终一致性,通过消息队列、定时对账就能解决,没必要上2PC。

如果确实需要强一致,优先考虑数据库层面的XA,把参与方控制在最小范围。如果参与方多、流程长,考虑TCC或Saga。选型时不要只看理论,要结合团队的技术储备和运维能力。2PC的协调者组件如果没人能维护,出了问题就是灾难。

5.4 一个务实的演进路径

从单库到分布式,我一般建议这样演进:先做业务拆分但共用一个数据库,用本地事务保证一致性;然后按业务边界拆库,跨库操作先用本地消息表做最终一致;如果某些核心链路必须强一致,再引入2PC或TCC。

这个路径的好处是每一步都有退路,不会一上来就搞最复杂的方案。很多团队一拆微服务就上分布式事务框架,结果业务还没跑通,先被事务问题拖垮了。

6. 写在最后的一点体会

2PC这个方案,理论很优雅,落地很骨感。它的投票机制和全局决策逻辑是理解分布式事务的绝佳入口,但它的阻塞特性和单点故障问题也是真实存在的。我在实际项目里用2PC,基本都是限定在数据库层面的XA,参与方控制在两三个,事务执行时间压到毫秒级。

如果你正在做分布式事务选型,我的建议是先问自己三个问题:业务真的需要强一致吗?参与方能不能再少一点?团队有没有能力维护协调者组件?这三个问题想清楚了,方案基本就定了。

最后分享一个小技巧:不管用哪种分布式事务方案,对账系统都是最后的防线。再完美的方案也可能出问题,定期对账能帮你及时发现不一致并修复。我在几个项目里都加了对账任务,虽然不能防止问题发生,但至少能在用户投诉之前把问题找出来。

相关新闻

别再滥用设计模式:用函数对象和新语言特性做“降维替代”

别再滥用设计模式:用函数对象和新语言特性做“降维替代”

当你在 Python、JavaScript、现代 Java 或 Go 中为一个“策略”写接口和三个实现类时&#xff0c;你很可能不是在运用设计智慧&#xff0c;而是在用代码模拟一种语言本该直接提供的能力。** 多数 GoF 模式诞生于语言表达力贫瘠的年代&#xff0c;它们的本质是“用对象结构弥补缺…

2026/10/20 14:40:21 阅读更多 →
基于改进遗传算法的城市交通信号配时优化:MATLAB实现与仿真对比

基于改进遗传算法的城市交通信号配时优化:MATLAB实现与仿真对比

简介&#xff1a;这份资源面向交通工程、智能优化与MATLAB算法学习者&#xff0c;围绕城市交通信号配时优化问题&#xff0c;提供基于改进遗传算法&#xff08;IGA&#xff09;的完整实现方案。压缩包共30个文件&#xff0c;以27个m脚本和3个mat数据文件为主&#xff0c;整体约…

2026/10/20 14:40:21 阅读更多 →
构建高质量邮件分类数据集:从CSV设计到BERT微调的实战指南

构建高质量邮件分类数据集:从CSV设计到BERT微调的实战指南

邮件分类数据的价值&#xff0c;很多人低估了它。做了几年邮件安全相关的数据处理和模型训练&#xff0c;我最大的体会是&#xff1a;垃圾邮件过滤这个赛道&#xff0c;真正的门槛早就不是"算法有多花哨"&#xff0c;而是"数据有多扎实"。最近我完成了一套…

2026/10/20 14:40:21 阅读更多 →

最新新闻

Context-Mode 上下文模式:多租户与异步场景下的工程实践指南

Context-Mode 上下文模式:多租户与异步场景下的工程实践指南

1. 从“上下文模式”这个命名说起&#xff1a;它到底在解决什么问题第一次看到“context-mode”这个词&#xff0c;很多人会下意识觉得它是个抽象概念&#xff0c;不像某个具体框架或库那样一眼能看懂。我最初也是这个反应——直到自己在几个项目里反复被同一类问题折磨之后&am…

2026/10/20 15:28:17 阅读更多 →
深入拆解Java包装类与泛型:集合中的类型陷阱与实战排查

深入拆解Java包装类与泛型:集合中的类型陷阱与实战排查

搞Java这么多年&#xff0c;我见过太多人在集合上栽跟头&#xff1a;往ArrayList<Integer>里存了个值&#xff0c;取出来用比较&#xff0c;结果在127和128两个数字上表现完全不一样&#xff1b;想写个List<int>省点内存&#xff0c;编辑器直接报错&#xff1b;自定…

2026/10/20 15:28:17 阅读更多 →
三相并网变流器与SVG无功补偿Simulink仿真建模全解析

三相并网变流器与SVG无功补偿Simulink仿真建模全解析

前段时间做了一套光伏并网方向的仿真模型&#xff0c;客户点名要三相并网变流器带上无功静态补偿的功能&#xff0c;还要带一份能讲清楚原理和结果分析的报告。我一开始也觉得这东西不就是把SVG并联在并网点上嘛&#xff0c;真动手建模才发现里面值得抠的细节比想象中多得多。这…

2026/10/20 15:28:17 阅读更多 →
Unity射击游戏工程快速上手:版本、管线与输入系统避坑指南

Unity射击游戏工程快速上手:版本、管线与输入系统避坑指南

简介&#xff1a;这是一份面向Unity3D初学者与游戏开发入门者的射击小游戏完整工程包&#xff0c;基于Unity5.0构建&#xff0c;适合想通过实战理解引擎核心机制的开发者练手。压缩包共28个文件&#xff0c;约33.73MB&#xff0c;包含dll动态库、config配置、assets资源、exe可…

2026/10/20 15:28:17 阅读更多 →
Django仓库管理系统实战:从库存流水设计到并发扣减与部署

Django仓库管理系统实战:从库存流水设计到并发扣减与部署

做仓库管理这套系统&#xff0c;说起来不算什么特别新鲜的事&#xff0c;但从需求梳理到真正能跑起来、能交到使用者手里&#xff0c;中间隔着的东西远比想象中多。我最近正好把一个基于 Python Django 的仓库管理系统从源码到数据库再到部署文档完整过了一遍&#xff0c;这套…

2026/10/20 15:28:17 阅读更多 →
计组与计网底层逻辑全解析:从CPU到TCP的软硬件协同

计组与计网底层逻辑全解析:从CPU到TCP的软硬件协同

我知道很多同学一看到“计组”和“计网”这两个词&#xff0c;脑子里浮现的就是“背不完的概念”和“算不完的题”。作为一个在计算机领域摸爬滚打多年的老兵&#xff0c;我可以负责任地说&#xff1a;CS基础里的计算机组成原理和计算机网络&#xff0c;恰恰是最能拉开技术上限…

2026/10/20 15:27:16 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →