news 2026/10/12 3:01:34

Flyway执行无限等待排查实战:从锁竞争到连接池的完整链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flyway执行无限等待排查实战:从锁竞争到连接池的完整链路

如果你见过这种场景:服务启动后,日志停在某一行,类似“Migrating schema to version 12 ...”,然后整个进程就像被按了暂停键,日志不再滚动,接口不响应,重启一次还是这样,那就已经掉进“flyway执行无限等待”的坑里了。

这篇文章不打算从 Flyway 是什么开始讲,直接就聊“卡死”这件事。我会把症状先分类,再拆解底层到底是谁在阻塞谁,接着给出一套从应用到数据库的完整排查链路,最后列出根治方案和预防手段。内容偏实战,适合正在被这个问题折磨的人,也适合打算把迁移流程规范化、避免线上翻车的团队参考。

1. 卡死现场的画像:先确认你遇到的是哪一种“不动了”

“无限等待”这四个字说起来简单,但实际现象可以分成好几种。不同表现对应的病因完全不一样,第一步必须先把手上的信息收集清楚。

1.1 三种典型表现

第一种:应用启动过程中卡住。这是最常见的。Spring Boot 应用启动时,日志停在 Flyway 迁移的那一行,后续的 Bean 初始化、端口监听、健康检查都没发生。这时应用从外部看就是没起来,端口都未必能连上。

第二种:显式调用迁移方法时卡住。有些项目没有把迁移绑定在启动流程里,而是通过运维脚本或独立任务去执行flyway.migrate(),结果命令行执行到一半没有退出,Ctrl+C 都救不回来。这种场景更容易让人误判成命令行工具本身出了问题。

第三种:多实例并发启动,其中一个实例等待其他实例。集群部署的时候所有实例几乎同一时间启动,Flyway 为了保证同一套数据库不会被两个实例同时迁移,会有锁机制。多个实例中只有一个能拿到执行权,其余的全部在等待。如果第一个实例顺利跑完,等待的实例会自动恢复;如果第一个实例卡住了,后面的实例就一直排队,看起来就像“无限等待”。

1.2 先别急着下结论,要看日志是否真的“不动”

我发现很多人遇到卡死,第一反应就是直接重启或者 kill 进程。但这往往是错误操作。正确的第一步是判断它到底在等什么。

把进程的日志打开,记录卡住的精确时间点,然后用jstack <pid>抓线程栈。如果线程栈里能看到java.net.SocketInputStream.socketRead0这种本地方法栈帧,而且线程状态是RUNNABLE,其实并不是死循环,而是在等待网络响应。具体等的是数据库还是别的服务,要看下一步去数据库侧验证。

还有一种情况需要区分:日志确实不滚动了,但数据库侧显示 SQL 还在执行中,只是由于大表扫描或者 DDL 阻塞导致执行时间特别长。这种不是“无限等待”,是“执行时间超过了你设置的超时时间”。虽然用户感知都是卡死,但处理方式完全不同。

1.3 越早留下现场,越好定位

遇到卡死的时候,如果条件允许,第一时间做三件事:

  1. 保存当前线程栈:jstack <pid> > /tmp/stack_$(date +%s).log
  2. 保存数据库当前会话快照:SELECT * FROM information_schema.processlist;
  3. 记录卡死时间点附近的 Flyway 日志,尤其是迁移脚本的版本号。

这三样东西能在后面几个小时的排查里省下一大半力气。很多人跳过这一步直接重启,等环境恢复以后就再也找不到现场了,只能靠猜。

2. Flyway到底在等谁:迁移机制的底层拆解

要理解为什么会出现无限等待,必须把 Flyway 迁移过程中最关键的几步搞明白。不是了解 API 怎么用,而是要理解它在和数据库交互时,哪些环节存在阻塞的可能。

2.1 迁移时 Flyway 会先拿一把“锁”

Flyway 的历史可以追溯到很早的版本,但它至今还保留着一个重要设计:同一时间只允许一个迁移者操作数据库。这个设计用来解决多实例部署时,两个节点同时执行版本迁移导致冲突的问题。

