MySQL事务隔离级别详解:脏读幻读不可重复读现象与实战排查
MySQL事务隔离级别详解:脏读幻读不可重复读现象与实战排查电商系统中,两个用户几乎同时下单购买最后一件库存,一个订单成功扣减库存,另一个却因为读到了未提交的“脏数据”误以为还有库存 —— 这种经典的并发问题背后,就是事务隔离级别在起作用。本文将用两个终端窗口+完整SQL脚本,手把手演示脏读、不可重复读和幻读现象,并给出生产级的排查思路。
环境准备本文所有操作基于 MySQL 8.0 ,使用 InnoDB 引擎。你需要打开两个独立的会话(终端1和终端2),方便观察并发行为。
123456789101112-- 创建测试库与测试表CREATE DATABASE IF NOT EXISTS isolation_test;USE isolation_test;CREATE TABLE account ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20), balance INT) ENGINE=InnoDB;-- 插入一条初始数据INSERT INTO account VALUES (1, 'Alice ...
MySQL 间隙锁深入解析与排查实战
MySQL 间隙锁深入解析与排查实战先看一个真实的生产故障:订单系统在并发创建订单时频繁出现 Deadlock found when trying to get lock,堆栈指向一个看似简单的 INSERT ... SELECT 语句。原以为只是行锁冲突,直到深入 InnoDB 的锁日志,才发现“罪魁祸首”是——间隙锁(Gap Lock)。本文就从这次故障出发,把间隙锁的原理、触发条件、排查工具和优化方案彻底讲透,所有示例均可在本地 MySQL 8.0 中复现。
从一个死锁场景开始我们有两张表:orders(订单)和 order_items(订单项),业务要求在创建订单时拷贝预置的商品模板到订单项。
1234567891011121314151617181920-- 建表语句(可复现环境)CREATE TABLE template_items ( id INT PRIMARY KEY AUTO_INCREMENT, template_id INT NOT NULL, product_name VARCHAR(100), INDEX idx_template ( ...
MySQL 锁机制深入:从行锁到间隙锁的加锁原理与死锁排查实战
MySQL 锁机制深入:从行锁到间隙锁的加锁原理与死锁排查实战凌晨两点,监控告警:订单服务频繁报出 Deadlock found when trying to get lock; try restarting transaction。紧急排查后发现,只是一个看似无害的“清理过期订单”的 DELETE 操作,却把整个事务链条弄崩了。日志里清晰写着:Gap lock 与 Insert intention lock 互相等待。
这就是今天要深挖的主题:从 InnoDB 的行锁,到间隙锁,再到生产环境死锁的排查与预防。
一、InnoDB 的三大行锁InnoDB 在 可重复读(RR) 隔离级别下,为了解决幻读问题,在行锁的基础上引入了 间隙锁,形成了三种锁结构:
123Record Lock:锁住索引记录本身。Gap Lock:锁住索引记录之间的间隙,是一个开区间。Next-Key Lock:Record Lock + Gap Lock,是一个前开后闭区间。
以一个简单的主键表为例,如果存在 id=10 和 id=20 两条记录,Next-Key Lock 的分布如下:
123(-∞, 10] ...
Redis 缓存与 MySQL 数据一致性保障实战
Redis 缓存与 MySQL 数据一致性保障实战某电商商品详情接口 QPS 突然从 2000 飙到 8000,MySQL 的 CPU 瞬间打满 95%,接口响应从 50ms 恶化到 3s。监控显示大量慢查询集中在 SELECT * FROM product WHERE id = ? ——这是典型的高并发读瓶颈。引入 Redis 缓存层是成本最低的读扩展方案,但缓存引入后,数据不一致、穿透、雪崩、击穿等问题会接踵而至。本文将用完整的 Spring Boot + RedisTemplate 代码,逐一解决这些生产故障。
1. 架构演进:从纯 MySQL 到 Redis 缓存层原始链路:Client -> Controller -> Service -> MySQL
改造后:Client -> Controller -> Service -> Redis(命中) | Redis(未命中) -> MySQL -> 回写 Redis
需要解决的问题:
缓存穿透:查询一个不存在的数据,请求直达 DB
缓存雪崩:大量 key 同时过期,请求同时打向 ...
Doris 分区分桶设计与数据倾斜问题排查实战
Doris 分区分桶设计与数据倾斜问题排查实战某天下午,业务方反馈「实时大盘」中今日订单统计查询耗时从 200ms 飙升至 3s,且伴随 BE 节点 CPU 使用不均。打开监控发现,集群中 3 个 BE 节点的磁盘 IO 和 CPU 出现了明显的高低差异——这正是典型的数据倾斜信号。对于依赖 MPP 并行查询的 OLAP 引擎而言,数据分布不均会让部分节点成为短板,拖垮整体查询性能。本文从原理出发,通过电商订单与日志分析两个实战场景,逐步演示 Doris 分区分桶的设计范式、倾斜排查方法和动态分区最佳实践。
1. 分区与分桶:把大象装进冰箱的正确姿势Doris 的表数据逻辑上被划分成 分区 (Partition) 和 桶 (Bucket/Tablet) 两级组织:
分区:按时间、地域等维度做逻辑切分,作用是管理数据生命周期和查询裁剪。一个分区通常对应一个目录,可以独立地进行备份、删除。
分桶:在一个分区内部,按指定列的 Hash 值将数据打散成多个物理 Tablet。Tablet 是 Doris 中最小的数据存储和负载均衡单元,分桶的均匀性直接决定了查询并行度和节点负载 ...
MySQL 8.0窗口函数实战:解决TopN与排名问题
MySQL 8.0窗口函数实战:解决TopN与排名问题业务开发中经常遇到排名、分组 TopN 需求:用户积分排行榜、各部门薪资前三的员工、最近 N 个月的销售滚动统计……MySQL 8.0 引入的窗口函数让你用一条 SQL 就能干净利落地完成这些操作,不需要再写晦涩的自连接或用户变量。
本文通过用户积分排名和部门薪资 Top3 两个生产级场景,给出可直接运行的示例,并对比传统 GROUP BY + 子查询 方案的性能差异,帮助你在项目中快速落地窗口函数。
1. 环境准备123456789101112131415161718192021222324252627-- 验证环境:MySQL 8.0.35,InnoDB引擎CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, points INT NOT NULL DEFAULT 0) ENGINE=InnoDB;INSERT INTO users (name, points) VALUES('Alice ...
MySQL死锁问题排查与解决方案实战
MySQL死锁问题排查与解决方案实战
真实场景:支付系统进行一笔退款操作时,后台日志赫然出现 Deadlock found when trying to get lock; try restarting transaction。退款失败,订单状态卡住,客服电话被打爆。作为后端开发,你能否快速定位死锁并拿出高可用方案?本文将通过 3 个完整可运行的案例,带你从死锁“现象”走到“根因”,再到“代码级修复”,并给出可落地的预防策略。
一、死锁是怎么发生的?在 MySQL InnoDB 引擎中,死锁是并发事务相互持有对方需要的锁,形成循环等待而无法继续执行的僵局。要打破死锁,必须先理解 InnoDB 的锁机制。
发生死锁的四个必要条件(缺一不可):
互斥:资源只能被一个事务独占。
持有并等待:事务已持有一部分锁,同时在等待其他锁。
不可抢占:已获取的锁不能被外界强制释放。
循环等待:事务间形成首尾相连的等待环。
InnoDB 会自动检测死锁,并选择回滚代价最小的事务(undo log 较小的那个)来打破死锁,同时将死锁信息记录到 SHOW ENGINE INNODB STATUS 中。
...
MySQL深分页查询性能优化实战
MySQL深分页查询性能优化实战想象一个场景:订单表里有上百万条数据,业务需要按创建时间倒序分页展示。前几页秒出,翻到第 1000 页时,SQL 却卡了 5 秒。这就是典型的深分页问题——当 OFFSET 变大时,查询性能急剧下降。
本文从真实业务场景出发,用可复现的示例带你定位问题,再给出 3 种实用的优化方案,所有 SQL 都能直接在你本地 MySQL 8.0 上运行。
1. 复现问题:200 万订单的深分页1.1 建表与造数据1234567891011121314151617181920212223242526272829303132-- 订单表,自增主键 + 创建时间索引CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, created ...
MySQL大表DDL操作风险与在线变更实战
MySQL大表DDL操作风险与在线变更实战凌晨两点,DBA 小张被紧急告警吵醒:订单表orders的status字段需要从TINYINT扩展为INT,当他在 2 亿行的表上执行ALTER TABLE之后,业务连接池瞬间打满,所有订单操作全部阻塞,最终靠重启数据库并回滚当天数据才恢复。事故报告里写满了 “锁表”、“主从延迟 20 分钟”、“数据不一致” 的字眼。
这不是杜撰的故障,而是每天都在发生的现实。本文将从一个真实的压测场景开始,展示原生 DDL 的问题,然后深入pt-online-schema-change与gh-ost的原理,手把手带你完成三次完整的在线变更演练——每个例子都可以在你的机器上复现。
1. 原生 DDL 到底有多可怕?我们先在测试环境复现一次原生 DDL 带来的伤害。接下来的脚本会创建一张包含 500 万行数据的表,同时用 sysbench 模拟持续读写,然后执行ALTER TABLE ... ADD COLUMN,再观察阻塞时间和复制延迟。
1.1 环境准备1234567891011121314151617181920212223242526272829303 ...
MyBatis ResultMap 吞行问题
MyBatis ResultMap 吞行问题
你写了一对多关联查询,数据库中明明有 10 行数据,MyBatis 返回的列表却只有 3 条。剩下的 7 行去哪了?这就是经典的 ResultMap “吞行”问题。
现象:数据去哪了?假设你有一张订单表 t_order 和一张订单明细表 t_order_item,一个订单可以包含多个商品明细:
123456789101112-- 订单CREATE TABLE t_order ( id BIGINT PRIMARY KEY, order_no VARCHAR(32));-- 订单明细CREATE TABLE t_order_item ( id BIGINT PRIMARY KEY, order_id BIGINT, product_name VARCHAR(64));
你写了一个 SQL 用 JOIN 关联查询:
123456789<select id="getOrders" resultMap="OrderResultMap"> SELECT ...
