Java 王者修炼手册【Mysql 篇 - 事务】:吃透 ACID 本质 + 隔离级别底层 + 大事务排查优化方案,掌控事务核心逻辑

35 阅读10分钟

大家好,我是程序员强子。

前面学习了Mysql 四大金刚其中的三大金刚 索引 & 日志 &

今天专注把 事务 相关给弄明白~

2043544.jpg

来看看今天学习的知识点:

  • ACID是什么,底层是怎么实现的
  • 隔离级别是什么,底层是怎么实现的
  • 大事务是什么,怎么解决,有哪些解决方案

不解释了,赶紧上号~巅峰开始了

ACID

原子性(A)

靠「Undo Log + 事务状态标记」实现

核心目标

失败时回滚到事务开始前的状态,成功时提交所有修改

即同时成功,同时失败~

底层机制

依赖Undo Log!!

undo log的作用就是记录事务执行前的数据 快照反向操作,供事务回滚时使用

工作流程如下

  • 事务开始前,InnoDB 为每个修改操作记录 Undo Log:

    • UPDATE:记录 旧值(如 A 余额 1000);
    • DELETE:记录 被删除的数据,相当于逻辑上的 INSERT 反向操作
    • INSERT:记录 待删除的标识,事务回滚时直接删除这条插入数据
  • 事务执行中,若发生错误(如代码异常、网络中断、死锁),数据库触发 ROLLBACK

    • 遍历该事务的 Undo Log,反向执行操作
  • 事务执行成功COMMIT 时,Undo Log 不会立即删除,后续用于 MVCC 读,会定期回收

持久性(D)

靠「Redo Log + WAL 机制」实现

核心目标

事务提交后,数据修改永久生效,不受崩溃影响

底层机制 1:Redo Log

作用

  • 记录事务执行的 物理修改操作
  • 比如 把数据页 X 中 A 的余额从 1000 改成 900,而非数据本身

特点

  • 日志是 顺序写入,这是因为磁盘顺序 IO 速度远快于随机 IO
  • 记录的是 已执行的修改,崩溃后可通过日志 重做 这些操作

底层机制 2:WAL

先写日志,再写数据文件

  • 数据修改最终要落盘,但属于随机IO,速度慢;
  • 若先数据落盘再写日志,中途崩溃,磁盘的数据 和 日志就不一致~ 而且也无法恢复 日志 相关数据
  • 先写 Redo Log(顺序 IO 快),再异步刷盘数据文件,即使崩溃,重启后可通过 Redo Log 重做未刷盘的修改

工作流程

  • 事务执行中,每个修改操作先写入 Redo Log Buffer

  • 事务 COMMIT 时,触发 fsync 把 Redo Log Buffer 中的日志刷到磁盘

  • 数据库后台线程会定期把内存中的数据页(脏页)刷到磁盘数据文件;

  • 若崩溃时数据页未刷盘,重启后数据库:

    • 读取 Redo Log,找到所有已提交但未刷盘的事务相关日志;
    • 重新执行这些事务的 Redo Log 操作,把数据刷到磁盘,保证持久性

隔离性(I)

靠「锁机制 + MVCC」实现

核心目标

解决并发事务的干扰问题,避免脏读不可重复读幻读

并发事务的三大问题

问题定义
脏读事务 A 读取了事务 B 未提交的修改,假如B 后续回滚,A 读的是 脏数据
不可重复读事务 A 两次读取同一数据,期间事务 B 提交了修改,导致 A 两次读取结果不一致
幻读事务 A 按条件查询,比如查出5条数据;期间事务 B 插入 / 删除了 相关记录,导致 A 再查发现 数据 少了或者多了

核心实现

锁机制 + MVCC

本质是 : 通过控制写冲突,通过MVCC 优化读并发

隔离也就说代表 多个事务并发进行 各自有序正常进行,互不影响