具体实现上,Flyway 会尝试获取模式级别的锁。不同数据库的锁实现方式不同,MySQL 上通常是借助历史表的锁或元数据锁,PostgreSQL 上会借助自定义函数和锁表机制。总体来说,都是“锁—执行—释放”的三段式。

问题就出在这里:锁的获取是有等待上限和重试上限的。Flyway 默认会重试若干次,每次等待一个固定间隔。如果轮询时间耗尽仍然拿不到锁,理论上应该报错退出,不会真的无限等待。但现实里为什么会有“无限等待”?因为锁没有拿到不代表阻塞是短暂的,比如一个长事务一直不提交,锁就一直被别人捏在手里,重试再多轮也没用,最终表现就是长时间无进展。

2.2 两条最经典的锁竞争路径

我梳理了自己这些年排查过的案例,发现“无限等待”绝大多数落在以下两条路径上。

路径一:长事务持有元数据锁,Flyway 的 DDL 被排到队尾。以 MySQL 为例,Flyway 执行ALTER TABLE这类 DDL 时,需要获取表的元数据锁(MDL)。如果有业务线程开启了一个事务,执行了查询或写入,但迟迟没有提交,这个事务就持有了 MDL 读锁。DDL 需要 MDL 写锁,读锁和写锁冲突,于是迁移只能排着。更要命的是,一旦 DDL 开始在队尾等待,后续所有的新查询也会被它挡住,形成滚雪球效应,整个应用连接池瞬间被耗尽。

路径二:另一个实例正在跑迁移,本实例拿不到历史表锁。这种常见于集群环境。两个节点同时启动,节点 A 拿到了锁,正在执行一个耗时很长的迁移脚本;节点 B 没有拿到锁,进入重试循环。如果节点 A 的脚本执行了 20 分钟还不停,节点 B 就让人觉得像卡死了。这种等待看起来是“完全正常的设计行为”,但问题在于很多团队对迁移耗时没有预期,导致报警和误判。

2.3 并不只有锁会引发阻塞

锁只是最出名的一个节点,还有三类情况经常伪装成“锁等待”。

第一类是连接池耗尽。Flyway 在 Spring Boot 环境下默认使用应用的数据源,它需要从连接池中取一个连接。如果连接池最大连接数只有 10,而业务线程已经把连接全部占用了,Flyway 就只能干等。这时候日志还是停在迁移这一行,但数据库侧根本看不到它的会话。

第二类是迁移脚本内部的非阻塞等待。比如脚本里加了一个SELECT SLEEP(300),或者调用了存储过程,存储过程内部有长时间的循环,又或者脚本里访问了外部网络资源。数据库锁没被占用,但脚本本身要跑 5 分钟,外部观察者看到的就是“卡住”。

第三类是历史表数据被篡改。如果有人手工删掉了flyway_schema_history表里的部分记录,或者把一个失败的版本改成了成功,Flyway 对版本号的判断就会错乱,可能出现多次尝试执行同一个迁移、或尝试获取一个不存在的锁的情况。

这三类问题有一个共同点:它们在 thread dump 里往往显示的是WAITING或者阻塞在资源获取上,和数据库锁等待的线程栈看起来很像,必须到数据库侧对照才能区分。

3. 完整排查链路:从应用线程到数据库会话的一步步定位

一次标准的 Flyway 卡死排查,应该是一条从应用侧到数据库侧的线性链路。不要一开始就钻进数据库,也不要在应用侧反复重启,按下面的顺序来。

3.1 第一步:抓线程栈,确认线程阻塞位置

先拿到卡住进程的 PID,然后执行:

jstack <pid> > /tmp/stack_$(date +%s).log

然后看线程栈找两条线索。

第一条线索:线程名。Spring Boot 集成的 Flyway 迁移通常跑在主线程main里,或者在springApplicationRunListeners线程里。如果你看到迁移操作是在 Tomcat 业务线程里执行,那就是代码里手动触发了flyway.migrate(),排查思路要稍微调整。

第二条线索:栈帧里的关键类。正常执行 SQL 的栈里会出现 JDBC 驱动相关的包名,比如 MySQL 驱动里的StatementImpl.execute。如果栈里出现 Flyway 内部关于锁获取的类名,比如Database.lock、MetaDataTableImpl.lock之类的字样(不同 Flyway 版本包名有所差异,但关键词是lock),那基本可以判断它在等锁。

