Mysql
事务:
四大特性:实现了原子性(Undo Log )+持久性(Redo Log)+隔离性(Mvcc和锁),才能实现一致性
并发事务问题:脏读,不可重复读,幻读
事务隔离级别:读未提交,RC,RR,串行化
图片
图片
mysql中的锁
图片
图片
图片
图片
存储引擎
图片
图片
图片
日志:
RedoLog重做日志主要解决崩溃回复问题(记录数据页的物理修改(在 X 表空间的 Y 页,偏移量 Z 处,值从 A 改成了 B。)
核心机制是Write-Ahead Logging,当进行数据的更新时,会先写日志,再写磁盘,后台异步刷盘,把极其耗时的磁盘IO"随机写"变成了高效的"顺序写"(写日志),大幅提升性能。在系统崩溃时,重启可以从重做日志里找回数据,确保持久性。
UndoLog回滚日志,主要解决事务回滚和 MVCC(多版本并发控制)
记录了数据修改操作的反向操作((INSERT 对应 DELETE,UPDATE 对应改回旧值),用于回滚,确保原子性
记录了数据的旧版本链,在可重复读隔离级别下,当一个事务要读取某行数据时,如果这行数据被其他事务修改了,就可以通过 undo log 找到之前的版本,实现快照读,隔离性。
BinLog日志(三种模式)主要解决数据备份、主从复制问题
记录的是记录 SQL 语句或行变更,它相当于数据库的'操作记录'
主从复制:
主库:执行更新操作,写 binlog
↓
从库:读取主库的 binlog,重放这些操作
↓
结果:从库数据与主库一致
Row:记录数据具体变化 如update记录当前行 更新前后的 所有字段列的值 绝对精确,主从数据强一致
-- 主库执行
UPDATE account SET balance = 900 WHERE user_id = 100;
-- Binlog 记录的内容(ROW 模式)
UPDATE db.account
WHERE
@1=100 -- user_id
@2=1000 -- balance(修改前)
SET
@1=100
@2=900 -- balance(修改后)
Statement:sql语句原文 now函数进行数据同步时 主从库执行时间不同,会导致数据不一致
-- 主库执行
UPDATE account SET balance = balance + 100 WHERE user_id = 100;
-- Binlog 记录的内容
UPDATE account SET balance = balance + 100 WHERE user_id = 100;
Mixed:自动切换 ROW/STATEMENT 普通 SQL 用 Statement 省空间,遇到动态函数自动切成 Row 保安全。
Redolog和binlog都可以数据恢复有什么区别?
1.场景不同:redolog是崩溃恢复,比如数据库宕机,undolog是任意时间点恢复,比如误删数据,需要回到某一时间点
2.redolog是数据库启动自动恢复,binlog是需要手动恢复
3.redolog是数据页级别的修改,速度快,binlog是重新执行sql语句,更慢
MVCC
MVCC 就是通过隐藏的事务ID和回滚指针构建数据版本链,结合Read View 的四要素来判断数据可见性。
通过控制 Read View 的生成时机实现RR和RC隔离级别
MVCC让读操作不加锁(读操作读取旧版本),写操作才加锁(读最新版本),实现了高并发下的读写并发。读操作不会阻塞写操作,写操作也不会阻塞读操作->读的是旧版本,写的是新版本
传统锁机制,读和写都是同一个版本,会冲突
依赖
事务id,回滚指针,视图(判断"哪个历史版本对当前事务可见"),undolog版本链
视图四要素
- m_ids(活跃事务列表):当前系统里正在运行、还没提交的事务 ID 集合。
- min_trx_id:活跃事务列表中最小的事务 ID。
- max_trx_id:系统即将分配的下一个事务 ID(也就是当前最大事务 ID + 1)。
- creator_trx_id:创建这个 Read View 的事务自己的 ID。
可见性规则(拿着尺子去量版本链上的 trx_id最近修改这行的事务ID):
- 如果 trx_id < min_trx_id:说明在视图创建前就已经提交了,可见。
- 如果 trx_id >= max_trx_id:说明是视图创建后才出现的事务,不可见。
- 如果 min_trx_id <= trx_id < max_trx_id:说明在活跃事务列表中,如果它没提交就不可见,如果提交了就可见。
- 如果 trx_id == creator_trx_id:自己改的数据,永远可见。
快照读和当前读(加锁)根据一定规则读对应可见版本 - 快照读(Snapshot Read):普通的 SELECT 语句。不加锁,直接根据 Read View 的规则,去 Undo Log 版本链上找那个"可见的历史版本"。
- 当前读(Current Read):SELECT ... FOR UPDATE、UPDATE、DELETE 等。这些操作必须拿到最新的数据才能修改,所以它们直接读取最新版本,并且必须加锁(防止别人同时改)。
实现RR(只在第一次 SELECT 时生成 ReadView,整个事务期间复用同一个 ReadView,避免了不可重复读,实现可重复读)
图片
实现RC(每次 SELECT 都生成新的 ReadView,能看到其他事务已提交的修改,会出现不可重复读)
图片
分库分表怎么做?会带来什么问题?
- 标准回答:当单表数据量过大(如千万级)时,可以按 Hash 或范围进行水平拆分。分库分表能解决单机存储和写入瓶颈。
- 我的理解/加分项:分库分表是一把双刃剑,它会带来很多架构上的痛点:比如跨库 Join 变得极其困难、分布式事务难以保证一致性、全局唯一 ID 的生成问题,以及数据扩容时的迁移成本。不到万不得已(单表确实扛不住了),绝不轻易分库分表,优先考虑加缓存、读写分离或归档历史数据。
在实际架构中,分库分表永远是最后的底牌,而不是首选方案。当遇到性能瓶颈时,我的排查和优化路径是:
- 第一步:先查慢 SQL,加索引、优化查询语句。
- 第二步:上缓存(Redis),把高频读取的压力挡在数据库前面。
- 第三步:做读写分离,把查询压力分摊到从库。
- 第四步:历史数据归档,把冷数据移到历史表,保持核心表轻量。
- 最后一步:如果以上全做了,单表依然突破千万级且扛不住,才真正开始规划分库分表。
图片
覆盖索引
- 普通回答:查询的字段都在索引里,不需要回表。
- 高分回答(结合项目):
- "在我的项目中,有一个'查询用户最近10条订单'的接口。起初我只建了user_id索引,但查询还需要order_time和amount,导致必须回表查,性能很差。
我的优化思路是,建立了(user_id, order_time, amount)的联合索引。这样查询所需的所有字段都在索引树上直接拿到了,完全避免了回表。
我的理解是:覆盖索引的本质是减少回表带来的随机IO。在数据量大时,回表的代价很大。
随机 IO:读写操作访问的数据地址是不连续的,磁头需要频繁移动到不同位置才能完成读写。
-- 通过索引查找单条记录SELECT * FROM users WHERE id = 1000;
-- 过程:-- 1. 读取根节点(随机IO)-- 2. 读取中间节点(随机IO)-- 3. 读取叶子节点(随机IO)-- 4. 读取数据行(随机IO)-- 总共 4 次随机 IO
图片
为什么用B+树索引,不用hash和B树
Hash等值查询虽然快,但不支持范围查询和排序,这在业务中太局限了。
我的理解是,B+树是专门为磁盘存储设计的。它的所有数据都存在叶子节点,非叶子节点只存索引,这意味着一个页能存更多索引项,树的高度通常只有3层左右。对于机械硬盘甚至SSD来说,3次IO就能查到数据,这是性能的关键。而且叶子节点用双向链表连接,完美解决了'范围查询'的需求。
图片
图片
聚簇索引和二级索引区别
图片
索引失效
索引失效的本质是SQL 写法破坏了 B+ 树的有序性,导致优化器无法利用索引做二分查找。
第一,违反最左前缀原则。 比如联合索引是 (a,b,c),查询条件只有 b 和 c,跳过了 a,索引就用不上。联合索引是先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序。如果没有 a,就不知道从哪里开始查。
第二,对索引列做了函数运算。 比如 WHERE YEAR(create_time)=2025 或者 WHERE id+1=10,索引存的是原始值,运算后的值没法在树上查。
第三,隐式类型转换。 比如 phone 字段是 varchar,查询时传了数字,MySQL 会对索引列做 CAST,将原本的varchar转为数字,转换后值变了,无法匹配索引。
第四,LIKE 以百分号开头。 LIKE '%张' 前缀不确定,表示前面有任意字符,MySQL 不知道从哪里开始查。
第五,OR 连接了非索引列。 只要 OR 右边没有索引,整个条件就只能全表扫描。
第六,NULL 判断:IS NOT NULL 匹配的记录通常很多,优化器认为全表扫描更快。
sql优化
开启慢查询日志定位耗时超过阈值(比如1秒)的SQL
首先利用EXPLAIN。 重点看 type 是不是 ALL(全表扫描,索引失效)、key 是不是 NULL(没用索引)
第二步针对性优化:
最高优先是索引优化。 一是设计合理的联合索引,等值查询列放前面、范围查询列放最后;二是尽量用覆盖索引,让查询列都在索引里,避免回表;三是注意刚才说的索引失效场景,别写了索引却用不上。
其次是写法优化。 永远不要 SELECT *,只查需要的列;
再次是 JOIN 优化。 确保小表驱动大表,并且被驱动表的关联列上要有索引,否则就是嵌套循环全表扫描。
最后是架构层面。 如果单表优化到极限还不够,才考虑分库分表、读写分离或者加缓存层。
-- 优化器会选择:-- 1. 行数少的表作为驱动表外层for-- 2. 有索引的表作为被驱动表内层for
- JOIN 本质是嵌套循环
- 外层循环次数越少越好
- 小表作为外层(驱动表),减少内层查找次数,内连接优化器自动选驱动表,左连接固定左表为驱动表
被驱动表建索引的原因: - 没有索引,内层每次都要全表扫描
- 时间复杂度 O(n×m),性能灾难
- 有索引,内层是 O(log n),性能提升数个量级
内连接(INNER JOIN):只要匹配的
左连接(LEFT JOIN):左表数据全有,右表匹配的补上,没有就 NULL
图片
索引
图片
图片
图片
图片