一、背景现象
1. 问题描述
① 数据表拆分,双写开启。
数据组监听Binlog变更消息清洗数据,灰度期间发现以updateTime
作为分区条件,新老表查询结果数据量相差较大。
取其中一条id,对比新老表数据发现除updateTime
外值都相同,此外业务流程正常运行。
② 新表设置了CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
,updateTime
显式塞值。
老表设置了CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
,但updateTime
没显式塞值。
③ 调Mapper更新后会校验结果是否大于0。
④ 以id=1847114853571559274
为例,分别使用Navicat和单测执行,验证问题,sql为:
UPDATE t_sales_order SET state = 20 WHERE order_id = 1847114853571559274;
Debug发现MyBatis 日志显示更新成功,但MySQL表中数据未变化。
Navicat 执行相同 SQL 返回“受影响的行: 0”,明确告知数据未变更。
2. 矛盾点
- MyBatis 认为操作成功,但数据未实际更新;
- 老表设置的
CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
未生效; - 老表没有Binlog(格式为ROW)产生,不会刷新数据;
- 新表显式更新
updateTime
,有Binlog产生,新表数据被刷新。
二、问题定位
1. 核心问题
- MyBatis 的误导性返回值 MyBatis 默认返回的是 MySQL 的 匹配行数(Rows Matched),而非 实际受影响行数(Affected Rows)。
- MySQL 的优化机制 当
UPDATE
语句设置的字段值与当前值完全一致时,MySQL 不会实际修改数据,因此 Affected Rows=0。
2. 示例对比
操作 | MyBatis 返回值(默认) | Navicat 返回值 | 实际数据变更 |
---|---|---|---|
更新字段值与原值一致的记录 | 1(匹配行数) | 0(受影响行数) | 否 |
更新字段值与原值不同的记录 | 1(受影响行数) | 1(受影响行数) | 是 |
三、底层原因分析
1. MySQL 的更新机制
(1) SQL 执行全流程
一条UPDATE
语句的执行流程如下(以 InnoDB 引擎为例)。
- 解析与优化
- 解析 SQL,验证语法和权限。
- 优化器生成执行计划(如选择索引或全表扫描)。
- 定位目标行(Rows Matched)
- 执行引擎根据
WHERE
条件遍历表或索引,找到所有符合条件的行。 - 此时确定 Rows Matched 的值(即匹配的行数)。
- 逐行处理(Affected Rows)
-
对每一匹配的行,执行引擎执行以下操作:
a. 读取当前值:从存储引擎(如 InnoDB)中读取行的当前值。
b. 比对字段值:将
SET
子句中的新值与当前值逐一比对。c. 决定是否修改:
-
若字段值未变化:跳过物理更新,但该行仍计入 Rows Matched。
-
若字段值已变化:修改数据页,记录 Redo Log 和 Undo Log,并累计 Affected Rows。
-
此时确定 Affected Rows 的值(即实际修改的行数)。
-
- 生成响应
- MySQL 服务器将Rows Matched 和 Affected Rows 写入 OK Packet,返回给客户端。
(2) 关键环节详解
优化器决策
- 若发现
UPDATE
语句设置的字段值与当前值 完全一致,优化器直接跳过物理更新(避免无意义 I/O)。
存储引擎(InnoDB)处理
- 定位目标行:通过索引或全表扫描找到符合 WHERE 条件的行。
- 值比对:若字段值未变化,跳过数据页修改和日志写入。
Binlog 写入规则
- ROW 格式:仅当数据实际变更时记录行变更事件。
- STATEMENT 格式:记录原始 SQL(可能导致从库重复无意义更新)。
2. JDBC 驱动的默认行为
- 默认配置(useAffectedRows=false) MySQL Connector/J 驱动默认返回 匹配行数(Rows Matched),而非实际受影响行数。
- 历史原因:早期 MySQL 版本仅返回匹配行数,JDBC 驱动为兼容性保留默认行为。
四、解决方案
1. 配置 JDBC 驱动
在数据库连接 URL 中添加 useAffectedRows=true:
jdbc:mysql://host:port/db?useAffectedRows=true
效果:
- MyBatis 将返回 实际受影响行数(而非匹配行数)。
- 示例:
int affectedRows = sqlSession.update("updateOrder", params);
// 若数据未变更,affectedRows=0
2. 代码层显式校验
即使配置了 useAffectedRows=true,仍可在代码中校验返回值:
public void updateOrderStatus(String orderId, int status) {
int affectedRows = sqlSession.update("updateOrder", Map.of("orderId", orderId, "status", status));
if (affectedRows == 0) {
throw new BusinessException("更新失败,数据未实际变更");
}
}
3. 强制触发数据变更
若业务要求必须记录更新时间,可显式修改时间戳字段:
UPDATE t_order
SET status=20, update_time=NOW() -- 强制更新时间戳
WHERE id=100;
4. 增加update_time字段,代码显示塞值
为核心表添加 update_time
字段,并配置 ON UPDATE CURRENT_TIMESTAMP
触发实际变更:
CREATE TABLE t_order (
id INT PRIMARY KEY,
status INT,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
五、总结
- MyBatis 的默认行为具有误导性:依赖默认返回值可能导致业务逻辑错误。
- 若依赖数据库事件(如 监听 Binlog),“伪更新”可能导致消息丢失。