如果线程栈显示线程处于WAITING状态,且停在LockSupport.park或Object.wait,同时附近的栈帧是连接池的等待队列,比如HikariCP的ConnectionPool.getConnection,那阻塞点就在连接池,而不是数据库锁。

3.2 第二步:数据库侧查会话,找未结束的事务

拿到线程栈之后,无论初步判断是锁还是连接池,都要去数据库确认会话状态。

MySQL 上最常用的两句话:

SELECT * FROM information_schema.innodb_trx\G SELECT * FROM information_schema.processlist WHERE COMMAND <> 'Sleep';

看两个关键点。一是innodb_trx里是否有长时间未结束的事务,注意看trx_started字段,如果一个事务已经存在了十几分钟还没提交,它就是重大嫌疑对象。二是processlist里是否有Waiting for table metadata lock或者Waiting for table lock状态的会话,这会直接告诉你当前有没有 DDL 被堵在锁队尾。

PostgreSQL 上则用:

SELECT pid, state, wait_event_type, wait_event, query FROM pg_stat_activity WHERE state <> 'idle' ORDER BY backend_start; SELECT pg_blocking_pids(pid) FROM pg_stat_activity WHERE pid = <卡住的pid>;

pg_blocking_pids这招非常直接,它会列出阻塞某个会话的完整 PID 链条,定位阻塞源只需要顺着这个列表往上找。

3.3 第三步:对照时间线,判断卡住的先后关系

很多团队卡在这一步:数据库里确实有一个长事务,但不确定它和 Flyway 卡死之间谁是因谁是果。

判断的方法很简单:把三张时间表对齐。

  • 日志中 Flyway 卡住的时间点
  • 数据库事务trx_started的起始时间
  • 进程日志里应用启动的时间点

如果事务开始时间早于迁移开始时间,说明业务长事务早就在那里了,Flyway 是在它之后开始排队等锁。如果事务开始时间晚于迁移开始时间,那这个事务可能是迁移本身发起的,比如迁移脚本里统一执行了一些 INSERT 操作,卡住的根本原因可能在脚本逻辑内,不在外部锁竞争。

3.4 第四步:检查历史表状态,排除“历史账本”问题

Flyway 的迁移历史表flyway_schema_history是排查中的最后一块拼图。执行:

SELECT installed_rank, version, description, script, success FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 10;

重点看success字段。正常记录应该全是1。如果最后一条记录是0,说明上一次迁移其实是失败的,可能存在残留的锁记录或半执行状态。此时直接盲目重启,Flyway 可能试图重放同一个脚本,或者因为无法确认这次执行成功而不停重试。

我遇到过一种极其隐蔽的情况:某同事手工执行 DDL 后顺手删除了历史表里的记录,导致表结构和版本记录不一致。后续迁移脚本再次尝试执行已经存在的 DDL 时,数据库报了重复字段之类的错误,但 Flyway 把数据库当成了新库,一直尝试获取和维护结构锁,然后卡住不动。

4. 高频根因与对应解法:一张表看清主流原因

排查链路走完以后,最终都会落到具体根因。我把这几年遇到的高频原因整理成了一张表,加上一条对应的处理思路。

典型症状可能根因关键判断手段处理方向
日志停在迁移行,数据库无新会话连接池耗尽线程栈停在连接池等待扩大连接池或释放业务连接
日志停在迁移行,数据库有等待 MDL长事务持有元数据锁processlist 有 Waiting for metadata lock找到长事务终结合并增强监控
多实例同时启动,一个卡死另一个实例持有迁移锁数据库锁表检查确认另一实例状态,等其完成
数据库锁检查无异常,脚本仍很慢脚本内部耗时过长查迁移脚本内容和执行计划优化脚本写法,拆分长事务
历史表 success 为 0上次迁移中断查询 flyway_schema_historyflyway repair 或人工修复记录状态
迁移脚本每次重启都重试且卡住历史记录与真实结构不一致对比脚本版本与数据库结构用 repair 纠正版本记录

下面挑几个最典型的展开。

4.1 长事务 MDL 锁导致迁移排队:越修越糟的典型

