MySQL 面试题集
总题数: 60道 | 重点领域: 索引、事务、锁机制 | 难度分布: 中高级
本文档整理了 MySQL 数据库的核心面试题目,涵盖索引原理、事务特性、锁机制、性能优化等各个方面。
面试题目列表
1. MySQL的存储引擎有哪些?它们有什么区别?
MySQL支持多种存储引擎,允许针对不同的应用场景选择最适合的存储引擎。主要的存储引擎包括:
InnoDB(默认存储引擎):
- 特点:
- 支持事务,遵循ACID特性
- 支持行级锁,提高并发性能
- 支持外键约束
- 实现了四种隔离级别
- 采用MVCC(多版本并发控制)来支持高并发
- 使用聚簇索引存储数据
- 适用场景:
- 需要事务支持的应用
- 需要外键约束的应用
- 高并发、数据一致性要求高的场景
MyISAM:
- 特点:
- 不支持事务
- 表级锁定,并发性能较差
- 不支持外键
- 更简单的表结构
- 拥有较高的插入和查询速度
- 支持全文索引
- 适用场景:
- 读密集型应用(如Web应用的只读数据)
- 需要全文索引的应用
- 不需要事务支持的简单应用
Memory(HEAP):
- 特点:
- 数据存储在内存中,速度极快
- 重启后数据丢失
- 表级锁
- 不支持BLOB和TEXT类型
- 适用场景:
- 临时表和中间结果的存储
- 需要快速访问且无需持久化的数据
Archive:
- 特点:
- 高度压缩存储
- 只支持INSERT和SELECT操作
- 不支持索引(除主键外)
- 适用场景:
- 日志和历史数据归档
- 需要高压缩率的数据
CSV:
- 特点:
- 以CSV格式存储数据
- 数据可直接被电子表格软件读取
- 适用场景:
- 数据导入导出
- 与其他应用程序交换数据
Blackhole:
- 特点:
- 接收数据但不存储
- 可用于主从复制或日志收集
- 适用场景:
- 主从复制中的中继
- 数据丢弃但需记录binlog
Federated:
- 特点:
- 访问远程MySQL服务器上的表
- 本地不存储数据
- 适用场景:
- 分布式数据库环境
- 多数据库整合
NDB Cluster:
- 特点:
- 分布式存储引擎
- 高可用性和可扩展性
- 适用场景:
- 高可用性集群环境
对比表:
| 特性 | InnoDB | MyISAM | Memory | Archive |
|---|---|---|---|---|
| 事务支持 | 是 | 否 | 否 | 否 |
| 锁粒度 | 行级锁 | 表级锁 | 表级锁 | 行级锁 |
| 外键支持 | 是 | 否 | 否 | 否 |
| 全文索引 | 支持(5.6+) | 支持 | 否 | 否 |
| 存储限制 | 64TB | 文件系统限制 | 内存大小 | 无 |
| 数据存储 | 聚簇索引 | 独立表 | 内存 | 压缩存储 |
| 性能优势 | 高并发写 | 高速读 | 极速查询 | 高压缩率 |
| B树索引 | 支持 | 支持 | 支持 | 不支持 |
| 哈希索引 | 自适应 | 不支持 | 支持 | 不支持 |
如何选择存储引擎:
- 需要事务和外键:使用InnoDB
- 只读或读多写少:考虑MyISAM
- 临时数据,速度优先:Memory
- 归档数据,压缩优先:Archive
查看和修改存储引擎:
1-- 查看支持的存储引擎2SHOW ENGINES;34-- 查看表的存储引擎5SHOW TABLE STATUS WHERE Name = 'table_name';67-- 创建表时指定存储引擎8CREATE TABLE mytable (id INT) ENGINE = InnoDB;910-- 修改表的存储引擎11ALTER TABLE mytable ENGINE = MyISAM;2. MySQL中的索引类型有哪些?如何选择合适的索引?
MySQL中的索引是提高查询效率的关键机制,根据不同的需求和数据特点,可以选择不同类型的索引。
主要索引类型:
1. 按数据结构分类:
-
B+树索引:
- MySQL中最常用的索引类型
- InnoDB和MyISAM默认使用
- 多路平衡查找树,叶节点存储数据
- 适合范围查询和排序
-
哈希索引:
- 基于哈希表实现,只有精确匹配才有效
- Memory存储引擎默认使用
- InnoDB支持自适应哈希索引
- 等值查询性能极高,但不支持范围查询和排序
-
R树索引:
- 用于空间数据索引(如地理信息)
- MyISAM支持空间索引
- 适用于多维数据的范围查询
-
全文索引:
- 用于全文搜索
- MyISAM和InnoDB(5.6+)支持
- 适用于文本内容的模糊查询
2. 按物理实现分类:
-
聚簇索引(Clustered Index):
- 索引和数据存储在一起
- 每个表只能有一个(通常是主键)
- InnoDB默认使用
- 通过主键访问数据非常快
-
非聚簇索引(Non-Clustered Index):
- 索引和数据分开存储
- 可以创建多个
- 查询需要额外的回表操作
3. 按索引列数分类:
-
单列索引:
- 只包含一个列的索引
- 适合单列查询条件
-
联合索引/复合索引:
- 包含多个列的索引
- 遵循最左前缀原则
- 适合多条件查询和覆盖索引查询
4. 按应用场景分类:
-
普通索引:
- 基本索引类型,无特殊限制
-
唯一索引:
- 要求索引列值唯一
- 可以包含NULL值(MySQL中NULL不等于NULL)
-
主键索引:
- 特殊的唯一索引,不允许NULL
- 用于唯一标识表中的记录
-
前缀索引:
- 对字符串列的前几个字符建立索引
- 减少索引空间占用
-
覆盖索引:
- 查询的列都包含在索引中,无需回表
- 可显著提高查询效率
选择合适索引的原则:
1. 列的选择:
- 在WHERE、JOIN、ORDER BY、GROUP BY子句中频繁出现的列
- 区分度高(基数大)的列优先
- 更新频率低的列优先
- 尽量选择数据类型较小的列
2. 索引类型选择:
- 等值查询多:考虑哈希索引或B+树
- 范围查询多:使用B+树索引
- 文本搜索:使用全文索引
- 多条件查询:考虑复合索引
- 唯一性约束:使用唯一索引或主键
3. 复合索引设计:
- 把区分度高的列放在前面
- 考虑查询条件的顺序
- 遵循最左前缀原则
4. 索引优化策略:
- 覆盖索引:尽量使用索引覆盖查询
- 索引下推:利用索引过滤更多数据
- 前缀索引:对长字符串使用前缀索引
- 避免过度索引:索引会占用空间并影响写性能
5. 常见索引陷阱:
- 在低基数列上建立索引可能无效
- 索引列上使用函数会导致索引失效
- 隐式类型转换会导致索引失效
- Like以通配符开头会导致索引失效
- Or条件可能导致索引失效
实际应用示例:
1-- 创建普通索引2CREATE INDEX idx_name ON users(name);34-- 创建唯一索引5CREATE UNIQUE INDEX idx_email ON users(email);67-- 创建复合索引8CREATE INDEX idx_name_age ON users(name, age);910-- 创建前缀索引11CREATE INDEX idx_address ON users(address(10));1213-- 创建全文索引14CREATE FULLTEXT INDEX idx_content ON articles(content);1516-- 分析索引使用情况17EXPLAIN SELECT * FROM users WHERE name = 'John' AND age > 20;3. 什么是回表查询?什么是覆盖索引?
回表查询和覆盖索引是理解MySQL索引使用效率的两个重要概念,它们与查询执行过程和性能息息相关。
回表查询:
定义: 回表查询是指在使用非聚簇索引(也称为二级索引或辅助索引)进行查询时,需要先通过索引找到对应的主键值,然后再根据主键值到聚簇索引中查找完整的行数据的过程。
产生原因:
- 在InnoDB存储引擎中,非聚簇索引的叶子节点存储的是该索引列的值和对应的主键值
- 而不是完整的行数据(这点与聚簇索引不同)
- 因此需要额外的步骤"回表"查找完整数据
工作流程:
- 通过非聚簇索引定位到对应的叶子节点,获取主键值
- 使用获取到的主键值在聚簇索引中进行第二次查找
- 从聚簇索引中获取完整的行数据
示例:
1-- 假设在name列上有索引,id是主键2SELECT * FROM users WHERE name = 'John';执行过程:
- 通过name索引找到'John'对应的记录,获取主键id值(例如id=10)
- 再通过主键索引查找id=10的完整行数据
- 这个过程就是"回表"
性能影响:
- 回表会增加额外的IO操作
- 大量回表操作会显著降低查询效率
- 特别是在数据量大的表上更为明显
覆盖索引:
定义: 覆盖索引是指查询的所有列都包含在索引中,数据库可以直接从索引中获取所需数据,而无需回表查询的情况。
工作原理:
- 当索引包含查询所需的所有列时,可以直接从索引获取数据
- MySQL可以在查询执行计划中通过"Using index"标识覆盖索引的使用
示例:
1-- 假设有一个联合索引(name, age)2SELECT name, age FROM users WHERE name = 'John';执行过程:
- 通过name索引找到'John'对应的记录
- 由于索引中已包含name和age列,直接返回结果
- 无需回表查询
覆盖索引的优势:
- 减少IO操作,避免回表
- 索引通常比表数据更小,可以减少数据扫描量
- 提高缓存效率,更多索引可以加载到内存
- 显著提升查询性能
如何实现覆盖索引:
- 创建包含查询所需所有列的联合索引
- 调整查询,只选择索引中包含的列
- 合理设计索引以支持频繁查询
1-- 创建覆盖常见查询需求的索引2CREATE INDEX idx_name_age_email ON users(name, age, email);34-- 现在可以覆盖以下查询5SELECT name, age, email FROM users WHERE name = 'John';两者对比:
| 特性 | 回表查询 | 覆盖索引 |
|---|---|---|
| IO次数 | 至少两次(索引+数据) | 一次(仅索引) |
| 性能 | 较慢 | 较快 |
| 索引设计 | 普通单列索引足够 | 需要精心设计包含所需列 |
| 内存占用 | 索引较小 | 索引可能较大 |
| 适用场景 | 通用查询 | 针对特定查询优化 |
| EXPLAIN特征 | NULL(ref列) | Using index |
实际应用中的优化策略:
- 识别频繁查询的列组合,为其创建覆盖索引
- 避免使用SELECT *,只查询必要的列
- 合理使用联合索引以支持更多的覆盖查询
- 针对大表和高频查询优先考虑覆盖索引优化
4. MySQL的事务特性(ACID)是什么?
MySQL事务的ACID特性是关系数据库管理系统保证数据完整性和可靠性的基础。ACID是四个特性的首字母缩写:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。InnoDB存储引擎完全支持ACID特性,而MyISAM等非事务存储引擎则不支持。
1. 原子性(Atomicity):
定义: 事务是不可分割的工作单位,事务中的操作要么全部完成,要么全部不执行。
实现机制:
- 基于重做日志(redo log)和回滚日志(undo log)
- 操作执行前,先写入回滚日志,记录修改前的数据
- 如果事务失败,使用回滚日志恢复到事务开始前的状态
示例:
1START TRANSACTION;2UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;3UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;4COMMIT; -- 两个更新操作同时成功56-- 或者7ROLLBACK; -- 两个更新操作都不执行2. 一致性(Consistency):
定义: 事务执行前后,数据库从一个一致性状态转变为另一个一致性状态,不会破坏数据库的完整性约束。
实现机制:
- 通过完整性约束(如主键、外键、唯一性约束)
- 通过触发器和存储过程中的业务逻辑
- 应用程序层面的业务规则
示例:
1-- 银行转账保持总金额不变2START TRANSACTION;3UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;4UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;5COMMIT; -- 总金额保持不变,一致性得到保障3. 隔离性(Isolation):
定义: 多个事务并发执行时,一个事务的执行不应该被其他事务干扰,就像它们是串行执行一样。
实现机制:
- 通过锁机制和**MVCC(多版本并发控制)**实现
- 不同的隔离级别提供不同程度的隔离性
- READ UNCOMMITTED(读未提交)
- READ COMMITTED(读已提交)
- REPEATABLE READ(可重复读,InnoDB默认)
- SERIALIZABLE(串行化)
示例:
1-- 设置事务隔离级别2SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;34-- 事务A5START TRANSACTION;6SELECT * FROM accounts WHERE user_id = 1; -- 读取时不受其他事务影响7-- 中间其他事务可能修改了数据8SELECT * FROM accounts WHERE user_id = 1; -- 在可重复读级别下,读到的数据与第一次相同9COMMIT;4. 持久性(Durability):
定义: 一旦事务提交,其所做的修改就会永久保存在数据库中,即使系统崩溃也不会丢失。
实现机制:
- 通过**重做日志(redo log)**实现
- 事务提交时,确保所有修改都已写入重做日志
- 系统崩溃后,通过重做日志恢复未写入磁盘的数据
示例:
1START TRANSACTION;2UPDATE important_data SET value = 'new_value' WHERE id = 1;3COMMIT; -- 数据永久保存,即使立即发生服务器崩溃MySQL InnoDB的ACID实现机制:
1. 原子性和持久性实现:
- 重做日志(redo log):记录事务修改的页面数据,用于恢复已提交事务的数据
- 回滚段(undo log):记录数据被修改前的值,用于回滚未提交的事务
- 两阶段提交:先写重做日志,再修改数据页
- 组提交:多个事务的日志一起刷入磁盘,提高性能
2. 一致性实现:
- 强制约束:主键、外键、唯一性约束等
- 回滚机制:当违反约束时回滚事务
- 崩溃恢复:服务器崩溃后自动恢复到一致状态
3. 隔离性实现:
- 锁机制:表锁、行锁、意向锁、间隙锁等
- MVCC:通过回滚段实现的多版本并发控制
- 事务ID和版本号:每个事务有唯一ID,每次修改生成新版本
- 一致性读视图:事务开始时确定可见的版本范围
事务相关配置:
1-- 查看当前会话隔离级别2SELECT @@transaction_isolation;34-- 查看自动提交状态5SELECT @@autocommit;67-- 设置自动提交8SET autocommit = 1; -- 开启9SET autocommit = 0; -- 关闭1011-- 手动控制事务12START TRANSACTION;13-- 执行SQL语句14COMMIT; -- 或 ROLLBACK;事务的性能影响:
- 更高的隔离级别通常意味着更低的并发性能
- 事务日志会增加I/O操作
- 长事务会占用系统资源,应避免
- 合理设置隔离级别可以平衡一致性和性能
5. MySQL的四种隔离级别是什么?分别会产生什么问题?
MySQL的四种事务隔离级别定义了在多个事务并发执行时,一个事务的操作对其他事务的可见程度。不同的隔离级别解决了不同的并发问题,但也带来了性能和功能上的权衡。
1. 读未提交(READ UNCOMMITTED):
定义: 一个事务可以读取另一个未提交事务的数据。
特点:
- 最低的隔离级别,提供最高的并发性
- 不使用锁机制,不阻塞其他事务
- 可能读取到其他事务未提交的"脏"数据
存在的问题:
- 脏读(Dirty Read):事务A读取了事务B修改但未提交的数据,如果事务B回滚,则事务A读取的数据是无效的
- 不可重复读(Non-repeatable Read):同一事务内,多次读取同一数据返回的结果不同
- 幻读(Phantom Read):同一事务内,同样的查询条件返回不同的结果集
示例:
1-- 会话12SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;3START TRANSACTION;4-- 此时会看到会话2未提交的修改5SELECT * FROM accounts WHERE id = 1;6COMMIT;78-- 会话29START TRANSACTION;10UPDATE accounts SET balance = 2000 WHERE id = 1;11-- 此时还未提交2. 读已提交(READ COMMITTED):
定义: 一个事务只能读取另一个已提交事务的数据。
特点:
- 使用行级锁和MVCC机制
- 每次查询都生成一个新的快照
- 只能读取到已提交的数据,解决了脏读问题
- 大多数数据库的默认隔离级别(但不是MySQL InnoDB)
存在的问题:
- 不可重复读(Non-repeatable Read):同一事务内,多次读取同一数据返回的结果不同
- 幻读(Phantom Read):同一事务内,同样的查询条件返回不同的结果集
示例:
1-- 会话12SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;3START TRANSACTION;4SELECT * FROM accounts WHERE id = 1; -- balance=10005-- 会话2执行更新并提交后6SELECT * FROM accounts WHERE id = 1; -- balance=2000,出现不可重复读7COMMIT;89-- 会话210START TRANSACTION;11UPDATE accounts SET balance = 2000 WHERE id = 1;12COMMIT; -- 提交事务3. 可重复读(REPEATABLE READ):
定义: 一个事务执行过程中看到的数据始终是一致的,无论其他事务对数据做了什么修改。
特点:
- MySQL InnoDB的默认隔离级别
- 使用行级锁和MVCC机制
- 事务开始时创建一个快照,整个事务期间都使用该快照
- 解决了脏读和不可重复读问题
存在的问题:
- 幻读(Phantom Read):理论上会存在,但InnoDB通过间隙锁(Gap Lock)解决了大部分幻读问题
- 间隙锁可能导致死锁几率增加
示例:
1-- 会话12SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;3START TRANSACTION;4SELECT * FROM accounts WHERE id = 1; -- balance=10005-- 会话2执行更新并提交后6SELECT * FROM accounts WHERE id = 1; -- balance仍为1000,解决了不可重复读7COMMIT;89-- 会话210START TRANSACTION;11UPDATE accounts SET balance = 2000 WHERE id = 1;12COMMIT;4. 串行化(SERIALIZABLE):
定义: 最高的隔离级别,强制事务串行执行,就像它们是一个接一个执行的一样。
特点:
- 使用表级读写锁或行级锁
- 读取数据时加共享锁,其他事务无法修改这些数据
- 写入数据时加排他锁,其他事务无法读取或修改这些数据
- 可能导致大量阻塞和超时
- 解决了所有并发问题
解决的问题:
- 完全解决了脏读、不可重复读和幻读问题
示例:
1-- 会话12SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;3START TRANSACTION;4SELECT * FROM accounts WHERE balance > 1000; -- 会对符合条件的行加锁5-- 此时会话2无法插入新符合条件的记录,避免了幻读6COMMIT;78-- 会话29START TRANSACTION;10INSERT INTO accounts VALUES(3, 'User3', 1500); -- 会被阻塞直到会话1提交或超时11COMMIT;四种隔离级别对比:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 锁机制 | 性能 |
|---|---|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 | 最少 | 最高 |
| 读已提交 | 避免 | 可能 | 可能 | 较少 | 高 |
| 可重复读 | 避免 | 避免 | 基本避免* | 较多 | 中等 |
| 串行化 | 避免 | 避免 | 避免 | 最多 | 最低 |
*注:InnoDB在可重复读级别下使用间隙锁解决了大部分幻读问题
MySQL中查看和设置隔离级别:
1-- 查看全局隔离级别2SELECT @@GLOBAL.transaction_isolation;34-- 查看当前会话隔离级别5SELECT @@SESSION.transaction_isolation;67-- 设置全局隔离级别8SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;910-- 设置当前会话隔离级别11SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;实际应用中的选择:
- 读未提交:几乎不使用,除非对数据一致性没有要求
- 读已提交:适合大多数Web应用,减少锁竞争
- 可重复读:适合对数据一致性要求较高的场景,如金融系统
- 串行化:只适用于事务量极低但对一致性要求极高的场景
InnoDB特殊处理:
- InnoDB在可重复读隔离级别下,通过Next-Key Lock(记录锁+间隙锁)解决了大部分幻读问题
- 这使得InnoDB的可重复读隔离级别比标准SQL规范中的定义更为严格
6. 什么是MVCC?它是如何工作的?
**MVCC(Multi-Version Concurrency Control,多版本并发控制)**是InnoDB实现高并发的核心机制,它通过保存数据的多个版本来实现读写不阻塞。
核心概念:
1. 版本链:
- 每行数据都有两个隐藏列:
DB_TRX_ID(事务ID)和DB_ROLL_PTR(回滚指针) DB_TRX_ID:记录最后修改该行的事务IDDB_ROLL_PTR:指向undo log中该行的上一个版本- 通过回滚指针形成版本链
2. Read View(读视图):
- 事务开始时创建的一致性视图
- 包含当前活跃事务列表
- 决定当前事务能看到哪些版本的数据
工作原理:
1当前数据行:2id | name | age | DB_TRX_ID | DB_ROLL_PTR31 | Tom | 25 | 100 | ptr145版本链(undo log):6ptr1 -> (name=John, age=20, TRX_ID=80)7 -> (name=Mike, age=18, TRX_ID=60)Read View判断规则:
1-- Read View包含:2- m_ids: 当前活跃事务ID列表3- min_trx_id: 最小活跃事务ID4- max_trx_id: 下一个要分配的事务ID5- creator_trx_id: 创建该Read View的事务ID67-- 可见性判断:81. 如果 DB_TRX_ID < min_trx_id:可见(已提交)92. 如果 DB_TRX_ID >= max_trx_id:不可见(未来事务)103. 如果 min_trx_id <= DB_TRX_ID < max_trx_id:11 - 如果 DB_TRX_ID 在 m_ids 中:不可见(未提交)12 - 否则:可见(已提交)不同隔离级别的MVCC实现:
READ COMMITTED:
- 每次查询都创建新的Read View
- 能读到其他事务已提交的修改
REPEATABLE READ:
- 事务开始时创建Read View,整个事务期间使用同一个
- 保证可重复读
示例:
1-- 事务A (TRX_ID=100)2START TRANSACTION;3SELECT * FROM users WHERE id = 1; -- age=2045-- 事务B (TRX_ID=101)6START TRANSACTION;7UPDATE users SET age = 25 WHERE id = 1;8COMMIT;910-- 事务A继续11SELECT * FROM users WHERE id = 1; 12-- RC级别:age=25 (看到新版本)13-- RR级别:age=20 (看到旧版本)MVCC的优势:
- 读不加锁,写不阻塞读
- 提高并发性能
- 实现一致性非锁定读
MVCC的局限:
- 只在RC和RR隔离级别下工作
- 不能完全避免幻读(需要配合间隙锁)
- undo log可能占用大量空间
7. MySQL中的锁有哪些类型?
MySQL的锁机制用于控制并发访问,保证数据一致性。根据不同的分类维度,MySQL有多种锁类型。
按锁粒度分类:
1. 表级锁(Table Lock):
- 锁定整张表
- 开销小,加锁快
- 锁粒度大,并发度低
- MyISAM默认使用
1-- 手动加表锁2LOCK TABLES users READ; -- 读锁3LOCK TABLES users WRITE; -- 写锁4UNLOCK TABLES;2. 行级锁(Row Lock):
- 锁定单行数据
- 开销大,加锁慢
- 锁粒度小,并发度高
- InnoDB默认使用
3. 页级锁(Page Lock):
- 锁定数据页
- 介于表锁和行锁之间
- BDB存储引擎使用
按锁类型分类:
1. 共享锁(S Lock,读锁):
- 允许多个事务同时读取
- 阻止其他事务写入
1SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;2. 排他锁(X Lock,写锁):
- 阻止其他事务读取和写入
- 只有持锁事务可以访问
1SELECT * FROM users WHERE id = 1 FOR UPDATE;InnoDB特有锁:
1. 记录锁(Record Lock):
- 锁定索引记录
- 防止其他事务修改该行
2. 间隙锁(Gap Lock):
- 锁定索引记录之间的间隙
- 防止其他事务插入数据
- 解决幻读问题
3. Next-Key Lock:
- 记录锁 + 间隙锁
- 锁定记录及其前面的间隙
- RR隔离级别默认使用
1-- 假设id有索引,值为1,5,102SELECT * FROM users WHERE id > 5 FOR UPDATE;3-- 锁定:(5,10]和(10,+∞)4. 插入意向锁(Insert Intention Lock):
- 插入前获取的特殊间隙锁
- 多个事务可以同时持有不同位置的插入意向锁
5. 自增锁(AUTO-INC Lock):
- 用于自增列
- 保证自增值的连续性
按锁模式分类:
1. 意向锁(Intention Lock):
- 表级锁,用于协调表锁和行锁
- 意向共享锁(IS):事务想获取行的共享锁
- 意向排他锁(IX):事务想获取行的排他锁
锁兼容性矩阵:
| X | IX | S | IS | |
|---|---|---|---|---|
| X | ✗ | ✗ | ✗ | ✗ |
| IX | ✗ | ✓ | ✗ | ✓ |
| S | ✗ | ✗ | ✓ | ✓ |
| IS | ✗ | ✓ | ✓ | ✓ |
查看锁信息:
1-- 查看当前锁2SHOW ENGINE INNODB STATUS;34-- 查看锁等待5SELECT * FROM information_schema.INNODB_LOCKS;67-- 查看锁等待关系8SELECT * FROM information_schema.INNODB_LOCK_WAITS;8. 什么是死锁?如何避免死锁?
死锁是指两个或多个事务互相持有对方需要的锁,导致所有事务都无法继续执行的情况。
死锁示例:
1-- 事务A2START TRANSACTION;3UPDATE users SET name='A' WHERE id=1; -- 获取id=1的锁4-- 等待获取id=2的锁5UPDATE users SET name='A' WHERE id=2;67-- 事务B8START TRANSACTION;9UPDATE users SET name='B' WHERE id=2; -- 获取id=2的锁10-- 等待获取id=1的锁11UPDATE users SET name='B' WHERE id=1;1213-- 形成死锁:A等B释放id=2,B等A释放id=1死锁的四个必要条件:
- 互斥:资源不能被共享
- 持有并等待:持有资源的同时等待其他资源
- 不可剥夺:资源不能被强制释放
- 循环等待:形成资源等待环路
MySQL的死锁检测:
- InnoDB自动检测死锁
- 选择回滚代价最小的事务
- 其他事务可以继续执行
避免死锁的策略:
1. 按相同顺序访问资源:
1-- 好的做法:统一按id升序访问2UPDATE users SET name='X' WHERE id IN (1,2,3) ORDER BY id;34-- 避免:不同事务以不同顺序访问2. 缩小事务范围:
1-- 不好:事务时间过长2START TRANSACTION;3SELECT * FROM users; -- 大量数据4-- 复杂业务逻辑5UPDATE users SET status=1;6COMMIT;78-- 好:只在必要时使用事务9-- 先查询数据10SELECT * FROM users;11-- 业务逻辑处理12START TRANSACTION;13UPDATE users SET status=1;14COMMIT;3. 降低隔离级别:
1-- 从RR降低到RC,减少间隙锁2SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;4. 添加合理的索引:
- 减少锁定的行数
- 避免全表扫描
5. 使用较小的事务:
- 减少锁持有时间
- 降低死锁概率
6. 设置锁等待超时:
1-- 设置锁等待超时时间(秒)2SET innodb_lock_wait_timeout = 50;7. 使用乐观锁:
1-- 使用版本号2UPDATE users SET balance=balance-100, version=version+1 3WHERE id=1 AND version=10;处理死锁:
1-- 查看最近的死锁信息2SHOW ENGINE INNODB STATUS;34-- 在应用层捕获死锁异常并重试5try {6 // 执行事务7} catch (DeadlockException e) {8 // 重试逻辑9 retry();10}9. MySQL的主从复制原理是什么?
MySQL主从复制是实现数据库高可用、读写分离和数据备份的基础技术。
复制架构:
1Master(主库) --binlog--> Slave(从库)2 | |3 写操作 读操作复制原理(三个线程):
1. Master的Binlog Dump线程:
- 读取binlog日志
- 发送给Slave的I/O线程
2. Slave的I/O线程:
- 连接到Master
- 请求binlog日志
- 写入本地relay log(中继日志)
3. Slave的SQL线程:
- 读取relay log
- 解析并执行SQL语句
- 更新从库数据
复制过程:
11. Master执行SQL -> 写入binlog22. Slave I/O线程 -> 请求binlog33. Master Dump线程 -> 发送binlog44. Slave I/O线程 -> 写入relay log55. Slave SQL线程 -> 读取relay log66. Slave SQL线程 -> 执行SQL更新数据配置主从复制:
Master配置:
1-- my.cnf2[mysqld]3server-id=14log-bin=mysql-bin5binlog-format=ROW67-- 创建复制用户8CREATE USER 'repl'@'%' IDENTIFIED BY 'password';9GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';1011-- 查看master状态12SHOW MASTER STATUS;Slave配置:
1-- my.cnf2[mysqld]3server-id=24relay-log=relay-bin5read-only=167-- 配置主库信息8CHANGE MASTER TO9 MASTER_HOST='master_ip',10 MASTER_USER='repl',11 MASTER_PASSWORD='password',12 MASTER_LOG_FILE='mysql-bin.000001',13 MASTER_LOG_POS=154;1415-- 启动复制16START SLAVE;1718-- 查看slave状态19SHOW SLAVE STATUS\G复制模式:
1. 异步复制(默认):
- Master不等待Slave确认
- 性能最好,但可能丢数据
2. 半同步复制:
- Master等待至少一个Slave确认
- 平衡性能和数据安全
3. 全同步复制:
- Master等待所有Slave确认
- 数据最安全,但性能最差
binlog格式:
1. Statement(语句):
- 记录SQL语句
- 日志量小,但可能不一致
2. Row(行):
- 记录每行的变化
- 日志量大,但完全一致
3. Mixed(混合):
- 自动选择Statement或Row
常见问题:
1. 主从延迟:
- 原因:从库性能不足、网络延迟、大事务
- 解决:并行复制、优化SQL、增加从库
2. 数据不一致:
- 原因:从库写入、binlog格式问题
- 解决:设置read-only、使用Row格式
3. 复制中断:
- 原因:网络问题、SQL执行失败
- 解决:跳过错误、重建复制
10. 如何优化MySQL查询性能?
MySQL查询优化是一个系统工程,需要从多个维度进行优化。
1. 索引优化:
创建合适的索引:
1-- 为WHERE条件列创建索引2CREATE INDEX idx_status ON orders(status);34-- 为JOIN列创建索引5CREATE INDEX idx_user_id ON orders(user_id);67-- 创建覆盖索引8CREATE INDEX idx_cover ON orders(user_id, status, create_time);避免索引失效:
1-- 不要在索引列上使用函数2-- 错误3SELECT * FROM users WHERE YEAR(create_time) = 2024;4-- 正确5SELECT * FROM users WHERE create_time >= '2024-01-01' 6 AND create_time < '2025-01-01';78-- 避免隐式类型转换9-- 错误(id是int类型)10SELECT * FROM users WHERE id = '123';11-- 正确12SELECT * FROM users WHERE id = 123;1314-- Like不要以%开头15-- 错误16SELECT * FROM users WHERE name LIKE '%john%';17-- 正确18SELECT * FROM users WHERE name LIKE 'john%';2. SQL语句优化:
**避免SELECT ***:
1-- 不好2SELECT * FROM users WHERE id = 1;34-- 好5SELECT id, name, email FROM users WHERE id = 1;使用LIMIT:
1-- 限制返回行数2SELECT * FROM users WHERE status = 1 LIMIT 100;优化子查询:
1-- 不好:子查询2SELECT * FROM orders WHERE user_id IN (3 SELECT id FROM users WHERE status = 14);56-- 好:JOIN7SELECT o.* FROM orders o8INNER JOIN users u ON o.user_id = u.id9WHERE u.status = 1;3. 表结构优化:
选择合适的数据类型:
1-- 使用最小的数据类型2age TINYINT UNSIGNED -- 而不是INT34-- 使用ENUM代替字符串5status ENUM('active', 'inactive', 'pending')67-- 固定长度字符串用CHAR8country_code CHAR(2) -- 而不是VARCHAR(2)垂直分表:
1-- 将大字段分离2-- users表:id, name, email3-- user_profiles表:user_id, bio, avatar4. 查询缓存优化:
1-- 启用查询缓存(MySQL 5.7及以前)2SET GLOBAL query_cache_type = ON;3SET GLOBAL query_cache_size = 268435456; -- 256MB45-- 注意:MySQL 8.0已移除查询缓存5. 配置参数优化:
1-- 增加缓冲池大小2innodb_buffer_pool_size = 8G34-- 调整日志文件大小5innodb_log_file_size = 512M67-- 增加连接数8max_connections = 1000910-- 调整线程缓存11thread_cache_size = 646. 硬件和架构优化:
- 使用SSD硬盘
- 增加内存
- 读写分离
- 分库分表
7. 使用EXPLAIN分析:
1EXPLAIN SELECT * FROM users WHERE name = 'John';23-- 关注:4-- type: ALL(全表扫描)需要优化5-- key: NULL表示未使用索引6-- rows: 扫描行数,越少越好7-- Extra: Using filesort/Using temporary需要优化8. 慢查询优化:
1-- 开启慢查询日志2SET GLOBAL slow_query_log = ON;3SET GLOBAL long_query_time = 2; -- 2秒45-- 分析慢查询6mysqldumpslow /var/log/mysql/slow.log11. 什么是慢查询日志?如何使用?
慢查询日志记录执行时间超过指定阈值的SQL语句,是定位性能问题的重要工具。
配置慢查询日志:
1-- 查看慢查询配置2SHOW VARIABLES LIKE 'slow_query%';3SHOW VARIABLES LIKE 'long_query_time';45-- 开启慢查询日志6SET GLOBAL slow_query_log = ON;78-- 设置慢查询阈值(秒)9SET GLOBAL long_query_time = 2;1011-- 设置日志文件路径12SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';1314-- 记录未使用索引的查询15SET GLOBAL log_queries_not_using_indexes = ON;my.cnf配置:
1[mysqld]2slow_query_log = 13slow_query_log_file = /var/log/mysql/slow.log4long_query_time = 25log_queries_not_using_indexes = 1分析慢查询日志:
1# 使用mysqldumpslow分析2mysqldumpslow -s t -t 10 /var/log/mysql/slow.log3# -s t: 按查询时间排序4# -t 10: 显示前10条56# 常用选项7mysqldumpslow -s c -t 10 slow.log # 按查询次数排序8mysqldumpslow -s r -t 10 slow.log # 按返回记录数排序9mysqldumpslow -s al -t 10 slow.log # 按平均锁时间排序使用pt-query-digest分析:
1# 更强大的分析工具2pt-query-digest /var/log/mysql/slow.log > report.txt12. Explain执行计划如何分析?
EXPLAIN显示MySQL如何执行查询,是SQL优化的核心工具。
基本用法:
1EXPLAIN SELECT * FROM users WHERE name = 'John';输出字段解析:
1. id:
- 查询的序列号
- id相同:执行顺序从上到下
- id不同:id越大越先执行
2. select_type:
- SIMPLE:简单查询
- PRIMARY:最外层查询
- SUBQUERY:子查询
- DERIVED:派生表
- UNION:UNION查询
3. table:
- 访问的表名
4. type(重要): 访问类型,性能从好到差:
- system:表只有一行
- const:主键或唯一索引查询
- eq_ref:唯一索引扫描
- ref:非唯一索引扫描
- range:范围扫描
- index:索引全扫描
- ALL:全表扫描(需要优化)
5. possible_keys:
- 可能使用的索引
6. key:
- 实际使用的索引
- NULL表示未使用索引
7. key_len:
- 使用的索引长度
- 越短越好
8. ref:
- 与索引比较的列或常量
9. rows:
- 预计扫描的行数
- 越少越好
10. Extra(重要):
- Using index:使用覆盖索引(好)
- Using where:使用WHERE过滤
- Using filesort:需要额外排序(需优化)
- Using temporary:使用临时表(需优化)
- Using index condition:索引下推
优化示例:
1-- 差的执行计划2EXPLAIN SELECT * FROM orders WHERE status = 1;3-- type: ALL, Extra: Using where45-- 优化:添加索引6CREATE INDEX idx_status ON orders(status);7-- type: ref, key: idx_status89-- 更好:使用覆盖索引10CREATE INDEX idx_cover ON orders(status, order_no, amount);11SELECT status, order_no, amount FROM orders WHERE status = 1;12-- Extra: Using index13. MySQL的分区表是什么?
分区表将一个大表的数据分散存储到多个物理分区中,提高查询性能和管理效率。
分区类型:
1. RANGE分区: 按范围分区
1CREATE TABLE orders (2 id INT,3 order_date DATE,4 amount DECIMAL(10,2)5) PARTITION BY RANGE (YEAR(order_date)) (6 PARTITION p2020 VALUES LESS THAN (2021),7 PARTITION p2021 VALUES LESS THAN (2022),8 PARTITION p2022 VALUES LESS THAN (2023),9 PARTITION p2023 VALUES LESS THAN (2024),10 PARTITION p_future VALUES LESS THAN MAXVALUE11);2. LIST分区: 按列表值分区
1CREATE TABLE users (2 id INT,3 region VARCHAR(20)4) PARTITION BY LIST COLUMNS(region) (5 PARTITION p_north VALUES IN ('Beijing', 'Tianjin'),6 PARTITION p_south VALUES IN ('Shanghai', 'Guangzhou'),7 PARTITION p_west VALUES IN ('Chengdu', 'Chongqing')8);3. HASH分区: 按哈希值分区
1CREATE TABLE logs (2 id INT,3 log_time DATETIME4) PARTITION BY HASH(YEAR(log_time))5PARTITIONS 4;4. KEY分区: 类似HASH,使用MySQL提供的哈希函数
1CREATE TABLE sessions (2 id INT,3 user_id INT4) PARTITION BY KEY(user_id)5PARTITIONS 4;分区管理:
1-- 查看分区信息2SELECT * FROM information_schema.PARTITIONS 3WHERE TABLE_NAME = 'orders';45-- 添加分区6ALTER TABLE orders ADD PARTITION (7 PARTITION p2024 VALUES LESS THAN (2025)8);910-- 删除分区11ALTER TABLE orders DROP PARTITION p2020;1213-- 重组分区14ALTER TABLE orders REORGANIZE PARTITION p_future INTO (15 PARTITION p2024 VALUES LESS THAN (2025),16 PARTITION p_future VALUES LESS THAN MAXVALUE17);分区的优势:
- 提高查询性能(分区裁剪)
- 便于数据管理(按分区删除)
- 提高并发性能
- 便于数据归档
注意事项:
- 分区键必须是主键或唯一键的一部分
- 最多支持8192个分区
- 分区表不支持外键
14. 什么是表锁和行锁?
表锁和行锁是MySQL中两种不同粒度的锁机制。
表锁(Table Lock):
特点:
- 锁定整张表
- 开销小,加锁快
- 不会出现死锁
- 锁粒度大,并发度低
- MyISAM默认使用
类型:
1-- 读锁(共享锁)2LOCK TABLES users READ;3-- 当前会话和其他会话都可以读,但不能写45-- 写锁(排他锁)6LOCK TABLES users WRITE;7-- 只有当前会话可以读写,其他会话被阻塞89-- 释放锁10UNLOCK TABLES;行锁(Row Lock):
特点:
- 锁定单行或多行
- 开销大,加锁慢
- 可能出现死锁
- 锁粒度小,并发度高
- InnoDB默认使用
类型:
1-- 共享锁(S锁)2SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;3-- 允许其他事务读取,但不能修改45-- 排他锁(X锁)6SELECT * FROM users WHERE id = 1 FOR UPDATE;7-- 其他事务不能读取和修改89-- UPDATE/DELETE自动加排他锁10UPDATE users SET name = 'John' WHERE id = 1;对比:
| 特性 | 表锁 | 行锁 |
|---|---|---|
| 锁粒度 | 整表 | 单行 |
| 并发性 | 低 | 高 |
| 死锁 | 不会 | 可能 |
| 开销 | 小 | 大 |
| 适用场景 | 批量操作 | 高并发 |
| 存储引擎 | MyISAM | InnoDB |
InnoDB的行锁实现:
- 基于索引实现
- 如果没有索引,会升级为表锁
- 锁定的是索引记录,不是数据行
示例:
1-- 有索引:行锁2UPDATE users SET name = 'John' WHERE id = 1; -- 只锁id=134-- 无索引:表锁5UPDATE users SET name = 'John' WHERE email = 'a@b.com'; -- 锁全表15. InnoDB的Buffer Pool是什么?
Buffer Pool是InnoDB最重要的内存结构,用于缓存数据和索引页。
作用:
- 缓存数据页和索引页
- 减少磁盘I/O
- 提高查询性能
- 缓冲写入操作
结构:
1Buffer Pool2├── 数据页缓存3├── 索引页缓存4├── 插入缓冲(Insert Buffer)5├── 自适应哈希索引6├── 锁信息7└── 数据字典信息页面管理:
- 默认页大小:16KB
- 使用LRU算法管理
- 分为young区和old区
LRU链表:
1New Page -> young区(5/8) -> old区(3/8) -> 淘汰配置:
1-- 查看Buffer Pool大小2SHOW VARIABLES LIKE 'innodb_buffer_pool_size';34-- 设置Buffer Pool大小(建议物理内存的50-80%)5SET GLOBAL innodb_buffer_pool_size = 8589934592; -- 8GB67-- Buffer Pool实例数8SET GLOBAL innodb_buffer_pool_instances = 8;910-- 查看Buffer Pool状态11SHOW ENGINE INNODB STATUS;监控指标:
1-- 查看Buffer Pool使用情况2SELECT 3 POOL_ID,4 POOL_SIZE,5 FREE_BUFFERS,6 DATABASE_PAGES7FROM information_schema.INNODB_BUFFER_POOL_STATS;89-- 命中率10SHOW STATUS LIKE 'Innodb_buffer_pool%';11-- 关注:12-- Innodb_buffer_pool_read_requests: 读请求数13-- Innodb_buffer_pool_reads: 从磁盘读取次数14-- 命中率 = 1 - (reads / read_requests)优化建议:
- 设置合适的大小(物理内存的50-80%)
- 使用多个Buffer Pool实例减少竞争
- 预热Buffer Pool(启动时加载热数据)
- 监控命中率(应>99%)
16. 什么是binlog、redo log和undo log?
MySQL的三种日志是保证数据一致性和持久性的核心机制。
binlog(二进制日志):
作用:
- 记录所有DDL和DML语句
- 用于主从复制
- 用于数据恢复
特点:
- Server层实现,所有存储引擎都可用
- 逻辑日志,记录SQL语句
- 追加写入,不会覆盖
格式:
1-- Statement:记录SQL语句2-- Row:记录每行的变化(推荐)3-- Mixed:混合模式45-- 查看binlog格式6SHOW VARIABLES LIKE 'binlog_format';78-- 查看binlog文件9SHOW BINARY LOGS;1011-- 查看binlog内容12SHOW BINLOG EVENTS IN 'mysql-bin.000001';redo log(重做日志):
作用:
- 保证事务持久性
- 崩溃恢复
- 实现WAL(Write-Ahead Logging)
特点:
- InnoDB特有
- 物理日志,记录数据页的修改
- 循环写入,固定大小
- 先写redo log,再写数据文件
工作原理:
11. 事务提交时,先写redo log22. redo log写入成功,事务提交成功33. 后台线程异步将脏页刷入磁盘44. 崩溃恢复时,重放redo log配置:
1-- redo log文件大小2innodb_log_file_size = 512M34-- redo log文件数量5innodb_log_files_in_group = 267-- 刷盘策略8innodb_flush_log_at_trx_commit = 19-- 0: 每秒刷盘10-- 1: 每次事务提交刷盘(最安全)11-- 2: 每次提交写入OS缓存,每秒刷盘undo log(回滚日志):
作用:
- 保证事务原子性
- 实现MVCC
- 事务回滚
特点:
- InnoDB特有
- 逻辑日志,记录相反操作
- 存储在回滚段中
工作原理:
1-- INSERT操作:记录DELETE2INSERT INTO users VALUES (1, 'John');3-- undo log: DELETE FROM users WHERE id = 1;45-- UPDATE操作:记录旧值6UPDATE users SET name = 'Tom' WHERE id = 1;7-- undo log: UPDATE users SET name = 'John' WHERE id = 1;89-- DELETE操作:记录INSERT10DELETE FROM users WHERE id = 1;11-- undo log: INSERT INTO users VALUES (1, 'John');三种日志对比:
| 特性 | binlog | redo log | undo log |
|---|---|---|---|
| 层次 | Server层 | InnoDB层 | InnoDB层 |
| 类型 | 逻辑日志 | 物理日志 | 逻辑日志 |
| 作用 | 复制、恢复 | 崩溃恢复 | 回滚、MVCC |
| 写入方式 | 追加写 | 循环写 | 随机写 |
| 持久化 | 是 | 是 | 否 |
两阶段提交:
11. 准备阶段:写入redo log,状态为prepare22. 提交阶段:写入binlog33. 完成阶段:redo log状态改为commit17. MySQL如何实现高可用?
MySQL高可用方案确保数据库服务的连续性和数据安全。
1. 主从复制(Master-Slave):
架构:
1Master(主库)2 ├── Slave1(从库)3 ├── Slave2(从库)4 └── Slave3(从库)优点:
- 简单易实现
- 读写分离
- 数据备份
缺点:
- 主库单点故障
- 需要手动切换
2. 主主复制(Master-Master):
架构:
1Master1 <--> Master2配置:
1-- Master12auto_increment_increment = 23auto_increment_offset = 145-- Master26auto_increment_increment = 27auto_increment_offset = 2优点:
- 双向复制
- 互为备份
缺点:
- 可能数据冲突
- 复杂度高
3. MHA(Master High Availability):
特点:
- 自动故障切换
- 监控主库状态
- 提升从库为主库
工作流程:
11. 监控主库心跳22. 检测主库故障33. 选择最新的从库44. 应用差异日志55. 提升为新主库66. 修改其他从库配置4. MGR(MySQL Group Replication):
特点:
- MySQL官方方案
- 多主模式
- 自动故障转移
- 强一致性
架构:
1Node1 <--> Node2 <--> Node32(任意节点可读写)5. Galera Cluster:
特点:
- 多主同步复制
- 真正的多主架构
- 无延迟复制
6. 使用中间件:
ProxySQL:
1应用 -> ProxySQL -> MySQL集群2- 读写分离3- 负载均衡4- 故障切换MaxScale:
1应用 -> MaxScale -> MySQL集群2- 自动路由3- 查询缓存4- 监控管理7. 云服务方案:
阿里云RDS:
- 自动备份
- 一键切换
- 读写分离
AWS RDS:
- Multi-AZ部署
- 自动故障转移
- 只读副本
高可用架构示例:
1VIP2 |3 +-----+-----+4 | |5 Master Slave6 | |7 (主库写入) (从库读取)8 | |9 MHA Manager10 (监控和切换)最佳实践:
- 使用半同步复制
- 配置监控告警
- 定期演练切换
- 保持数据一致性
- 使用VIP实现透明切换
18. 什么是读写分离?
读写分离是将数据库的读操作和写操作分散到不同的数据库服务器上,提高系统性能和可用性。
架构:
1应用层2 |3中间件/代理4 |5 ├── Master(写)6 └── Slave1, Slave2, Slave3(读)实现方式:
1. 应用层实现:
1public class DataSourceRouter {2 private DataSource masterDS;3 private List<DataSource> slaveDS;4 5 public DataSource getDataSource(boolean isRead) {6 if (isRead) {7 // 负载均衡选择从库8 return selectSlave();9 }10 return masterDS;11 }12 13 private DataSource selectSlave() {14 // 轮询、随机、权重等策略15 int index = random.nextInt(slaveDS.size());16 return slaveDS.get(index);17 }18}1920// 使用21@Transactional(readOnly = true)22public User getUser(Long id) {23 // 自动路由到从库24 return userMapper.selectById(id);25}2627@Transactional28public void updateUser(User user) {29 // 自动路由到主库30 userMapper.updateById(user);31}2. 中间件实现:
MyCAT:
1<dataHost name="localhost1" maxCon="1000" minCon="10" 2 balance="1" writeType="0" dbType="mysql">3 <!-- 写主机 -->4 <writeHost host="master" url="192.168.1.1:3306" 5 user="root" password="123456">6 <!-- 读主机 -->7 <readHost host="slave1" url="192.168.1.2:3306" 8 user="root" password="123456"/>9 <readHost host="slave2" url="192.168.1.3:3306" 10 user="root" password="123456"/>11 </writeHost>12</dataHost>ProxySQL:
1-- 配置读写分离规则2INSERT INTO mysql_query_rules (rule_id, active, match_pattern, 3 destination_hostgroup, apply)4VALUES 5(1, 1, '^SELECT.*FOR UPDATE$', 1, 1), -- 写6(2, 1, '^SELECT', 2, 1); -- 读78-- 配置服务器组9INSERT INTO mysql_servers (hostgroup_id, hostname, port)10VALUES 11(1, '192.168.1.1', 3306), -- 写组12(2, '192.168.1.2', 3306), -- 读组13(2, '192.168.1.3', 3306); -- 读组3. Spring Boot实现:
1@Configuration2public class DataSourceConfig {3 4 @Bean5 public DataSource routingDataSource() {6 Map<Object, Object> targetDataSources = new HashMap<>();7 targetDataSources.put("master", masterDataSource());8 targetDataSources.put("slave", slaveDataSource());9 10 DynamicDataSource dataSource = new DynamicDataSource();11 dataSource.setTargetDataSources(targetDataSources);12 dataSource.setDefaultTargetDataSource(masterDataSource());13 return dataSource;14 }15}1617// 动态数据源18public class DynamicDataSource extends AbstractRoutingDataSource {19 @Override20 protected Object determineCurrentLookupKey() {21 return DataSourceContextHolder.getDataSource();22 }23}2425// AOP拦截26@Aspect27public class DataSourceAspect {28 @Around("@annotation(readOnly)")29 public Object setReadDataSource(ProceedingJoinPoint point, 30 ReadOnly readOnly) {31 DataSourceContextHolder.setDataSource("slave");32 try {33 return point.proceed();34 } finally {35 DataSourceContextHolder.clearDataSource();36 }37 }38}负载均衡策略:
1. 轮询(Round Robin):
1int index = counter++ % slaves.size();2. 随机(Random):
1int index = random.nextInt(slaves.size());3. 权重(Weighted):
1// slave1: 权重3, slave2: 权重12// slave1被选中概率75%, slave2被选中概率25%4. 最少连接(Least Connections):
1// 选择当前连接数最少的从库注意事项:
1. 主从延迟问题:
1// 写后立即读可能读不到2user.setName("Tom");3userService.update(user); // 写主库4User u = userService.get(id); // 读从库,可能还是旧数据56// 解决方案:7// 1. 强制读主库8@ReadMaster9public User getUser(Long id);1011// 2. 延迟读取12Thread.sleep(100);1314// 3. 使用缓存2. 事务一致性:
1@Transactional2public void transfer() {3 // 事务内的所有操作都应该在主库4 accountService.deduct(fromId, amount);5 accountService.add(toId, amount);6}优势:
- 提高查询性能
- 降低主库压力
- 提高系统可用性
19. 如何进行MySQL备份和恢复?
MySQL备份和恢复是保证数据安全的重要手段。
备份类型:
1. 逻辑备份:
mysqldump:
1# 备份单个数据库2mysqldump -u root -p database_name > backup.sql34# 备份多个数据库5mysqldump -u root -p --databases db1 db2 > backup.sql67# 备份所有数据库8mysqldump -u root -p --all-databases > all_backup.sql910# 备份表结构11mysqldump -u root -p -d database_name > schema.sql1213# 备份数据(不含结构)14mysqldump -u root -p -t database_name > data.sql1516# 压缩备份17mysqldump -u root -p database_name | gzip > backup.sql.gz1819# 备份时添加选项20mysqldump -u root -p \21 --single-transaction \ # 一致性备份(InnoDB)22 --master-data=2 \ # 记录binlog位置23 --flush-logs \ # 刷新日志24 --routines \ # 备份存储过程25 --triggers \ # 备份触发器26 database_name > backup.sql恢复:
1# 恢复数据库2mysql -u root -p database_name < backup.sql34# 恢复压缩文件5gunzip < backup.sql.gz | mysql -u root -p database_name67# 在MySQL中执行8mysql> source /path/to/backup.sql;2. 物理备份:
直接复制文件:
1# 停止MySQL2systemctl stop mysqld34# 复制数据目录5cp -r /var/lib/mysql /backup/mysql_$(date +%Y%m%d)67# 启动MySQL8systemctl start mysqld使用XtraBackup:
1# 全量备份2xtrabackup --backup --target-dir=/backup/full34# 增量备份5xtrabackup --backup --target-dir=/backup/inc1 \6 --incremental-basedir=/backup/full78# 准备恢复9xtrabackup --prepare --target-dir=/backup/full10xtrabackup --prepare --target-dir=/backup/full \11 --incremental-dir=/backup/inc11213# 恢复14xtrabackup --copy-back --target-dir=/backup/full15chown -R mysql:mysql /var/lib/mysql16systemctl start mysqld3. 二进制日志备份:
1# 刷新日志2mysqladmin -u root -p flush-logs34# 备份binlog5cp /var/lib/mysql/mysql-bin.* /backup/binlog/67# 使用binlog恢复(时间点恢复)8mysqlbinlog --start-datetime="2024-01-01 00:00:00" \9 --stop-datetime="2024-01-01 23:59:59" \10 mysql-bin.000001 | mysql -u root -p1112# 使用binlog恢复(位置恢复)13mysqlbinlog --start-position=1000 \14 --stop-position=2000 \15 mysql-bin.000001 | mysql -u root -p备份策略:
1. 全量备份 + 增量备份:
1# 周日:全量备份2mysqldump --all-databases > full_backup_sunday.sql34# 周一到周六:增量备份(binlog)5mysqladmin flush-logs6cp mysql-bin.* /backup/incremental/2. 定时备份脚本:
1#!/bin/bash2DATE=$(date +%Y%m%d_%H%M%S)3BACKUP_DIR="/backup/mysql"4DB_NAME="mydb"56# 创建备份目录7mkdir -p $BACKUP_DIR89# 执行备份10mysqldump -u root -p'password' \11 --single-transaction \12 --master-data=2 \13 --flush-logs \14 $DB_NAME | gzip > $BACKUP_DIR/${DB_NAME}_${DATE}.sql.gz1516# 删除7天前的备份17find $BACKUP_DIR -name "*.sql.gz" -mtime +7 -delete1819# 记录日志20echo "Backup completed at $(date)" >> $BACKUP_DIR/backup.log3. 定时任务:
1# 添加到crontab2crontab -e34# 每天凌晨2点执行备份50 2 * * * /path/to/backup.sh恢复场景:
1. 完全恢复:
1mysql -u root -p < full_backup.sql2. 时间点恢复:
1# 1. 恢复全量备份2mysql -u root -p < full_backup.sql34# 2. 应用binlog到指定时间点5mysqlbinlog --stop-datetime="2024-01-01 10:00:00" \6 mysql-bin.000001 mysql-bin.000002 | mysql -u root -p3. 跳过错误恢复:
1# 跳过一个错误2SET GLOBAL sql_slave_skip_counter = 1;3START SLAVE;备份验证:
1# 定期验证备份可用性2mysql -u root -p test_db < backup.sql20. MySQL的字符集和排序规则是什么?
**字符集(Character Set)**定义了字符的编码方式,**排序规则(Collation)**定义了字符的比较和排序规则。
常用字符集:
1. latin1:
- MySQL默认字符集(5.7及以前)
- 单字节编码
- 不支持中文
2. utf8:
- 最多3字节
- 不完整的UTF-8实现
- 不支持emoji等4字节字符
3. utf8mb4(推荐):
- 完整的UTF-8实现
- 最多4字节
- 支持emoji
- MySQL 8.0默认字符集
4. gbk:
- 中文编码
- 双字节
查看字符集:
1-- 查看支持的字符集2SHOW CHARACTER SET;34-- 查看当前字符集5SHOW VARIABLES LIKE 'character%';67-- 查看数据库字符集8SELECT DEFAULT_CHARACTER_SET_NAME 9FROM information_schema.SCHEMATA 10WHERE SCHEMA_NAME = 'mydb';1112-- 查看表字符集13SHOW CREATE TABLE users;设置字符集:
1-- 服务器级别(my.cnf)2[mysqld]3character-set-server=utf8mb44collation-server=utf8mb4_unicode_ci56[client]7default-character-set=utf8mb489-- 数据库级别10CREATE DATABASE mydb 11CHARACTER SET utf8mb4 12COLLATE utf8mb4_unicode_ci;1314ALTER DATABASE mydb 15CHARACTER SET utf8mb4 16COLLATE utf8mb4_unicode_ci;1718-- 表级别19CREATE TABLE users (20 id INT,21 name VARCHAR(50)22) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;2324ALTER TABLE users 25CHARACTER SET utf8mb4 26COLLATE utf8mb4_unicode_ci;2728-- 列级别29ALTER TABLE users 30MODIFY name VARCHAR(50) 31CHARACTER SET utf8mb4 32COLLATE utf8mb4_unicode_ci;排序规则:
常用Collation:
1. utf8mb4_general_ci:
- 不区分大小写
- 性能较好
- 准确性稍差
2. utf8mb4_unicode_ci:
- 不区分大小写
- 准确性好
- 性能稍差
3. utf8mb4_bin:
- 区分大小写
- 二进制比较
4. utf8mb4_0900_ai_ci:
- MySQL 8.0默认
- 支持更多语言
- ai: accent insensitive(不区分重音)
- ci: case insensitive(不区分大小写)
示例:
1-- 创建测试表2CREATE TABLE test (3 name VARCHAR(50)4) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;56INSERT INTO test VALUES ('A'), ('a'), ('B'), ('b');78-- 不区分大小写9SELECT * FROM test WHERE name = 'a';10-- 返回:A, a1112-- 区分大小写13SELECT * FROM test 14WHERE name = 'a' COLLATE utf8mb4_bin;15-- 返回:a1617-- 排序18SELECT * FROM test ORDER BY name;19-- utf8mb4_general_ci: A, a, B, b20-- utf8mb4_bin: A, B, a, b字符集转换:
1-- 转换表字符集2ALTER TABLE users 3CONVERT TO CHARACTER SET utf8mb4 4COLLATE utf8mb4_unicode_ci;56-- 只修改表定义,不转换数据7ALTER TABLE users 8DEFAULT CHARACTER SET utf8mb4 9COLLATE utf8mb4_unicode_ci;连接字符集:
1-- 设置连接字符集2SET NAMES utf8mb4;34-- 等价于5SET character_set_client = utf8mb4;6SET character_set_results = utf8mb4;7SET character_set_connection = utf8mb4;最佳实践:
- 统一使用utf8mb4
- 使用utf8mb4_unicode_ci或utf8mb4_0900_ai_ci
- 在my.cnf中配置默认字符集
- 创建数据库时明确指定字符集
- 应用程序连接时设置字符集
21. 什么是联合索引的最左前缀原则?
最左前缀原则是联合索引使用的核心规则,决定了索引是否能被有效利用。
原理:
联合索引按照索引列的顺序进行排序和查找,只有从最左边的列开始连续匹配,索引才能生效。
示例:
1-- 创建联合索引2CREATE INDEX idx_abc ON users(a, b, c);34-- 索引结构(逻辑上)5a | b | c | 主键61 | 1 | 1 | id171 | 1 | 2 | id281 | 2 | 1 | id392 | 1 | 1 | id4能使用索引的情况:
1-- 1. 使用a(✓)2SELECT * FROM users WHERE a = 1;34-- 2. 使用a, b(✓)5SELECT * FROM users WHERE a = 1 AND b = 2;67-- 3. 使用a, b, c(✓)8SELECT * FROM users WHERE a = 1 AND b = 2 AND c = 3;910-- 4. 使用a, c(✓ 部分使用,只用到a)11SELECT * FROM users WHERE a = 1 AND c = 3;1213-- 5. 顺序无关(✓ 优化器会调整)14SELECT * FROM users WHERE b = 2 AND a = 1;不能使用索引的情况:
1-- 1. 跳过a,从b开始(✗)2SELECT * FROM users WHERE b = 2;34-- 2. 跳过a,从c开始(✗)5SELECT * FROM users WHERE c = 3;67-- 3. 只用b和c(✗)8SELECT * FROM users WHERE b = 2 AND c = 3;范围查询的影响:
1-- 1. a使用等值,b使用范围,c失效2SELECT * FROM users WHERE a = 1 AND b > 2 AND c = 3;3-- 使用索引:a, b4-- 未使用:c56-- 2. a使用范围,b和c失效7SELECT * FROM users WHERE a > 1 AND b = 2 AND c = 3;8-- 使用索引:a9-- 未使用:b, c索引设计建议:
1. 区分度高的列放前面:
1-- 好的设计2CREATE INDEX idx_status_create_time ON orders(status, create_time);3-- status区分度低,但查询频繁4-- create_time区分度高56-- 如果主要查询是:7WHERE status = 1 AND create_time > '2024-01-01'8-- 这个索引很合适2. 考虑查询频率:
1-- 查询1:WHERE a = 1 AND b = 2(频繁)2-- 查询2:WHERE a = 1(偶尔)3-- 查询3:WHERE b = 2(很少)45-- 推荐索引6CREATE INDEX idx_ab ON table(a, b);7-- 可以覆盖查询1和查询23. 覆盖索引优化:
1-- 查询2SELECT a, b, c FROM users WHERE a = 1 AND b = 2;34-- 索引5CREATE INDEX idx_abc ON users(a, b, c);6-- 覆盖索引,无需回表验证索引使用:
1EXPLAIN SELECT * FROM users WHERE a = 1 AND b = 2;2-- 查看key_len判断使用了几个列3-- a(INT): 4字节4-- a+b(INT): 8字节5-- a+b+c(INT): 12字节22. 为什么InnoDB使用B+树而不是B树?
InnoDB选择B+树作为索引结构是经过深思熟虑的设计决策。
B树 vs B+树结构对比:
B树:
1[10, 20]2 / | \3 [5,8] [15] [25,30]4 / | \ | / | \5 数据节点(每个节点都存数据)B+树:
1[10, 20]2 / | \3 [5,8] [15] [25,30]4 / | \ | / | \5 叶子节点(只有叶子节点存数据)6 [5]→[8]→[10]→[15]→[20]→[25]→[30]7 (叶子节点通过指针连接)B+树的优势:
1. 更高的扇出度(Fan-out):
1B树:2- 非叶子节点存储键和数据3- 每个节点能存储的键较少4- 树的高度较高56B+树:7- 非叶子节点只存储键8- 每个节点能存储更多的键9- 树的高度较低1011示例(假设页大小16KB):12B树:一个节点可能存100个键13B+树:一个节点可能存1000个键1415查找效率:163层B+树可以存储:1000 * 1000 * 1000 = 10亿条记录173层B树可能只能存储:100 * 100 * 100 = 100万条记录2. 范围查询效率高:
1-- 查询范围数据2SELECT * FROM users WHERE id BETWEEN 100 AND 200;34-- B树:5需要多次随机访问不同的节点67-- B+树:81. 定位到起始叶子节点(id=100)92. 顺序遍历叶子节点链表到结束(id=200)103. 所有数据都在叶子节点,且有序连接3. 全表扫描效率高:
1SELECT * FROM users;23-- B树:4需要遍历所有节点(包括非叶子节点)56-- B+树:7只需遍历叶子节点链表4. 稳定的查询性能:
1B树:2- 数据分布在各层3- 查询性能不稳定4- 最好情况:O(1)(根节点)5- 最坏情况:O(log n)(叶子节点)67B+树:8- 所有数据都在叶子节点9- 查询性能稳定10- 所有查询都是:O(log n)5. 更好的磁盘预读:
1操作系统的磁盘预读:2- 一次读取一个页(通常4KB或16KB)3- B+树的节点大小通常设计为页的整数倍45B+树优势:6- 非叶子节点只存键,可以在一个页中存更多键7- 减少磁盘I/O次数8- 提高缓存命中率6. 支持数据库的顺序访问:
1-- ORDER BY查询2SELECT * FROM users ORDER BY id;34-- B+树:5直接遍历叶子节点链表,数据已排序67-- B树:8需要中序遍历整棵树实际案例:
1-- 假设InnoDB页大小16KB,主键INT(4字节),指针6字节23-- B+树非叶子节点:4每个键值对:4 + 6 = 10字节5一个节点可存:16KB / 10B ≈ 1600个键67-- 3层B+树:8第1层:1个节点(根)9第2层:1600个节点10第3层:1600 * 1600 = 256万个节点11可存储:256万条记录1213-- 查询任意记录:14最多3次磁盘I/O为什么不用其他数据结构:
哈希表:
- 不支持范围查询
- 不支持排序
- 不支持模糊查询
二叉搜索树:
- 树的高度太高
- 磁盘I/O次数多
红黑树:
- 树的高度仍然较高
- 不适合磁盘存储
23. MySQL的JOIN查询有哪些类型?
MySQL支持多种JOIN类型,用于连接多个表的数据。
JOIN类型:
1. INNER JOIN(内连接):
1-- 只返回两表都匹配的记录2SELECT u.name, o.order_no3FROM users u4INNER JOIN orders o ON u.id = o.user_id;56-- 简写(等价)7SELECT u.name, o.order_no8FROM users u, orders o9WHERE u.id = o.user_id;结果:
1users: orders: 结果:2id | name user_id | no name | no31 | Alice 1 | A1 Alice| A142 | Bob 1 | A2 Alice| A253 | Carol 2 | B1 Bob | B12. LEFT JOIN(左连接):
1-- 返回左表所有记录,右表匹配的记录2SELECT u.name, o.order_no3FROM users u4LEFT JOIN orders o ON u.id = o.user_id;结果:
1users: orders: 结果:2id | name user_id | no name | no31 | Alice 1 | A1 Alice | A142 | Bob 1 | A2 Alice | A253 | Carol 2 | B1 Bob | B16 Carol | NULL3. RIGHT JOIN(右连接):
1-- 返回右表所有记录,左表匹配的记录2SELECT u.name, o.order_no3FROM users u4RIGHT JOIN orders o ON u.id = o.user_id;4. FULL OUTER JOIN(全外连接):
1-- MySQL不直接支持,需要用UNION模拟2SELECT u.name, o.order_no3FROM users u4LEFT JOIN orders o ON u.id = o.user_id5UNION6SELECT u.name, o.order_no7FROM users u8RIGHT JOIN orders o ON u.id = o.user_id;5. CROSS JOIN(交叉连接):
1-- 笛卡尔积,返回所有可能的组合2SELECT u.name, o.order_no3FROM users u4CROSS JOIN orders o;56-- 结果:3个用户 × 3个订单 = 9条记录6. SELF JOIN(自连接):
1-- 表与自己连接2-- 查找同一部门的员工3SELECT e1.name AS employee, e2.name AS colleague4FROM employees e15JOIN employees e2 ON e1.dept_id = e2.dept_id6WHERE e1.id != e2.id;JOIN优化技巧:
1. 使用小表驱动大表:
1-- 好:小表在前2SELECT *3FROM small_table s4JOIN large_table l ON s.id = l.small_id;56-- 不好:大表在前7SELECT *8FROM large_table l9JOIN small_table s ON l.small_id = s.id;2. 确保JOIN列有索引:
1-- 为JOIN列创建索引2CREATE INDEX idx_user_id ON orders(user_id);34-- 查询会使用索引5SELECT *6FROM users u7JOIN orders o ON u.id = o.user_id;3. 避免JOIN过多表:
1-- 不好:JOIN太多表2SELECT *3FROM t14JOIN t2 ON t1.id = t2.t1_id5JOIN t3 ON t2.id = t3.t2_id6JOIN t4 ON t3.id = t4.t3_id7JOIN t5 ON t4.id = t5.t4_id; -- 性能差89-- 好:分步查询或使用中间表4. 使用STRAIGHT_JOIN强制顺序:
1-- 强制MySQL按指定顺序JOIN2SELECT *3FROM small_table4STRAIGHT_JOIN large_table ON small_table.id = large_table.small_id;实际应用示例:
查找没有订单的用户:
1SELECT u.*2FROM users u3LEFT JOIN orders o ON u.id = o.user_id4WHERE o.id IS NULL;查找有订单的用户:
1SELECT DISTINCT u.*2FROM users u3INNER JOIN orders o ON u.id = o.user_id;统计每个用户的订单数:
1SELECT u.name, COUNT(o.id) AS order_count2FROM users u3LEFT JOIN orders o ON u.id = o.user_id4GROUP BY u.id, u.name;24. 什么是临时表?
临时表是MySQL中用于存储临时数据的特殊表,会话结束后自动删除。
临时表类型:
1. 用户创建的临时表:
1-- 创建临时表2CREATE TEMPORARY TABLE temp_users (3 id INT,4 name VARCHAR(50)5);67-- 插入数据8INSERT INTO temp_users VALUES (1, 'Alice');910-- 查询11SELECT * FROM temp_users;1213-- 会话结束后自动删除特点:
- 只对当前会话可见
- 会话结束自动删除
- 可以与普通表同名(临时表优先)
- 存储在tmpdir目录
2. MySQL内部临时表:
MySQL在执行某些查询时会自动创建内部临时表:
触发场景:
1-- 1. UNION查询2SELECT id FROM users3UNION4SELECT id FROM orders;56-- 2. DISTINCT + ORDER BY不同列7SELECT DISTINCT name FROM users ORDER BY age;89-- 3. GROUP BY + ORDER BY不同列10SELECT name, COUNT(*) FROM users 11GROUP BY name ORDER BY age;1213-- 4. 子查询在FROM子句14SELECT * FROM (15 SELECT * FROM users WHERE age > 1816) AS t;1718-- 5. 使用SQL_BUFFER_RESULT19SELECT SQL_BUFFER_RESULT * FROM users;内部临时表类型:
Memory引擎临时表:
- 存储在内存中
- 速度快
- 受限于tmp_table_size和max_heap_table_size
InnoDB引擎临时表:
- 存储在磁盘
- 当内存临时表超过限制时转换
- 性能较差
查看临时表使用:
1-- 查看临时表统计2SHOW STATUS LIKE 'Created_tmp%';3-- Created_tmp_tables: 创建的临时表数4-- Created_tmp_disk_tables: 磁盘临时表数56-- 使用EXPLAIN查看7EXPLAIN SELECT DISTINCT name FROM users ORDER BY age;8-- Extra: Using temporary优化临时表:
1. 增加内存临时表大小:
1SET tmp_table_size = 64M;2SET max_heap_table_size = 64M;2. 避免产生临时表:
1-- 不好:产生临时表2SELECT DISTINCT name FROM users ORDER BY age;34-- 好:使用索引避免临时表5CREATE INDEX idx_name_age ON users(name, age);6SELECT DISTINCT name FROM users ORDER BY name;3. 使用索引覆盖:
1-- 创建覆盖索引2CREATE INDEX idx_cover ON users(name, age);34-- 查询可以使用覆盖索引5SELECT name, age FROM users WHERE name = 'Alice';临时表应用场景:
1. 复杂查询的中间结果:
1-- 分步处理复杂查询2CREATE TEMPORARY TABLE temp_result AS3SELECT user_id, SUM(amount) AS total4FROM orders5WHERE create_time > '2024-01-01'6GROUP BY user_id;78-- 使用临时表继续处理9SELECT u.name, t.total10FROM users u11JOIN temp_result t ON u.id = t.user_id12WHERE t.total > 1000;2. 数据去重:
1CREATE TEMPORARY TABLE temp_unique AS2SELECT DISTINCT * FROM users;34DELETE FROM users;5INSERT INTO users SELECT * FROM temp_unique;3. 批量数据处理:
1-- 导入数据到临时表2LOAD DATA INFILE 'data.csv'3INTO TABLE temp_import;45-- 验证和清洗6DELETE FROM temp_import WHERE invalid_data;78-- 导入到正式表9INSERT INTO users SELECT * FROM temp_import;25. 如何处理大表的DDL操作?
大表DDL操作(如ALTER TABLE)可能导致长时间锁表,影响业务。
问题:
1-- 传统DDL会锁表2ALTER TABLE large_table ADD COLUMN new_col VARCHAR(50);3-- 可能锁表数小时,期间无法读写解决方案:
1. 使用Online DDL(MySQL 5.6+):
1-- 在线添加列2ALTER TABLE users 3ADD COLUMN phone VARCHAR(20),4ALGORITHM=INPLACE, LOCK=NONE;56-- ALGORITHM选项:7-- COPY: 创建临时表,复制数据(锁表)8-- INPLACE: 原地修改(不锁表或短暂锁表)910-- LOCK选项:11-- NONE: 不锁表12-- SHARED: 允许读,不允许写13-- EXCLUSIVE: 完全锁表支持Online DDL的操作:
1-- 添加索引(INPLACE)2ALTER TABLE users ADD INDEX idx_name(name);34-- 删除索引(INPLACE)5ALTER TABLE users DROP INDEX idx_name;67-- 添加列(INPLACE,末尾)8ALTER TABLE users ADD COLUMN age INT;910-- 修改列默认值(INPLACE)11ALTER TABLE users ALTER COLUMN status SET DEFAULT 1;1213-- 重命名列(INPLACE)14ALTER TABLE users CHANGE old_name new_name VARCHAR(50);不支持Online DDL的操作:
1-- 修改列类型(需要COPY)2ALTER TABLE users MODIFY COLUMN name VARCHAR(100);34-- 添加主键(需要重建表)5ALTER TABLE users ADD PRIMARY KEY (id);2. 使用pt-online-schema-change:
Percona Toolkit工具,适用于所有DDL操作。
1pt-online-schema-change \2 --alter "ADD COLUMN phone VARCHAR(20)" \3 --execute \4 D=mydb,t=users56# 工作原理:7# 1. 创建新表(带新结构)8# 2. 创建触发器(同步数据变更)9# 3. 分批复制数据10# 4. 切换表名11# 5. 删除旧表3. 使用gh-ost:
GitHub开源工具,无触发器方案。
1gh-ost \2 --user=root \3 --password=password \4 --host=localhost \5 --database=mydb \6 --table=users \7 --alter="ADD COLUMN phone VARCHAR(20)" \8 --execute910# 工作原理:11# 1. 创建ghost表(新结构)12# 2. 通过binlog同步数据变更13# 3. 分批复制数据14# 4. 切换表名4. 分批处理:
对于某些操作,可以分批执行:
1-- 删除大量数据2-- 不好:一次删除3DELETE FROM users WHERE create_time < '2020-01-01';45-- 好:分批删除6DELIMITER $$7CREATE PROCEDURE batch_delete()8BEGIN9 DECLARE rows INT DEFAULT 1;10 WHILE rows > 0 DO11 DELETE FROM users 12 WHERE create_time < '2020-01-01'13 LIMIT 1000;14 15 SET rows = ROW_COUNT();16 SELECT SLEEP(0.1); -- 避免影响业务17 END WHILE;18END$$19DELIMITER ;2021CALL batch_delete();5. 使用新表替换:
1-- 1. 创建新表2CREATE TABLE users_new LIKE users;3ALTER TABLE users_new ADD COLUMN phone VARCHAR(20);45-- 2. 复制数据6INSERT INTO users_new SELECT *, NULL FROM users;78-- 3. 切换表名9RENAME TABLE users TO users_old, users_new TO users;1011-- 4. 删除旧表12DROP TABLE users_old;6. 在从库执行:
1-- 1. 在从库停止复制2STOP SLAVE;34-- 2. 执行DDL5ALTER TABLE users ADD COLUMN phone VARCHAR(20);67-- 3. 主从切换8-- 提升从库为主库910-- 4. 在新从库(原主库)执行DDL最佳实践:
1. 选择低峰期:
1-- 在凌晨或业务低峰期执行2. 提前测试:
1-- 在测试环境验证DDL时间2-- 评估对业务的影响3. 监控执行进度:
1-- 查看DDL进度(MySQL 5.7+)2SELECT * FROM performance_schema.events_stages_current;4. 准备回滚方案:
1-- 保留旧表备份2-- 准备快速回滚脚本5. 通知相关人员:
1-- 提前通知运维和开发2-- 准备应急预案26. MySQL的分库分表策略有哪些?
分库分表是应对海量数据和高并发的核心解决方案。
为什么需要分库分表:
- 单表数据量过大(>1000万)
- 并发压力大
- 磁盘I/O瓶颈
- 单库连接数限制
拆分方式:
1. 垂直拆分:
垂直分库:
1原来:2user_db3├── users(用户表)4├── orders(订单表)5└── products(商品表)67拆分后:8user_db(用户库)9└── users1011order_db(订单库)12└── orders1314product_db(商品库)15└── products垂直分表:
1-- 原表:users(字段很多)2id, name, email, phone, address, avatar, bio, settings...34-- 拆分后:5-- users(常用字段)6id, name, email, phone78-- user_profiles(不常用字段)9user_id, address, avatar, bio, settings2. 水平拆分:
水平分库:
1user_db_02└── users(id % 4 = 0)34user_db_15└── users(id % 4 = 1)67user_db_28└── users(id % 4 = 2)910user_db_311└── users(id % 4 = 3)水平分表:
1-- 同一个库,多个表2users_0(id % 4 = 0)3users_1(id % 4 = 1)4users_2(id % 4 = 2)5users_3(id % 4 = 3)分片策略:
1. 范围分片(Range):
1-- 按ID范围2users_0: id 1-10000003users_1: id 1000001-20000004users_2: id 2000001-300000056-- 按时间范围7orders_2024_018orders_2024_029orders_2024_03优点:
- 扩容简单
- 范围查询效率高
缺点:
- 数据分布可能不均匀
- 热点问题
2. 哈希分片(Hash):
1// 按用户ID取模2int shardIndex = userId % 4;3String tableName = "users_" + shardIndex;优点:
- 数据分布均匀
- 无热点问题
缺点:
- 扩容困难
- 范围查询需要查所有分片
3. 一致性哈希:
1// 使用一致性哈希环2int hash = consistentHash(userId);3String shardKey = getShardByHash(hash);优点:
- 扩容时数据迁移少
4. 地理位置分片:
1-- 按地区2users_beijing3users_shanghai4users_guangzhou5. 时间分片:
1-- 按月份2orders_2024013orders_2024024orders_202403分库分表中间件:
1. ShardingSphere(推荐):
1# 配置示例2shardingSphere:3 dataSources:4 ds0: # 数据源05 ds1: # 数据源16 shardingRule:7 tables:8 users:9 actualDataNodes: ds${0..1}.users_${0..3}10 databaseStrategy:11 inline:12 shardingColumn: user_id13 algorithmExpression: ds${user_id % 2}14 tableStrategy:15 inline:16 shardingColumn: user_id17 algorithmExpression: users_${user_id % 4}2. MyCAT:
1<table name="users" dataNode="dn1,dn2,dn3,dn4" rule="mod-long">2 <childTable name="orders" joinKey="user_id" parentKey="id"/>3</table>分库分表带来的问题:
1. 跨库JOIN:
1// 解决方案:2// 1. 应用层JOIN3List<User> users = userDao.getUsers(ids);4List<Order> orders = orderDao.getOrders(ids);5// 在应用层关联67// 2. 数据冗余8// 在订单表中冗余用户信息910// 3. 全局表11// 字典表在每个库都有一份2. 分布式事务:
1// 解决方案:2// 1. 避免跨库事务3// 2. 使用最终一致性4// 3. 使用分布式事务框架(Seata)3. 全局唯一ID:
1// 解决方案:2// 1. 雪花算法(Snowflake)3long id = snowflake.nextId();45// 2. 数据库号段模式6// 3. Redis自增7// 4. UUID4. 分页查询:
1-- 问题:跨分片分页2SELECT * FROM users ORDER BY create_time LIMIT 0, 20;34-- 解决方案:5-- 1. 每个分片查询,应用层合并排序6-- 2. 使用ES等搜索引擎最佳实践:
- 优先考虑优化,再考虑分库分表
- 选择合适的分片键(高频查询字段)
- 避免跨库JOIN和事务
- 提前规划扩容方案
- 做好数据迁移工具
27. 什么是SQL注入?如何防止?
SQL注入是最常见的Web安全漏洞,攻击者通过在输入中插入恶意SQL代码来攻击数据库。
SQL注入示例:
场景1:登录绕过:
1// 危险代码2String sql = "SELECT * FROM users WHERE username = '" + username 3 + "' AND password = '" + password + "'";45// 攻击输入6username: admin' OR '1'='17password: anything89// 实际执行的SQL10SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = 'anything'11// 永远返回true,绕过登录场景2:数据泄露:
1// 危险代码2String sql = "SELECT * FROM products WHERE id = " + productId;34// 攻击输入5productId: 1 UNION SELECT username, password FROM users67// 实际执行的SQL8SELECT * FROM products WHERE id = 1 UNION SELECT username, password FROM users9// 泄露用户数据场景3:数据破坏:
1// 危险代码2String sql = "UPDATE users SET email = '" + email + "' WHERE id = " + userId;34// 攻击输入5email: test@test.com'; DROP TABLE users; --67// 实际执行的SQL8UPDATE users SET email = 'test@test.com'; DROP TABLE users; --' WHERE id = 19// 删除整个表防止SQL注入的方法:
1. 使用预编译语句(PreparedStatement):
1// 安全代码2String sql = "SELECT * FROM users WHERE username = ? AND password = ?";3PreparedStatement pstmt = conn.prepareStatement(sql);4pstmt.setString(1, username);5pstmt.setString(2, password);6ResultSet rs = pstmt.executeQuery();78// 原理:参数会被正确转义,不会被当作SQL代码执行2. 使用ORM框架:
1// MyBatis(安全)2@Select("SELECT * FROM users WHERE username = #{username}")3User findByUsername(String username);45// 注意:使用#{}而不是${}6// #{} 会预编译7// ${} 会直接拼接(危险)3. 输入验证和过滤:
1// 白名单验证2public boolean isValidUsername(String username) {3 return username.matches("^[a-zA-Z0-9_]{3,20}$");4}56// 过滤特殊字符7public String escapeSQL(String input) {8 return input.replace("'", "''")9 .replace("\\", "\\\\")10 .replace(";", "");11}4. 最小权限原则:
1-- 应用程序使用的数据库账号只授予必要权限2GRANT SELECT, INSERT, UPDATE ON mydb.* TO 'app_user'@'localhost';3-- 不要授予DROP、CREATE等危险权限5. 错误信息处理:
1// 不好:暴露数据库信息2catch (SQLException e) {3 return "Error: " + e.getMessage();4}56// 好:返回通用错误信息7catch (SQLException e) {8 logger.error("Database error", e);9 return "操作失败,请稍后重试";10}6. 使用存储过程:
1-- 创建存储过程2DELIMITER $$3CREATE PROCEDURE getUserByUsername(IN p_username VARCHAR(50))4BEGIN5 SELECT * FROM users WHERE username = p_username;6END$$7DELIMITER ;89-- 调用存储过程10CALL getUserByUsername('admin');7. Web应用防火墙(WAF):
- 使用ModSecurity等WAF
- 检测和阻止SQL注入攻击
检测SQL注入:
1# 使用sqlmap工具2sqlmap -u "http://example.com/product?id=1" --dbs34# 手动测试5# 1. 单引号测试6id=1'78# 2. 逻辑测试9id=1 OR 1=11011# 3. 时间延迟测试12id=1 AND SLEEP(5)28. MySQL的存储过程和函数有什么区别?
**存储过程(Procedure)和函数(Function)**都是预编译的SQL代码块,但有重要区别。
主要区别:
| 特性 | 存储过程 | 函数 |
|---|---|---|
| 返回值 | 可以有多个(OUT参数) | 只能返回一个值 |
| 调用方式 | CALL procedure_name() | SELECT function_name() |
| 事务 | 可以包含事务 | 不能包含事务 |
| DML操作 | 可以执行 | 受限制 |
| 使用场景 | 复杂业务逻辑 | 计算和转换 |
存储过程示例:
1-- 创建存储过程2DELIMITER $$3CREATE PROCEDURE transfer_money(4 IN from_account INT,5 IN to_account INT,6 IN amount DECIMAL(10,2),7 OUT result VARCHAR(50)8)9BEGIN10 DECLARE from_balance DECIMAL(10,2);11 12 -- 开启事务13 START TRANSACTION;14 15 -- 检查余额16 SELECT balance INTO from_balance 17 FROM accounts WHERE id = from_account FOR UPDATE;18 19 IF from_balance < amount THEN20 SET result = '余额不足';21 ROLLBACK;22 ELSE23 -- 扣款24 UPDATE accounts SET balance = balance - amount 25 WHERE id = from_account;26 27 -- 入账28 UPDATE accounts SET balance = balance + amount 29 WHERE id = to_account;30 31 SET result = '转账成功';32 COMMIT;33 END IF;34END$$35DELIMITER ;3637-- 调用存储过程38CALL transfer_money(1, 2, 100.00, @result);39SELECT @result;函数示例:
1-- 创建函数2DELIMITER $$3CREATE FUNCTION calculate_age(birth_date DATE)4RETURNS INT5DETERMINISTIC6BEGIN7 DECLARE age INT;8 SET age = YEAR(CURDATE()) - YEAR(birth_date);9 10 IF DATE_FORMAT(CURDATE(), '%m%d') < DATE_FORMAT(birth_date, '%m%d') THEN11 SET age = age - 1;12 END IF;13 14 RETURN age;15END$$16DELIMITER ;1718-- 使用函数19SELECT name, calculate_age(birth_date) AS age FROM users;2021-- 在WHERE子句中使用22SELECT * FROM users WHERE calculate_age(birth_date) > 18;存储过程的优点:
1-- 1. 可以返回多个值2CREATE PROCEDURE get_user_stats(3 IN user_id INT,4 OUT order_count INT,5 OUT total_amount DECIMAL(10,2)6)7BEGIN8 SELECT COUNT(*), SUM(amount) 9 INTO order_count, total_amount10 FROM orders WHERE user_id = user_id;11END;1213-- 2. 可以执行复杂逻辑14CREATE PROCEDURE process_orders()15BEGIN16 DECLARE done INT DEFAULT 0;17 DECLARE order_id INT;18 DECLARE cur CURSOR FOR SELECT id FROM orders WHERE status = 'pending';19 DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;20 21 OPEN cur;22 23 read_loop: LOOP24 FETCH cur INTO order_id;25 IF done THEN26 LEAVE read_loop;27 END IF;28 29 -- 处理订单30 UPDATE orders SET status = 'processing' WHERE id = order_id;31 END LOOP;32 33 CLOSE cur;34END;函数的限制:
1-- 函数不能执行以下操作:2-- 1. 不能修改表数据(在某些配置下)3-- 2. 不能使用事务4-- 3. 不能调用存储过程5-- 4. 不能使用动态SQL何时使用:
使用存储过程:
- 复杂的业务逻辑
- 需要事务处理
- 需要多个返回值
- 批量数据处理
使用函数:
- 简单的计算
- 数据转换
- 需要在SELECT中使用
- 返回单个值
29. 什么是触发器?
**触发器(Trigger)**是在特定数据库事件发生时自动执行的存储过程。
触发器类型:
按时机分类:
- BEFORE:在事件发生前触发
- AFTER:在事件发生后触发
按事件分类:
- INSERT:插入数据时
- UPDATE:更新数据时
- DELETE:删除数据时
创建触发器:
1. BEFORE INSERT触发器:
1-- 自动设置创建时间2DELIMITER $$3CREATE TRIGGER before_user_insert4BEFORE INSERT ON users5FOR EACH ROW6BEGIN7 SET NEW.create_time = NOW();8 SET NEW.update_time = NOW();9END$$10DELIMITER ;1112-- 测试13INSERT INTO users (name, email) VALUES ('Alice', 'alice@test.com');14-- create_time和update_time会自动设置2. AFTER INSERT触发器:
1-- 插入订单后更新统计2DELIMITER $$3CREATE TRIGGER after_order_insert4AFTER INSERT ON orders5FOR EACH ROW6BEGIN7 UPDATE user_stats 8 SET order_count = order_count + 1,9 total_amount = total_amount + NEW.amount10 WHERE user_id = NEW.user_id;11END$$12DELIMITER ;3. BEFORE UPDATE触发器:
1-- 更新时自动记录修改时间2DELIMITER $$3CREATE TRIGGER before_user_update4BEFORE UPDATE ON users5FOR EACH ROW6BEGIN7 SET NEW.update_time = NOW();8 9 -- 防止某些字段被修改10 IF OLD.id != NEW.id THEN11 SIGNAL SQLSTATE '45000'12 SET MESSAGE_TEXT = '不能修改ID';13 END IF;14END$$15DELIMITER ;4. AFTER DELETE触发器:
1-- 删除用户时记录日志2DELIMITER $$3CREATE TRIGGER after_user_delete4AFTER DELETE ON users5FOR EACH ROW6BEGIN7 INSERT INTO user_delete_log (user_id, username, delete_time)8 VALUES (OLD.id, OLD.name, NOW());9END$$10DELIMITER ;实际应用场景:
1. 审计日志:
1CREATE TRIGGER audit_user_changes2AFTER UPDATE ON users3FOR EACH ROW4BEGIN5 INSERT INTO audit_log (6 table_name, 7 record_id, 8 old_value, 9 new_value, 10 change_time11 ) VALUES (12 'users',13 NEW.id,14 CONCAT('name:', OLD.name, ',email:', OLD.email),15 CONCAT('name:', NEW.name, ',email:', NEW.email),16 NOW()17 );18END;2. 数据验证:
1CREATE TRIGGER validate_order2BEFORE INSERT ON orders3FOR EACH ROW4BEGIN5 IF NEW.amount <= 0 THEN6 SIGNAL SQLSTATE '45000'7 SET MESSAGE_TEXT = '订单金额必须大于0';8 END IF;9 10 IF NEW.quantity <= 0 THEN11 SIGNAL SQLSTATE '45000'12 SET MESSAGE_TEXT = '订单数量必须大于0';13 END IF;14END;3. 数据同步:
1CREATE TRIGGER sync_to_cache2AFTER UPDATE ON products3FOR EACH ROW4BEGIN5 -- 标记缓存需要更新6 INSERT INTO cache_invalidation (table_name, record_id)7 VALUES ('products', NEW.id);8END;查看和管理触发器:
1-- 查看所有触发器2SHOW TRIGGERS;34-- 查看特定表的触发器5SHOW TRIGGERS WHERE `Table` = 'users';67-- 查看触发器定义8SHOW CREATE TRIGGER trigger_name;910-- 删除触发器11DROP TRIGGER IF EXISTS trigger_name;触发器的注意事项:
优点:
- 自动执行,无需应用层干预
- 保证数据完整性
- 集中管理业务规则
缺点:
- 隐藏的业务逻辑,难以调试
- 可能影响性能
- 触发器链可能导致复杂问题
- 不易维护
最佳实践:
- 避免复杂逻辑
- 不要在触发器中调用存储过程
- 避免触发器链
- 做好文档记录
- 考虑使用应用层逻辑替代
30. MySQL的视图是什么?
**视图(View)**是基于SQL查询结果的虚拟表,不存储实际数据。
创建视图:
1-- 简单视图2CREATE VIEW active_users AS3SELECT id, name, email4FROM users5WHERE status = 'active';67-- 使用视图8SELECT * FROM active_users;910-- 复杂视图(多表JOIN)11CREATE VIEW order_details AS12SELECT 13 o.id AS order_id,14 o.order_no,15 u.name AS user_name,16 p.name AS product_name,17 o.quantity,18 o.amount19FROM orders o20JOIN users u ON o.user_id = u.id21JOIN products p ON o.product_id = p.id;视图的优点:
1. 简化复杂查询:
1-- 不用每次都写复杂的JOIN2SELECT * FROM order_details WHERE user_name = 'Alice';34-- 而不是5SELECT o.*, u.name, p.name6FROM orders o7JOIN users u ON o.user_id = u.id8JOIN products p ON o.product_id = p.id9WHERE u.name = 'Alice';2. 数据安全性:
1-- 只暴露部分字段2CREATE VIEW public_users AS3SELECT id, name, email4FROM users;5-- 隐藏了password、phone等敏感字段67-- 授权给应用8GRANT SELECT ON public_users TO 'app_user'@'localhost';3. 逻辑独立性:
1-- 表结构变化时,只需修改视图2-- 应用代码不需要改动可更新视图:
1-- 简单视图可以更新2CREATE VIEW simple_view AS3SELECT id, name, email FROM users;45-- 可以执行INSERT、UPDATE、DELETE6UPDATE simple_view SET name = 'Bob' WHERE id = 1;78-- 复杂视图(包含JOIN、聚合等)通常不可更新WITH CHECK OPTION:
1-- 确保通过视图的更新符合视图条件2CREATE VIEW active_users AS3SELECT * FROM users WHERE status = 'active'4WITH CHECK OPTION;56-- 这个更新会失败,因为违反了视图条件7UPDATE active_users SET status = 'inactive' WHERE id = 1;视图管理:
1-- 查看所有视图2SHOW FULL TABLES WHERE TABLE_TYPE = 'VIEW';34-- 查看视图定义5SHOW CREATE VIEW view_name;67-- 修改视图8ALTER VIEW active_users AS9SELECT id, name, email, phone10FROM users11WHERE status = 'active';1213-- 删除视图14DROP VIEW IF EXISTS view_name;物化视图:
MySQL不直接支持物化视图,但可以模拟:
1-- 创建实体表存储视图数据2CREATE TABLE mv_order_stats AS3SELECT 4 user_id,5 COUNT(*) AS order_count,6 SUM(amount) AS total_amount7FROM orders8GROUP BY user_id;910-- 创建索引11CREATE INDEX idx_user_id ON mv_order_stats(user_id);1213-- 定期刷新(使用事件调度器)14CREATE EVENT refresh_order_stats15ON SCHEDULE EVERY 1 HOUR16DO17BEGIN18 TRUNCATE TABLE mv_order_stats;19 INSERT INTO mv_order_stats20 SELECT user_id, COUNT(*), SUM(amount)21 FROM orders22 GROUP BY user_id;23END;视图的性能考虑:
问题:
- 视图每次查询都会执行底层SQL
- 复杂视图可能性能较差
优化:
1-- 1. 为视图涉及的表创建索引2CREATE INDEX idx_status ON users(status);34-- 2. 避免嵌套视图5-- 不好6CREATE VIEW view1 AS SELECT * FROM table1;7CREATE VIEW view2 AS SELECT * FROM view1;89-- 3. 使用物化视图(实体表)代替最佳实践:
- 用于简化复杂查询
- 用于数据安全控制
- 避免过度使用
- 注意性能影响
- 做好文档记录
31. 如何进行MySQL性能监控?
MySQL性能监控帮助及时发现和解决性能问题。
监控工具和方法:
1. SHOW STATUS:
1-- 查看服务器状态2SHOW GLOBAL STATUS;34-- 关键指标5SHOW STATUS LIKE 'Threads_connected'; -- 当前连接数6SHOW STATUS LIKE 'Threads_running'; -- 活跃连接数7SHOW STATUS LIKE 'Queries'; -- 查询总数8SHOW STATUS LIKE 'Slow_queries'; -- 慢查询数9SHOW STATUS LIKE 'Innodb_buffer_pool_read_requests'; -- Buffer Pool读请求10SHOW STATUS LIKE 'Innodb_buffer_pool_reads'; -- 磁盘读取次数2. Performance Schema:
1-- 启用Performance Schema2UPDATE performance_schema.setup_instruments 3SET ENABLED = 'YES', TIMED = 'YES';45-- 查看最耗时的SQL6SELECT 7 DIGEST_TEXT,8 COUNT_STAR,9 AVG_TIMER_WAIT/1000000000 AS avg_ms10FROM performance_schema.events_statements_summary_by_digest11ORDER BY AVG_TIMER_WAIT DESC12LIMIT 10;1314-- 查看表的I/O统计15SELECT * FROM performance_schema.table_io_waits_summary_by_table16ORDER BY sum_timer_wait DESC LIMIT 10;3. 慢查询日志:
1-- 启用慢查询日志2SET GLOBAL slow_query_log = ON;3SET GLOBAL long_query_time = 1;45-- 分析慢查询6mysqldumpslow -s t -t 10 /var/log/mysql/slow.log4. SHOW PROCESSLIST:
1-- 查看当前执行的查询2SHOW FULL PROCESSLIST;34-- 杀死长时间运行的查询5KILL QUERY 12345;5. 监控工具:
- Prometheus + Grafana:开源监控方案
- Percona Monitoring and Management (PMM):专业MySQL监控
- MySQL Enterprise Monitor:官方商业监控
- Zabbix:通用监控平台
关键性能指标(KPI):
11. QPS(每秒查询数)22. TPS(每秒事务数)33. 连接数44. 慢查询数量55. Buffer Pool命中率66. 锁等待时间77. 复制延迟88. 磁盘I/O32. 什么是MySQL的连接池?
连接池是预先创建并维护一定数量的数据库连接,供应用程序重复使用。
为什么需要连接池:
1// 不使用连接池(性能差)2for (int i = 0; i < 1000; i++) {3 Connection conn = DriverManager.getConnection(url, user, password);4 // 执行SQL5 conn.close(); // 每次都创建和关闭连接,开销大6}78// 使用连接池(性能好)9for (int i = 0; i < 1000; i++) {10 Connection conn = dataSource.getConnection(); // 从池中获取11 // 执行SQL12 conn.close(); // 归还到池中,不是真正关闭13}常用连接池:
1. HikariCP(推荐):
1HikariConfig config = new HikariConfig();2config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");3config.setUsername("root");4config.setPassword("password");5config.setMaximumPoolSize(10); // 最大连接数6config.setMinimumIdle(5); // 最小空闲连接数7config.setConnectionTimeout(30000); // 连接超时8config.setIdleTimeout(600000); // 空闲超时9config.setMaxLifetime(1800000); // 最大生命周期1011HikariDataSource dataSource = new HikariDataSource(config);2. Druid:
1DruidDataSource dataSource = new DruidDataSource();2dataSource.setUrl("jdbc:mysql://localhost:3306/mydb");3dataSource.setUsername("root");4dataSource.setPassword("password");5dataSource.setInitialSize(5); // 初始连接数6dataSource.setMaxActive(20); // 最大连接数7dataSource.setMinIdle(5); // 最小空闲连接数8dataSource.setMaxWait(60000); // 获取连接最大等待时间9dataSource.setValidationQuery("SELECT 1"); // 验证查询连接池参数配置:
1# 核心参数2maximumPoolSize=20 # 最大连接数(CPU核心数 * 2 + 磁盘数)3minimumIdle=5 # 最小空闲连接数4connectionTimeout=30000 # 获取连接超时时间(毫秒)5idleTimeout=600000 # 空闲连接超时时间(10分钟)6maxLifetime=1800000 # 连接最大生命周期(30分钟)78# 连接测试9connectionTestQuery=SELECT 110validationTimeout=5000连接池监控:
1// HikariCP监控2HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();3System.out.println("Active: " + poolMXBean.getActiveConnections());4System.out.println("Idle: " + poolMXBean.getIdleConnections());5System.out.println("Total: " + poolMXBean.getTotalConnections());6System.out.println("Waiting: " + poolMXBean.getThreadsAwaitingConnection());33. InnoDB和MyISAM的索引实现有什么区别?
核心区别:聚簇索引 vs 非聚簇索引
InnoDB(聚簇索引):
1主键索引(聚簇索引):2 索引和数据存储在一起3 叶子节点存储完整的行数据4 5 [主键索引树]6 107 / \8 5 159 / \ / \10 [完整行数据]1112二级索引(非聚簇索引):13 叶子节点存储主键值14 需要回表查询15 16 [name索引树]17 'Bob'18 / \19 'Alice' 'Carol'20 | |21 [主键5] [主键15]MyISAM(非聚簇索引):
1主键索引:2 索引和数据分开存储3 叶子节点存储数据文件指针4 5 [主键索引树]6 107 / \8 5 159 / \ / \10 [指针] [指针]11 ↓ ↓12 [数据文件]1314二级索引:15 与主键索引结构相同16 都存储数据文件指针对比:
| 特性 | InnoDB | MyISAM |
|---|---|---|
| 索引类型 | 聚簇索引 | 非聚簇索引 |
| 主键查询 | 一次查询 | 两次查询(索引+数据) |
| 二级索引查询 | 两次查询(索引+回表) | 两次查询(索引+数据) |
| 数据存储 | 按主键顺序 | 按插入顺序 |
| 表文件 | .ibd(索引+数据) | .MYI(索引)+ .MYD(数据) |
性能影响:
1-- InnoDB主键查询(快)2SELECT * FROM users WHERE id = 100; -- 一次B+树查找34-- InnoDB二级索引查询(需回表)5SELECT * FROM users WHERE name = 'Alice'; 6-- 1. name索引查找 -> 得到主键7-- 2. 主键索引查找 -> 得到完整数据89-- MyISAM所有查询都需要两次10SELECT * FROM users WHERE id = 100;11-- 1. 索引查找 -> 得到指针12-- 2. 根据指针读取数据34. 什么是自增主键?为什么推荐使用?
自增主键是数据库自动生成的递增整数主键。
定义自增主键:
1CREATE TABLE users (2 id INT AUTO_INCREMENT PRIMARY KEY,3 name VARCHAR(50)4);56-- 插入数据时不需要指定id7INSERT INTO users (name) VALUES ('Alice');8INSERT INTO users (name) VALUES ('Bob');9-- id自动为1, 2, 3...为什么推荐使用:
1. 顺序插入,减少页分裂:
1使用自增主键:2[1][2][3][4][5] -> 插入6 -> [1][2][3][4][5][6]3顺序插入,不需要移动数据45使用UUID:6[uuid1][uuid3][uuid5] -> 插入uuid2 -> [uuid1][uuid2][uuid3][uuid5]7需要移动数据,页分裂频繁2. 索引效率高:
1-- 自增主键:B+树紧凑2-- 范围查询效率高3SELECT * FROM users WHERE id BETWEEN 100 AND 200;45-- UUID:B+树稀疏6-- 范围查询效率低3. 占用空间小:
1-- INT:4字节2id INT AUTO_INCREMENT34-- BIGINT:8字节5id BIGINT AUTO_INCREMENT67-- UUID:36字节(字符串)或16字节(BINARY)8id CHAR(36)4. 性能对比:
1插入性能:2- 自增主键:顺序写入,快3- UUID:随机写入,慢45索引大小:6- 自增主键:小7- UUID:大(约3-4倍)89查询性能:10- 自增主键:快11- UUID:慢自增主键的问题:
1. 分布式环境ID冲突:
1// 解决方案1:雪花算法2long id = snowflake.nextId();34// 解决方案2:数据库号段5// 每个服务器分配不同的起始值和步长6// 服务器1:1, 3, 5, 7...7// 服务器2:2, 4, 6, 8...2. 数据迁移:
1-- 设置起始值2ALTER TABLE users AUTO_INCREMENT = 10000;3. 安全问题:
1暴露数据量:2/user/13/user/24/user/35可以看出有3个用户67解决:使用业务ID(订单号)而不是自增ID35. MySQL的COUNT(*)和COUNT(1)有什么区别?
结论:在InnoDB中,COUNT(*) 和 COUNT(1) 性能基本相同。
COUNT的几种形式:
1-- 1. COUNT(*):统计所有行2SELECT COUNT(*) FROM users;34-- 2. COUNT(1):统计所有行5SELECT COUNT(1) FROM users;67-- 3. COUNT(列名):统计非NULL的行8SELECT COUNT(name) FROM users;910-- 4. COUNT(DISTINCT 列名):统计不重复的非NULL行11SELECT COUNT(DISTINCT name) FROM users;性能对比:
InnoDB:
1-- COUNT(*) 和 COUNT(1) 性能相同2-- 都会选择最小的索引进行扫描3EXPLAIN SELECT COUNT(*) FROM users;4EXPLAIN SELECT COUNT(1) FROM users;5-- 执行计划相同67-- COUNT(列名) 需要判断NULL8SELECT COUNT(name) FROM users;9-- 稍慢,因为需要检查每行的name是否为NULLMyISAM:
1-- MyISAM维护了行数,COUNT(*)非常快2SELECT COUNT(*) FROM users; -- O(1)34-- 但有WHERE条件时,仍需扫描5SELECT COUNT(*) FROM users WHERE age > 18; -- O(n)优化COUNT查询:
1. 使用覆盖索引:
1-- 创建索引2CREATE INDEX idx_status ON users(status);34-- 使用覆盖索引5SELECT COUNT(*) FROM users WHERE status = 1;6-- 只扫描索引,不回表2. 使用近似值:
1-- 使用EXPLAIN估算2EXPLAIN SELECT * FROM users;3-- rows列显示估算行数45-- 查询统计信息6SELECT TABLE_ROWS 7FROM information_schema.TABLES 8WHERE TABLE_NAME = 'users';3. 维护计数表:
1-- 创建计数表2CREATE TABLE user_count (3 count INT4);56-- 使用触发器维护7CREATE TRIGGER after_user_insert8AFTER INSERT ON users9FOR EACH ROW10UPDATE user_count SET count = count + 1;1112-- 查询时直接读取13SELECT count FROM user_count;4. 使用Redis缓存:
1// 插入用户时2userDao.insert(user);3redis.incr("user:count");45// 查询总数6long count = redis.get("user:count");实际应用建议:
1-- 1. 小表(<10万):直接COUNT2SELECT COUNT(*) FROM small_table;34-- 2. 大表无WHERE:使用统计表或缓存5SELECT count FROM count_cache;67-- 3. 大表有WHERE:优化索引8CREATE INDEX idx_condition ON big_table(condition_column);9SELECT COUNT(*) FROM big_table WHERE condition_column = value;1011-- 4. 分页场景:使用游标分页代替COUNT12-- 不要:SELECT COUNT(*) + LIMIT offset13-- 使用:WHERE id > last_id LIMIT 2036. 如何处理MySQL的大数据量分页?
传统分页问题:
1-- 深度分页性能差2SELECT * FROM users ORDER BY id LIMIT 1000000, 20;3-- 需要扫描1000020行,丢弃前1000000行优化方案:
1. 使用子查询优化:
1-- 先查ID,再关联2SELECT * FROM users 3WHERE id >= (SELECT id FROM users ORDER BY id LIMIT 1000000, 1)4LIMIT 20;2. 使用游标分页(推荐):
1-- 记录上次最后一条的ID2SELECT * FROM users WHERE id > 1000000 ORDER BY id LIMIT 20;3. 延迟关联:
1SELECT * FROM users u2INNER JOIN (3 SELECT id FROM users ORDER BY id LIMIT 1000000, 204) AS t ON u.id = t.id;37. 什么是MySQL的半同步复制?
半同步复制是介于异步复制和全同步复制之间的方案。
工作原理:
11. 主库执行事务22. 写入binlog33. 等待至少一个从库确认接收binlog44. 返回客户端成功配置:
1-- 主库安装插件2INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';34-- 从库安装插件5INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';67-- 启用半同步8SET GLOBAL rpl_semi_sync_master_enabled = 1;9SET GLOBAL rpl_semi_sync_slave_enabled = 1;38. 如何优化INSERT语句?
优化方法:
1. 批量插入:
1-- 不好2INSERT INTO users VALUES (1, 'A');3INSERT INTO users VALUES (2, 'B');45-- 好6INSERT INTO users VALUES (1, 'A'), (2, 'B'), (3, 'C');2. 使用LOAD DATA:
1LOAD DATA INFILE 'data.csv'2INTO TABLE users3FIELDS TERMINATED BY ','4LINES TERMINATED BY '\n';3. 禁用索引:
1ALTER TABLE users DISABLE KEYS;2-- 批量插入3ALTER TABLE users ENABLE KEYS;4. 调整参数:
1SET autocommit = 0;2-- 批量插入3COMMIT;39. MySQL的GROUP BY和HAVING有什么区别?
区别:
| 特性 | WHERE | HAVING |
|---|---|---|
| 作用时机 | 分组前 | 分组后 |
| 作用对象 | 行 | 组 |
| 可用函数 | 不能用聚合函数 | 可以用聚合函数 |
示例:
1SELECT 2 department,3 COUNT(*) as emp_count,4 AVG(salary) as avg_salary5FROM employees6WHERE salary > 5000 -- 过滤行7GROUP BY department8HAVING COUNT(*) > 10; -- 过滤组40. 什么是外键约束?
外键约束保证引用完整性。
创建外键:
1CREATE TABLE orders (2 id INT PRIMARY KEY,3 user_id INT,4 FOREIGN KEY (user_id) REFERENCES users(id)5 ON DELETE CASCADE6 ON UPDATE CASCADE7);级联操作:
- CASCADE:级联删除/更新
- SET NULL:设置为NULL
- RESTRICT:拒绝操作
- NO ACTION:不做任何操作
41-60题简要答案
41. MySQL的UNION和UNION ALL有什么区别?
UNION:合并结果集并去重 UNION ALL:合并结果集不去重(性能更好)
1SELECT id FROM users2UNION -- 去重,慢3SELECT id FROM orders;45SELECT id FROM users6UNION ALL -- 不去重,快7SELECT id FROM orders;42. 如何查看MySQL的执行计划?
使用EXPLAIN或EXPLAIN ANALYZE:
1EXPLAIN SELECT * FROM users WHERE name = 'Alice';2EXPLAIN ANALYZE SELECT * FROM users WHERE name = 'Alice';43. 什么是MySQL的全文索引?
全文索引用于文本搜索:
1CREATE FULLTEXT INDEX idx_content ON articles(content);23SELECT * FROM articles 4WHERE MATCH(content) AGAINST('MySQL' IN NATURAL LANGUAGE MODE);44. 如何处理MySQL的乱码问题?
原因:字符集不一致
解决:
1-- 统一使用utf8mb42ALTER DATABASE mydb CHARACTER SET utf8mb4;3ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4;4SET NAMES utf8mb4;45. MySQL的LEFT JOIN和RIGHT JOIN有什么区别?
LEFT JOIN:保留左表所有记录 RIGHT JOIN:保留右表所有记录
1SELECT * FROM users u LEFT JOIN orders o ON u.id = o.user_id;2-- 返回所有用户,包括没有订单的用户46. 什么是MySQL的复合索引?
复合索引是包含多个列的索引:
1CREATE INDEX idx_name_age ON users(name, age);2-- 遵循最左前缀原则47. 如何进行MySQL的容量规划?
评估因素:
- 数据增长速度
- 查询QPS
- 存储空间需求
- 备份策略
- 扩展性需求
48. 什么是MySQL的查询缓存?
查询缓存(MySQL 8.0已移除):
1-- MySQL 5.72SET GLOBAL query_cache_type = ON;3SET GLOBAL query_cache_size = 256M;49. 如何优化MySQL的配置参数?
关键参数:
1innodb_buffer_pool_size = 8G # 物理内存的50-80%2innodb_log_file_size = 512M3max_connections = 10004innodb_flush_log_at_trx_commit = 150. 什么是MySQL的事件调度器?
事件调度器用于定时任务:
1CREATE EVENT clean_old_data2ON SCHEDULE EVERY 1 DAY3DO4DELETE FROM logs WHERE create_time < DATE_SUB(NOW(), INTERVAL 30 DAY);51. 如何处理MySQL的锁等待超时?
配置超时时间:
1SET innodb_lock_wait_timeout = 50; -- 秒52. 什么是MySQL的游标?
游标用于逐行处理结果集:
1DECLARE cur CURSOR FOR SELECT id FROM users;2OPEN cur;3FETCH cur INTO user_id;4CLOSE cur;53. 如何进行MySQL的版本升级?
步骤:
- 备份数据
- 测试环境验证
- 检查兼容性
- 灰度升级
- 监控观察
54. 什么是MySQL的分布式事务?
XA事务支持分布式事务:
1XA START 'xid1';2UPDATE accounts SET balance = balance - 100 WHERE id = 1;3XA END 'xid1';4XA PREPARE 'xid1';5XA COMMIT 'xid1';55. 如何处理MySQL的主键冲突?
方法:
1-- 忽略冲突2INSERT IGNORE INTO users VALUES (1, 'Alice');34-- 更新冲突5INSERT INTO users VALUES (1, 'Alice')6ON DUPLICATE KEY UPDATE name = 'Alice';78-- 替换9REPLACE INTO users VALUES (1, 'Alice');56. 什么是MySQL的存储函数?
存储函数返回单个值:
1CREATE FUNCTION get_user_name(uid INT)2RETURNS VARCHAR(50)3BEGIN4 DECLARE uname VARCHAR(50);5 SELECT name INTO uname FROM users WHERE id = uid;6 RETURN uname;7END;57. 如何进行MySQL的安全加固?
措施:
- 最小权限原则
- 禁用root远程登录
- 使用SSL连接
- 定期更新密码
- 启用审计日志
58. 什么是MySQL的复制延迟?
复制延迟是主从数据同步的时间差。
监控:
1SHOW SLAVE STATUS\G2-- 查看Seconds_Behind_Master59. 如何优化MySQL的磁盘I/O?
优化方法:
- 使用SSD
- RAID配置
- 调整innodb_flush_method
- 增加Buffer Pool
- 优化SQL减少I/O
60. MySQL 8.0有哪些新特性?
主要新特性:
- 窗口函数:ROW_NUMBER(), RANK()
- CTE(公用表表达式):WITH子句
- JSON增强:JSON_TABLE()
- 隐藏索引:测试索引影响
- 降序索引:真正的降序
- 默认utf8mb4
- 原子DDL
- 角色管理
1-- 窗口函数2SELECT 3 name,4 salary,5 ROW_NUMBER() OVER (ORDER BY salary DESC) as rank6FROM employees;78-- CTE9WITH high_salary AS (10 SELECT * FROM employees WHERE salary > 1000011)12SELECT * FROM high_salary;总结
本文档涵盖了MySQL面试的60个核心问题,包括:
- 存储引擎和索引:InnoDB、MyISAM、B+树、索引优化
- 事务和锁:ACID、隔离级别、MVCC、死锁
- 性能优化:慢查询、EXPLAIN、分库分表、缓存
- 高可用:主从复制、读写分离、备份恢复
- 安全:SQL注入、权限管理、字符集
- 高级特性:存储过程、触发器、视图、分区表
掌握这些知识点,能够应对大部分MySQL相关的面试问题。
评论区 / Comments