锁机制解决了什么问题?

  • S锁 保证 多个事务可同时加 S 锁(读 - 读不冲突);
  • 一个事务加 X 锁后,其他事务不能加 S 锁X 锁(写 - 读、写 - 写冲突)
  • 核心作用是 保证 写操作互斥

MVCC解决了什么问题?

  • 数据保留多个版本的快照,读事务访问 旧版本快照,写事务修改 新版本数据无需加锁,是非阻塞读

  • 读已提交(RC)

    • 解决脏读,允许不可重复读
    • 每次查询生成新的 Read View,只能读到 查询瞬间已提交的版本
  • 可重复读(RR)

    • 解决不可重复读部分幻读
    • 事务开始时生成一次 Read View,全程使用该视图

一致性(C)

靠「A+I+D + 约束校验」实现

  • 原子性(A):避免 部分修改生效 导致的数据不一致
  • 隔离性(I):避免 并发事务干扰导致的数据不一致(如脏读、幻读)
  • 持久性(D):避免 提交后数据丢失 导致的不一致
  • 数据库层面:主键唯一、外键关联

事务隔离级别

定义

隔离级别核心定义备注
读未提交事务可读取其他事务未提交的修改
读已提交事务仅能读取其他事务已提交的修改Oracle 默认级别
可重复读同一事务内,多次读取同一数据的结果完全一致,不受其他事务提交的修改影响MySQL 默认级别
串行化所有事务串行执行(本质是单线程处理),完全避免并发干扰并发性能极差

实现原理

  • 通过控制 写冲突
  • 通过 MVCC 优化 读并发
  • Read View 的生成策略

通过以上三个核心,实现不同的隔离强度

读未提交(RU)

无锁 + 无 MVCC

  • 直接读取数据的最新版本,不加任何读锁,也不通过 MVCC 生成历史快照
  • 实际工作中很少使用

读已提交(RC)

行锁 + MVCC

  • 写操作加行级排他锁(避免写冲突)
  • 读操作通过 MVCC 实现
  • 每次查询都生成新的 Read View
  • 电商高并发场景会使用到,可重复读靠应用程序保证

可重复读(RR)

行锁 + 间隙锁 + MVCC

  • 写操作加行级排他锁
  • 范围查询加间隙锁 / Next-Key Lock
  • 读操作通过 MVCC 实现
  • 事务开始时生成一次 Read View,全程复用
  • mysql默认使用这个级别

串行化(S)

表锁 / 行锁串行执行

  • 读操作加表级共享锁
  • 写操作加表级排他锁,或通过行锁强制事务按顺序执行
  • 完全禁用并发;
  • 仅适用于数据一致性要求极高并发量极低的场景

线上排查大事务问题

核心原则: 先止血再定位后优化

优先保证业务可用性,再通过监控日志数据库工具逐层拆解根因

线上大事务不会凭空出现,通常伴随

  • 接口超时
  • 数据库连接耗尽
  • 锁等待飙升

等一系列现象,所以需要使用相关工具去排查

依赖工具

工具 / 组件作用
Prometheus + Grafana快速定位耗时超标的事务接口
Spring 事务日志明确事务执行时长
MySQL 慢查询日志定位事务内的慢 SQL
MySQL 表performance_schemaMysql相关资源占用情况
应用日志(ELK / 日志平台)通过输出日志定位步骤,或者错误日志
数据库连接池监控判断连接池是否足够

排查步骤

从应用层锁定嫌疑接口

  • 查看监控平台:筛选 事务耗时 p95>500ms 的接口

  • 检索应用日志

    • 在 ELK 中搜索关键词 @Transactional、Transaction committed、Transaction rolled back,
    • 结合接口路径和耗时,找到执行时间长的事务方法

P90 ,P95,P99 是什么意思?有什么用?

比如王者,

  • 统计 100 次技能释放的延迟(单位:ms),按从小到大排序
  • P95就是 找到第 95 个数值,这个值就是 P95
  • P90 P99同理

同理,请求的时延,也是100次,然后排序好取95个