先说一个高频场景:发布窗口选在了业务高峰期,Flayway 要执行ALTER TABLE修改某张核心业务表,但线上有一个持续十几分钟的报表查询事务一直没提交。这个事务持有该表的 MDL 读锁,迁移的 DDL 写锁只能排队。这个队一排不要紧,后续所有访问该表的查询也全部被阻塞。

这时候最错误的操作是直接重启应用。因为新启动的实例还会再次尝试跑迁移,又一次进入同一个锁队列,反而让问题变得更严重。

正确处理是:先在 processlist 中找到持有锁的长事务来源,评估是否可以结束该事务;然后在业务低谷期或专门的维护窗口重试迁移。对核心大表,DDL 尽量拆成多个小步骤,并提前通过异步方式执行,避免一次性锁大表太久。

4.2 连接池耗尽伪装成 Flyway 卡死

很多人在排查初期会忽略连接池这一层。以为卡的是 Flyway,其实是应用的数据源连接池已经没有空闲连接了,Flyway 只是众多求连接者中的一个而已。

这种场景的经典线程栈表现是:main线程停在 Hikari 连接池的connectionBag.borrow,同一时间有很多 Tomcat 业务线程也停在这里。

解决办法不是调 Flyway 参数,而是看业务代码为什么持有了那么多连接不释放。排查无外乎两种情况:连接泄漏,或者业务并发量超过了池子上限。可以用SELECT * FROM information_schema.processlist;看同一个应用实例的会话数,如果连接数已经达到池子上限且每条连接都处于长时间非空闲状态,那大概率连接泄漏。需要结合线程栈找到谁在借连接后无资源释放点。

4.3 多实例并发启动的锁竞争

集群部署时所有实例同时滚动发布,是一种很常见的触发条件。Flyway 本身设计上是支持多实例并发迁移的,但它要求拿不到锁的实例具备等待重试能力。

真正的风险点在于:持有锁的实例在迁移执行过程中如果崩溃或被杀掉,锁的释放时间取决于数据库侧是否会清理残留状态。某些情况下,残留锁会让后续实例反复尝试后最终超时。如果集群规模很大,锁竞争引发的累积等待可能让整个发布过程看起来很混乱。

最稳妥的解法是不要让所有实例同时启动迁移。如果使用 Kubernetes,可以用 InitContainer 或者其他方式让迁移任务先执行完成,再扩容业务实例。或者只在其中一个节点启动时开启 Flyway 迁移,其余节点关闭自动迁移,由运维流程统一触发。

4.4 脚本、历史表、版本错乱的隐性故障

数据库没有锁问题、连接池也正常,但迁移就是卡住,这时候十有八九问题出在脚本或者历史表上。

举例来说,某个版本的历史记录success是 0,之后 Flyway 尝试继续执行后续版本,就有可能在获取版本信息时反复崩溃并重试。另外,如果一个迁移脚本里包含多个 DDL 语句并且没有包在事务里(MySQL 不支持 DDL 回滚),前一条执行成功、后一条因为某种原因失败,Flyway 会把这个版本标记为失败。下一次启动时,Flyway 的行为取决于配置,有些配置下它会重试这个失败的版本,有些则跳过。如果团队之前手工在执行失败后改了历史表,把success改成 1,后续版本号判断就可能错位。

这种情况下最有效的工具是 Flyway 自带的repair命令,它会清理历史表中失败的记录、重新对齐版本校验和,然后再跑一次迁移。但使用repair前一定要确认数据库的真实结构状态,绝不能盲目修复后去重跑已经被部分执行的脚本。

5. 修复动作与参数建议:可直接上手的操作清单

定位到根因之后,就到了操作的环节。这部分我给的是可以直接落地的修复路径和配置参照,不保证每个项目百分百适用,因为数据库类型、Flyway 版本和部署方式会影响细节,但大方向是通用的。

5.1 临场止损:正确的打断顺序

先强调一个原则:不要第一时间重启应用,也不要直接 kill 数据库连接。

第一步,先在数据库侧找到持有阻塞一个锁的会话。MySQL 上可以用 sys 库的sys.innodb_lock_waits,里面直接给出了阻塞者和等待者的对应关系:

