MySql面试总结
NOSQL和SQL的区别?
对比维度 | SQL(关系型数据库) | NoSQL(非关系型数据库) |
---|---|---|
数据模型 | 基于结构化表格(Table),通过行(Row)、列(Column)和关系(外键)组织数据,遵循严格的二维结构。 | 支持多种非结构化 / 半结构化模型,如: - 键值对(Key-Value) - 文档(Document) - 列族(Column-Family) - 图(Graph) |
Schema(模式) | 固定 Schema,需预先定义表结构(字段名、类型、约束等),修改需 ALTER TABLE,成本高。 | 动态 Schema 或无 Schema,数据结构可灵活调整(如 MongoDB 文档可随时增减字段),无需预定义。 |
查询语言 | 使用标准化 SQL(Structured Query Language),支持 SELECT、INSERT、JOIN 等复杂操作。 | 无统一查询语言,依赖数据库特定 API(如 MongoDB 的 find ()、Redis 的 SET/GET 命令)。 |
事务支持 | 强事务支持,严格遵循 ACID 特性(原子性、一致性、隔离性、持久性),适合高一致性场景。 | 多数支持最终一致性或弱事务,部分支持有限事务(如 MongoDB 4.0 + 支持多文档事务),更侧重可用性。 |
扩展性 | 垂直扩展为主(升级硬件),水平扩展需分库分表(Sharding),复杂度高。 | 原生支持分布式架构,水平扩展简单(添加节点即可),适合海量数据存储。 |
数据一致性 | 强一致性,写入后立即读取到最新数据。 | 多为最终一致性(数据更新后需一段时间同步到所有节点),遵循 CAP 定理中的 AP(可用性 + 分区容忍性)。 |
典型场景 | 金融交易、电商订单、ERP 系统等需复杂关联查询和强事务的场景。 | 社交数据(如用户动态)、物联网日志、缓存系统、推荐引擎、图关系分析(如社交网络好友链)。 |
代表产品 | MySQL、PostgreSQL、Oracle、SQL Server、SQLite。 | MongoDB(文档)、Redis(键值)、Cassandra(列族)、Neo4j(图)、Elasticsearch(搜索引擎)。 |
性能特点 | 复杂查询(如多表 JOIN)效率高,但高并发写入受锁机制限制。 | 读写性能优异(尤其键值型),但复杂查询能力弱,需通过应用层实现关联逻辑。 |
数据库的三大范式是什么?
第一范式(1NF):原子性
定义:
表的每一列都是不可再分的原子值,同一列中不能有多个值,且所有字段都是单一数据类型。
示例
不符合 1NF 的表:
学生 ID | 姓名 | 课程 |
---|---|---|
101 | 张三 | 数学,英语,物理 |
102 | 李四 | 语文,化学 |
符合 1NF 的表:
学生 ID | 姓名 | 课程 |
---|---|---|
101 | 张三 | 数学 |
101 | 张三 | 英语 |
101 | 张三 | 物理 |
102 | 李四 | 语文 |
102 | 李四 | 化学 |
说明:
原表中 “课程” 列包含多个值,违反原子性。拆分后每行只存储一个课程,符合 1NF。
第二范式(2NF):完全依赖
定义:
- 满足 1NF。
- 非主属性必须完全依赖于主键,而不是仅依赖主键的一部分(针对复合主键的情况)。
示例
不符合 2NF 的表(复合主键:(订单ID, 商品ID)
):
订单 ID | 商品 ID | 商品名称 | 数量 | 订单日期 |
---|---|---|---|---|
OD1001 | P001 | 手机 | 2 | 2023-01-01 |
OD1001 | P002 | 充电器 | 1 | 2023-01-01 |
问题:
订单日期
只依赖于订单ID
(部分依赖)。商品名称
只依赖于商品ID
(部分依赖)。
符合 2NF 的表:
订单表(主键:订单ID
):
订单 ID | 订单日期 |
---|---|
OD1001 | 2023-01-01 |
订单商品表(主键:(订单ID, 商品ID)
):
订单 ID | 商品 ID | 商品名称 | 数量 |
---|---|---|---|
OD1001 | P001 | 手机 | 2 |
OD1001 | P002 | 充电器 | 1 |
说明:
拆分后,每个非主属性(如订单日期
、商品名称
)都完全依赖于对应的主键,消除了部分依赖。
第三范式(3NF):消除传递依赖
定义:
- 满足 2NF。
- 非主属性不能依赖于其他非主属性(即消除传递依赖)。
示例
不符合 3NF 的表(主键:员工ID
):
员工 ID | 部门 ID | 部门名称 | 部门地址 |
---|---|---|---|
E001 | D001 | 技术部 | 北京市 |
E002 | D001 | 技术部 | 北京市 |
E003 | D002 | 市场部 | 上海市 |
问题:
部门名称
和部门地址
依赖于部门ID
,而部门ID
是非主属性(传递依赖:员工ID → 部门ID → 部门名称/地址
)。
符合 3NF 的表:
员工表(主键:员工ID
):
员工 ID | 部门 ID |
---|---|
E001 | D001 |
E002 | D001 |
E003 | D002 |
部门表(主键:部门ID
):
部门 ID | 部门名称 | 部门地址 |
---|---|---|
D001 | 技术部 | 北京市 |
D002 | 市场部 | 上海市 |
说明:
拆分后,部门名称
和部门地址
直接依赖于部门ID
(主键),消除了传递依赖。
非主属性的定义
非主属性=表中所有属性-主属性
主属性:包含在任何候选键中的属性
候选键:能够唯一标识表中每行数据的的最小属性集
Mysql怎么连表查询?
连接类型 | 语法示例 | 特点 |
---|---|---|
内连接 | SELECT * FROM 表1 INNER JOIN 表2 ON 表1.列 = 表2.列; | 仅返回两个表中匹配的行,丢弃不匹配的行。 |
左连接 | SELECT * FROM 表1 LEFT JOIN 表2 ON 表1.列 = 表2.列; | 返回左表所有行,右表无匹配项时用NULL 填充。 |
右连接 | SELECT * FROM 表1 RIGHT JOIN 表2 ON 表1.列 = 表2.列; | 返回右表所有行,左表无匹配项时用NULL 填充。 |
全连接 | SELECT * FROM 表1 LEFT JOIN 表2 UNION SELECT * FROM 表1 RIGHT JOIN 表2; | 返回左右表所有行,无匹配项时用NULL 填充。 |
Mysql如何避免重复插入数据?
- 添加唯一约束:在表结构设计时,为需要唯一性的字段添加唯一索引,插入重复值时会触发错误。
- INSERT IGNORE 语句:当插入重复数据时,忽略错误并继续执行后续语句。
- REPLACE INTO 语句:若数据已存在,则删除原记录并插入新记录(相当于先
DELETE
再INSERT
)。 - INSERT … ON DUPLICATE KEY UPDATE:当插入重复数据时,执行更新操作而非报错。
CHAR 和 VARCHAR有什么区别?
CHAR 是 MySQL 中用于存储固定长度字符串的类型,无论实际存储的字符数量多少,都会占用预先定义的字节空间(最大 255 字符)。它会自动用空格填充不足的部分,但在查询时会去除尾部空格。由于长度固定,读取性能较高,适合存储性别、国家代码、哈希值等长度稳定的数据,能避免存储碎片,但可能浪费空间。
VARCHAR 则用于存储可变长度字符串,实际占用空间为字符长度加 1 或 2 字节(用于记录长度),最大支持 65,535 字节。它保留所有空格,包括尾部,并根据实际内容动态调整存储空间,因此更节省空间,适合存储用户昵称、文章内容等长度波动较大的数据。不过,由于需要额外处理长度信息,其读取速度略低于 CHAR,且频繁更新可能产生碎片。
varchar括号里的数字代表字符数还是字节数?
在 MySQL 中,VARCHAR(n)
的括号内数字 始终表示字符数,但实际占用的 字节数 取决于具体字符集:
1. ASCII 字符集(单字节)
- 每个字符固定占 1 字节。
- 示例:
VARCHAR(10)
最多存储 10 个 ASCII 字符,实际占用 10 + 1 字节(额外 1 字节记录长度)。
2. UTF-8 字符集(可变长度)
- 它的每个字符可能占用 1 到 4 个字节,对于 VARCHAR(10) 的字段,它最多可以存储 10 个字符,但占用的字节数会根据字符的不同而变化。
int(1),int(10) 在mysql有什么不同?
在 MySQL 中,INT(1)
和 INT(10)
的存储范围和占用空间完全相同(均为 4 字节,范围 -2147483648 到 2147483647),括号中的数字仅影响 显示宽度 和 ZEROFILL 填充行为。以下是详细区别:
1. 显示宽度(仅视觉效果)
括号中的数字表示 最小显示宽度,仅在配合 ZEROFILL
时生效:
INT(1)
:显示至少 1 位数字。INT(10)
:显示至少 10 位数字,不足时左侧补零。
2. ZEROFILL 的实际影响
- 自动添加 UNSIGNED 属性:使用
ZEROFILL
时,MySQL 会隐式将字段转为UNSIGNED
,导致存储范围变为 0 到 4294967295。 - 仅影响显示:存储值本身不变,补零仅在查询结果中呈现。
TEXT 类型的分类及上限
类型 | 最大存储容量 | 实际存储空间 | 说明 |
---|---|---|---|
TINYTEXT | 255 字节(2⁸-1) | 实际长度 + 1 字节 | 适合短文本(如简短描述) |
TEXT | 65,535 字节(2¹⁶-1) | 实际长度 + 2 字节 | 常用文本存储(如文章摘要) |
MEDIUMTEXT | 16,777,215 字节(2²⁴-1) | 实际长度 + 3 字节 | 适合大型文本(如博客文章、书籍) |
LONGTEXT | 4,294,967,295 字节(2³²-1 ≈ 4GB) | 实际长度 + 4 字节 | 可存储极大型文本(如整本书籍) |
IP地址如何在数据库里存储?
-
字符串存储
直接将 IP 地址作为字符串存储在数据库中,比如可以用 VARCHAR(15)来存储。
优点:直观易懂,方便直接进行数据的插入、查询和显示,不需要进行额外的转换操作。
缺点:占用存储空间较大,字符串比较操作的性能相对较低,不利于进行范围查询。
-
无符号整数存储
将 IPv4 地址转换为 32 位无符号整数进行存储,常用的数据类型有 INT UNSIGNED。
-- 存储时转换为整数 INSERT INTO users (ip) VALUES (INET_ATON('192.168.1.1')); -- 查询时转回字符串 SELECT INET_NTOA(ip) FROM users;
优点:占用存储空间小,整数比较操作的性能较高,便于进行范围查询。
缺点:需要进行额外的转换操作,不够直观,增加了开发的复杂度。
说一下外键约束
外键约束(Foreign Key Constraint)是关系型数据库中用于维护表之间关联关系的重要机制,它确保了数据的一致性和完整性,尤其在多表关联场景中(如订单表与用户表、商品表与分类表)至关重要。
1. 基本语法
创建外键时,需指定:
- 子表的外键字段。
- 父表的名称及被引用的字段。
- 可选的级联操作(当父表数据被修改 / 删除时,子表的处理方式)。
-- 创建父表(用户表)
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT, -- 主键
username VARCHAR(50) NOT NULL
);
-- 创建子表(订单表),并添加外键约束
CREATE TABLE orders (
order_id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT, -- 外键字段,关联 users.user_id
order_time DATETIME,
-- 定义外键约束
CONSTRAINT fk_orders_user
FOREIGN KEY (user_id)
REFERENCES users(user_id) -- 引用父表的主键
ON DELETE CASCADE -- 级联删除:父表用户删除时,子表关联订单也删除
ON UPDATE CASCADE -- 级联更新:父表用户ID更新时,子表关联ID也更新
);
2. 关键参数说明
级联操作 | 作用 |
---|---|
ON DELETE CASCADE | 当父表记录被删除时,子表中所有关联的记录自动删除。 |
ON DELETE SET NULL | 当父表记录被删除时,子表中关联的外键字段设为 NULL (需外键允许为 NULL)。 |
ON DELETE RESTRICT | 禁止删除父表中被子表引用的记录(默认行为,会报错)。 |
ON UPDATE CASCADE | 当父表被引用的字段(如主键)更新时,子表的外键字段自动同步更新。 |
ON UPDATE SET NULL | 父表字段更新时,子表外键设为 NULL (需外键允许为 NULL)。 |
MySQL的关键字in和exist
特性 | IN | EXISTS |
---|---|---|
执行逻辑 | 先执行子查询,将结果集缓存后与主查询匹配 | 主查询逐行检查子查询是否返回结果(短路逻辑) |
结果集处理 | 依赖子查询的全部结果 | 仅关心子查询是否有返回(不关心具体值) |
适用场景 | 子查询结果集较小,且主表数据量较大 | 子查询结果集较大,或需基于主表条件过滤 |
空值处理 | 若子查询返回 NULL ,则 IN 永远为 FALSE | 子查询返回 NULL 不影响结果(只要有行返回) |
mysql中的一些基本函数,你知道哪些?
一、数值函数
函数 | 描述 | 示例 |
---|---|---|
ABS(x) | 返回 x 的绝对值 | ABS(-5) → 5 |
ROUND(x, d) | 四舍五入到 d 位小数(d 默认为 0) | ROUND(3.1415, 2) → 3.14 |
CEIL(x) / FLOOR(x) | 向上 / 向下取整 | CEIL(3.2) → 4 ,FLOOR(3.9) → 3 |
POWER(x, y) | 返回 x 的 y 次幂 | POWER(2, 3) → 8 |
SQRT(x) | 返回 x 的平方根 | SQRT(16) → 4 |
MOD(x, y) | 返回 x 除以 y 的余数 | MOD(10, 3) → 1 |
RAND() | 返回 0~1 之间的随机浮点数 | RAND() → 0.78234 (每次不同) |
二、字符串函数
函数 | 描述 | 示例 |
---|---|---|
CONCAT(s1, s2, ...) | 连接多个字符串 | CONCAT('Hello', ' ', 'World') → 'Hello World' |
SUBSTRING(s, start, len) | 从 s 的 start 位置截取长度为 len 的子串(start 从 1 开始) | SUBSTRING('abcdef', 2, 3) → 'bcd' |
LENGTH(s) / CHAR_LENGTH(s) | 返回字符串的字节数 / 字符数(多字节字符有差异) | LENGTH('你好') → 6 (UTF-8 下),CHAR_LENGTH('你好') → 2 |
UPPER(s) / LOWER(s) | 转为大写 / 小写 | UPPER('hello') → 'HELLO' |
TRIM(s) | 去除字符串首尾空格 | TRIM(' abc ') → 'abc' |
REPLACE(s, old, new) | 替换字符串中的子串 | REPLACE('hello', 'l', 'L') → 'heLLo' |
POSITION(sub IN s) | 返回子串在字符串中的位置(从 1 开始) | POSITION('ll' IN 'hello') → 3 |
三、日期时间函数
函数 | 描述 | 示例 |
---|---|---|
NOW() | 返回当前日期和时间 | NOW() → 2023-10-01 12:34:56 |
CURDATE() / CURTIME() | 返回当前日期 / 时间 | CURDATE() → 2023-10-01 ,CURTIME() → 12:34:56 |
DATE_ADD(date, INTERVAL value unit) | 日期加减 | DATE_ADD('2023-10-01', INTERVAL 1 DAY) → 2023-10-02 |
DATEDIFF(date1, date2) | 返回两个日期相差的天数 | DATEDIFF('2023-10-10', '2023-10-01') → 9 |
DATE_FORMAT(date, format) | 按指定格式格式化日期 | DATE_FORMAT(NOW(), '%Y-%m-%d') → '2023-10-01' |
YEAR(date) / MONTH(date) / DAY(date) | 提取日期的年 / 月 / 日 | YEAR('2023-10-01') → 2023 |
四、条件函数
函数 | 描述 | 示例 |
---|---|---|
IF(condition, value_if_true, value_if_false) | 条件判断 | IF(5 > 3, 'Yes', 'No') → 'Yes' |
IFNULL(value1, value2) | 若 value1 为 NULL ,则返回 value2 | IFNULL(NULL, 'default') → 'default' |
CASE WHEN condition1 THEN value1 ... ELSE value END | 多条件分支 | CASE WHEN 1 > 2 THEN 'A' WHEN 2 > 1 THEN 'B' ELSE 'C' END → 'B' |
五、聚合函数
函数 | 描述 | 示例 |
---|---|---|
SUM(column) | 返回某列的总和 | SUM(sales) → 12345.67 |
AVG(column) | 返回某列的平均值 | AVG(age) → 28.5 |
COUNT(column) | 返回某列的非 NULL 值数量 | COUNT(id) → 100 |
COUNT(*) | 返回表的总行数 | COUNT(*) → 100 |
MAX(column) / MIN(column) | 返回某列的最大值 / 最小值 | MAX(price) → 99.99 ,MIN(price) → 0.99 |
六、其他常用函数
函数 | 描述 | 示例 |
---|---|---|
NULLIF(value1, value2) | 若 value1 等于 value2,则返回 NULL | NULLIF(5, 5) → NULL |
COALESCE(value1, value2, ...) | 返回第一个非 NULL 值 | COALESCE(NULL, 'A', 'B') → 'A' |
MD5(s) | 返回字符串的 MD5 哈希值 | MD5('password') → '5f4dcc3b5aa765d61d8327deb882cf99' |
SHA2(s, bits) | 返回字符串的 SHA-2 哈希值(bits 可选 224/256/384/512) | SHA2('password', 256) → '5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d154' |
INET_ATON(ip) / INET_NTOA(num) | IP 地址与整数互转 | INET_ATON('192.168.1.1') → 3232235777 |
SQL查询语句的执行顺序是怎么样的?
SQL 查询语句完整的执行顺序通常有十二步,每个步骤都会生成一个虚拟表,作为下一个步骤的输入。具体如下:
- FROM:对 FROM 子句中的前两个表执行笛卡尔积(交叉联接),生成虚拟表 VT1。若 FROM 子句包含多个表,则会依次对后续表重复此操作,将前一次连接结果与下一个表进行笛卡尔积运算。
- ON:对虚拟表 VT1 应用 ON 筛选器,根据 ON 子句中的逻辑表达式筛选行,满足条件的行进入虚拟表 VT2。
- JOIN:如果是外连接(如 LEFT OUTER JOIN、RIGHT OUTER JOIN 等),将 VT2 中未找到匹配的行作为外部行添加进来。左连接添加左表未匹配行,右连接添加右表未匹配行,生成虚拟表 VT3。
- WHERE:根据 WHERE 子句的条件对 VT3 进行筛选,只有满足条件的行才能插入到虚拟表 VT4 中。
- GROUP BY:将 VT4 中的行按照 GROUP BY 子句指定的列或列列表进行分组,具有相同值的行归为一组,生成虚拟表 VT5。
- AGG_FUNC(计算聚合函数):对 VT5 中的每组数据应用聚合函数(如 SUM、COUNT、AVG、MAX、MIN 等),计算出相应的聚合结果。此时结果集中每个组只有一行,包含聚合值和分组列的值。
- WITH(应用 ROLLUP 或 CUBE):对虚拟表 VT5 应用 ROLLUP 或 CUBE 选项(如果有使用的话),生成虚拟表 VT6。CUBE 生成所选列中值的所有组合的聚合,ROLLUP 生成所选列中值的某一层次结构的聚合。
- HAVING:根据 HAVING 子句中的条件对 VT6 进行筛选,只有满足条件的组才能进入虚拟表 VT7。HAVING 通常用于过滤分组后的结果,可使用聚合函数。
- SELECT:从 VT7 中筛选出 SELECT 子句中指定的列,并计算 SELECT 列表中的表达式(如列的运算、函数调用等),生成虚拟表 VT8。
- DISTINCT:将 VT8 中重复的行移除,只保留唯一的行,生成虚拟表 VT9。若使用了 GROUP BY,通常 DISTINCT 是多余的,因为分组后本身每组就只有一行唯一数据。
- ORDER BY:按照 ORDER BY 子句指定的列或列列表对 VT9 进行排序,可指定升序(ASC)或降序(DESC),生成游标 VC10。ORDER BY 是唯一一个可以使用 SELECT 列表中别名的步骤。
- LIMIT/OFFSET:从 VC10 的开始处选择指定数量的行,若有 OFFSET 则先偏移指定行数再选取。最终生成虚拟表 VT11,并将其返回给调用者,这就是查询的最终结果。
如何用 MySQL 实现一个可重入的锁?
创建一个保存锁记录的表
CREATE TABLE `lock_table` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
//该字段用于存储锁的名称,作为锁的唯一标识符。
`lock_name` VARCHAR(255) NOT NULL,
// holder_thread该字段存储当前持有锁的线程的名称,用于标识哪个线程持有该锁。
`holder_thread` VARCHAR(255),
// reentry_count 该字段存储锁的重入次数,用于实现锁的可重入性
`reentry_count` INT DEFAULT
);
加锁的实现逻辑
开启事务
执行 SQL SELECT holder_thread, reentry_count FROM lock_table WHERE lock_name =? FOR UPDATE,查询是否存在该记录:
如果记录不存在,则直接加锁,执行 INSERT INTO lock_table (lock_name, holder_thread, reentry_count) VALUES (?,?, 1)
如果记录存在,且持有者是同一个线程,则可冲入,增加重入次数,执行 UPDATE lock_table SET reentry_count = reentry_count + 1 WHERE lock_name =?
提交事务
解锁的逻辑:
开启事务
执行 SQL SELECT holder_thread, reentry_count FROM lock_table WHERE lock_name =? FOR UPDATE,查询是否存在该记录:
如果记录存在,且持有者是同一个线程,且可重入数大于 1 ,则减少重入次数 UPDATE lock_table SET reentry_count = reentry_count - 1 WHERE lock_name =?
如果记录存在,且持有者是同一个线程,且可重入数小于等于 0 ,则完全释放锁,DELETE FROM lock_table WHERE lock_name =?
提交事务
执行一条SQL请求的过程是什么?
- 连接器:建立连接,管理连接、校验用户身份;
- 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;
- 解析 SQL,通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
- 执行 SQL:执行 SQL 共有三个阶段:
- 预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。
- 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;
- 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;
讲一讲mysql的引擎吧,你有什么了解?
InnoDB
特性:MySQL 5.5+ 默认引擎,支持事务(ACID)、行级锁、外键约束与聚簇索引。通过 MVCC 实现非锁定读,写操作性能优异。数据存储在 .ibd
文件或共享表空间 ibdata1
中,支持自动崩溃恢复(依赖 redo/undo 日志)。
适用场景:高并发事务处理(如电商订单、金融交易)、需外键约束的关系型数据、写密集型业务(行级锁减少冲突)。
典型应用:电商订单表、用户账户表。
MyISAM
特性:早期 MySQL 的主力引擎,不支持事务与外键,采用表级锁,支持全文索引(5.6 版本前唯一支持)。数据与索引分离存储在 .MYD
(数据)与 .MYI
(索引)文件中,支持压缩表(myisampack
)。
适用场景:只读或写操作极少的场景(如日志分析)、全文搜索需求(5.6 版本前)、空间敏感的静态数据(如配置表)。
典型应用:系统配置表、历史日志表。
Memory
特性:数据完全存储在内存中,读写性能极高,默认使用哈希索引(仅支持等值查询),不支持持久化(重启数据丢失)。表大小受 max_heap_table_size
限制,不支持 TEXT/BLOB 类型。
适用场景:临时表(如子查询结果缓存)、高并发缓存(如会话数据)、快速查找(等值匹配)。
典型应用:查询中间结果集、实时统计临时表。
Archive
特性:专为高压缩比设计,数据行采用 zlib 压缩(通常为原数据的 1/10),仅支持 INSERT/SELECT 操作(不支持 UPDATE/DELETE),使用行级锁。数据存储在 .frm
(表结构)与 .arc
(数据)文件中。
适用场景:历史数据归档(如用户行为日志、历史订单)、写多读少的大数据量存储。
典型应用:用户历史行为记录、过期订单归档。
MySQL为什么InnoDB是默认引擎?
- 事务支持:InnoDB 完整支持 ACID 特性,通过 undo/redo 日志实现事务回滚与持久化,外键约束确保关联数据完整。而 MyISAM 无事务和外键,仅能依赖应用层保证数据正确性
- 行级锁与高并发:InnoDB 采用行级锁,仅锁定操作行,多会话可并发读写不同数据;MyISAM 用表级锁,写操作阻塞全表,并发性能极差。
- 数据安全性与崩溃恢复:InnoDB 通过双写缓冲区防止数据页损坏,崩溃后自动用 redo 日志重做提交事务、undo 日志回滚未提交事务,确保数据一致。MyISAM 无恢复机制,需手动修复且可能丢数据。
- 聚簇索引与查询优化:InnoDB 聚簇索引将主键与数据绑定,主键查询直接定位数据,减少 IO;二级索引通过主键回表,覆盖索引可避免回表。MyISAM 索引与数据分离,需两次 IO。
说一下mysql的innodb与MyISAM的区别?
1. 事务与数据完整性
- InnoDB:
完全支持事务的 ACID 特性(原子性、一致性、隔离性、持久性),通过 redo 日志(确保提交后数据不丢失)和 undo 日志(支持事务回滚)实现。
支持外键约束(FOREIGN KEY
)和级联操作(如ON DELETE CASCADE
),能强制保证关联表之间的数据一致性,例如订单表的user_id
必须对应用户表中存在的主键。 - MyISAM:
不支持事务,所有操作都是原子性的单条 SQL 语句,无法通过ROLLBACK
撤销错误操作。
完全不支持外键约束,即使表定义中声明外键,MySQL 也会忽略,需依赖应用层保证数据关联完整性。
2. 锁机制与并发性能
- InnoDB:
采用行级锁,仅锁定当前操作的行(如UPDATE users SET age=20 WHERE id=1
仅锁id=1
的行),不同行的写操作可并发执行,大幅降低锁冲突。
支持间隙锁(Gap Lock) 和意向锁,通过 MVCC(多版本并发控制)实现非锁定读,读操作不会阻塞写操作,写操作也不会阻塞读操作,高并发场景下性能更稳定。 - MyISAM:
采用表级锁,写操作(INSERT/UPDATE/DELETE
)会锁定整张表,此时其他写操作需等待锁释放,读操作也可能被阻塞(取决于concurrent_insert
配置)。
例如,当一个会话更新表中某行时,其他会话无法写入任何行,甚至读取可能被阻塞,并发写性能极差。
3. 索引与数据存储
- InnoDB:
采用聚簇索引,数据按主键顺序存储在 B + 树的叶子节点,主键索引与数据物理存储绑定,查询主键时可直接定位数据,减少 IO 次数。
二级索引(非主键索引)的叶子节点存储主键值,而非数据地址,查询时需通过主键回表获取完整数据(覆盖索引可避免回表)。
不保存表的总行数,执行COUNT(*)
时需全表扫描(除非有主键索引且无WHERE
条件)。 - MyISAM:
采用非聚簇索引,索引与数据分离存储:索引文件(.MYI
)的叶子节点存储数据在数据文件(.MYD
)中的物理地址,查询时需先查索引再读数据,多一次 IO。
表中保存一个计数器,记录总行数,执行COUNT(*)
时可直接返回结果,无需扫描表(无WHERE
条件时)。
4. 数据安全性与崩溃恢复
- InnoDB:
支持崩溃自动恢复,通过双写缓冲区(Doublewrite Buffer)防止部分写失效(如断电导致数据页不完整),重启时通过 redo 日志重做已提交事务,通过 undo 日志回滚未提交事务,确保数据一致性。
数据持久化依赖事务日志,配置innodb_flush_log_at_trx_commit=1
可保证事务提交后日志立即刷盘。 - MyISAM:
无崩溃恢复机制,数据写入依赖操作系统缓存,若意外宕机可能导致索引损坏或数据丢失。
修复损坏表需手动执行REPAIR TABLE
命令,过程耗时且可能丢失部分数据,安全性远低于 InnoDB。
5. 存储文件与空间占用
- InnoDB:
表结构存储在.frm
文件(MySQL 8.0 后合并到数据字典),数据和索引存储在独立表空间文件(.ibd
)或共享表空间(ibdata1
)中,支持动态行格式,可节省空间。
由于需存储事务日志、回滚段等额外信息,相同数据量下空间占用通常比 MyISAM 大。 - MyISAM:
表结构(.frm
)、数据(.MYD
)、索引(.MYI
)三个文件分离存储,支持压缩表(myisampack
工具),压缩后空间占用较小,适合静态数据存储。
6. 适用场景
- InnoDB:
适合高并发事务场景(如电商订单、金融交易)、写密集型业务(如用户注册、实时数据更新)、需要外键约束或数据一致性的场景。 - MyISAM:
仅适合读多写少、无事务需求的场景(如博客文章、静态配置表),或依赖全文索引(MySQL 5.6 前唯一支持全文索引的引擎)的场景,但现代已逐渐被 InnoDB 替代
创建一个数据库和数据表,会有哪几个文件
我们每创建一个 database(数据库) 都会在 /var/lib/mysql/ 目录里面创建一个以 database 为名的目录,然后保存表结构和表数据的文件都会存放在这个目录里。
db.opt,用来存储当前数据库的默认字符集和字符校验规则。
t_order.frm ,t_order 的表结构会保存在这个文件。在 MySQL 中建立一张表都会生成一个.frm 文件,该文件是用来保存每个表的元数据信息的,主要包含表结构定义。
t_order.ibd,t_order 的表数据会保存在这个文件。表数据既可以存在共享表空间文件(文件名:ibdata1)里,也可以存放在独占表空间文件(文件名:表名字.ibd)。这个行为是由参数 innodb_file_per_table 控制的,若设置了参数 innodb_file_per_table 为 1,则会将存储的数据、索引等信息单独存储在一个独占表空间,从 MySQL 5.6.6 版本开始,它的默认值就是 1 了,因此从这个版本之后, MySQL 中每一张表的数据都存放在一个独立的 .ibd 文件。
索引是什么,有什么好处?
索引(Index) 是一种特殊的数据结构,用于快速定位和访问表中的数据行,其作用类似于书籍的目录。
2. 索引的核心好处
2.1 大幅提升查询速度
- 减少 IO 次数:
全表扫描需读取所有数据页,而索引通常比表小得多,可快速定位到目标数据。
例如:查询SELECT * FROM users WHERE id = 1000
,若无索引需扫描 100 万行数据;有主键索引时,通过 B-Tree 仅需 3-4 次 IO 即可定位。 - 覆盖索引优化:
若查询的列全部包含在索引中(如SELECT id, name FROM users WHERE name = 'Alice'
),可直接从索引获取结果,无需访问数据行,进一步提升效率。
2.2 加速排序和分组操作
- 避免文件排序:
若查询包含ORDER BY
或GROUP BY
,且对应列有索引,数据库可直接利用索引的有序性,减少额外的排序操作。
例如:SELECT * FROM users ORDER BY age
,若age
列有索引,则无需额外排序。
2.3 强制数据唯一性
-
唯一索引(Unique Index):
确保索引列不包含重复值,可用于实现业务唯一性约束(如用户邮箱、身份证号)。
CREATE UNIQUE INDEX idx_email ON users (email);
2.4 优化连接查询
- 加速表关联:
在连接条件(如JOIN users ON orders.user_id = users.id
)上创建索引,可快速定位匹配的记录,减少嵌套循环次数。
MySQL聚簇索引和非聚簇索引的区别是什么?
1. 聚簇索引(Clustered Index)
- 数据与索引合一:聚簇索引的叶子节点直接存储整行数据,索引键(通常为主键)与数据物理存储绑定在一起。一个表只能有一个聚簇索引。
- 存储引擎支持:仅 InnoDB 支持聚簇索引,MyISAM 不支持。
- 主键自动聚簇:若表定义了主键,InnoDB 会自动将主键作为聚簇索引;若未定义主键,则选择第一个非空唯一索引作为聚簇索引;若均不存在,InnoDB 会隐式创建一个 6 字节的 ROWID 作为聚簇索引。
2. 非聚簇索引(二级索引)
- 索引与数据分离:非聚簇索引的叶子节点存储的是索引键和主键值(InnoDB)或数据物理地址(MyISAM),而非完整数据。查询时需通过主键值或物理地址 “回表” 获取完整数据。
- 可创建多个:一个表可创建多个非聚簇索引,用于加速不同列的查询。
区别:
对比维度 | 聚簇索引(Clustered Index) | 非聚簇索引(Non-Clustered Index) |
---|---|---|
数据存储方式 | 数据行物理存储在索引的叶子节点中,索引键(通常为主键)与数据绑定。 叶子节点直接包含整行数据。 | 数据行独立存储,索引的叶子节点仅存储索引键和主键值(InnoDB)或数据物理地址(MyISAM)。 |
索引与数据关系 | 索引即数据,数据即索引,一个表只能有一个聚簇索引。 数据按聚簇索引键的物理顺序存储。 | 索引与数据分离,一个表可创建多个非聚簇索引。 非聚簇索引通过主键值或物理地址间接访问数据。 |
唯一性要求 | 聚簇索引键通常是主键,必须唯一(UNIQUE)。 若未定义主键,InnoDB 会选择第一个非空唯一索引或生成隐藏 ROWID。 | 非聚簇索引不强制唯一(除非显式定义为 UNIQUE INDEX)。 允许索引键重复,叶子节点存储多个相同键值。 |
主键查询效率 | 极高:直接通过聚簇索引定位数据页,通常 1-2 次磁盘 IO。 适合高频主键查询(如WHERE id = 100 )。 | 较低:需先查非聚簇索引获取主键值,再通过聚簇索引回表,至少 2 次 IO。 回表操作在大数据量时开销显著。 |
范围查询效率 | 极高:数据按聚簇索引键有序存储,范围查询(如BETWEEN 、> )可直接扫描物理相邻页。 | 较低:需逐行回表获取完整数据,尤其当结果集较大时,IO 次数显著增加。 |
非主键列查询效率 | 依赖索引:若无对应非聚簇索引,需全表扫描;若有,则先查非聚簇索引再回表。 | 直接利用索引:通过非聚簇索引快速定位,但可能需多次回表(除非是覆盖索引)。 |
覆盖索引优化 | 若查询仅需主键列(如SELECT id FROM users ),无需回表。 | 若索引包含所有查询列(如INDEX(email, id) ),可避免回表,提升效率。 |
写入性能影响 | 较高:插入 / 更新可能导致页分裂(尤其主键非自增时),需调整物理存储位置。 | 较低:仅更新索引结构,不涉及数据物理位置变动。 |
索引大小 | 与数据量相同(索引即数据),占用空间较大。 | 通常远小于数据量,仅存储索引键和主键值 / 物理地址。 |
典型适用场景 | 主键查询、范围查询、聚簇索引键参与的 JOIN 操作。 | 非主键列的等值查询、复合索引优化多条件查询、覆盖索引场景。 |
聚簇索引页分裂
一、聚簇页分裂的原理
1. 数据页(Page)的基本结构
InnoDB 以 ** 页(Page)** 为单位管理数据,默认每页大小为 16KB。聚簇索引的数据按主键顺序存储在页中,每个页包含多个数据行,且页之间通过双向链表连接。
2. 页分裂的触发条件
当向已满的页(填充率达到 100%)插入新数据时,InnoDB 会触发页分裂:
- 主键有序插入(如自增 ID):新数据通常插入到页的尾部,若尾部空间不足,则可能触发页分裂。
- 主键随机插入(如 UUID):新数据可能插入到页的任意位置,导致频繁的页分裂和碎片。
3. 页分裂的过程
- 创建新页:分配一个新的数据页(16KB)。
- 数据迁移:将原页中的约一半数据(按主键顺序)迁移到新页。
- 更新索引:调整页之间的双向链表指针,确保数据顺序正确。
- 插入新数据:将新记录插入到合适的页中。
二、页分裂的影响
1. 性能开销
- 写入变慢:页分裂涉及磁盘 IO(创建新页、迁移数据)和内存操作,显著降低写入性能。
- 缓存失效:页分裂后,原页和新页的缓存可能失效,增加后续查询的 IO 成本。
2. 空间碎片
- 内部碎片:分裂后的页通常填充率为 50%-60%,导致存储空间浪费。
- 外部碎片:页在磁盘上的物理位置可能不连续,增加磁盘寻道时间。
3. 索引结构不稳定
频繁的页分裂会导致 B+Tree 结构频繁调整,尤其在随机主键场景下,可能使树的高度增加,降低查询效率。
三、页分裂的典型场景
1. 随机主键(如 UUID)
- 问题:UUID 生成的主键值是随机的,新数据可能插入到聚簇索引的任意位置,导致频繁页分裂。
2. 自增主键的高并发插入
- 问题:多个事务同时插入自增主键时,可能导致页争用(Page Contention),触发更多页分裂。
- 优化:通过
innodb_autoinc_lock_mode
参数调整自增锁模式。
四、优化聚簇页分裂的策略
1. 使用自增整数作为主键
- 原理:自增主键保证新数据按顺序插入到页的尾部,减少页分裂。
2. 控制页填充率
通过innodb_fill_factor
参数控制页的初始填充率(默认 100%):
3. 避免随机主键
- 不推荐使用 UUID、MD5 等随机值作为主键。
- 若业务需要全局唯一 ID,可考虑 雪花算法(Snowflake) 生成趋势递增的 ID。
最左前缀原则
一、最左前缀原则的原理
1. 复合索引的结构
复合索引是指在多个列上创建的索引,例如INDEX(a, b, c)
。其内部结构特点:
- 索引键按定义顺序排序:先按
a
排序,若a
相同则按b
排序,若a
和b
均相同则按c
排序。 - 叶子节点存储完整索引键:每个叶子节点包含
a
,b
,c
三列的值及对应的主键值。
2. 最左前缀的定义
查询条件必须从索引的最左侧列开始,且不跳过中间列,才能有效利用复合索引。例如:
-
有效匹配:
WHERE a = 1 -- 匹配索引的第一列 WHERE a = 1 AND b = 2 -- 匹配索引的前两列 WHERE a = 1 AND b = 2 AND c = 3 -- 匹配全部三列
-
无效匹配:
WHERE b = 2 -- 跳过索引的第一列a WHERE a = 1 AND c = 3 -- 跳过索引的第二列b WHERE b = 2 AND c = 3 -- 完全不包含索引的第一列a
回表查询
回表查询是指当查询使用非聚簇索引(二级索引)时,若索引未覆盖查询所需的全部字段(即索引叶子节点仅存储索引键和主键值,缺少查询需要的其他字段),数据库会先通过非聚簇索引定位到叶子节点,获取对应的主键值,再通过主键值访问聚簇索引,最终从聚簇索引的叶子节点中读取完整数据行以获取所需字段的过程。
主键值是如何进入非聚簇索引的?
当创建非聚簇索引(如INDEX(email)
)时,InnoDB 会:
- 扫描聚簇索引:遍历聚簇索引的叶子节点,获取每个数据行的
email
值和对应的主键值(如id
)。 - 构建非聚簇索引:将
email
值和主键值(如id
)作为索引键 - 值对,按email
排序后存入非聚簇索引的 B+Tree 结构中。
什么字段适合当做主键?
在数据库设计中,选择主键需遵循 “唯一、稳定、高效” 原则,最推荐自增整数(如 BIGINT),其由数据库自动生成、占用空间小、索引效率高,适配多数业务表(尤其高频插入或大数据量场景);分布式环境下优先用雪花算法等分布式 ID 保障全局唯一与有序性;若业务存在天然短且稳定的唯一标识(如身份证号、企业信用代码),可直接复用作为主键,但需避免过长字符串、可修改字段、复合主键(多对多关系表除外)等类型。核心目标是通过紧凑、稳定的标识,确保数据完整性并提升查询与存储性能。
如果聚簇索引的数据更新,它的存储要不要变化?
不修改索引键的更新
当聚簇索引的更新不涉及索引键(如主键)时,若数据行大小未超过原磁盘页剩余空间,仅在原位置修改字段值,存储物理位置不变;若行大小增加导致原页空间不足,则可能触发行迁移(数据移至新页,原位置留指针)或页分裂(拆分原页为两页重新分配数据),以维持索引键的物理顺序,此时存储结构会因数据位置调整或页布局变化而改变,但索引键的逻辑顺序不受影响
修改索引键的更新
当聚簇索引的更新涉及索引键(如主键)时,由于索引键决定数据的物理存储顺序,原数据行的物理位置不再符合新索引键的排序要求,因此会被视为 “删除旧行 + 插入新行”:旧行标记为删除(可能产生存储空洞),新行按新索引键顺序插入到目标磁盘页,可能触发页分裂或页合并以适配新的排序逻辑,此时存储结构会发生显著变化,包括数据行物理位置迁移、索引页布局重构,同时还需同步更新依赖聚簇索引的二级索引,存储变化的范围和成本更高。
性别字段能加索引么?为啥?
从 B + 树结构角度看,性别字段不适合建索引的核心原因是:其低选择性导致索引 B + 树的叶子节点中,相同值(如 “女”)对应的主键指针会密集分布在连续的多个叶子节点中。当执行WHERE gender = '女'
时,数据库虽能通过非叶子节点快速定位到 “女” 的叶子节点范围,但这些叶子节点会存储大量重复值关联的主键 ID(可能占全表 50% 记录)。由于每个主键 ID 都需单独回表查询完整数据,而这些主键在聚簇索引中分布零散,会引发大量随机 IO 的回表操作。相比之下,全表扫描通过顺序 IO 即可读取数据,成本反而更低,因此性别这类低选择性字段的索引不仅无法优化性能,反而可能因高频回表成为效率负担。
先明确:磁盘 IO 的 “顺序” vs “随机” 成本天差地别
磁盘(尤其是机械硬盘)的物理结构决定了:
- 顺序 IO:数据在磁盘上连续存储,磁头只需一次定位后,顺着磁道连续读取即可,效率极高(类似读一本书时从第一页按顺序读到最后一页)。
- 随机 IO:数据在磁盘上分散存储,磁头需要频繁移动到不同位置定位数据,每次定位都有 “寻道时间” 和 “旋转延迟”,效率极低(类似读一本书时,每读一页都要先从书架上找到书、翻到任意一页,再放下找下一本)。
表中十个字段,你主键用自增ID还是UUID,为什么?
从 B + 树索引结构与存储效率看:自增 ID 更适配底层机制
主键是聚簇索引的核心,而聚簇索引的叶子节点直接存储整行数据(包括 10 个字段的完整内容),其性能依赖于主键的有序性:
- 自增 ID:数值按顺序递增(如 1→2→3→…),新数据插入时会直接追加到聚簇索引叶子节点的末尾,不会破坏现有节点的连续性。这意味着:
- 插入操作无需频繁分裂叶子节点(B + 树节点满时才分裂),减少索引维护成本;
- 叶子节点中的数据按顺序存储,全表扫描或范围查询(如
WHERE id > 1000
)时可通过顺序 IO 高效读取,与磁盘 IO 的 “顺序偏好” 完全匹配。
- UUID:由 32 位随机字符组成(如a1b2c3…),本质是无序的随机值。新数据插入时,主键值可能落在现有索引的任意位置,导致:
- 频繁触发 B + 树叶子节点的分裂(为插入随机值腾出空间),产生大量碎片,降低索引存储效率;
- 聚簇索引叶子节点的数据分布零散,即使表中只有 10 个字段,全表扫描或基于主键的范围查询也会因随机 IO 增多而变慢。
为什么自增ID更快一些,UUID不快吗?
自增的主键的值是顺序的,所以 Innodb 把每一条记录都存储在一条记录的后面,所以自增 id 更快的原因:
下一条记录就会写入新的页中,一旦数据按照这种顺序的方式加载,主键页就会近乎于顺序的记录填满,提升了页面的最大填充率,不会有页的浪费
新插入的行一定会在原有的最大数据行下一行,mysql定位和寻址很快,不会为计算新行的位置而做出额外的消耗
减少了页分裂和碎片的产生
但是 UUID 不是递增的,MySQL 中索引的数据结构是 B+Tree,这种数据结构的特点是索引树上的节点的数据是有序的,而如果使用 UUID 作为主键,那么每次插入数据时,因为无法保证每次产生的 UUID 有序,所以就会出现新的 UUID 需要插入到索引树的中间去,这样可能会频繁地导致页分裂,使性能下降。
而且,UUID 太占用内存。每个 UUID 由 36 个字符组成,在字符串进行比较时,需要从前往后比较,字符串越长,性能越差。另外字符串越长,占用的内存越大,由于页的大小是固定的,这样一个页上能存放的关键字数量就会越少,这样最终就会导致索引树的高度越大,在索引搜索的时候,发生的磁盘 IO 次数越多,性能越差。
UUID在B+树里面存储是有序的吗?
UUID 在 B + 树中是逻辑有序的(按字符串字典序排列),物理存储无序,但这种有序性仅体现在索引的逻辑结构上(叶子节点通过指针按字典序串联),无法转化为物理存储的连续性。由于 UUID 随机生成的特性,新数据会被分散插入到 B + 树的不同位置,导致物理存储碎片化,查询时需频繁随机访问磁盘。相比之下,自增 ID 的有序性与物理存储的连续性完全匹配,能充分利用磁盘顺序 IO 的优势,因此 UUID 的 “逻辑有序” 对性能提升并无实际价值,反而因频繁分裂节点增加了维护成本。
Mysql中的索引是怎么实现的?
一、索引底层结构与示例表
1. 表结构与数据
假设有 products
表,主键为自增 id
,并为 category_id
建立二级索引:
CREATE TABLE products (
id INT PRIMARY KEY, -- 聚簇索引
category_id INT, -- 二级索引字段
name VARCHAR(50),
price DECIMAL(10,2),
INDEX idx_category (category_id) -- 二级索引
);
插入数据(按主键 id
有序存储):
id | category_id | name | price |
---|---|---|---|
1 | 100 | 手机充电器 | 99.00 |
2 | 100 | 手机支架 | 199.00 |
3 | 200 | 无线耳机 | 299.00 |
4 | 200 | 蓝牙音箱 | 499.00 |
5 | 300 | 机械键盘 | 599.00 |
6 | 300 | 游戏鼠标 | 699.00 |
2. 底层结构核心
-
聚簇索引:叶子节点存储完整数据行,按主键
id
物理排序,非叶子节点存储主键分界值。 -
二级索引:叶子节点存储
category_id
和主键id
(回表指针),按category_id
排序,叶子节点通过双向链表连接。在 B+ 树索引的实现中,数据按照索引键(如示例中的
id
)有序存储,整个结构由根节点、中间节点和叶子节点三级构成。根节点作为入口,存储了用于快速定位的索引范围边界(如判断id=5
大于根节点的3
后转向右侧分支);中间节点进一步细化范围划分(如通过[4,6]
的区间判断定位到目标叶子节点所在分支);所有实际数据行仅保存在叶子节点中,且叶子节点之间通过指针串联形成有序链表,既保证了按索引键快速查找(类似字典目录定位),又支持范围扫描(如从某一id
开始连续读取后续数据)。
二、Mermaid 10.9.1 版本 B + 树图(含双向链表)
1. 聚簇索引(主键 id
)
2. 二级索引 idx_category
三、查询执行流程(含双向链表的关键作用)
场景 1:范围查询 WHERE category_id BETWEEN 100 AND 200
graph LR
A[发起查询: category_id BETWEEN 100 AND 200] --> B[访问二级索引]
style A fill:#ecf0f1,stroke:black
B --> C[根节点判断: 100 ≤ 200 → 中间节点1]
style B fill:#9b59b6,color:white
style C fill:#1abc9c,color:black
C --> D[中间节点1定位: 100 ≤ 200 → 叶子节点1-3]
style D fill:#f39c12,color:black
D --> E[沿双向链表遍历: leaf2_1 → leaf2_2 → leaf2_3]
style E fill:#f39c12,color:black,stroke:#e74c3c,stroke-width:2px
E --> F[获取主键: id=1,2,3]
style F fill:#f39c12,color:black
F --> G[回表: 通过 id=1,2,3 访问聚簇索引]
style G fill:#4287f5,color:white
G --> H[返回完整数据行]
style H fill:#27ae60,color:white
执行步骤详解(双向链表的作用):
- 定位起始节点:通过二级索引找到第一个满足条件的叶子节点
leaf2_1
(category_id=100
)。 - 链表遍历:利用双向链表的后向指针,依次访问
leaf2_1 → leaf2_2 → leaf2_3
,收集所有category_id ≤ 200
的主键id=1,2,3
。 - 回表查询:通过主键访问聚簇索引,获取完整数据行。
场景2:SELECT * FROM products WHERE id = 5
执行步骤详解:
- 根节点导航:
- 根节点存储分界值
[3, 6]
,判断5 > 3
,进入右侧子树(中间节点 2)。
- 根节点存储分界值
- 中间节点定位:
- 中间节点 2 存储
[4, 6]
,判断5
在范围内,直接定位到叶子节点 5。
- 中间节点 2 存储
- 叶子节点读取:
- 叶子节点 5 存储完整数据行:
id=5, cat=300, name=机械键盘, price=599.00
,直接返回。
- 叶子节点 5 存储完整数据行:
B+树的根节点,中间节点,叶子节点怎么划分?
一、数据准备与叶子节点分组
假设我们有以下 12 个数据行,需要为其 id
字段构建 B+ 树索引:
id: 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23
- 叶子节点分组(假设每个叶子节点最多存 3 个键值)
将数据按顺序分组到叶子节点中:
叶子节点1: [1, 3, 5] → 最大键值 = 5
叶子节点2: [7, 9, 11] → 最大键值 = 11
叶子节点3: [13, 15, 17] → 最大键值 = 17
叶子节点4: [19, 21, 23] → 最大键值 = 23
叶子节点之间通过双向链表连接(叶子节点1 ↔ 叶子节点2 ↔ ...
)。
二、中间节点的生成(第一层中间节点)
- 中间节点的键值选择
中间节点需要存储子节点(叶子节点)的最大键值,以划分范围:
- 中间节点 1:管辖叶子节点 1 和 2 → 最大键值为
5
和11
; - 中间节点 2:管辖叶子节点 3 和 4 → 最大键值为
17
和23
。
因此,中间节点的结构为:
中间节点1: 键值 [5, 11] → 指针指向 叶子节点1、叶子节点2
中间节点2: 键值 [17, 23] → 指针指向 叶子节点3、叶子节点4
- 中间节点的范围划分逻辑
- 中间节点 1 的
5
表示:“≤5” 的数据在叶子节点 1; - 中间节点 1 的
11
表示:“>5 且 ≤11” 的数据在叶子节点 2; - 同理,中间节点 2 的
17
和23
分别标记叶子节点 3 和 4 的范围。
三、根节点的生成
- 根节点的键值选择
根节点作为中间节点的父节点,需要存储中间节点的最大键值:
- 中间节点 1 的最大键值是
11
; - 中间节点 2 的最大键值是
23
。
因此,根节点的结构为:
根节点: 键值 [11, 23] → 指针指向 中间节点1、中间节点2
- 根节点的范围划分逻辑
- 根节点的
11
表示:“≤11” 的数据由中间节点 1 管辖; - 根节点的
23
表示:“>11 且 ≤23” 的数据由中间节点 2 管辖。
五、查询示例:查找 id = 15
- 根节点判断:
- 根节点键值
[11, 23]
,目标15 > 11
→ 走右侧指针,进入中间节点 2。
- 根节点键值
- 中间节点 2 判断:
- 中间节点 2 键值
[17, 23]
,目标15 < 17
→ 走左侧指针,进入叶子节点 3。
- 中间节点 2 键值
- 叶子节点读取:
- 叶子节点 3 存储
[13, 15, 17]
→ 找到id = 15
,返回对应数据行
- 叶子节点 3 存储
B+树的特性是什么?
一、结构特性:层级分明的平衡树设计
B + 树采用根节点、中间节点和叶子节点的三级结构,层级严格平衡且所有叶子节点处于同一层级。中间节点和根节点仅存储键值与子节点指针,负责范围导航;叶子节点作为数据存储的终点,通过双向链表串联成全局有序结构。多路分支设计大幅降低树高(百万级数据通常仅 3-4 层),避免了二叉树层级过深的问题,确保查询路径长度稳定可控。
二、数据存储特性:有序性与集中性
B + 树的键值在节点内按序排列,叶子节点通过链表连接形成全局有序数据集,为高效查询奠定基础。与 B 树不同,实际数据仅集中存储在叶子节点中,中间节点和根节点的键值仅作为范围分隔符,不存储具体数据。这种设计既提高了中间节点的键值存储密度(减少层级),又保证了所有查询最终都能在叶子节点中获取完整数据,逻辑统一且高效。
三、查询特性:高效支持单点与范围查询
单点查询通过根节点到中间节点的逐层范围过滤,精准定位到目标叶子节点,查询复杂度稳定为 O (log n),树高直接决定最大磁盘 I/O 次数(通常 3-4 次)。范围查询借助叶子节点的双向链表,在定位起始节点后无需回退上层节点,可通过链表顺序遍历相邻叶子节点,实现类似数组的连续读取效率,尤其适合分页、区间统计等场景。
四、存储优化:适配磁盘 I/O 特性
B + 树的节点大小与磁盘页(如 4KB)严格匹配,一个节点对应一个磁盘页,读取节点即一次磁盘 I/O。节点最大键值数量由页大小决定,确保数据不跨页存储,避免多次 I/O 开销。叶子节点的有序链表结构适配操作系统预读机制,加载当前节点时自动预读相邻节点到内存缓存,后续查询可直接从内存读取,显著降低实际 I/O 次数。
五、维护特性:动态平衡与高效插入删除
B + 树通过分裂与合并机制实现动态平衡:插入数据导致节点溢出时,自动分裂节点并向上层插入分隔键;删除数据导致节点稀疏时,合并相邻节点。这一过程确保树始终保持平衡,避免层级失衡。同时,数据集中存储在叶子节点的设计减少了冗余,相同存储空间可容纳更多键值,间接降低树高,使插入、删除操作的性能损耗可控。
说说B+树和B树的区别?
对比维度 | B 树 | B + 树 |
---|---|---|
数据存储位置 | 所有节点(根、中间、叶子)均存储实际数据或数据指针。 | 仅叶子节点存储实际数据,非叶子节点(根、中间)仅存索引键值和子节点指针。 |
叶子节点连接方式 | 叶子节点之间无链表连接,相互独立。 | 叶子节点通过双向链表串联,形成全局有序的连续数据集。 |
单点查询路径 | 可能在中间节点或叶子节点命中数据,路径长度不固定。 | 必须遍历至叶子节点才能获取数据,路径长度固定(等于树高)。 |
范围查询效率 | 需回溯上层节点判断下一分支,多次跳转,效率低(O (n) 级)。 | 定位起始节点后通过叶子链表顺序遍历,无需回退,效率高(接近 O (k),k 为结果数量)。 |
树高与 I/O 次数 | 非叶子节点存储数据,键值密度低,树高更高,磁盘 I/O 次数更多。 | 非叶子节点仅存索引,键值密度高,树高更低(百万级数据通常 3-4 层),I/O 次数更少。 |
数据冗余情况 | 非叶子节点与叶子节点可能重复存储数据,存在冗余。 | 数据仅在叶子节点存储,无冗余,空间利用率更高。 |
插入删除复杂度 | 节点分裂 / 合并时需移动数据,操作复杂,性能损耗大。 | 仅叶子节点存储数据,分裂 / 合并仅需调整索引键,操作简单,性能稳定。 |
缓存利用率 | 非叶子节点含数据,缓存中有效索引键占比低,命中率低。 | 非叶子节点全为索引键,缓存可容纳更多索引,命中率更高。 |
典型适用场景 | 文件系统索引(如 ext4)、随机读写为主的场景。 | 数据库索引(如 MySQL InnoDB)、范围查询频繁的场景(分页、区间统计)。 |
B+树的叶子节点链表是单向的还是双向的?
双向链表可以让节点既能向前访问也能向后访问,在进行范围查询时,能够方便地从一个方向或相反方向遍历叶子节点,获取所需数据,无需回溯至根节点,大幅减少范围查询的 I/O 次数。
MySQL为什么用B+树结构?和其他结构比的优点?
数据结构 | 优势场景 | 在数据库中的局限性 | B + 树为何更优 |
---|---|---|---|
二叉查找树 | 内存中简单查询 | 树高过高(O (log₂n)),磁盘 I/O 次数爆炸 | 多路结构降低树高,适配磁盘 I/O |
红黑树 | 内存中有序数据维护 | 二叉结构树高仍过高,范围查询需全树遍历 | 叶子链表支持高效范围查询,树高仅 3-4 层 |
B 树 | 多路查找,比二叉树 I/O 少 | 非叶子节点存数据,树高更高,范围查询效率低 | 索引与数据分离,树高更低,范围查询无回溯 |
哈希表 | 等值查询快(O (1)) | 不支持范围查询、排序,哈希冲突和扩容成本高 | 天然支持范围查询和有序遍历,维护成本低 |
跳表 | 内存中有序查询 | 磁盘存储时节点分散,I/O 次数多,无链表优化范围查询 | 叶子链表 + 低树高,磁盘 I/O 效率碾压 |
为什么MysSQL不用跳表?
-
磁盘 I/O 效率:节点物理存储分散,查询需随机访问多个不连续节点,导致大量随机 I/O;节点非连续性降低缓存命中率,无预读优化效果。
-
- 什么是预读机制?
- 磁盘和操作系统为优化性能,会采用预读策略:当程序访问某个磁盘页(如 4KB)时,系统会自动将相邻的几个磁盘页(如连续的 8 个页)一起加载到内存缓存(如操作系统的 Page Cache 或数据库的 Buffer Pool)中。
- 原因:磁盘的 “顺序读写速度” 远高于 “随机读写速度”(顺序读写可达数百 MB/s,随机读写仅数十 MB/s),预读能将未来可能访问的相邻数据提前加载,减少后续磁盘 I/O。
- 适用场景:数据在物理存储上连续或按顺序排列(如 B + 树的叶子节点链表)。
-
-
范围查询支持: 需从高层逐层向下定位起始节点,再在底层链表逐节点遍历,路径长且节点分散,随机 I/O 增多,效率远低于 B + 树的叶子链表顺序遍历。
-
并发控制: 层级结构复杂,插入 / 删除需调整多个层级指针,难以实现细粒度锁,常需锁定多个节点甚至整棵树,并发性能差。
-
实现复杂度与维护成本:依赖随机层级保证平衡,极端情况可能退化为链表(查询复杂度 O (n));插入 / 删除时动态调整层级指针,实现复杂且性能波动大。
在复合索引(a,b,c)中,where b=?无法使用复合索引的原因?
在复合索引 (a, b, c)
中,WHERE b=?
无法利用索引的根本原因在于:复合索引遵循最左前缀排序规则,即先按 a
全局排序,仅在 a
相同的分组内 b
才有序,导致 b
在整个索引中全局无序。B + 树的查询逻辑依赖非叶子节点的索引键(通常为 a
或 a+b
)快速定位,而 WHERE b=?
缺乏 a
的过滤条件,使得索引无法通过非叶子节点缩小范围,只能扫描所有 a
分组,逐一检查每个分组内的 b
值是否匹配。这种操作的效率远低于直接全表扫描,因此 MySQL 优化器通常不会选择使用该索引。
复合索引中为什么说b是全局无序,局部有序?
b只有在同一个a的分组下才是有序的,如果是跨a分组则是无序的。
a=1, b=5, c=10
a=1, b=8, c=20
a=2, b=3, c=15 ← `b=3` 出现在 `b=5` 和 `b=8` 之后
a=2, b=7, c=25
创建联合索引需要注意什么?
把高基数的字段放到复合索引的左侧。
区分度 = 列中不同值的数量(基数,Cardinality) / 表的总记录数(Row Count)
联合索引 (a, b,c),where条件是 a=2 and c = 1,能用到联合索引吗?
会用到联合索引,但是只有 a 才能走索引,c 无法走索引,因为不符合最左匹配原则。虽然 c 无法走索引, 但是 c 字段在 5.6 版本之后,会有索引下推的优化,能减少回表查询的次数。
索引下推(ICP)优化
一、无 ICP 时的执行流程
- 索引扫描:
通过(a, b, c)
索引定位所有a=2
的记录。假设匹配 100 条,索引项格式为(a=2, b, c)
。- 关键点:索引按
a → b → c
排序,但查询未指定b
,因此c
在a=2
的范围内无序(不同b
值下的c
随机分布)。
- 关键点:索引按
- 逐行回表:
对每条索引记录,回表(读取聚簇索引)获取完整数据行,再检查c=1
是否成立。 - 最终结果:
100 条中仅 10 条满足c=1
,需回表 100 次,其中 90 次回表是无效的。
二、ICP 过滤的执行顺序:先范围锁定,再条件过滤
在 WHERE a=2 AND c=1
的查询中,ICP 的执行流程严格遵循 “先锁定范围,再过滤细节” 的逻辑:
- 第一步:锁定
a=2
的范围
索引首先通过最左前缀a=2
快速定位到所有a
为 2 的索引记录(这一步是索引扫描的基础,无法跳过)。此时得到的是一个 “候选集”:所有a=2
的索引项,格式为(a=2, b, c)
。 - 第二步:在候选集中过滤
c=1
在 ICP 优化下,数据库会在这个候选集中逐行检查索引项中的c
值,只保留c=1
的记录。这一步的过滤是在索引层完成的,无需回表。
索引失效有哪些情况?
索引失效场景 | 典型示例(基于索引 (a, b, c) 或单列索引) | 失效原因分析 |
---|---|---|
最左前缀原则不满足 | WHERE b=1 、WHERE c=1 、WHERE a=1 AND c=1 | 复合索引按 a→b→c 排序,缺少左列无法定位索引范围,中间列缺失导致后续列失效。 |
索引列参与计算 / 函数 | WHERE YEAR(create_time)=2023 、WHERE id+1=100 | 索引存储原始值,计算 / 函数结果无法直接匹配索引键值。 |
LIKE 以通配符开头 | WHERE name LIKE '%张' | 索引按前缀排序,无法快速定位任意字符开头的匹配项。 |
隐式类型转换 | WHERE id='123' (id 为 INT 类型) | 类型不一致触发隐式转换,转换后的值无法匹配原始索引键。 |
范围查询后列失效 | WHERE a>10 AND b=2 | 范围查询破坏索引有序性,后续列无法继续利用索引。 |
OR 条件部分无索引 | WHERE a=1 OR b=2 (b 无索引) | 数据库无法高效合并不同索引路径,选择全表扫描。 |
什么是覆盖索引?
指的是一个索引包含了查询所需的所有列数据,使得数据库无需再回表查询数据行本身。这种优化可以显著提高查询效率,尤其是在处理大型数据集时。
索引的优缺点?
- 优点:索引通过预排序和快速定位机制显著提升数据库查询效率,尤其在过滤、排序、连接及聚合操作中表现突出。它能避免全表扫描,直接定位目标数据,甚至通过覆盖索引避免回表查询,减少 I/O 开销。此外,唯一索引可强制数据唯一性,替代手动约束检查,而索引的有序性也能加速统计函数(如 COUNT、SUM)的执行。
- 缺点:索引的代价主要体现在空间占用、写操作开销及维护复杂性上。索引需要额外存储,尤其复合索引会显著增加空间消耗;每次数据写入、更新或删除时,数据库需同步维护索引,导致写操作变慢;随着数据变化,索引可能碎片化,需要定期重建以保证性能。此外,错误的索引设计(如选择性低的字段索引)或查询条件(如通配符开头的 LIKE)可能导致索引失效,反而增加查询负担。
什么是前缀索引?
前缀索引(Prefix Index)是数据库索引优化的一种技术,它只对列的前 N 个字符建立索引,而非整列。这种方式可以显著减少索引大小,提升存储和查询效率,尤其适用于大文本字段(如 VARCHAR、TEXT)。
核心原理
- 索引压缩:
- 对长文本字段(如 URL、邮箱、JSON),仅索引前 N 个字符,大幅减少索引体积。
- 例如,对
email
字段取前 10 个字符建立索引:CREATE INDEX idx_email ON users (email(10));
- 查询优化:
- 当查询条件为前缀匹配时(如
WHERE email LIKE 'user%'
),前缀索引可直接生效。 - 但无法支持后缀或中间匹配(如
LIKE '%@gmail.com'
)。
- 当查询条件为前缀匹配时(如
索引优化有哪些?
-
索引设计优化:覆盖索引,联合索引,前缀索引,索引下推
-
查询优化
避免索引失效
利用索引排序
索引与排序方向一致:
CREATE INDEX idx_time ON logs (create_time DESC); -- 与ORDER BY方向匹配 SELECT * FROM logs ORDER BY create_time DESC LIMIT 10;
优化join操作
在 JOIN 字段建索引:
CREATE INDEX idx_dept ON employees (department_id); -- 加速JOIN SELECT e.name, d.dept_name FROM employees e JOIN departments d ON e.dept_id = d.id;
事务的特性是什么?
原子性(Atomicity)
原子性确保事务是一个不可分割的操作单元,事务中的所有操作要么全部成功执行并提交,要么在任何一步失败时,所有已执行的操作都被回滚到初始状态,不会留下部分完成的中间结果。这一特性通过回滚日志(Undo Log)实现,例如转账时若扣款成功但收款失败,系统会自动撤销扣款操作,避免数据不一致。其核心作用是保障事务的 “全成或全败”,是数据完整性的基础。
一致性(Consistency)
一致性要求事务执行前后,数据库从一个合法的状态转换到另一个合法的状态,始终满足预设的业务规则和约束条件。这意味着数据必须符合定义的完整性规则(如主键唯一、外键关联、字段格式限制等),且业务逻辑(如转账总金额不变)在事务过程中不会被破坏。例如,用户注册时若邮箱唯一性约束被触发,事务会因一致性校验失败而中断,确保不符合规则的数据不会进入数据库。
隔离性(Isolation)
隔离性控制多个并发事务之间的相互干扰程度,使每个事务在执行时仿佛独立于其他事务,如同串行执行一般。数据库通过锁机制(行锁、表锁)和 MVCC(多版本并发控制)实现不同隔离级别,从低到高包括读未提交、读已提交、可重复读和串行化,分别解决脏读、不可重复读、幻读等问题。例如,在可重复读级别下,事务执行期间多次读取同一数据,结果始终一致,不受其他并发事务的更新影响。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(Read Uncommitted) | ✅ | ✅ | ✅ |
读已提交(Read Committed) | ❌ | ✅ | ✅ |
可重复读(Repeatable Read) | ❌ | ❌ | ✅ |
串行化(Serializable) | ❌ | ❌ | ❌ |
✅表示不能解决,❌表示能解决。
持久性(Durability)
持久性保证事务一旦提交,其对数据的修改将永久保存到数据库中,即使发生系统崩溃、断电等故障,数据也不会丢失。这一特性主要通过预写日志(WAL)机制实现,即事务提交前先将修改记录写入 redo log 并强制刷盘,再异步更新数据页。例如,设置innodb_flush_log_at_trx_commit = 1
时,MySQL 每次提交事务都会将 redo log 同步到磁盘,确保极端情况下数据仍可通过日志恢复,从而保障结果的永久性。
讲一下脏读,不可重复读,幻读
1. 脏读(Dirty Read)
定义:一个事务读取到了另一个未提交事务修改的数据,而如果后者发生回滚,前者读取的数据就是 “无效的脏数据”。
通俗例子:
小明和小红同时操作一个银行账户,账户原本有 1000 元。
- 小明发起转账:向账户转入 500 元(未提交),此时账户临时显示 1500 元。
- 小红此时查询账户,读到了 1500 元这个 “未提交的临时数据”,并基于此做了后续操作(比如再转出 300 元)。
- 突然小明的转账失败并回滚,账户恢复为 1000 元。但小红已经基于 1500 元的 “脏数据” 完成了操作,导致实际账户余额变成 700 元(1000 - 300),但小红误以为自己是从 1500 元里转出的,数据逻辑混乱。
2. 不可重复读(Non-Repeatable Read)
定义:一个事务在执行过程中,多次读取同一数据,却得到了不同的结果,因为中间被另一个已提交事务修改并提交了。
通俗例子:
小红在核对账单,需要两次确认账户余额:
- 第一次查询:账户余额 1000 元(事务未结束)。
- 此时小明向账户转入 500 元并提交了事务,账户实际变成 1500 元。
- 小红第二次查询:发现余额变成 1500 元,和第一次结果不一致,导致她无法确认哪次是正确的,核对工作被干扰。
3. 幻读(Phantom Read)
定义:一个事务在执行过程中,基于相同条件多次查询,结果集的行数发生了变化,因为另一个已提交事务新增或删除了符合条件的数据,如同出现了 “幻觉”。
通俗例子:
小红负责统计公司 “月薪 5000 元以下” 的员工数量:
- 第一次查询:有 3 名员工符合条件,小红开始逐个处理。
- 此时小明新增了 1 名月薪 4000 元的员工并提交事务。
- 小红处理完后再次查询确认,发现符合条件的员工变成了 4 名,多出了一个 “之前没见过” 的员工,导致统计结果和实际操作范围不一致(漏处理了新增的员工)。
Mysql怎么解决并发问题的?
- 锁机制:锁机制是数据库控制并发修改冲突的关键手段,通过对数据或索引加锁实现操作互斥。InnoDB 提供行锁、表锁、共享锁、排他锁等多种类型,结合间隙锁和临键锁防止幻读。锁机制确保同一时间只有符合规则的事务能修改数据,避免并发写入导致的数据混乱,同时通过死锁检测和超时机制减少长期阻塞,平衡一致性与并发效率。
- MVCC版本控制:MVCC 是 InnoDB 实现高并发读的核心技术,通过保存数据的历史版本(借助 undo log 和隐藏列记录修改轨迹),让读操作无需加锁即可访问历史快照。事务启动时生成读视图(Read View),依据活跃事务 ID 判断数据可见性,实现 “读写不阻塞”,既支持可重复读隔离级别下的一致读,又大幅提升了读操作的并发性能。
- 隔离级别:隔离级别是数据库定义的并发事务间数据可见性规则,通过限制事务对其他未提交 / 已提交数据的读取权限,平衡一致性与并发性能。从低到高分为读未提交、读已提交、可重复读、串行化,级别越高越能避免脏读、不可重复读、幻读等问题,但并发效率越低,是并发控制的基础规则框架
事务的隔离级别的定义以及如何实现的?
- 读未提交:允许事务读取其他事务未提交的修改,无特殊隔离控制,事务读取数据时直接访问最新版本,不等待其他事务提交,也不生成稳定的 Read View,几乎无并发控制开销。
- 读已提交:允许事务读取其他事务已经提交的修改,每次执行查询时生成新的 Read View(读视图),通过 MVCC 机制过滤未提交事务的修改(依据
DB_TRX_ID
判断可见性),确保只能看到已提交数据。 - 可重复读:保证事务内多次读取同一数据结果一致,事务启动时生成一次 Read View 并全程复用,通过 MVCC 回溯历史版本确保重复读一致;同时通过间隙锁(Gap Lock)和临键锁(Next-Key Lock) 锁定数据间隙,防止其他事务插入新数据,避免幻读。
- 串行化:会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;通过表级锁强制事务排队执行:读操作加共享锁(S 锁),写操作加排他锁(X 锁),同一时间仅允许一个事务修改数据,彻底消除并发冲突。
Mysql 设置了可重复读隔离级后,怎么保证不发生幻读?
使用for update语句锁定。
当执行范围查询或非唯一索引查询时,通过间隙锁(锁定数据空白区间)和临键锁(记录锁 + 间隙锁组合)锁定查询涉及的所有数据间隙,从物理上阻止其他事务插入符合查询条件的新数据,避免因新增数据导致两次查询结果行数不一致的幻读现象(唯一索引精确匹配时会降级为记录锁,无需间隙锁,此时无法防止幻读发生)。
举个例子说可重复读下的幻读问题
在 MySQL 可重复读(RR)隔离级别下,当使用唯一索引精确匹配加锁时,可能因缺乏间隙锁保护而出现幻读,以下是具体例子:
假设表 user
有唯一索引 id
,初始数据为 id=10、20
。事务 A 启动后,先执行 SELECT * FROM user WHERE id = 10 FOR UPDATE
(加记录锁锁定 id=10
),接着查询 SELECT * FROM user WHERE id BETWEEN 5 AND 15
,结果为 id=10
(1 行)。此时事务 B 插入 id=12
的新记录(因 id=10
的记录锁不覆盖 (10, 20)
间隙,插入成功)。随后事务 A 再次执行相同的范围查询 id BETWEEN 5 AND 15
,会发现结果新增了 id=12
(变为 2 行),两次查询行数不一致,即发生了幻读 —— 这正是由于唯一索引精确匹配仅加记录锁,无法阻止间隙中插入新数据导致的。
串行化隔离级别是通过什么实现的?
串行化(Serializable)隔离级别通过强制事务完全串行执行实现,核心机制是对读写操作施加严格的表级锁或行级锁:读操作会对涉及的数据加共享锁(S 锁),写操作加排他锁(X 锁),且同一时间只允许一个事务持有排他锁,其他事务需等待锁释放后才能执行读写操作。这种 “排队执行” 的方式彻底消除了并发冲突,确保事务如同依次执行般无干扰,从而避免脏读、不可重复读和幻读等所有并发问题,但会显著降低并发性能。
介绍一下MVCC的原理
MVCC 的核心组成
MVCC 的实现依赖于三个关键组件:隐藏列、Undo Log(回滚日志) 和 Read View(读视图)。
1.隐藏列:记录数据版本的元信息
InnoDB 会为表中的每一行数据自动添加三个隐藏列(用户不可见,由引擎维护),用于标记数据的版本和历史关联:
- DB_TRX_ID(事务 ID):记录最后一次修改该行数据的事务 ID(每次事务启动时,InnoDB 会分配一个全局唯一的递增事务 ID)。
- DB_ROLL_PTR(回滚指针):指向该行数据的上一个历史版本(存储在 Undo Log 中),形成一条 “版本链”。
- DB_ROW_ID(隐藏主键):若表没有显式定义主键,InnoDB 会自动生成该列作为隐含主键,用于唯一标识行记录。
2.Undo Log:存储历史版本的 “时光机”
Undo Log(回滚日志)是记录数据修改前历史版本的日志文件。当事务修改数据时,InnoDB 会先将数据的旧版本写入 Undo Log,再更新当前行的数据。
- 版本链的形成:每次数据被修改时,旧版本会被存入 Undo Log,同时当前行的
DB_ROLL_PTR
会指向 Undo Log 中的旧版本,形成一条串联的 “版本链”。例如:- 事务 10 修改了行数据,旧版本存入 Undo Log,当前行
DB_TRX_ID=10
,DB_ROLL_PTR
指向旧版本。 - 事务 20 再次修改该行,新版本的
DB_TRX_ID=20
,DB_ROLL_PTR
指向事务 10 生成的版本,以此类推。
- 事务 10 修改了行数据,旧版本存入 Undo Log,当前行
- Undo Log 的类型:
INSERT Undo
:记录插入操作的旧版本(仅包含新插入的行),事务提交后可直接删除(因插入的行无历史版本关联)。UPDATE/DELETE Undo
:记录更新 / 删除操作的旧版本,需长期保留供 MVCC 快照读使用,直到不再被任何 Read View 引用时,由后台 Purge 线程清理。
3.Read View:判断版本可见性的 “规则”
Read View(读视图)是事务在执行快照读(普通 SELECT
语句)时生成的一个 “可见性规则集合”,用于判断版本链中的哪个历史版本对当前事务可见。
Read View 包含四个核心参数:
- m_ids:生成 Read View 时,当前所有活跃事务的 ID 列表(未提交的事务)。
- min_trx_id:
m_ids
中的最小事务 ID(当前活跃事务的最小 ID)。 - max_trx_id:生成 Read View 时,下一个即将分配的事务 ID(可理解为 “当前最大事务 ID + 1”)。
- creator_trx_id:生成该 Read View 的事务自身的 ID。
MVCC 的核心规则:版本可见性判断
事务在读取数据时,会通过 Read View 检查版本链中的每个历史版本是否 “可见”,判断规则如下:
- 若版本的
DB_TRX_ID
(修改该版本的事务 ID)等于creator_trx_id
:当前事务修改的版本,可见。 - 若版本的
DB_TRX_ID < min_trx_id
:修改该版本的事务在当前事务启动前已提交,可见。 - 若版本的
DB_TRX_ID > max_trx_id
:修改该版本的事务在当前 Read View 生成后才启动,不可见。 - 若min_trx_id ≤ DB_TRX_ID ≤ max_trx_id:
- 若
DB_TRX_ID
在m_ids
中(事务仍活跃未提交):不可见。 - 若
DB_TRX_ID
不在m_ids
中(事务已提交):可见。
- 若
若当前版本不可见,则通过 DB_ROLL_PTR
回溯版本链,检查上一个历史版本,直到找到可见版本或版本链结束(返回空)。
具体例子(对应数据库事务)
步骤 1:初始文档(版本 0)
- 文档最初由管理员创建,内容是
name='初始值'
。 - 此时没有未提交的修改,所有人都能看到这个版本。
步骤 2:同事 A 修改(事务 100,已提交)
- A 将内容改为
name='A修改后'
,并点击 “提交”。 - 系统生成新版本 1,所有人刷新后都能看到
name='A修改后'
。
步骤 3:同事 B 修改(事务 200,已提交)
- B 将内容改为
name='B修改后'
,并点击 “提交”。 - 系统生成新版本 2,所有人刷新后看到
name='B修改后'
。
步骤 4:同事 C 修改(事务 300,未提交)
- C 将内容改为
name='C修改后'
,但未提交(编辑框中显示修改,但其他人看不到)。 - 此时文档的 “正式版本” 仍是 B 修改后的内容(版本 2)。
步骤 5:同事 D 删除(事务 400,未提交)
- D 看到 C 的草稿(
name='C修改后'
),点击 “删除整行”,但未提交(删除操作仅存在于 D 的编辑框中)。 - 此时文档的 “正式版本” 依然是 B 修改后的内容(版本 2)。
此时你打开文档(事务 500)
你在 步骤 5 之后 打开文档,系统会给你一个 “时间点快照”,规则是:
- 你只能看到在你打开前已提交的修改,看不到任何人的未提交草稿。
具体判断过程:
- 系统检查当前最新版本 → 发现是 D 的删除操作(版本 4),但 D 未提交 → 这个删除对你无效(相当于 “D 的草稿没保存,你看不到”)。
- 系统回溯到上一版本 → 是 C 的修改(版本 3),但 C 未提交 → 这个修改对你无效(相当于 “C 的草稿没保存”)。
- 系统继续回溯 → 是 B 的修改(版本 2),且 B 已提交 → 这个版本对你可见。
最终结果:你看到的内容是 name='B修改后'
,完全不受 C 和 D 的未提交操作影响。
一条update语句是不是原子性的?
是原子性,主要通过锁+undolog 日志保证原子性的
执行 update 的时候,会加行级别锁,保证了一个事务更新一条记录的时候,不会被其他事务干扰。
事务执行过程中,会生成 undolog,如果事务执行失败,就可以通过 undolog 日志进行回滚
滥用事务,或者一个事务里有特别多sql的弊端?
锁竞争加剧
事务长且 SQL 多会延长锁持有时间,导致其他事务阻塞、锁等待队列变长,死锁概率升高,并发性能暴跌。
连接池耗尽
长事务独占数据库连接时间过长,导致其他请求无法获取连接,引发业务卡顿或超时失败。
日志与回滚成本激增
大量 SQL 产生海量 redo/undo log,占用磁盘和 IO 资源;若事务失败,回滚操作复杂耗时,可能导致系统暂时无响应。
超时与失败风险升高
执行时间越长,遭遇网络波动、宕机等异常的概率越高,易因超时而被强制终止,数据恢复成本大。
拖慢查询性能
未提交的长事务导致 undo log 膨胀,查询时需遍历更长版本链,同时占用磁盘空间,降低查询效率。
讲一下Mysql里有哪些锁?
1. 全局锁(Global Lock)
全局锁是对整个数据库实例加锁的机制,加锁后整个库处于只读状态,所有更新操作(增删改、DDL、事务提交等)都会被阻塞。
- 作用:保证全库数据的一致性,常用于全库逻辑备份(如
mysqldump
加--single-transaction
可替代全局锁,但 MyISAM 等不支持事务的引擎仍需全局锁)。 - 加锁方式:通过
FLUSH TABLES WITH READ LOCK (FTWRL)
命令加锁,解锁需执行UNLOCK TABLES
或断开连接。 - 注意:全局锁会阻塞所有业务更新,长时间持有会严重影响业务,因此 InnoDB 更推荐用事务快照(
--single-transaction
)替代。
2. 表级锁(Table-Level Lock)
表级锁是对整个表加锁的机制,粒度比全局锁小,比行级锁大,适用于并发要求不高的场景(如 MyISAM 引擎)。MySQL 中的表级锁主要包括以下几种:
(1)表锁(Table Lock)
最基本的表级锁,分为读锁(共享锁,Read Lock) 和写锁(排他锁,Write Lock),通过LOCK TABLES
手动加锁,UNLOCK TABLES
解锁。
- 读锁(Read Lock):
加锁后,当前事务可读取表,但不能修改;其他事务也可加读锁读取,但不能加写锁修改(会阻塞)。 - 写锁(Write Lock):
加锁后,当前事务可读写表;其他事务既不能加读锁也不能加写锁(全部阻塞)。 - 互斥规则:读锁与读锁兼容,读锁与写锁互斥,写锁与写锁互斥。
- 适用场景:MyISAM 引擎默认使用表锁(因不支持行锁);InnoDB 中一般不手动用表锁,除非需显式控制全表操作。
(2)元数据锁(Metadata Lock,MDL)
MySQL 5.5 引入的表级锁,自动加锁,无需手动干预,用于保证表元数据(表结构)的一致性。
- 作用:防止事务执行期间表结构被修改(如 ALTER TABLE),避免 “事务看到的表结构与实际操作不一致” 的问题。
- 加锁规则:
- 事务对表执行读操作(如 SELECT)时,自动加MDL 读锁(SHARED_READ);
- 事务对表执行写操作(如 INSERT/UPDATE/DELETE)时,自动加MDL 写锁(SHARED_WRITE);
- 执行DDL 操作(如 ALTER TABLE)时,需加MDL 排他锁(EXCLUSIVE)。
- 互斥规则:读锁之间兼容,读写锁互斥,写锁与排他锁互斥,排他锁之间互斥。
- 常见问题:长事务持有 MDL 读锁时,后续 DDL 会被阻塞,且新的读操作也会排队等待,导致 “表卡死”。需避免长事务,或快速提交事务释放 MDL 锁。
(3)意向锁(Intention Lock)
InnoDB 为协调表锁与行锁引入的表级锁,用于 “提前声明事务对表中行的锁类型”,减少表锁检查的开销。
- 类型:
- 意向读锁(Intention Shared Lock,IS):事务计划对表中的某些行加行级读锁(S 锁)前,需先加 IS 锁;
- 意向写锁(Intention Exclusive Lock,IX):事务计划对表中的某些行加行级写锁(X 锁)前,需先加 IX 锁。
- 互斥规则:
- 意向锁之间兼容(IS 与 IS、IS 与 IX、IX 与 IX 均兼容);
- 意向锁与表锁的互斥:IS 与表读锁(Read Lock)兼容,与表写锁(Write Lock)互斥;IX 与表读锁和表写锁均互斥。
- 本质:意向锁是 “标记”,告诉其他事务 “我要对表中的行加锁了”,避免表锁检查时需逐行判断是否有行锁,提高效率。
(4)自增锁(AUTO-INC Lock)
针对表中自增列(AUTO_INCREMENT) 的表级锁,用于保证自增值的唯一性和连续性。
- 作用:当插入语句涉及自增列时,MySQL 需为自增列分配唯一值,自增锁确保同一时间只有一个事务能获取自增值,避免重复。
- 锁模式:由innodb_autoinc_lock_mode参数控制(默认值为 1):
0
:传统模式,插入时加全表自增锁,直到语句结束释放,并发低;1
:连续模式(默认),简单插入(如单条 INSERT)不锁全表,预分配连续自增值后立即释放锁;批量插入(如 INSERT … SELECT)仍需锁至语句结束,平衡并发与连续性;2
:交错模式,完全不锁,自增值可能不连续(如事务回滚),但并发最高。
3. 行级锁(Row-Level Lock)
InnoDB 特有的锁机制,粒度最小,仅对表中特定行加锁,大幅提高并发性能,是 InnoDB 支持高并发的核心原因。行级锁基于索引实现,若查询未命中索引,会退化为表锁。
(1)共享锁与排他锁(S 锁与 X 锁)
行级锁的基础模式,控制行的读写权限:
- 共享锁(S 锁,读锁):
事务对某行加 S 锁后,可读取该行,其他事务可加 S 锁读取,但不能加 X 锁修改(会阻塞)。
加锁方式:SELECT ... FOR SHARE
(MySQL 8.0 后,替代SELECT ... LOCK IN SHARE MODE
)。 - 排他锁(X 锁,写锁):
事务对某行加 X 锁后,可读写该行,其他事务既不能加 S 锁也不能加 X 锁(全部阻塞)。
加锁方式:SELECT ... FOR UPDATE
;InnoDB 对INSERT/UPDATE/DELETE
语句会自动加 X 锁。 - 互斥规则:S 与 S 兼容,S 与 X 互斥,X 与 X 互斥。
(2)记录锁(Record Lock)
最基础的行级锁,直接锁住具体的行记录,防止其他事务修改或删除该行。
- 作用:保证行记录的一致性,避免并发更新冲突。
- 示例:
UPDATE t SET name='a' WHERE id=10;
会对id=10
的行加 X 类型的记录锁。 - 基于索引:记录锁依赖索引,若 WHERE 条件未命中索引(如
UPDATE t SET name='a' WHERE name='b'
且 name 无索引),会扫描全表并对所有行加记录锁(等效表锁),需避免。
(3)间隙锁(Gap Lock)
锁住索引记录之间的间隙(不包括记录本身),防止其他事务在间隙中插入新记录,主要用于解决幻读问题(RR 隔离级别下生效)。
- 作用:阻止间隙插入,避免事务重复读取到新插入的 “幻影行”。
- 间隙定义:索引记录之间的区间,如表中存在
id=10、20
的记录,则间隙包括(-∞,10)、(10,20)、(20,+∞)
。 - 示例:事务 A 执行
SELECT * FROM t WHERE id BETWEEN 10 AND 20 FOR UPDATE
,会对(10,20)
间隙加 X 类型的间隙锁,此时事务 B 插入id=15
的记录会被阻塞。
(4)临键锁(Next-Key Lock)
InnoDB 在RR 隔离级别下的默认行锁机制,是记录锁 + 间隙锁的组合,锁住 “记录本身 + 前面的间隙”,范围为 “左开右闭”。
- 作用:同时防止行修改和间隙插入,彻底解决幻读问题。
- 范围规则:对索引
idx
的查询,临键锁的范围是 “上一个索引记录的间隙到当前记录”。例如表中id=10、20
,查询id>10 AND id<=20
时,临键锁范围是(10,20]
(锁住 10-20 的间隙及 20 本身)。
(5)插入意向锁(Insert Intention Lock)
一种特殊的间隙锁,用于插入操作,表示事务 “想要在某个间隙中插入记录”,但不会阻塞其他事务在同一间隙的不同位置插入。
- 作用:解决插入冲突,允许并发插入。例如表中
id=10、20
,事务 A 插入id=15
,事务 B 插入id=16
,两者的插入意向锁兼容,可同时执行;若两者都插入id=15
,则会因主键冲突阻塞。 - 加锁时机:插入语句执行时,先在目标间隙加插入意向锁,成功后再插入记录并加记录锁。
表锁和行锁有什么用?
-
表锁
- 整体控制:表锁可以用来控制整个表的并发访问,当一个事务获取了表锁时,其他事务无法对该表进行任何读写操作,从而确保数据的完整性和一致性。
- 粒度大:表锁的粒度比较大,在锁定表的情况下,可能会影响到整个表的其他操作,可能会引起锁竞争和性能问题。
- 适用于大批量操作:表锁适合于需要大批量操作表中数据的场景,例如表的重建、大量数据的加载等。
-
行锁
- 细粒度控制:行锁可以精确控制对表中某行数据的访问,使得其他事务可以同时访问表中的其他行数据,在并发量大的系统中能够提高并发性能。
- 减少锁冲突:行锁不会像表锁那样造成整个表的锁冲突,减少了锁竞争的可能性,提高了并发访问的效率。
- 适用于频繁单行操作:行锁适合于需要频繁对表中单独行进行操作的场景,例如订单系统中的订单修改、删除等操作。
MySQL两个线程的update语句同时处理一条数据,会不会有阻塞?
InnoDB 使用行锁机制,当两个线程的 UPDATE 同时处理同一行数据时,会因排他锁(X 锁)互斥而阻塞。第一个线程自动获取该行的 X 锁,第二个线程需等待锁释放(提交 / 回滚),超时(默认 50 秒)则报错。
两条update语句处理一张表的不同的主键范围的记录,会不会遇到阻塞?底层是为什么的?
- READ COMMITTED(RC)隔离级别
-
锁机制:仅对被更新的记录加 记录锁(Record Lock),不使用间隙锁。
-
示例:
-- 线程 1:锁定 id=1~9 的记录,插入2.5,5.6不会阻塞,进阻塞表中有的记录,例如插入1,2,3....9。 UPDATE users SET balance = 0 WHERE id < 10; -- 线程 2:锁定 id=16~∞ 的记录(假设表中最大 id=20) UPDATE users SET balance = 100 WHERE id > 15;
- 结果:两个范围无交集,锁不冲突,不会阻塞。
- REPEATABLE READ(RR)隔离级别
-
锁机制:使用 临键锁(Next-Key Lock),锁定索引记录 + 间隙,防止幻读。
-
示例:假设表中存在
id=5、10、20
三条记录:-- 线程 1:锁定 (-∞,5]、(5,10] 两个临键区间,插入2.3,5.6会阻塞 UPDATE users SET balance = 0 WHERE id < 10; -- 线程 2:锁定 (10,20]、(20,+∞) 两个临键区间 UPDATE users SET balance = 100 WHERE id > 15;
- 结果:范围不冲突,不会阻塞。
-
关键细节:
若表中不存在id=10~15
之间的记录,线程 1 的间隙锁可能延伸至(10,+∞)
,导致线程 2 完全被阻塞。
- 未命中索引
-
锁机制:全表扫描,对所有记录加锁(等效于表锁)。
-
示例:
-- 假设 name 字段无索引 UPDATE users SET age = 20 WHERE name LIKE 'A%'; -- 全表锁 -- 任何其他 UPDATE 都会被阻塞 UPDATE users SET balance = 0 WHERE id < 10;
日志文件分成了哪几种?
- 错误日志:记录 MySQL 服务器启动、运行和关闭过程中的错误信息、警告和关键状态。
- 二进制日志:记录所有更改数据的 SQL 语句(如 INSERT、UPDATE、DELETE),用于主从复制和数据恢复。
- 慢查询日志:记录执行时间超过阈值(
long_query_time
,默认 10 秒)的 SQL 语句,用于性能优化。 - 通用查询日志:记录所有客户端连接和 SQL 语句(包括查询和修改),用于调试。
- 中继日志:在从库上记录从主库接收的 binlog 事件,用于从库执行复制。
- Redo Log:确保事务的持久性(Durability),记录事务对数据页的物理修改,用于崩溃恢复。
- Undo Log:确保事务的原子性(Atomicity)**和**MVCC(多版本并发控制),记录事务修改前的数据版本。
讲一下binlog
它以二进制格式记录了数据库中所有数据修改操作(如 INSERT
、UPDATE
、DELETE
、CREATE
、ALTER
等),但不记录查询操作(如 SELECT
、SHOW
)。它的核心作用是数据恢复和主从复制,是 MySQL 高可用和数据一致性保障的关键组件。
Binlog 的工作原理
- 日志写入时机
当 MySQL 执行数据修改操作时,会先将操作记录到内存中的 Binlog 缓存(binlog_cache_size
控制大小),事务提交时再将缓存中的日志写入磁盘的 Binlog 文件(刷盘策略由sync_binlog
控制)。 - 日志文件管理
Binlog 以多个文件的形式存储(如binlog.000001
、binlog.000002
),每个文件达到max_binlog_size
上限后会自动切换到新文件。同时,MySQL 会生成一个索引文件(binlog.index
)记录所有 Binlog 文件的列表,方便管理和读取。 - 日志刷盘策略
sync_binlog=0
:依赖操作系统缓存,不主动刷盘,性能最高但宕机可能丢失日志。sync_binlog=1
:每次事务提交后立即刷盘,安全性最高(不丢失日志),但性能开销较大。sync_binlog=N
:每 N 个事务刷一次盘,平衡性能和安全性(推荐生产环境设置为 1 或较小值)。
binlog的三种格式类型:
**STATEMENT:**每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
**ROW:**记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;
**MIXED:**包含了 STATEMENT 和 ROW 模式,它会根据不同的情况自动使用 ROW 模式和 STATEMENT 模式;
Undo Log日志的作用是什么?
UndoLog(回滚日志) 是保证事务原子性、一致性和实现多版本并发控制(MVCC)的核心机制之一。它记录了事务执行过程中对数据的修改操作的反向操作,用于在事务异常终止或需要回滚时,将数据恢复到修改前的状态。
三种操作状态下的undo log记录
- 插入操作:记录该记录的删除操作,用于回滚时撤销插入。
- 更新操作:记录被删除记录的完整数据(包括主键和所有字段),用于回滚时恢复。
- 删除操作:记录被修改字段的原始值,用于回滚时恢复。
Redo Log日志的作用是什么?
1. 保证事务的持久性(Durability)
- 问题:若没有 Redo Log,当事务提交后(如写入内存中的 Buffer Pool),数据库崩溃,内存中的修改会丢失。
- 解决方案:Redo Log 记录物理修改(如 “将数据页 X 的偏移量 Y 处的值改为 Z”),崩溃恢复时,通过 Redo Log 将未刷盘的修改重做到磁盘,确保已提交事务的数据不丢失。
2. 实现高效的刷盘策略
- 延迟写机制:数据库为提升性能,不会每次事务提交都立即将数据写入磁盘(随机 IO 慢),而是先写入 Redo Log(顺序 IO 快),再由后台线程(如 Checkpoint)异步将脏页(Dirty Page)刷盘。
- Redo Log 的作用:即使脏页未刷盘,只要 Redo Log 已持久化,事务就被认为已提交。崩溃恢复时,通过 Redo Log 重放未刷盘的修改。
3. 支持组提交(Group Commit)
- 多个事务的 Redo Log 可以批量写入磁盘,减少 IO 次数,提升吞吐量。例如,事务 T1、T2、T3 先后提交,它们的 Redo Log 可以合并一次写入磁盘。
Redo Log是怎么保持持久性的?
**Write-ahead logging(WAL):**在事务提交之前,将事务所做的修改操作记录到redo log中,然后再将数据写入磁盘。这样即使在数据写入磁盘之前发生了宕机,系统可以通过redo log中的记录来恢复数据。
**Redo log的顺序写入:**redo log采用追加写入的方式,将redo日志记录追加到文件末尾,而不是随机写入。这样可以减少磁盘的随机I/O操作,提高写入性能。
**检查点(Checkpoint)😗*减少崩溃恢复的时间(无需扫描整个 Redo Log)。数据库定期将 Buffer Pool 中的脏页(已修改但未刷盘)刷盘,并在 Redo Log 中记录 Checkpoint 位置。崩溃恢复时,只需从 Checkpoint 位置开始重做,而非整个日志。
能不能只用binlog不用relo log?
为什么必须同时存在?
场景 | Redo Log 的不可替代性 | Binlog 的局限性 |
---|---|---|
崩溃恢复 | 物理日志 + 快速重做,毫秒级恢复脏页 | 逻辑日志 + 全量重放,耗时极长且可能有副作用 |
事务持久性 | 预写日志 + 顺序刷盘,确保提交即持久 | 事后记录,无法关联内存修改与磁盘状态 |
性能优化 | 顺序 IO 替代随机 IO,支撑高并发写入 | 依赖随机刷数据页,性能暴跌 |
事务完整性 | 与 Undo Log 配合实现 ACID | 不参与存储引擎层事务状态管理,无法保证原子性 / 一致性 |
binlog 两阶段提交过程是怎么样的?
两阶段提交(Two-Phase Commit, 2PC) 是保证Redo Log(存储引擎层) 和Binlog(服务器层) 逻辑一致性的核心机制。由于两者分属不同组件(Redo Log 由 InnoDB 管理,Binlog 由 MySQL Server 管理),必须通过两阶段提交确保:一个事务的修改要么在两种日志中都完整记录,要么都不记录,避免出现 “Redo Log 已提交但 Binlog 缺失” 或反之的不一致问题(这会导致主从同步异常或崩溃恢复后数据错乱)。
具体步骤
阶段 1:Prepare 阶段(Redo Log 预提交)
- 事务执行完成,进入提交流程:
事务的所有 SQL 操作已执行完毕(数据修改已写入内存 Buffer Pool,脏页生成),Redo Log 已在内存中记录了该事务的所有物理修改(但未持久化到磁盘 Redo Log 文件)。 - 写入 Redo Log 并标记为 “Prepare 状态”:
- InnoDB 将该事务的 Redo Log 从内存刷新到磁盘(确保 Redo Log 持久化,通过
innodb_flush_log_at_trx_commit
控制刷盘策略,默认 1 = 每次提交刷盘)。 - 此时 Redo Log 中该事务的状态标记为Prepare(表示 “事务已准备提交,但需等待 Binlog 确认”)。
- 同时,InnoDB 会生成该事务的XID(全局唯一事务 ID),用于关联 Redo Log 和 Binlog。
- InnoDB 将该事务的 Redo Log 从内存刷新到磁盘(确保 Redo Log 持久化,通过
阶段 2:Commit 阶段(Binlog 写入 + Redo Log 最终提交)
- 写入并持久化 Binlog:
- MySQL Server 层获取该事务的 XID,将事务的逻辑操作(如 SQL 语句或行级变更)写入 Binlog,并刷新到磁盘(通过
sync_binlog
控制刷盘策略,默认 0 = 依赖 OS 缓存,1 = 每次提交刷盘)。 - Binlog 中会记录该事务的 XID,确保与 Redo Log 中的 XID 对应。
- MySQL Server 层获取该事务的 XID,将事务的逻辑操作(如 SQL 语句或行级变更)写入 Binlog,并刷新到磁盘(通过
- Redo Log 标记为 “Commit 状态”:
- 当 Binlog 成功写入并持久化后,MySQL Server 通知 InnoDB:“Binlog 已完成”。
- InnoDB 将 Redo Log 中该事务的状态从Prepare更新为Commit,并将这个状态变更刷盘(此操作极快,仅修改 Redo Log 中的事务状态标记,无需重写完整日志)。
- 事务提交完成:
- 此时,Redo Log 和 Binlog 均已完整记录该事务,事务正式提交,内存中对应的脏页可等待后台线程异步刷盘。
崩溃恢复时的一致性保障
场景 1:崩溃发生在 Prepare 阶段之后、Binlog 写入之前
-
现象:Redo Log 已持久化(状态为 Prepare),但 Binlog 中无该事务记录。
-
恢复逻辑:
重启后,InnoDB 扫描 Redo Log,发现状态为 Prepare 的事务。此时需要查询 Binlog 中是否存在该事务的 XID:
- 若 Binlog 中无对应 XID(说明 Binlog 未写入),则 InnoDB 回滚该事务(通过 Undo Log 撤销修改),确保 “Redo Log 和 Binlog 均无该事务”。
场景 2:崩溃发生在 Binlog 写入之后、Redo Log Commit 之前
-
现象:Binlog 已持久化(含 XID),但 Redo Log 中事务状态仍为 Prepare。
-
恢复逻辑:
重启后,InnoDB 扫描到 Redo Log 中状态为 Prepare 的事务,查询 Binlog 发现对应 XID 已存在(说明 Binlog 已完成):
- InnoDB 会自动将 Redo Log 中该事务的状态从 Prepare 更新为 Commit,相当于 “补全提交”,确保 “Redo Log 和 Binlog 均有该事务”。
update语句的具体执行过程是怎样的?
一、执行器(Executor)阶段
- 调用存储引擎 API
执行器根据优化器生成的执行计划,调用存储引擎(如 InnoDB)的接口逐行处理符合条件的记录。 - 定位记录
通过索引(若有)或全表扫描定位WHERE
子句匹配的记录,调用存储引擎的read_record()
方法读取记录到内存。 - 加锁(若需要)
根据事务隔离级别对记录加锁(如 RR 隔离级别下加行锁),防止其他事务并发修改。 - 计算新值
在内存中执行SET
子句的表达式计算(如age = age + 1
)。 - 修改记录
更新内存中记录的字段值。
二、日志记录阶段
- 生成 Undo Log
存储引擎生成 Undo Log,记录修改前的值(如age = OLD_AGE
),用于事务回滚。 - Redo Log 预提交(Prepare)
- 将修改写入 Redo Log Buffer,并标记为Prepare 状态(表示事务已准备提交,但需等待 Binlog 确认)。
- Redo Log 记录物理修改(如 “数据页 X 的偏移量 Y 处的值从 A 改为 B”)。
- 写入 Binlog
- MySQL Server 层将
UPDATE
的逻辑操作(如 “UPDATE users SET age=age+1 WHERE …”)写入 Binlog。 - 刷盘(取决于
sync_binlog
参数,默认 1 = 每次提交刷盘)。
- MySQL Server 层将
- Redo Log 最终提交(Commit)
- 当 Binlog 写入完成后,将 Redo Log 中的事务状态从 Prepare 更新为 Commit,并刷盘。
三、提交与持久化阶段
- 事务提交确认
Redo Log 和 Binlog 均写入完成后,事务正式提交。 - 更新内存数据页
将修改应用到 Buffer Pool 中的数据页,使其变为脏页(Dirty Page)。 - 异步刷盘
后台线程(如 Page Cleaner)在合适时机(如系统空闲、Checkpoint 时)将脏页刷新到磁盘。
四、索引与结果处理阶段
- 索引更新(若需要)
如果更新的字段是索引列,存储引擎会更新对应 B + 树索引(删除旧值,插入新值,可能触发页分裂)。 - 返回结果
执行器向客户端返回更新成功的消息及受影响行数。
分库分表是什么?有什么区别?
一、分库与分表的区别
维度 | 分库(Database Sharding) | 分表(Table Sharding) |
---|---|---|
拆分对象 | 对数据库实例进行拆分,将数据分散到多个独立数据库中。 | 对单张数据表进行拆分,将数据分散到多张结构相同的子表中。 |
解决的问题 | 缓解单库的 CPU、内存、IO 资源瓶颈,降低数据库实例负载。 | 解决单表数据量过大导致的查询缓慢(如索引失效、扫描耗时)。 |
核心目标 | 分散数据库级别的资源压力,提高系统整体并发能力。 | 减小单表数据规模,优化查询和写入效率。 |
典型场景 | 高并发读写场景(如电商订单库按区域拆分)。 | 历史数据沉淀场景(如用户行为表按时间拆分)。 |
二、分库分表的常见拆分方式
- 分库的拆分方式
- 水平分库:按数据行拆分,将同一类数据(如用户数据)按规则(如用户 ID 哈希)分散到多个库,每个库结构相同。
- 垂直分库:按业务模块拆分,将不同业务数据(如用户库、订单库、商品库)分离到独立数据库,降低耦合。
- 分表的拆分方式
- 水平分表:按数据行拆分,将单表数据按规则(如时间范围、ID 范围)拆分到多张子表,子表结构完全一致。
- 垂直分表:按列拆分,将表中不常用或大字段(如 text、blob)拆分到独立子表,减少主表数据量,提升查询效率。
mysql主从复制了解吗?
核心流程如下:
- 主库记录 Binlog:主库将所有对数据的修改操作(如 INSERT/UPDATE/DELETE)记录到二进制日志(Binlog)中。
- 从库同步 Binlog:从库通过 IO 线程连接主库,请求获取 Binlog 内容。
- 主库发送 Binlog:主库将 Binlog 事件(Events)发送给从库的 IO 线程。
- 从库写入中继日志(Relay Log):从库 IO 线程将接收到的 Binlog 写入本地的中继日志(Relay Log)。
- 从库应用 Relay Log:从库 SQL 线程读取 Relay Log,并在本地执行其中的 SQL 语句,实现数据同步。
Mysql两次写了解吗?
工作原理:
- 写双写缓冲区:
当 InnoDB 要将脏页(内存中已修改的数据页)刷新到磁盘时,不会直接写入数据文件,而是先将数据页写入到共享表空间(ibdata)中的双写缓冲区(连续存储,顺序 IO)。双写缓冲区默认大小为 2MB,可容纳多个数据页。 - 写入数据文件:
双写缓冲区写入完成后,InnoDB 再将数据页异步写入对应的表空间文件(可能是随机 IO)。 - 确认与清理:
若数据页成功写入表空间文件,双写缓冲区中的对应页会被标记为可覆盖,后续新的脏页可复用该空间。