那为什么是P90/P95/P99而不是其他的?

  • P95 能 避开极端值干扰 , 能反映绝大多数用户的真实体验
  • 行业惯例 导致了通用性
  • 主流压测工具(JMeter、Gatling)、监控系统(SkyWalking、New Relic)默认展示 P90/P95/P99 这三个指标

不同场景下的百分位选型参考

  • 核心接口(支付、登录):用P99(仅 1% 异常),优先保障核心流程零卡顿;
  • 非核心接口(商品列表查询、历史记录统计):可用P90,降低服务器负载压力;
  • 通用业务接口(订单查询、用户信息修改):P95是默认最优解。

从数据库层锁定嫌疑 SQL

  • 通过监控,或者通过查看慢sql日志 定位慢sql

  • 找到sql后,分析sql执行慢的原因?

    • 是否加索引,索引是否失效
    • 是否是数据量太大
    • 是否是深度分页导致
    • 是否是锁竞争激烈导致,是否有死锁

定位根因

排查维度 1:事务边界是否过大

  • 远程调用(Feign/HTTP 接口):比如下单接口中调用支付服务、物流服务;
  • 消息发送(RocketMQ/Kafka):同步发送消息且未异步化;
  • 文件 IO / 缓存操作:比如事务内写文件、同步更新 Redis(非核心操作);
  • 循环操作:比如事务内循环插入 1000 条数据、循环查询数据库。

排查维度 2:事务内 SQL 是否低效

  • 分析 SQL 执行计划:对每条 SQL 执行 EXPLAIN,重点关注:

    • type 字段:是否为 ALL(全表扫描),需优化为 range/ref/eq_ref;
    • key 字段:是否为 NULL(未使用索引),需补充索引;
    • rows 字段:预估扫描行数是否过大(比如超过 1000 行);
    • Extra 字段:是否有 Using filesort(文件排序)、Using temporary(临时表),需优化 SQL 结构
  • 排查批量操作

排查维度 3:是否存在锁竞争

大事务是否长时间持有锁,导致其他事务阻塞

  • 分析锁类型:通过 show engine innodb status\G 查看 InnoDB 状态,重点看 TRANSACTIONS 部分,判断是行锁、表锁还是间隙锁导致的阻塞
  • 表锁:通常因 DDL 操作、无索引条件的 UPDATE/DELETE 导致
  • 间隙锁:RR 隔离级别下的范围查询(如 WHERE id BETWEEN 1 AND 100)可能触发,导致插入阻塞

排查维度 4:是否存在资源瓶颈

数据库连接池、磁盘 IOCPU 是否达到瓶颈,导致事务执行变慢

  • 监控连接池状态(通过 Prometheus):活跃连接数空闲连接数等待队列长度

  • 检查数据库服务器资源

    • CPUtop 命令查看 MySQL 进程 CPU 占用率(是否持续 > 80%);
    • 磁盘 IOiostat命令(--help 查看完整命令) 查看磁盘读写 IO(%util 持续 > 90% 表示 IO 瓶颈);
    • 内存free命令查看 MySQL 缓存(innodb_buffer_pool_size)是否足够,是否频繁换页

验证优化

类型方案
SQL 低效(无索引 / 全表扫)补充索引、优化 SQL
事务边界过大(含远程调用)异步化非核心操作,如消息队列
批量操作(循环单条)改为批量 SQL(MyBatis foreach)
锁竞争(行锁 / 间隙锁)降低隔离级别(RR→RC)、优化查询条件,不过要谨慎
连接池瓶颈调整连接池大小、读写分离

mvp.png

总结

作为 Java 后端开发者,事务是我们绕不开的核心技能,更是构建可靠系统的 地基

希望这篇文章能帮你打通 ACID隔离级别大事务的知识任督二脉,在实际项目中少踩坑

如果觉得帮到你们,烦请加个点赞关注推荐 三连,感谢感谢~~

熟练度刷不停,知识点吃透稳,下期接着练~