SELECT * FROM sys.innodb_lock_waits;

这个视图把等待锁的线程、持有锁的线程、对应的查询语句全部列出来了,比手工 joininformation_schema快得多。确认阻塞源之后,评估是否可以安全结束那个阻塞会话。如果是业务长事务,需要和业务负责人确认后再 kill。如果只是外部工具建立的多余连接,直接 kill 即可。

第二步,如果数据库侧没有任何阻塞项,但连接池耗尽,那就别重启了,先找到连接泄漏的根因,否则起来多少次都会被耗尽。

第三步,确认所有阻塞源清理完毕后,再重启应用让 Flyway 重新执行。重启前顺手检查一下历史表最新的success记录,如果是 0,先执行flyway repair再启动应用。

5.2 调整 Flyway 本身的等待参数

Flyway 提供了一些控制锁等待行为的配置项。虽然不是根治方案,但在锁竞争比较短暂、几秒内能结束的情况下,调整后能有效减少“假死”。

Spring Boot 项目里对应的配置示例:

spring: flyway: lock-retry-count: 30 lock-retry-interval: 1s

lock-retry-count表示 Flyway 获取锁失败后重试的次数,lock-retry-interval是每次重试之间的间隔。比如上面的配置,最多等待 30 秒就会放弃并抛出异常。

注意,它解决不了“长事务持股五分钟后终于释放”这种情况。锁只被占用 10 秒,默认重试参数也许不够,调大一点就能扛住;锁被占用 30 分钟,再大也白搭。所以最佳用法是配一个合理的重试上限,用来快速失败并暴露问题,而不是无限重试,否则就真的会从“等待”变成“无限等待”。

另外,如果你的服务用到了多个数据源,需要明确指定每个数据源对应的 Flyway 配置,否则迁移可能走向错误的数据源,造成完全不同的阻塞链路。

5.3 迁移脚本的规范化写法

结合我发现的各种卡死案例,在这里给迁移脚本的编写提几个建议。

第一个建议:一个迁移版本只做一件事。不要在一个脚本里既改表结构又灌数据又调用存储过程。迁移脚本是线性执行的,任何一个环节卡住,后续版本全部阻塞。把大数据迁移、数据订正和大结构变更加载拆成不同版本,可以让问题的影响面降低到某个具体版本内。

第二个建议:脚本内尽量不调用外部系统。我看到过有人在迁移脚本里通过 MySQL 的SELECT SLEEP(5)模拟等待,也有人在存储过程里调用外部 API。这在开发环境也许没问题,生产环境一个网络抖动就能让迁移脚本挂上一个小时,而且难以发现,因为它们不体现在锁上。

第三个建议:写脚本之前评估耗时。如果是几十万行的大表,直接加索引或改字段类型会让 MySQL 锁表很久。提前评估执行计划,在维护窗口运行,或者考虑使用在线 DDL 工具,而非放任迁移脚本在生产峰值期执行。

5.4 修复后的验证步骤

修复完成不代表结束,还要验证一下迁移链路恢复了正常。我一般按以下步骤检查:

  1. 应用启动日志完整经过迁移阶段,出现Successfully applied N migrations之类的记录(不同版本措辞有差异)。
  2. 数据库侧确认历史表中新增了本次迁移记录,且success = 1。
  3. 连接池中的连接数回落到正常水位,没有堆积。
  4. 再次执行几个核心业务接口,确认数据库读写正常。

如果验证时发现历史表中有缺失或不匹配的校验和记录,跑一下:

flyway -url=jdbc:mysql://... -user=... -password=... repair

repair会删除失败的记录并重新对齐校验和。执行完以后再看flyway info输出是否正常。

6. 预防无限等待:从机制上避免同类问题

修复一次问题只能算救火,真正要做的是建立预防机制。这方面我认为有三个点是值得投入精力的。

6.1 让“迁移”从应用启动中剥离出来

最彻底的做法是让 Flyway 从应用主进程里独立出来,变成发布流程中的一个独立环节。比如在 CI/CD 管道里单独跑一个迁移任务,等迁移确认成功后再进行业务实例的滚动发布。只要迁移不和应用生命周期绑定,就不会出现“业务连接池和迁移抢连接”“业务长事务挡住迁移 DDL”这种交织问题。

