MySQL 的逻辑架构
可以把 MySQL 想象成一家餐厅:
- 连接器 (Connectors): 迎宾员。负责跟客户端建立连接、获取权限。
- 解析器 (Parser): 报单员。分析你的 SQL 语法对不对,识别出表名、字段名。
- 优化器 (Optimizer): 厨师长(核心)。决定用哪个索引,按什么顺序连表。它不一定选最快的,但会选它认为代价最小的。
- 执行器 (Executor): 服务员。调用存储引擎的接口,把数据取出来返回给你。
- 存储引擎 (Storage Engines): 仓库管理员。MySQL 最牛的地方是插件式存储引擎,最常用的是 InnoDB。
索引
没有索引,必须全表扫描(Full Table Scan);有了索引,可以通过索引寻址(Index Seek),速度极快。
数据库中的索引
在 MySQL 中,索引其实是一堆独立的数据结构(通常是 B+ 树)。它把表里某个字段(比如 name 或 id)拿出来,排好序,单独存一个文件。
索引的核心就是“排序”。因为排好了序,数据库就可以用二分查找法。
索引的代价
-
占空间: 索引也是要存硬盘的。索引太多,数据库文件会变得非常臃肿。
-
慢了“写”操作: 当你往表里
INSERT(新增)或UPDATE(修改)一条数据时,MySQL 不仅要改原始数据,还要重新维护和排序那一堆索引。
同步维护索引树,会带来一定的性能开销。
何时使用索引
我们通常只在经常出现在 WHERE 条件、ORDER BY 排序或 JOIN 连接的字段上建立索引。
经常被查询、排序,且更新频率低、字段长度要小的字段需要索引。
何时不适用索引
- 数据量太小: 几十行数据,全表扫描比读索引文件还快。
- 经常更新的字段: 每次改数据都要重新调整 B+ 树,开销很大。
- 区分度低的字段: 比如“性别”。B+ 树里一半是男一半是女,这目录跟没写一样。
常见的几种索引
- 主键索引 (Primary Key): 书的正文页码,唯一的,不能重复,不能为空。
- 唯一索引 (Unique Index): 比如身份证号,不能重复。
- 普通索引 (Normal Index): 最基本的,为了加速查询。
- 复合索引 (Composite Index): 比如你按“省份 + 城市”一起找。注意: 这里有个“最左匹配原则”,即你查“省份”能用到索引,但只查“城市”用不到这个索引(就像目录是先按省排再按市排,你直接跳过省找市就乱了)。
DRF 中 定义索引
单字段索引
db_index=Trueunique=True:也会自动创建唯一索引models.ForeignKey:外键字段自动加上索引
复核索引
经常同时根据两个字段查询,你需要复合索引。
class Article(models.Model):
category = models.CharField(max_length=50)
is_published = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
# 复合索引:加速同时过滤 category 和 is_published 的查询
models.Index(fields=['category', 'is_published'], name='cat_pub_idx'),
]
B+树
- B 树很“矮胖”: 一个节点可以存上百个索引。100 万个数据,B 树可能只需要 3 层。这意味着你只需要读 3 次硬盘就能找到数据。
- B+ 树的非叶子节点只存索引,不存真实数据,这样每一层能装下的索引更多,树更矮。
- B+ 树的所有叶子节点是连在一起的(双向链表)。如果你查
WHERE age > 20,它找到 20 后,顺着链表往后一拉就全出来了,不需要再回过头去爬树。
从上面的特点可以得知:
- 最左匹配原则: 如果你建了复合索引
(name, age),B+ 树是先按name排序,再在name一样的情况下按age排序。如果你查询时跳过name只查age,这棵树就没法用了,因为age在全局看是乱序的。 - 范围查询: 为什么
>和<可以用索引,但LIKE '%keyword'(前缀模糊查询)不能用?因为前缀模糊会导致你没法在有序的树里做二分定位,只能苦逼地全表扫描。
日志
在 MySQL 中,日志不只是为了记录错误,它们是数据恢复、主从复制以及性能优化的核心。
MySQL 的四大日志
为了显得专业,你明天可以顺带提一下这几个日志,证明你对数据库底层有全局观:
- 错误日志 (Error Log): 记录 MySQL 启动、运行或停止时出现的问题。打不开数据库先看它。
- 查询日志 (General Query Log): 记录建立的客户端连接和执行的所有 SQL。一般不开启,因为记录每一条 SQL 太耗性能。
- 二进制日志 (Binlog): 极其重要! 记录所有修改数据的语句。它是做数据恢复和主从复制(读写分离) 的基础。
- 慢查询日志 (Slow Query Log): 优化的起点。
慢查询日志
慢查询日志就像一个“监控摄像头”,它专门盯着那些执行时间超过你设定阈值(比如 1 秒)的 SQL 语句。
- 发现问题: 开启慢查询日志。MySQL 会把所有跑得慢的 SQL 记录在一个文件里。
- 分析问题: 你打开日志,看到某条 SELECT ... JOIN ... 跑了 3 秒,扫描了 100 万行数据,却只返回了 10 行。
- 解决问题: 针对这条具体的 SQL,使用 EXPLAIN 命令看它为什么慢(比如没走索引),然后去加索引或改写 SQL。
配置慢查询日志
slow_query_log:开关(ON/OFF)。long_query_time:阈值(比如设为 1,表示执行超过 1 秒的就记录)。log_queries_not_using_indexes:进阶配置。如果设为 ON,即使 SQL 跑得很快,但只要没用索引,也会被记录下来。这对早期优化非常有帮助!
拿到日志后怎么看
可视化工具:比如 Navicat 或阿里云的数据库控制台,都有现成的图表。
一个接口响应很慢,作为后端开发会怎么排查?
- 先看是不是网络或业务逻辑代码的问题。
- 如果确定是数据库慢,我会查看 MySQL 的慢查询日志 (Slow Query Log),找出具体的慢 SQL。
- 拿到 SQL 后,我会观察它的执行时间以及扫描的行数 (Rows_examined)。
- 会使用 EXPLAIN 命令查看执行计划,重点关注 type(连接类型)和 key(是否命中索引)这两个字段。
- 通常通过增加合适的索引、优化复合索引,能解决大部分问题。
EXPLAIN 命令
EXPLAIN 命令 是后端开发在优化数据库时最核心、最常用的工具。
EXPLAIN 是一个命令前缀。你只需要在任何 SELECT 语句前面加上这个词,MySQL 就会告诉你:它是打算如何执行这条查询的。
type(连接类型)
- system / const:查主键或唯一索引,只有一行数据。最快。
- eq_ref / ref:Join 查询或普通索引查询。非常快。
- range:范围查询(比如
age > 20)。只要有索引,也可以接受。 - index:扫描了整棵索引树(虽然比全表快,但也慢)。
- ALL:全表扫描。必须优化!
key(实际使用的索引)
如果这个字段是 NULL,说明没走索引 或者 索引失效了。
rows(预估扫描行数)
数值越小越好。它是评估 SQL 性能最直观的指标。
读写分离
横向扩展的架构思维。
- 主库 (Master / Primary): 专门负责“写”操作(
INSERT、UPDATE、DELETE)。 - 从库 (Slave / Replica): 专门负责“读”操作(
SELECT)。
在绝大多数互联网应用(包括 AI 应用)中,读的次数远多于写的次数。多搞几台“只读”的从库,把压力分担出去。
核心原理
- 主库记录: 当你在主库写下一条数据时,主库会把这个动作记录到 Binlog 里。
- 从库复制: 从库会一直盯着主库的 Binlog,一旦发现有更新,就赶紧把日志拉过来。
- 重做日志: 从库把拉过来的日志存进自己的 Relay Log(中继日志) ,然后按照日志里的指令,在自己的数据库里把这个操作再做一遍。
面试避坑点: :主从同步通常是异步的。这意味着主库写完后,从库可能要等个几十毫秒甚至几秒才能同步过去。这就会导致一个问题:主从延迟。
主从延迟怎么办
- 二次查询: 如果从库查不到,强制去主库查一次(慎用)。
- 关键业务走主库: 比如订单支付、个人中心修改等敏感操作,直接在代码里指定查主库。
- 缓存同步: 写入主库的同时,顺便把数据塞进 Redis 缓存,读的时候先看 Redis。
DRF中的主从分离
在 Django 中实现读写分离,通常是在 settings.py 里配置多个 DATABASES,然后编写一个 Router 类。在这个类里重写 db_for_read 和 db_for_write 方法:
- 如果是查询操作,返回
slave数据库; - 如果是写入操作,返回
default(主库)数据库。”
定义多个数据库
# settings.py
DATABASES = {
'default': { # 主库,负责写
'ENGINE': 'django.db.backends.mysql',
'NAME': 'my_db_master',
'USER': 'admin',
'PASSWORD': 'password',
'HOST': '192.168.1.10',
},
'replica': { # 从库,负责读
'ENGINE': 'django.db.backends.mysql',
'NAME': 'my_db_master', # 注意:数据库名通常是一样的
'USER': 'admin',
'PASSWORD': 'password',
'HOST': '192.168.1.11', # 不同的 IP
}
}
编写数据库路由 (Router)
告诉 Django:什么时候用 default,什么时候用 replica。
# my_project/router.py
class MySQLRouter:
def db_for_read(self, model, **hints):
"""所有的读取操作都指向从库"""
return 'replica'
def db_for_write(self, model, **hints):
"""所有的写入操作都指向主库"""
return 'default'
def allow_relation(self, obj1, obj2, **hints):
"""允许跨库关联(因为数据其实是一样的)"""
return True
def allow_migrate(self, db, app_label, model_name=None, **hints):
"""只允许在主库进行数据表结构迁移"""
return db == 'default'
激活路由
在 settings.py 中注册这个路由:
# settings.py
DATABASE_ROUTERS = ['my_project.router.MySQLRouter']
手动强制切换
# 强制从主库读取最新数据,不走从库
latest_data = Article.objects.using('default').get(id=1)
分库分表
垂直拆分 (Vertical Splitting) —— “按功能拆”
把一个表里的字段拆到不同的表里,或者把不同的表拆到不同的数据库。
- 例子: 你的
User表原本有 50 个字段(包括用户名、密码、头像、个人简介、联系方式、家庭住址)。 - 怎么拆:
- 基础表:
user_base(id, username, password)—— 经常查,体积小。 - 详情表:
user_extra(id, intro, address)—— 不常用,字段长(比如TextField)。
- 基础表:
- 作用: 减少单行数据的大小,让内存能缓存更多行,提升查询速度。
水平拆分 (Horizontal Splitting) —— “按行数拆”
把同一个表的数据,按某种规则散落到不同的库或表里。这是真正的“大招”。
- 例子: 你的
Order(订单)表有 1 亿行。 - 怎么拆: 准备 10 个订单表(
order_0到order_9)。 - 规则(Shard Key): 按照
user_id % 10来存。- 用户 ID 为 123 的订单,永远存到
order_3。 - 用户 ID 为 456 的订单,永远存到
order_6。
- 用户 ID 为 123 的订单,永远存到
- 作用: 把 1 亿行的数据压力,平均分摊到 10 个小表里,每个表只有 1000 万行,恢复了索引性能。
数据一致性
无论发生什么,数据库里的数据逻辑必须是正确且合法的。MySQL 的 InnoDB 引擎之所以成为行业标配,就是因为它完美实现了 ACID 特性.
InnoDB 与一致性的关系(核心:ACID)
MySQL 的 InnoDB 引擎之所以成为行业标配,就是因为它完美实现了 ACID 特性,而 C (Consistency - 一致性) 是 ACID 的最终目的。其他三个字母都是为了保障一致性而存在的手段。
① A (Atomicity - 原子性):靠 Undo Log 实现
- 原理: 把事务看成一个不可分割的原子。
- InnoDB 的做法: 记录 Undo Log(回滚日志) 。在你改数据之前,先把旧数据存起来。如果中间出错了,InnoDB 就按着 Undo Log 把数据改回去。
② I (Isolation - 隔离性):靠 MVCC 和 锁 实现
- 原理: 当两个人在同时改数据时,互不干扰。
- InnoDB 的做法: 使用 MVCC(多版本并发控制) 。就像给数据拍快照,你读你的旧版本,我改我的新版本,互不冲突,防止“脏读”。
③ D (Durability - 持久性):靠 Redo Log 实现
- 原理: 只要事务提交了,哪怕这时候电脑爆炸了,数据也得在磁盘里。
- InnoDB 的做法: Redo Log(重做日志) 。MySQL 不是立刻把数据写入磁盘(太慢),而是先写一份日志。哪怕断电,重启后也能根据日志把没写进磁盘的数据“重做”一遍。
强一致性 vs 最终一致性(面试进阶点)
你在聊读写分离和分库分表时,面试官可能会追问这个。
- 强一致性(Strong Consistency): 写入后,立刻就能读到最新的。单机数据库(InnoDB)能做到。
- 最终一致性(Eventual Consistency): 写入后,允许有几秒的延迟(主从同步延迟),但最终数据会一致。读写分离架构下,通常是最终一致性。
总结
数据一致性是指数据库从一个合法状态转变到另一个合法状态,不违反业务规则和完整性约束。
在 InnoDB 中,一致性是通过 ACID 模型来保障的:
- 通过 Undo Log 保证原子性,支持事务回滚;
- 通过 Redo Log 保证持久性,确保断电不丢数据(WAL 技术);
- 通过 锁机制 和 MVCC 保证隔离性,防止并发下的数据混乱。
另外,在分布式或读写分离场景下,我们要权衡强一致性和性能。通常我们会接受最终一致性,并利用 Redis 或强制路由主库来解决关键业务的实时性问题。