A/B 设备状态不一致排查实录:从“看起来没更新”到 binlog 定位“谁把 state 改回 0”
适用人群:后端同学、运维同学、需要排查“两个库同一条设备状态不一致”的场景
关键词:MySQL 跨库事务、binlog(ROW)、mysqlbinlog、时区、触发器审计
背景:为什么要做 A 与 B 状态强一致
在项目里,A 系统与 B 系统都有设备表,并且设备的“运行状态(0~9)”在业务上要求一致。
早期做法通常是:
- 只更新 A 库,B 通过异步事件/定时补偿同步;或
- 两边各写各的,偶发不一致后靠人工 SQL 修正。
当设备在线/离线、告警、离网运行等状态频繁跳变时,这些方案会出现典型问题:
- 消息并发、乱序导致写入覆盖;
- 异步同步失败或延迟,导致页面展示不一致;
- 运维部署时系统卡顿,某些更新丢失或延迟执行。
因此我们选择了“方案A:同 MySQL 实例两库(两个 schema),A 为主,B 同步写入,强一致”:
- 在一次事务中同时更新
A.eiot_device_info与B.device - 任何一边更新失败,整体回滚,避免“只更新一半”
现象:日志显示 B 更新成功,但最终 B 表里 state 变成 0
我们在 DevicePropertyHandler 里根据设备上报 runStatus 更新状态:
- 设备状态更新日志(示例):
2026-03-13 10:57:43.077 DevicePropertyHandler | 设备:PCSEPCS105202512050001状态更新 [8 -> 7]
2026-03-13 10:57:43.080 DeviceInfoServiceImpl | [A]更新设备状态成功: deviceSn=PCSEPCS105202512050001, state=7
2026-03-13 10:57:43.082 DeviceInfoServiceImpl | [B]更新B设备状态成功: deviceSn=PCSEPCS105202512050001, state=7
但事后在数据库里看到:
A.eiot_device_info.state = 7B.device.state = 0
直觉上会怀疑:
- 事务没生效?
- 并发乱序覆盖?
- 读到了从库旧数据?
但这类问题一定要用“证据链”说话:日志 + 数据库写入记录(binlog)。
先把链路串起来:到底是谁在更新状态
状态更新调用链(简化):
DevicePropertyHandler.updateDeviceStatus(...)deviceApi.updateDeviceState(deviceSn, runStatus)DeviceInfoServiceImpl.updateDeviceState(deviceSn, state)(@Transactional)- 事务内:
- 更新
A.eiot_device_info - 更新
B.device(同一个 MySQL 实例,不走 MQ)
- 更新
我们在 DeviceInfoServiceImpl.updateDeviceState 里打印了成对日志:
[A]更新设备状态成功 ...[B]更新B设备状态成功 ...
这意味着:10:57:43 这次写入,A 和 B 在那一刻确实都写到了 state=7。
那么“B 最后变成 0”只有一种解释:
在 10:57:43 之后,又有另外一个写入把
B.device.state从 7 改回了 0。
接下来我们要找的就是:这次“改回 0”的写入来自哪里。
用 binlog 取证:谁在什么时候把 state 改成 0
1)确认 binlog 是否开启、格式是什么
进入 MySQL:
SHOW BINARY LOGS;
SHOW VARIABLES LIKE 'binlog_format';
要点:
- binlog 文件名的数字 越大越新(如
binlog.000157比binlog.000156新) binlog_format=ROW很常见:写入记录按“行变更”存储,适合精准还原字段变化
2)注意:mysqlbinlog 不是 SQL 命令,要在 Linux shell 执行
很多人第一次会在 mysql> 里敲 mysqlbinlog ...,会得到语法错误。
正确方式:退出 mysql 客户端,在服务器终端执行:
cd /var/lib/mysql
# 这个命令容易卡住,用ctrl+z退出,有时候ctrl+c退不出
mysqlbinlog binlog.000157 | less
# 如果只需要看最新一百行,可以这样:先生成 SQL 文件,再用 tail 或 less
mysqlbinlog --no-defaults binlog.000157 > binlog.sql
tail -n 100 binlog.sql | less
3)时区坑:应用日志是本地时间,binlog 时间可能是 PST/UTC
我们在 binlog 尾部看到了:
# original_commit_timestamp=... (2026-03-13 16:52:16.381990 PST)
这说明:binlog 输出使用了 PST。
如果你拿应用日志的 2026-03-13 10:57 直接作为 --start-datetime,会查错时间段。
实践建议:
- 先
mysqlbinlog ... | tail看它标注的时区 - 把应用时间换算到 binlog 输出的时区后再查
4)用 grep 快速定位某设备相关写入(按 device_sn)
我们要找 PCSEPCS105202512050001 这台设备在 B 表的更新:
mysqlbinlog --no-defaults --base64-output=DECODE-ROWS -vv \
--start-datetime="2026-03-12 18:50:00" \
--stop-datetime="2026-03-13 16:10:00" \
binlog.000156 | grep -n "PCSEPCS105202512050001" -C5
这一步能初步确认:
- 有没有出现
UPDATE \B`.`device`` - 大概在哪个时间点附近发生
5)拿到关键证据:state 从 7 被写成 0
进一步缩小时间窗(示例):
mysqlbinlog --no-defaults --base64-output=DECODE-ROWS -vv \
--start-datetime="2026-03-13 15:28:20" \
--stop-datetime="2026-03-13 15:28:50" \
binlog.000156 \
| sed -n '/### UPDATE `B`.`device`/,/COMMIT/p' \
| sed -n '/PCSEPCS105202512050001/,$p'
输出里会出现类似(重点看 @8):
### UPDATE `B`.`device`
### WHERE
### @2='PCSEPCS105202512050001'
### @8=7
...
### SET
### @2='PCSEPCS105202512050001'
### @8=0
...
COMMIT
这就是铁证:
- 15:28:33(PST)
B.device.state7 → 0
6)把 @n 映射回列名(避免看错字段)
binlog 的 ROW 输出用 @1/@2/... 表示列序号,所以必须确认列顺序:
SHOW CREATE TABLE `B`.`device`\G
我们看到列顺序里:
@2对应device_sn@8对应state
因此 @8=7 → @8=0 就是 state 被改回 0,不是别的字段(比如 dept_id)。
结论:不是“强一致没生效”,而是“B 侧又写了一次覆盖”
证据链合并:
- 10:57:43 应用日志显示
[A]与[B]都更新成功到 state=7 - 15:28:33 binlog 显示
B.device.state7 → 0 - 且这次写入 只更新了 B 表,没有同时更新 A 表(说明不是 A 事务链路写的)
因此根因不是事务问题,也不是“那次没更新到 B”,而是:
B(或某个独立进程/脚本/任务)在后续把 state 覆盖成 0。
典型来源包括:
- B 内部“离线判定/状态纠正”定时任务
- 后台保存设备信息接口,携带默认 state=0 覆盖
- 运维脚本批量修正
如何继续追“到底是谁写的”:两种抓凶手方案
binlog(ROW)能告诉你“改了什么”,但通常不直接带 user/host。
要找来源,推荐以下两种方式。
方案1:短时间开启 general_log(需要能复现/等待再次发生)
查看 general_log 状态:
SHOW VARIABLES LIKE 'general_log%';
如果 general_log=OFF,可以短时间打开(建议 1~2 分钟内关掉):
SET GLOBAL general_log = 'ON';
-- 等待/复现
SET GLOBAL general_log = 'OFF';
然后查看 general_log_file 中记录的 user@host 与 SQL。
注意:general_log 开销大,尽量短开。
方案2:触发器审计(不需要立刻复现,推荐)
如果你不知道怎么复现,最好用触发器在 DB 侧“长期盯梢”,只记录 state 变更。
1)建审计表
CREATE TABLE IF NOT EXISTS `B`.`device_state_audit` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`device_sn` VARCHAR(64) NOT NULL,
`old_state` TINYINT NULL,
`new_state` TINYINT NULL,
`change_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`connection_id` BIGINT NOT NULL,
`current_user_name` VARCHAR(128) NOT NULL,
`session_user_name` VARCHAR(128) NOT NULL,
`host_name` VARCHAR(128) NULL,
PRIMARY KEY (`id`),
KEY `idx_device_sn_time` (`device_sn`, `change_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2)建触发器(只要 state 变就记录)
DROP TRIGGER IF EXISTS `B`.`trg_device_state_audit`;
DELIMITER //
CREATE TRIGGER `B`.`trg_device_state_audit`
BEFORE UPDATE ON `B`.`device`
FOR EACH ROW
BEGIN
IF (NOT (OLD.`state` <=> NEW.`state`)) THEN
INSERT INTO `B`.`device_state_audit`(
`device_sn`, `old_state`, `new_state`,
`connection_id`, `current_user_name`, `session_user_name`, `host_name`
) VALUES (
NEW.`device_sn`, OLD.`state`, NEW.`state`,
CONNECTION_ID(), CURRENT_USER(), USER(), @@hostname
);
END IF;
END//
DELIMITER ;
3)查询审计结果
SELECT *
FROM `B`.`device_state_audit`
WHERE device_sn = 'PCSEPCS105202512050001'
ORDER BY id DESC
LIMIT 50;
你会得到:
- 哪个账号(
current_user_name/session_user_name) - 哪个连接(
connection_id) - 什么时候把状态改成了什么
这就能把“写入来源”锁定到某个服务/脚本账号上。
最终怎么解决(建议落地步骤)
这类问题最终要“堵住覆盖写”的口子,而不是在 A 侧不断重写。
推荐落地步骤:
- 先用触发器审计(方案2)抓到 “state=0 的写入来源账号/时间”
- 在 B 代码/脚本里定位对应逻辑:
- 是否有离线判定任务?
- 是否有保存设备信息接口把 state 默认写 0?
- 按业务规则修复:
- 如果 B 的离线判定只想改“在线/离线”,不要覆盖 0~9 运行态:
- 要么改写另一个字段(例如
online) - 要么只在特定条件下更新(如 state 为某些值才允许覆盖)
- 要么改写另一个字段(例如
- 如果 B 的离线判定只想改“在线/离线”,不要覆盖 0~9 运行态:
- 增加防御:
- 对写入 0 的逻辑加日志(deviceSn、old/new、调用栈)
- 或加 DB 侧约束/触发器拦截(谨慎使用)
附:常用命令速查
查看 binlog 文件列表(数字越大越新)
SHOW BINARY LOGS;
解析 binlog(ROW)为可读输出
mysqlbinlog --no-defaults --base64-output=DECODE-ROWS -vv binlog.000156 | less
快速搜某设备 SN 的写入
mysqlbinlog --no-defaults --base64-output=DECODE-ROWS -vv binlog.000156 \
| grep -n "PCSEPCS105202512050001" -C5
确认列顺序(把 @n 映射到列名)
SHOW CREATE TABLE `B`.`device`\G
总结
这次问题的关键不是“猜”,而是“取证”:
- 日志证明:A→B 强一致更新在 10:57:43 是成功的
- binlog 证明:15:28:33 有另外一次写入把 B state 从 7 改成 0
- 下一步:用触发器审计/短期开 general_log 找出写入来源,修复 B 侧覆盖写逻辑
只要把“覆盖写 state=0 的源头”处理掉,A 与 B 的状态就能稳定一致。
更多推荐


所有评论(0)