如果你不方便做完整剥离,至少可以确保多实例部署时只有一个实例开启自动迁移,或者通过配置开关临时关闭迁移,由运维手动触发。

6.2 给迁移配上可见性监控

无限等待最怕的不是等待,而是没人知道它在等什么。给 Flyway 迁移加上日志耗时埋点,是成本又低收益又高的做法。

具体来说,至少记录三类指标:

  • 每次迁移执行的起始和结束时间,计算总耗时
  • 数据库中锁等待发生时的等待时长
  • 历史表记录状态异常时的数量变化

这些指标一旦接入监控系统,再配合阈值告警,下次迁移只要超过预期耗时或出现锁等待,团队就能第一时间收到通知,而不是等用户来报障。

6.3 维护迁移脚本的“演算清单”

这不是什么复杂工具,就是一份文档,记录每个迁移版本执行前需要确认的事项。我建议至少包含这些检查项:

  • 目标表当前的记录规模,DDL 大概耗时预估
  • 是否需要联系业务方协调运行窗口
  • 底层数据库版本是否支持脚本中使用的语法
  • 脚本是否在测试环境完整演练过
  • 如果脚本执行失败,如何回滚或跳过

很多无限等待的问题,其实在做上线前评估时就有迹可循。大表加索引、核心表改结构这类操作,不应该悄无声息地夹在应用发布里,而是要像做手术一样提前排期。

6.4 维持一个健康的限制条件:迁移脚本只增不减

这个原则其实涉及 Flyway 的常规设计,但需要强调一个容易踩的坑:迁移脚本一旦发布,就不要再去修改它的内容。如果脚本内容变化,校验和会不匹配,Flyway 会拒绝执行,或者在某些配置下表现得很奇怪。正确做法是发现问题后新增一个修复脚本,让历史记录始终是追加式变更。

我见过一个项目因为需求变更,直接改了一个早期版本迁移脚本中的一段 SQL,后来所有促进环境里校验和全部对不上,每次启动迁移都可能出现异常等待。最后团队花了一个多小时重新梳理历史记录才恢复。

最后再补一点个人体会

Flyway 执行无限等待这个问题,表面上是数据库锁和连接池的资源竞争,背后其实是启动顺序、业务并发和迁移脚本质量这三件事有没有设计清楚。

我在排查类似问题的时候,感受最深的一点是:很多团队把 Flyway 当成“数据库的 Git”,以为它天然安全、不会出问题,结果忽略了它也是一个需要连接、需要锁、需要配合应用生命周期运转的组件。它确实能保证迁移版本的一致性,但它不负责帮你判断迁移脚本合不合理、迁移窗口选得对不对、应用连接池是否够用。

如果这篇文章看完只记住一句话,我会希望是这句:等你真正遇到无限等待时,别急着重启,先抓线程栈、查数据库锁、看历史表,把因果关系理顺了再动手。这套思路不仅仅适用于 Flyway,排查任何数据库相关卡死问题,都是同一个套路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 2:59:22

pocketsphinx.js:纯前端离线语音识别方案

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

作者头像 李华
网站建设 2026/10/12 2:59:05

PyTorch强化学习动态路径规划:43页实战文档拆解与避坑指南

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

作者头像 李华
网站建设 2026/10/12 2:58:19

PLC五种编程语言详解:梯形图、结构化文本怎么选

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

作者头像 李华
网站建设 2026/10/12 2:58:10

HALCON-DeepLearningTool工业视觉深度学习部署指南

简介&#xff1a;本资源是HALCON官方深度学习工具包DeepLearningTool的完整集成版&#xff0c;面向工业机器视觉领域的算法工程师、自动化设备开发人员及高校研究者&#xff0c;专为解决工业缺陷检测、字符识别、部件分类等实际场景中深度学习模型开发周期长、部署门槛高的问题…

作者头像 李华
网站建设 2026/10/12 2:57:39

自动机理论实战:从习题推导到代码验证与工程落地

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

作者头像 李华
网站建设 2026/10/12 2:57:37

车载显示屏重影与残影的本质区别及四层排查法

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

作者头像 李华