上一篇的日常巡检,主要是做接口账号的连接、长事务和慢 SQL ,实际业务中经常出现的问题还是医院接口平台侧:LIS 的待发送消息越来越多,数据库这一侧应该怎么查。
HIS 往 LIS 发送检验申请,往 PACS 发送检查申请,EMR、集成平台和第三方系统之间也会不断产生接口数据。业务系统把消息写进表,接口程序读取、发送,再更新处理状态。待发送数量增加时,先看消息停在哪个状态,再查连接和 SQL。下游接收失败、消费线程卡住、重试增多,都可能让表里的消息堆起来,不能只凭数量增加就认定是慢查询。
一、准备接口表和检查账号
测试库沿用 hip_lab,接口表使用 hip_interface_message。建表前确认 hip_lab 已存在;应用账号为 hip_app,检查时使用具备相应权限的 DBA 账号。查看其他账号的完整会话和事务信息、查询锁等待和死锁表,需要 PROCESS 权限。
CREATE TABLE hip_lab.hip_interface_message (
id BIGINT NOT NULL AUTO_INCREMENT,
message_no VARCHAR(40) NOT NULL COMMENT '接口消息唯一号',
visit_no VARCHAR(32) DEFAULT NULL COMMENT '测试就诊号',
source_system VARCHAR(16) NOT NULL COMMENT '来源系统',
target_system VARCHAR(16) NOT NULL COMMENT '目标系统',
message_type VARCHAR(24) NOT NULL COMMENT '消息类型',
process_status TINYINT NOT NULL DEFAULT 0
COMMENT '0待发送 1处理中 2成功 9失败',
retry_count SMALLINT NOT NULL DEFAULT 0 COMMENT '重试次数',
created_at DATETIME(3) NOT NULL COMMENT '消息产生时间',
next_retry_at DATETIME(3) DEFAULT NULL COMMENT '下次重试时间',
processed_at DATETIME(3) DEFAULT NULL COMMENT '处理完成时间',
payload MEDIUMTEXT DEFAULT NULL COMMENT '接口报文',
PRIMARY KEY (id),
UNIQUE KEY uk_message_no (message_no),
KEY idx_visit_no (visit_no),
KEY idx_poll_target_status_time (
target_system,
process_status,
created_at,
id
)
);
这里的状态值由接口程序约定,主要看目标系统、处理状态和消息时间,再结合重试次数判断处理进度。7.5 系列已经讨论过轮询 SQL 和组合索引,这篇保留原有索引,从消息状态开始排查。
二、确认哪些消息没有处理完
收到“LIS 消息积压”的反馈后,先按目标系统和状态统计最近一天产生的消息。成功历史记录也会保留在接口表里,需要和待发送、处理中、失败的消息分开看。
SELECT
target_system,
process_status,
COUNT(*) AS message_count,
MIN(created_at) AS oldest_message,
MAX(created_at) AS newest_message
FROM hip_lab.hip_interface_message
WHERE created_at >= NOW() - INTERVAL 1 DAY
GROUP BY
target_system,
process_status
ORDER BY
target_system,
process_status;
结果示例,只摘录待发送和失败的几行,时间显示到分钟:
| target_system | process_status | message_count | oldest_message |
|---|---|---|---|
| EMR | 0 | 3 | 2026-09-17 22:57 |
| LIS | 0 | 426 | 2026-09-17 22:21 |
| LIS | 9 | 61 | 2026-09-17 22:24 |
| PACS | 0 | 1 | 2026-09-17 23:08 |
这组数据里,LIS 待发送消息较多,最早一条的产生时间也比 EMR、PACS 更早。我把 LIS 作为后面检查的重点,同时检查状态为 1(处理中)的消息;处理了多久,还要用应用日志里的领取时间核对,不能直接拿产生时间计算。单个目标系统异常可以帮助缩小范围,但还不能排除它使用的 SQL、事务或访问节点有问题。
这条 SQL 只覆盖最近一天产生的消息,更早的积压不会出现在结果里。下面查询全部待发送记录时,去掉产生时间限制,避免漏掉旧消息。
三、看最老的一条已经等了多久
只看待发送数量,还不知道消息有没有继续向前处理。把最早产生时间和等待分钟数一起查出来,并和前一次记录对照。
SELECT
target_system,
COUNT(*) AS pending_count,
MIN(created_at) AS oldest_created_at,
TIMESTAMPDIFF(
MINUTE,
MIN(created_at),
NOW()
) AS oldest_wait_minutes
FROM hip_lab.hip_interface_message
WHERE process_status = 0
GROUP BY target_system
ORDER BY oldest_wait_minutes DESC;
延续上面的结果示例,LIS 的待发送数量较多,最早消息的产生时间也明显早于另外两个系统。等待了多少分钟,要以这次查询的时间计算;是否超出正常范围,还要对照接口约定的处理时限。
然后隔一段时间再查,比较待发送数量和最老消息时间。连续记录才能判断数量是否持续增加;最老消息时间一直不变时,还要查是否有消息长期滞留。接口表里保留几百万条成功记录,本身不能说明积压;这条查询也只统计状态 0,处理中和失败的消息还要分别检查。
四、比较十分钟内的新增量和成功完成量
消息可能一直在处理,只是完成速度没有跟上。把观察窗口固定为十分钟,分别按 created_at 和 processed_at 统计,完成量只计状态为 2 的成功消息。
SET @window_end = NOW(3);
SET @window_begin = @window_end - INTERVAL 10 MINUTE;
SELECT
COUNT(
CASE
WHEN created_at >= @window_begin
AND created_at < @window_end
THEN 1
END
) AS created_last_10m,
COUNT(
CASE
WHEN process_status = 2
AND processed_at >= @window_begin
AND processed_at < @window_end
THEN 1
END
) AS succeeded_last_10m
FROM hip_lab.hip_interface_message
WHERE target_system = 'LIS';
新增消息和成功消息分别按自己的时间字段统计。十分钟前产生、在这十分钟内完成的消息,也要计入成功完成量,不能在外层再用 created_at 把它过滤掉。这项统计要求成功记录仍保留在表中,完成状态和时间没有被重置;失败消息即使填写了 processed_at,也不会被算成成功。
比较多个时间段时,记录各自的起止时间,使用相邻、不重叠的窗口。在没有清理消息或重置状态的情况下,新增量持续大于成功完成量,未成功完成的消息就会累积。这些消息可能处于 0、1 或 9,差值不能直接当作待发送数量的增量,原因还要结合失败记录和接口程序继续查。这里的汇总需要读取 LIS 消息记录,不能把它当成高频轮询语句反复执行。
五、把失败消息和重试记录单独查出来
前面的示例还有 61 条 LIS 失败消息。按目标系统和消息类型展开,看失败是否集中在某一类业务消息,再看重试次数。
SELECT
target_system,
message_type,
COUNT(*) AS failed_count,
MAX(retry_count) AS max_retry_count,
MIN(created_at) AS oldest_message_created_at,
MAX(next_retry_at) AS latest_next_retry_at
FROM hip_lab.hip_interface_message
WHERE process_status = 9
AND created_at >= NOW() - INTERVAL 1 DAY
GROUP BY
target_system,
message_type
ORDER BY failed_count DESC;
oldest_message_created_at 是这批失败消息中最早的产生时间,不能当作首次失败时间;latest_next_retry_at 是最晚的计划重试时间,也不能当作最近一次实际重试的时间。当前表没有记录每次失败发生的时间,这部分要去应用日志里找。如果积压已经超过一天,失败消息的查询范围也要相应放宽。
按状态约定,失败记录和非零重试次数表明应用写入过失败、重试信息。要确认重试是否还在增加,对照同一 message_no 前后的 retry_count,不能只看一次 MAX(retry_count)。这些字段无法证明请求已经发到 LIS,也无法区分下游业务拒绝、网络超时和报文解析失败,需要继续核对接口日志。
六、检查接口账号的数据库连接
消息状态看完,查 hip_app 的连接。集群有多个 TiDB Server,使用 CLUSTER_PROCESSLIST 可以汇总各节点的会话,避免只看到当前连接节点的情况。
SELECT
INSTANCE,
USER,
DB,
COMMAND,
COUNT(*) AS session_count
FROM information_schema.CLUSTER_PROCESSLIST
WHERE USER = 'hip_app'
GROUP BY
INSTANCE,
USER,
DB,
COMMAND
ORDER BY
INSTANCE,
COMMAND;
如果没有查到 hip_app 连接,先确认接口应用是否运行,查看连接池、网络和客户端报错。查询账号权限也要核对,不能把看不到其他用户的连接当成没有连接。连接仍在时,再查这些会话当前执行什么;连接数本身不能证明消费线程正常。
七、查看接口会话当前在执行什么
下面排除 Sleep 会话,按持续时间排序。主要看有没有长时间不结束的 SQL,以及多个会话是否都停在同一类更新语句上。
SELECT
INSTANCE,
ID,
USER,
HOST,
DB,
COMMAND,
TIME,
STATE,
LEFT(INFO, 300) AS SQL_TEXT
FROM information_schema.CLUSTER_PROCESSLIST
WHERE USER = 'hip_app'
AND COMMAND <> 'Sleep'
ORDER BY TIME DESC;
PROCESSLIST 反映查询时正在处理的请求。轮询 SQL 可能很快结束,查系统表时不一定正好碰上。当前没有非空闲会话,只能记录这次没有看到正在执行的语句,不能据此判断接口程序已经停止工作;近期是否持续访问,继续查 Statement Summary。
八、用 Statement Summary 查看近期访问
TiDB 默认开启 Statement Summary,按 SQL Digest、Plan Digest 等信息聚合语句,可以查看执行次数、使用的索引和最近出现时间。用 CLUSTER_STATEMENTS_SUMMARY 查询整个集群的当前汇总周期。
SELECT
INSTANCE,
DIGEST_TEXT,
EXEC_COUNT,
INDEX_NAMES,
FIRST_SEEN,
LAST_SEEN
FROM information_schema.CLUSTER_STATEMENTS_SUMMARY
WHERE SCHEMA_NAME = 'hip_lab'
AND DIGEST_TEXT LIKE '%hip_interface_message%'
ORDER BY LAST_SEEN DESC
LIMIT 20;
这里按默认库 hip_lab 筛选。如果应用连接使用其他默认库,再通过全限定表名访问接口表,就要调整筛选条件,不能直接用空结果判断没有访问。查询也没有限定执行账号,同类语句可能来自其他账号,归属还要结合连接信息和应用日志确认。
下面是轮询和状态更新两类语句的形态示例。? 表示参数占位,省略了归一化文本中的引号等格式,不能直接复制执行。
SELECT id, message_no, visit_no, message_type, created_at
FROM hip_interface_message
WHERE target_system = ?
AND process_status = ?
ORDER BY created_at, id
LIMIT ?;
UPDATE hip_interface_message
SET process_status = ?,
processed_at = ?
WHERE id = ?;
对照同一汇总周期内的记录,看 EXEC_COUNT 是否增加、LAST_SEEN 是否向后推进。这能确认同类 SQL 仍有执行记录,但不能直接证明消费线程处理正常,或 LIS 已经成功接收消息。状态更新 SQL 出现过,也不等于每次都更新成功或实际影响了记录。
默认配置下,语句摘要保存在内存中,汇总周期为 1800 秒;当前周期以外的数据要查 CLUSTER_STATEMENTS_SUMMARY_HISTORY,历史保留数量和摘要容量也有限。如果启用了语句摘要持久化,历史保存方式会变化。查询没有结果时,还要核对采集开关、时间范围和是否发生淘汰。
九、检查接口事务是否长时间未结束
接口程序如果把数据库事务和外部调用放在一起,LIS 响应慢时,数据库事务也可能一直没有结束。例如,程序开启事务后取出 100 条消息,把状态改成处理中,随后调用 LIS、等待响应,再更新状态并提交。这段等待时间需要和数据库里的事务持续时间对照。
SELECT
INSTANCE,
ID AS trx_id,
START_TIME,
TIMESTAMPDIFF(
SECOND,
START_TIME,
NOW()
) AS trx_seconds,
STATE,
USER,
DB,
SESSION_ID,
MEM_BUFFER_KEYS,
MEM_BUFFER_BYTES,
LEFT(
CURRENT_SQL_DIGEST_TEXT,
300
) AS current_sql
FROM information_schema.CLUSTER_TIDB_TRX
WHERE USER = 'hip_app'
ORDER BY START_TIME;
CLUSTER_TIDB_TRX 提供各 TiDB 节点当前事务的信息,包括开始时间、会话 ID、当前语句和内存缓冲区写入量。事务状态可能是 Idle、Running、LockWaiting、Committing 或 RollingBack。Idle 表示当前没有执行语句,不代表事务已经结束;CURRENT_SQL_DIGEST_TEXT 是归一化文本,也可能为 NULL。如果积压发生在更早的时间,还要对照当时的应用和数据库记录。
十、查看当前锁等待和保留的死锁记录
多个消费线程同时更新同一批消息时,还需要看锁等待。用 DATA_LOCK_WAITS 查看从 TiKV 收集到的当前等待信息,再根据等待事务和持锁事务继续定位。
SELECT
TRX_ID,
CURRENT_HOLDING_TRX_ID,
SQL_DIGEST_TEXT,
KEY_INFO
FROM information_schema.DATA_LOCK_WAITS;
| 字段 | 检查内容 |
|---|---|
TRX_ID |
正在等待锁的事务 ID |
CURRENT_HOLDING_TRX_ID |
持有锁的事务 ID |
SQL_DIGEST_TEXT |
被阻塞语句的归一化文本,可能无法获取 |
KEY_INFO |
被锁 Key 对应的表、索引或记录信息 |
空结果示例:
Empty set
这个结果只能记为“本次查询未返回锁等待记录”。DATA_LOCK_WAITS 实时向 TiKV 收集信息,各节点的数据也不保证来自同一时刻。它不保留历史等待,之前短暂发生的阻塞,事后可能查不到;集群较大或负载较高时,最好不要过于频繁查询这个表。
如果怀疑发生过死锁,再查最近一小时仍保留在 CLUSTER_DEADLOCKS 中的记录:
SELECT
INSTANCE,
DEADLOCK_ID,
OCCUR_TIME,
RETRYABLE,
TRY_LOCK_TRX_ID,
CURRENT_SQL_DIGEST_TEXT,
TRX_HOLDING_LOCK
FROM information_schema.CLUSTER_DEADLOCKS
WHERE OCCUR_TIME >= NOW() - INTERVAL 1 HOUR
ORDER BY OCCUR_TIME DESC;
这条查询中的“一小时”只是过滤范围。默认情况下,每个 TiDB 节点只保留最近 10 个死锁事件,且不收集可重试死锁;节点重启后记录也不会保留。即使查询为空,也不能排除之前发生过死锁,更不能排除普通锁等待。
十一、回头检查轮询 SQL
前面的 7.5 系列已经讨论过接口轮询 SQL 和索引优化。这次查看轮询语句有没有变慢,保留原来的索引设计。Dashboard 概况页可以查看近期 SQL、慢查询和整体延迟;需要按库和表筛选时,我用 CLUSTER_SLOW_QUERY。
SELECT
INSTANCE,
TIME,
USER,
QUERY_TIME,
PROCESS_KEYS,
TOTAL_KEYS,
DIGEST,
LEFT(`QUERY`, 300) AS SQL_TEXT
FROM information_schema.CLUSTER_SLOW_QUERY
WHERE DB = 'hip_lab'
AND TIME >= NOW() - INTERVAL 30 MINUTE
AND `QUERY` LIKE '%hip_interface_message%'
ORDER BY QUERY_TIME DESC
LIMIT 20;
TiDB 8.5 的 tidb_slow_log_threshold 默认是 300 ms,现场要以实际配置为准;查询结果中的 QUERY_TIME 单位为秒。这里同样按默认库过滤,并通过语句文本匹配表名,必要时要调整筛选范围
十二、整理数据库侧的检查结果
结果示例按检查顺序放在一起,供后面核对接口日志时使用。
| 检查项 | 结果示例中的现象 | 还需要核对的内容 |
|---|---|---|
| 待发送消息 | LIS 数量较多,连续观察时仍在增加 | 记录观察时间和最老消息时间,核对接口处理时限 |
| 失败和重试 | LIS 有失败记录,部分消息有多次重试 | 同一消息的重试变化、失败发生时间和应用错误 |
| 数据库连接 | hip_app 连接仍在 |
消费线程是否正常,之前是否发生过连接异常 |
| 近期 SQL | 同类轮询和状态更新语句持续出现 | 执行账号、语句执行是否成功及业务处理结果 |
| 当前事务 | 未观察到明显长事务 | 更早时间段是否有事务未及时提交 |
| 锁等待和死锁 | 当前查询没有等待记录,保留的死锁记录中未查到相关信息 | 未被当前查询和保留记录覆盖的阻塞情况 |
| 轮询 SQL | 未集中进入慢日志,原有组合索引仍被使用 | 日志配置、历史耗时和实际执行计划 |
按这组检查结果,暂时没有取得长事务、锁等待或轮询 SQL 明显变慢导致积压的证据。现实生产业务中,LIS 失败和重试消息还需要关注核对接口程序、网络及下游响应。
十三、提取能关联应用日志的消息
失败记录主要集中在 LIS 时,把对应消息编号和时间范围整理给接口开发或应用运维。下面只取排查需要的字段,不读取完整报文。
SELECT
message_no,
visit_no,
message_type,
retry_count,
created_at,
next_retry_at
FROM hip_lab.hip_interface_message
WHERE target_system = 'LIS'
AND process_status = 9
ORDER BY created_at
LIMIT 50;
这条查询取最早的 50 条失败消息,message_no、消息类型、产生时间和重试次数可以用于查找对应应用日志,例如测试消息号 HIP202609172241580031。就诊号只在确有业务关联需要时使用;完整 HL7、XML 或 JSON 报文包含更多患者信息,不能为方便排查就整段复制、转发。
十四、继续检查接口程序和下游响应
数据库连接仍在、同类 SQL 也有执行记录时,把消息编号交给接口人员,对照发送线程、线程池和应用日志,确认消息在哪一步停住。需要查清楚请求有没有发出、TCP 或 HTTP 连接有没有报错,以及 LIS 返回了什么响应;消费并发是否调整、失败消息为什么持续重试,也要一并核对。
如果同一批消息的 retry_count 持续增加,失败又集中在 LIS,优先核对这批消息的发送和响应记录。失败状态可能在报文解析、连接下游或收到业务拒绝后写入,具体停在哪一步,不能只靠消息表判断。
十五、出现这些现象时,继续查数据库
应用侧的检查进行时,数据库侧仍按实际现象处理。下面几种情况需要继续定位,不能因为前一次查询没有异常就停止检查。
| 现象 | 数据库侧继续检查的内容 |
|---|---|
hip_app 无法建立连接 |
TiDB Server、代理入口、权限、连接数和客户端报错 |
多个事务长时间处于 LockWaiting |
用 DATA_LOCK_WAITS 查等待事务和持锁事务,结合事务持续时间判断 |
接口 UPDATE 长时间不结束 |
对照 CLUSTER_PROCESSLIST、CLUSTER_TIDB_TRX 和语句摘要 |
| 轮询 SQL 频繁进入慢日志,或耗时明显偏离以往 | 检查执行计划、统计信息、扫描 Key 数和索引 |
| 多个业务系统同时出现数据库访问异常 | 扩大到 TiDB、PD、TiKV、监控和资源使用情况 |