在一次线上问题排查中,遇到了一个非常"反直觉"的现象:
新增、修改操作看起来都成功了,但数据库里却没有任何数据变化;
而删除操作却直接抛异常。
一、问题现象
系统运行过程中,出现了如下情况:
-
新增 / 编辑接口
-
接口返回成功
-
程序日志无异常
-
数据库中无任何数据变更
-
-
删除接口
-
一执行就直接失败
-
Hibernate 抛出异常
-
异常信息如下:
org.hibernate.exception.GenericJDBCException: JDBC exception executing SQL [delete from system_operation_log where create_time <= ?] [The MySQL server is running with the --read-only option so it cannot execute this statement]
从表面看,这个问题非常矛盾:
如果数据库是只读的,
为什么 INSERT / UPDATE 不报错,
DELETE 却会立刻失败?
二、根本原因:MySQL 开启了只读模式
异常信息中已经明确给出了关键线索:
java
The MySQL server is running with the --read-only option
说明当前连接的 MySQL 实例处于只读状态,常见原因包括:
-
连接的是 MySQL 从库
-
主库被临时设置为:
-
read_only = ON -
或
super_read_only = ON
-
-
云数据库发生主从切换,应用未及时切换连接
可以通过如下 SQL 验证:
sql
SHOW VARIABLES LIKE 'read_only';
SHOW VARIABLES LIKE 'super_read_only';
只要其中任意一个为 ON,写操作都会受到限制。
三、为什么 INSERT / UPDATE 不报错?
很多人第一次遇到这个问题都会被误导,其核心原因在于:
👉 Hibernate 采用了延迟执行 SQL 的机制
3.1 Hibernate 的延迟执行机制
在使用 JPA / Hibernate 时:
java
repository.save(entity);
这一步并不会立刻执行 SQL,而是:
-
将实体对象放入 Persistence Context(一级缓存)
-
标记为"待插入 / 待更新"
-
等待事务提交或 flush 时才真正执行
真正发送 SQL 的时机通常是:
-
事务提交(commit)
-
显式调用
flush() -
查询前触发自动 flush
3.2 只读库 + 事务 = "假成功"
当数据库处于只读状态时:
-
Hibernate 可以正常生成 INSERT / UPDATE SQL
-
但在事务提交或 flush 阶段:
-
SQL 被数据库拒绝
-
事务回滚
-
-
异常被框架处理或被吞掉
最终表现为:
Java 代码无异常
接口返回成功
数据库无任何变化
这种情况在以下场景中尤为常见:
-
使用了
@Transactional -
上层代码 catch 了异常但未抛出
-
定时任务 / 异步任务
-
Spring 的只读事务优化
四、为什么 DELETE 会立刻报错?
DELETE 操作与 INSERT / UPDATE 最大的不同在于:
👉 DELETE 必须立即执行 SQL 并返回影响行数
例如:
sql
delete from system_operation_log where create_time <= ?
其执行流程为:
Hibernate
↓
JDBC
↓
MySQL(只读模式)
↓
立即抛 SQLException
因此:
-
DELETE 无法被延迟
-
MySQL 会立刻拒绝写操作
-
Hibernate 直接抛出
GenericJDBCException
五、INSERT / UPDATE / DELETE 行为对比
| 操作类型 | Hibernate 执行方式 | 只读库下表现 |
|---|---|---|
| INSERT | 延迟执行 | 不一定立刻报错 |
| UPDATE | 延迟执行 | 不一定立刻报错 |
| DELETE | 立即执行 | 立刻报错 |
这正是问题"看起来很诡异"的根本原因。
六、如何快速定位和验证问题?
6.1 确认当前连接的数据库角色
sql
SELECT @@hostname, @@read_only, @@super_read_only;
6.2 检查是否使用了只读事务
java
@Transactional(readOnly = true)
在只读事务中:
-
Hibernate 可能直接跳过 flush
-
不报错,但永远不会写库
6.3 检查是否使用了读写分离数据源
常见风险点包括:
-
AbstractRoutingDataSource
-
AOP 动态切换数据源
-
MyBatis / JPA 混用
-
删除走主库,新增却被路由到从库
6.4 强制 flush 验证(非常有效)
java
repository.save(entity);
entityManager.flush();
如果这里直接抛出只读异常,
可以 100% 确认:当前连接的是只读库。
七、经验总结
新增修改不报错却没数据,删除直接报错,99% 是连错库了。
这并不是 Hibernate 的 Bug,而是:
-
MySQL 只读模式
-
Hibernate 延迟执行机制
-
Spring 事务处理
三者叠加后的典型"假成功"陷阱。
八、实践建议
-
明确主从库职责,写操作必须走主库
-
关键写操作可显式调用
flush()进行早失败 -
数据库主从切换后第一时间检查
read_only -
定时清理类 DELETE 操作务必绑定主库
九、结语
如果你正在使用:
-
Spring Boot + JPA
-
MySQL 主从复制
-
读写分离架构
那么这个问题几乎是必踩的一坑。
希望这篇文章能帮你少踩一次坑。
欢迎在评论区分享你遇到过的读写分离或事务相关问题 👇