大家好,我是程序员强子。
前面学习了Mysql 四大金刚其中的三大金刚 索引 & 日志 & 锁
今天专注把 事务 相关给弄明白~
来看看今天学习的知识点:
- 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_schema | Mysql相关资源占用情况 |
| 应用日志(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:是否存在资源瓶颈
数据库连接池、磁盘 IO、CPU 是否达到瓶颈,导致事务执行变慢
-
监控连接池状态(通过 Prometheus):活跃连接数、空闲连接数、等待队列长度
-
检查数据库服务器资源
- CPU:top 命令查看 MySQL 进程 CPU 占用率(是否持续 > 80%);
- 磁盘 IO:iostat命令(--help 查看完整命令) 查看磁盘读写 IO(%util 持续 > 90% 表示 IO 瓶颈);
- 内存:free命令查看 MySQL 缓存(innodb_buffer_pool_size)是否足够,是否频繁换页
验证优化
| 类型 | 方案 |
|---|---|
| SQL 低效(无索引 / 全表扫) | 补充索引、优化 SQL |
| 事务边界过大(含远程调用) | 异步化非核心操作,如消息队列 |
| 批量操作(循环单条) | 改为批量 SQL(MyBatis foreach) |
| 锁竞争(行锁 / 间隙锁) | 降低隔离级别(RR→RC)、优化查询条件,不过要谨慎! |
| 连接池瓶颈 | 调整连接池大小、读写分离 |
总结
作为 Java 后端开发者,事务是我们绕不开的核心技能,更是构建可靠系统的 地基
希望这篇文章能帮你打通 ACID、隔离级别、大事务的知识任督二脉,在实际项目中少踩坑
如果觉得帮到你们,烦请加个点赞关注推荐 三连,感谢感谢~~
熟练度刷不停,知识点吃透稳,下期接着练~