MySQL
mysql基础
什么是主键
唯⼀标识⼀条记录,不允许重复,不允许为空
主键、外键、索引的区别
主键:唯⼀标识⼀条记录,不允许重复,不允许为空,⽤于唯⼀标识表中每⼀⾏的字段
外键:外键是⼀个表中的字段,其值是另⼀个表的主键,⽤于建⽴两个表之间的关系。
索引:没有重复值,但可以有⼀个空值 ,⽤于快速查询到数据。
MySQL表连接
连接:将各个表中的记录都取出来进⾏依次匹配,将匹配后的结果发给客户端
笛卡尔积:连接查询的结果中包含⼀个表的每⼀条记录与另⼀个表中每⼀条记录的组合
连接过程:从驱动表中取每一条符合搜索条件的记录,到被驱动表中查找匹配的记录。只需要访问驱动表⼀次,可能会多次访问被驱动表。
内连接:驱动表中的记录在被驱动表中找不到匹配的记录,那么驱动表的这条记录不会加⼊到最后的结果中。
外连接:驱动表中的记录在被驱动表中找不到匹配的记录,也仍需要加⼊到最后结果中
左外连接:语句左侧的表为驱动表,右外连接:语句右侧的表为驱动表
过滤条件
where:
不论内外连接,只要是不符合 where ⼦句的记录都不会加⼊到最后的结果中
on:
在内连接中与 where 等价;
在外连接中,如果驱动表中的记录在被驱动表中没有记录可以匹配,该驱动表记录仍会加⼊到结果中,对应的被驱
动表字段以 null 填充
嵌套循环连接
如果有3个表进⾏连接,那么表1和表2完成连接后的结果作为驱动表,将表3作为被驱动表进⾏连接查询
执行一条SQL请求的执行过程
用户账号
查用户表:select user from user
新增用户:CREATE USER [user_name] IDENTIFIED BY [user_pwd]
修改用户:RENAME USER [user_name] TO [new_user_name]
修改用户密码:SET PASSWORD FOR [user_name] = Password('[new_pwd]')
删除用户账号:DROP USER [user_name]
查看用户权限:SHOW GRANTS FOR [user_name]
给用户授权:GRANT [权限名] ON [数据库名].[表名] TO [⽤户名]
撤销授权:REVOKE [权限名] ON [数据库名].[表名] FROM [⽤户名]
索引
什么是索引
索引类似与书的目录,为了提⾼数据查询的效率。
索引操作
创建索引:create index 索引名 on 表名(列名);
删除索引:drop index 索引名 on 表名;
怎么查看⼀个SQL语句是否使⽤了索引进⾏检索
索引有哪些类别
按数据结构分类:
哈希表:使用key-value对,存储数据,可能存在哈希冲突,多个value对应同一个key
优点:key无序,插入数据无需维护顺序,效率高
缺点:区间查询速度慢
适用场景:适用于等值查询有序数组:
优点:有序数组适合查询
缺点:有序数组不适合增删
适用场景:适用于静态存储引擎,适合等值查询和区间查询。二叉搜索树:有序数组的查询速度 + 链表的增删速度;但数据库索引要落磁盘,二叉树节点太稀、树太高,磁盘 I/O 扛不住,所以换成一节点存多 key 的 B+ 树。
N叉树:二叉树节点太稀、树太高,N 叉树一个节点装满一整个磁盘页,把树压矮、把磁盘 I/O 从几十次降到 3~4 次
按存储分类
按照字段特性分类
按字段个数分类
什么是最左匹配原则
最左匹配原则要求查询条件中的列应该从索引的最左边的列开始,并且不能跳过中间的列。
如果查询条件不按照索引的顺序进⾏匹配,那么索引可能会失效。
范围查询的字段可以⽤到联合索引,但是范围查询字段的后⾯的字段⽆法⽤到联合索引。
索引下推优化
可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满⾜条件的记录,减少回表次数。
索引区分度
索引区分度表示某个字段不同值的个数占整个表的⽐例,建⽴联合索引时,要把区分度⼤的字段排在前⾯。
建立索引的注意事项
不需要建立索引的情况:
- 数据量小的表
- 不经常使用的列
- 频繁更新的列
- 字段中存在大量重复数据
索引的优缺点
优点:
1、大大加快数据的检索速度
2、唯一性索引可以保证数据库表中每一行数据的唯一性
缺点:
1、一个索引对应一颗B+树,每个节点是一个16kb大小的页,占用空间较大
2、如果数据有索引,频繁增删改会降低sql执行效率
什么时候需要创建索引
- 频繁查询的列
- 大表
- 唯一性要求
- 连接表的外键列
- 频繁使用排序和分组的列
索引优化的方法
1、前缀索引优化:使用某个字段中的字符串的前几个字符建立索引
2、覆盖索引优化:从⼆级索引中可以查询得到记录,避免回表
3、主键索引最好是⾃增的;这样每次插⼊⼀条新记录,都是追加操作,不需要重新移动数据
4、避免过多的索引
索引什么时候会失效
- 使⽤左或者左右模糊匹配
- 在索引列上使⽤函数或表达式
- 在 WHERE ⼦句中,如果在 OR 前的条件列是索引列,⽽在 OR 后的条件列不是索引列,那么索引会失效。
- 违背最左匹配原则
- 数据分布不均匀
- 查询中的条件涉及到隐式类型转换
为什么使用B+树索引
事务
事务的四⼤特性 ACID
- 原⼦性
事务是⼀个不可分割的⼯作单元,要么完全执⾏,要么完全不执⾏。通过undo log(回滚日志)保证。 - ⼀致性
确保事务将数据库从⼀个⼀致的状态转变为另⼀个⼀致的状态。
通过持久性+原⼦性+隔离性来保证的。 - 隔离性
多个事务并发执⾏时,每个事务都不能看到其他事务的中间状态。
通过 MVCC(多版本并发控制) 或锁机制来保证的。 - 持久性
⼀旦事务被提交,其结果将永久保存在数据库中,即使系统发⽣故障。
通过 redo log (重做⽇志)来保证的。
并⾏事务会出现什么问题
- 脏读:读到其他事务未提交的数据
- 不可重复读:前后读取的数据不⼀致
- 幻读:前后读取的记录数量不⼀致
严重性排序:脏读>不可重复读>幻读
隔离级别
- 读未提交:
⼀个事务可以读取到另⼀个事务未提交的数据。这可能导致脏读(Dirty Reads)和不可重复读、幻读等问题。 - 读提交:
⼀个事务只能读取到已经提交的其他事务的数据。这解决了脏读的问题,但仍可能遇到不可重复读的问题。 - 可重复读
⼀个事务在其⽣命周期内多次执⾏相同的查询,将始终看到相同的数据,但是,仍可能发⽣幻读。也是MySQL InnoDB 引擎的默认隔离级别; - 可串⾏化
会对记录加上读写锁。事务的执⾏效果就好像它们是按顺序执⾏的,事务之间没有并发。这可以防⽌脏读、不可重复读和幻读,但也可能导致性能下降,因为并发性降低。
幻读是如何解决的
针对快照读(普通 select 语句),是通过 MVCC ⽅式解决了幻读
针对当前读:(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)⽅式解决了幻读。
事务隔离的实现
Read View在MVCC中是如何⼯作的
读已提交是怎么实现的
使⽤ MVCC(多版本并法控制) 实现的
读操作:在MVCC中,读已提交只能看到已提交事务的版本。
写操作: 写操作创建⼀个新的数据版本,⽽不是直接修改原始数据。这确保了正在进⾏的事务不会看到未提交事务的更改。
事务的启动方式
锁
锁的种类
锁的种类
1、全局锁
对整个数据库实例加锁
使用场景:全库逻辑备份
语法:flush tables with read lock (FTWRL)。
2、表级锁
(1)表锁
特点:
- 每次操作锁住整张表
- 开销小、加锁快
- 并发度最低
语法:lock tables … read/write
主动释放锁:unlock tables
自动释放锁:客户端断开的时候
(1)元数据锁
MDL 不需要显式使⽤,在访问⼀个表的时候会被⾃动加上。
当对⼀个表做增删改查操作的时候,加 MDL 读锁;
当要对表做结构变更操作的时候,加 MDL 写锁。
读锁之间不互斥,读写锁之间、写锁之间互斥。
MDL 的作⽤:保证读写的正确性。
事务中的 MDL 锁,在语句执⾏开始时申请,但是语句结束后并不会⻢上释放,⽽会等到整个事务提交后再释放。
(这可能会产⽣死锁的问题)
(3)意向锁
定义:意向锁是一种表级锁,用于提前标识事务未来可能要对表中某些行加什么类型的锁,避免逐行检查表是否已被锁定。
两种类型:
- 意向共享锁(IS):事务打算给某些行加共享锁(S锁)
- 意向排他锁(IX):事务打算给某些行加排他锁(X锁)
加锁规则:
- 给行加 S 锁前 → 先给表加 IS 锁
- 给行加 X 锁前 → 先给表加 IX 锁
冲突关系:
- 意向锁之间互不冲突(IS 与 IX 兼容)
- 意向锁与行级锁不冲突
- 仅与表级的 S 锁、X 锁发生冲突
核心作用:当事务想对整张表加表级锁时,只需检查表上是否有意向锁,就能快速判断表里是否有行被锁定,无需逐行扫描,大幅提升加锁效率。
(4)AUTO-INC锁
作用:实现AUTO_INCREMENT自增主键字段的自动赋值,保证插入数据时主键值连续递增。
传统表级 AUTO-INC 锁:
- 插入时加表级锁,为自增字段赋值后,等整个插入语句执行完才释放
- 期间其他事务的插入操作全部被阻塞
- 缺陷:批量插入时性能差,并发插入被严重阻塞
InnoDB 轻量级改进方案:
- 插入时对自增字段加轻量级锁
- 赋值一个自增值后立即释放锁,无需等待整个插入语句执行完成
- 大幅提升并发插入性能,同时仍保证自增值的唯一性和递增性
3、行级锁
一、定义
针对数据表中行记录的锁,不同事务操作同一行时需排队等待。
二、三种类型
| 类型 | 作用 | 特点 |
|---|---|---|
| Record Lock(记录锁) | 仅锁单条记录 | 分共享锁、排他锁两种 |
| Gap Lock(间隙锁) | 锁定记录之间的间隙范围,不含记录本身 | 仅存在于可重复读级别,解决幻读;间隙锁之间互相兼容、不互斥 |
| Next-Key Lock | Record Lock + Gap Lock 的组合 | 前开后闭区间,既锁记录本身,又阻止其他事务在该记录前的间隙插入新行 |
三、特点
- 粒度细,每次只锁一行
- 开销大、加锁慢
- 锁冲突概率最低,并发度最高
- 两阶段锁协议:行锁在需要时才加,但直到事务提交/回滚才释放
- 实践建议:事务中需要锁多行时,把最可能造成锁冲突的锁尽量往后放,缩短锁持有时间
四、加锁规则(可重复读级别下)
两个原则:
- 加锁基本单位是Next-Key Lock(前开后闭区间)
- 查找过程中访问到的对象才会加锁
两个优化:
- 唯一索引等值查询命中时,Next-Key Lock退化为行锁
- 等值查询向右遍历,最后一个值不满足等值条件时,Next-Key Lock退化为间隙锁
一个 Bug:
- 唯一索引上的范围查询会多访问到不满足条件的第一个值(MySQL 8.0 已修复)
五、隔离级别影响
- 可重复读(RR):完整遵守两阶段锁协议,存在间隙锁
- 读提交(RC):去掉间隙锁部分,仅剩行锁
锁的划分
锁的划分总结
一、数据库角度(数据库实现的锁)
- 共享锁(S锁 / 读锁)
- 锁定的资源可以被其他事务读取,但不能修改
SELECT ... LOCK IN SHARE MODE手动加行级共享锁- 作用:保证读取过程中数据不被修改
- 表级共享锁:
LOCK TABLE ... READ
- 排他锁(X锁 / 写锁)
- 锁定的数据只允许加锁事务操作,其他事务无法查询或修改
SELECT ... FOR UPDATE手动加行级排他锁INSERT / DELETE / UPDATE时数据库自动加排他锁- 表级排他锁:
LOCK TABLE ... WRITE,仅加锁事务可读写,其他事务需等待
- 意向锁(Intent Lock)
- 作用:表级标识,提前告知"表中某些行已被加锁"
- 类比:给整个房子挂个"里面有人"的牌子,别人想拿整把钥匙时看牌子即可,不用逐间房检查
- 加行级 S 锁前 → 自动加表级意向共享锁(IS)
- 加行级 X 锁前 → 自动加表级意向排他锁(IX)
- 核心价值:快速判断表中是否有行被锁,避免逐行扫描,提升表级加锁效率
二、程序员角度(加锁思想)
- 乐观锁
- 思想:认为并发修改是小概率事件,不加数据库锁,由程序实现
- 实现方式:版本号机制、时间戳机制
- 优点:无死锁问题,性能好
- 适用场景:读多写少的场景
- 局限:只能阻止程序内的并发,挡不住程序外对数据库的直接操作
- 悲观锁
- 思想:对并发修改持保守态度,依赖数据库自身的锁机制保证排他性
- 实现方式:共享锁、排他锁、行锁、表锁等数据库锁
- 优点:数据库层面强制隔离,防止读-写、写-写冲突
- 缺点:加锁时间长,并发访问性差
- 适用场景:写多的场景
Innodeb使用表锁还是行锁
对于Innodb,绝⼤部分情况应该使⽤⾏锁
使⽤表锁的情况
(1)表⽐较⼤,事务需要更新全部或者⼤部分数据
(2)事务涉及到多个表,⽐较复杂,可能引起死锁,造成⼤量的事务回滚
日志
- undo log(回滚⽇志):是 Innodb 存储引擎层⽣成的⽇志,实现了事务中的原⼦性,主要⽤于事务回滚和MVCC。
- redo log(重做⽇志):是 Innodb 存储引擎层⽣成的⽇志,实现了事务中的持久性,主要⽤于掉电等故障恢复;
- binlog (归档⽇志):是 Server 层⽣成的⽇志,主要⽤于数据备份和主从复制;
什么是undo logo
定义
事务对数据进行更新(插入/修改/删除)时,记录的反向操作日志,用于事务回滚和系统崩溃后的数据恢复。
记录内容
- 操作类型(插入/删除/更新)
- 修改前的旧数据值
- 被修改数据的位置
- 事务标识 ID(trx_id)
数据结构
每条 undo log 包含两个关键字段: roll_pointer:指向上一条 undo log,将所有版本串成版本链trx_id:记录最后修改该行的事务 ID
两大核心作用
1. 事务回滚
- 事务执行出错或被手动回滚时,根据 undo log逆向执行所有操作,将数据库还原到事务开始前的状态
2. 实现 MVCC(多版本并发控制)
- undo log 为每条记录保存多份历史数据版本
- 快照读(普通 SELECT)时,事务根据自身的ReadView,顺着版本链找到对当前事务可见的历史版本,实现非阻塞读
什么是redo log
redo log 是物理⽇志,记录了某个数据⻚做了什么修改,在事务提交时,只要先将 redo log 持久化到磁盘即可。
redo log和undo log的区别是什么
什么是binlog
MySQL 在完成⼀条更新操作后,Server 层还会⽣成⼀条 binlog,等之后事务提交的时候,会将该事物执⾏过程中产⽣的所有 binlog 统⼀写 ⼊ binlog ⽂件。
binlog ⽂件是记录了所有数据库表结构变更和表数据修改的⽇志
redo log 和 binlog 有什么区别
redo log:
1、InnoDB 存储引擎层实现,仅 InnoDB 使用
2、物理日志,记录某个数据页上做了什么修改
3、循环写,大小固定,写满后从头覆盖,保存未刷盘的脏页日志
4、用于掉电/崩溃恢复,保证事务持久性
binlog:
1、MySQL Server 层实现,所有存储引擎都可使用
2、逻辑日志,有 Statement、Row、Mixed 三种格式
3、追加写,写满一个文件再开新文件,不覆盖旧日志,保存全量日志
4、用于备份恢复和主从复制
为什么需要两阶段提交
问题根源
事务提交时,redo log(影响主库数据)和binlog(影响从库数据)是两份独立的日志,各自刷盘,可能出现"一份成功、一份失败"的半成功状态,导致主从数据不一致。
两种异常场景
| 宕机时机 | 主库状态 | 从库状态(binlog复制) | 结果 |
|---|---|---|---|
| redo log 已刷盘,binlog 未写 | 崩溃恢复后数据是新值 | binlog 丢失这条更新,执行后是旧值 | 主新从旧 |
| binlog 已刷盘,redo log 未写 | 崩溃恢复后事务无效,数据是旧值 | binlog 有这条更新,执行后是新值 | 主旧从新 |
解决方案:两阶段提交
将事务提交拆成两个阶段,保证两份日志要么都成功、要么都失败:
- Prepare 阶段:写 redo log,事务进入准备状态
- Commit 阶段:写 binlog,正式提交事务
通过两阶段提交,确保 redo log 和 binlog 的逻辑一致性,避免主从不一致。
两阶段提交的过程
背景
开启 binlog 时,MySQL 用内部 XA 事务保证 redo log 与 binlog 的一致性:
- 协调者:binlog(Server 层)
- 参与者:InnoDB 存储引擎
完整流程(拆 redo log 为两步,中间穿插 binlog)
① Prepare 阶段
- 将内部 XA 事务 ID 写入 redo log
- 将 redo log 的事务状态设置为prepare
- redo log持久化到磁盘
② Commit 阶段
- 将 XA 事务 ID 写入 binlog
- binlog持久化到磁盘
- 调用引擎提交接口,将 redo log 状态改为commit(此状态只需 write 到 page cache,不需要刷盘)
关键结论
只要binlog 成功刷盘,即使 redo log 还停留在 prepare 状态,崩溃恢复时也会认为该事务已成功提交——这就是两阶段提交保证两份日志一致性的核心机制。
执行引擎
有哪些执⾏引擎
InnoDB: MySQL默认的事务性存储引擎,⽀持事务的提交(commit)和回滚(rollback),提供了⾏级锁定,⽀持外键约束和MVCCMyISAM: MyISAM 使⽤表级锁定, 不⽀持事务,⽀持全⽂索引,适⽤于以读操作为主的应⽤Memory: 将数据放在内存中,数据处理速度很快,但是当数据库重启或崩溃时,存储在内存中的数据将丢失。
数据库引擎InnoDB与MyISAM的区别和适⽤场景?
innoDB:支持事务;行级锁,并发性能好;支持外键约束;支持崩溃恢复;支持全文索引
MyISAM:不支持事务;表级锁,并发性能低;不支持外键约束;崩溃恢复后可能有数据损失;也支持全文索引,表现的更好。
分库分表
分库分表的原因
- 单库太大:磁盘空间不足、IO 瓶颈、连接数不够 → 拆库
- 单表太大:CRUD 慢、索引膨胀、查询超时 → 拆表
四种拆分方式
| 方式 | 拆分对象 | 核心特点 | 解决什么问题 |
|---|---|---|---|
| 垂直分表 | 按字段拆 | 表结构变了,大字段/不常用字段拆出去 | 避免大字段 IO,分离热点/非热点 |
| 垂直分库 | 按业务拆 | 不同业务表放到不同库 | 突破单机 CPU/内存/连接数瓶颈,业务解耦 |
| 水平分表 | 按数据行拆 | 表结构一样,数据拆开,还在同一个库 | 解决单表数据量过大问题 |
| 水平分库 | 按数据行拆 | 表结构一样,数据拆到不同库/服务器 | 同时解决单表数据量大 + 单机并发压力 |
切分规则:range 范围、hash 取模、地理区域、时间范围
分库分表带来的问题
- 跨库 JOIN 关联查询麻烦
- 分布式事务问题
- 跨库排序、分页复杂(非分片字段排序要各节点查完再汇总)
- 全局 ID 自增冲突,需要单独方案
- 二次扩容数据迁移难
主从复制
用途:数据备份、读写分离、架构扩展(分担读压力)
原理(三个线程):
- 主库:binlog dump 线程,负责发送 binlog
- 从库 I/O 线程:拉取主库 binlog,写入本地 relay log(中继日志)
- 从库 SQL 线程:读取 relay log,重放 SQL,保证主从一致
复制模式:异步、半同步、全同步、GTID
五、必考面试题:单表千万级数据慢,优化思路
千万别一上来就说分库分表,按这个顺序答:
- 先软优化:慢查询分析 → 加索引/改 SQL → 优化表结构 → 参数调优 → 引入缓存/NoSQL
- 再硬优化:升级硬件(更快的磁盘、更大内存)
- 还不够才考虑分库分表:先分表试试,解决单表数据量大;并发还是不够再分库
结论:优先索引 + 缓存 + 读写分离,数据量极大且增长快才上分库分表。
Redis
Redis基础
Redis是什么,有哪些特点
Redis是⼀个开源的基于内存的数据库,读写速度快,通常被⽤作缓存、消息队列、分布式锁和键值存储数据库。
⽀持多种数据结构,如字符串、哈希表、列表、集合、有序集合等。
还⽀持事务 、持久化、Lua 脚本、发布/订阅模式,内存淘汰机制、过期删除机制等。
特点:
基于内存:数据存在内存中,读写极快,适合高并发场景
支持持久化:通过 RDB 快照 + AOF 日志把数据存到磁盘,宕机不丢数据
多数据结构:字符串、哈希、列表、集合、有序集合等,灵活适配不同场景
原子性操作:单个操作要么全成功要么全失败,并发下数据安全
支持分布式:提供主从、哨兵、集群等方案,可水平扩展
为什么要使⽤ Redis ⽽不仅仅依赖 MySQL
1、Redis读写快速,将频繁访问的数据存储在Redis中,可有效减轻Mysql数据库的压力
2、Redis提供了丰富的数据结构,处理特定类型数据和实现特定功能时更为灵活
3、redis能更好的处理高并发请求,通过缓存和原子性操作,可以降低数据库的并发压力。
Redis是单线程吗
Redis 不是绝对的单线程。它的单线程指的是命令执行是单线程,这样避免了多线程切换和锁的开销。
但像 AOF 刷盘、内存释放这些耗时任务是交给后台线程做的;
6.0 之后网络 I/O 也用了多线程,只是命令执行还是单线程
Redis单线程为什么还这么快
- 基于内存存储:内存访问速度远远快于磁盘访问速度,因此,Redis 能够快速读取和写⼊数据。‘
- ⾮阻塞单线程:避免了多线程带来的竞争和同步开销。⾮阻塞 I/O可以更⾼效地处理并发请求。
- ⾼效的数据结构:Redis专⻔设计了STRING、LIST、HASH等⾼效的数据结构,提升读写的效率。
- I/O多路复用:利用 I/O 多路复用让内核统一监听多个 Socket,事件一到达就调用对应处理器,从而实现单线程同时处理多个 I/O 流。
Redis数据类型和数据结构
String 字符串:可以是字符串、整数或浮点数
应用场景:
- 缓存对象:缓存整个对象的 JSON
- 计数:Redis 单线程执行命令是原子的,适合访问次数、点赞、转发、库存数量等场景
- 分布式锁:利用 SETNX 命令
- 共享 Session 信息:解决分布式系统下 Session 存储问题
命令:get set del
List 列表:底层由双向链表或压缩列表实现(元素个数 < 512 且每个元素值 < 64 字节时用压缩列表,否则用双向链表;3.2 之后统一由 quicklist 实现,7.0 压缩列表废弃、由 listpack 实现)
应用场景:
- 微信朋友圈点赞:按点赞顺序显示,取消点赞移除对应好友
- 消息队列:生产者 LPUSH 左进,消费者 BRPOP 阻塞 “抢” 尾部数据
命令:rpush lrange lindex lpop
Set 集合:底层由哈希表或整数集合实现(元素全为整数且个数 < 512 时用整数集合,否则用哈希表)
应用场景:
- 点赞:key 为文章 id,value 为用户 id
- 共同关注:利用交集运算计算共同关注的好友、公众号
- 抽奖活动:去重保证同一用户不会中奖两次
命令:sadd smembers sismember srem
Hash 哈希:底层由压缩列表或哈希表实现(元素个数 < 512 且所有值 < 64 字节时用压缩列表,否则用哈希表;7.0 压缩列表废弃、由 listpack 实现)
应用场景:
- 缓存对象:对象中频繁变化的属性抽出来用 Hash 存储
- 购物车:用户 id 为 key、商品 id 为 field、商品数量为 value
命令:hset hget hgetall hdel
Zset 有序集合:底层由压缩列表或跳表实现,可按元素权重排序,权重可自定义(如按插入时间,先插入的权重小)
应用场景:
- 排行榜:学生成绩、游戏积分、视频播放、商品销量排名
- 需要展示最新列表、更新频繁或需分页显示的排序场景
命令:zadd zrange zrangebyscore zrem
BitMap 位图(2.2 新增):以 bit 为最小单位存储,非常节省空间,适合数据量大且二值统计的场景
应用场景:
- 签到统计
- 判断用户登录态
HyperLogLog(2.8 新增):基于概率的基数统计,标准误算率 0.81%;输入元素数量或体积非常大时,内存占用固定且很小
应用场景:
- 百万级网页 UV 计数
GEO(3.2 新增):存储地理位置信息并对其操作,底层由 Zset 实现,通过 GeoHash 编码把经纬度转为 Zset 权重分数(关键机制:二维地图区间划分 + 区间编码)
Stream(5.0 新增):Redis 专门为消息队列设计的数据类型,自动生成全局唯一消息 ID,支持消费组消费;弥补了 List 消息队列不能持久化、离线重连无法读取历史消息的缺陷
应用场景:
- 可靠的消息队列
Redis底层数据结构
SDS(简单动态字符串):String 类型的底层实现
- 可存储字符串和二进制数据(包括空字符),不受空字符限制
- 头部保存长度信息,O (1) 获取字符串长度
- 动态扩容,无需像 C 字符串那样手动管理内存
双端链表:List 的底层实现之一
- 节点可保存不同类型的值,支持两端快速插入和删除
- 有表头、表尾指针,获取头尾节点 O (1),获取链表数量 O (1)
- 缺陷:节点内存不连续,无法很好利用 CPU 缓存;每个节点需额外分配结构头,内存开销大
压缩列表(ziplist):List、Hash 的底层实现之一
- 紧凑、可变长度的连续内存顺序型结构(类似数组),内存使用效率高,可按数据大小灵活扩容收缩
- 缺陷:扩容需重新分配内存,一旦发生连锁更新会导致内存多次重新分配,影响访问性能;只适用于节点数量不多的场景
哈希表:Hash、Set 的底层实现之一
- 保存键值对,O (1) 快速查询
- 采用拉链法解决哈希冲突:不扩容时把哈希值相同的数据串成链表
整数集合(intset):Set 的底层实现之一(元素全为整数时)
- 专门存储整数值,紧凑的二进制表示,存储效率高
跳表(skiplist):Zset 的底层实现
- 在链表基础上改进的「多层」有序链表,数据量大时查找复杂度 O (logN)
- 查找过程:从头节点最高层开始逐层遍历,按权重和 SDS 元素判断 —— 当前节点权重小于目标则访问该层下一节点;权重相等且数据小于目标也访问下一节点;以上都不满足或下一节点为空时,用当前节点 level 数组的下一层指针继续查找
quicklist:双向链表 + 压缩列表的组合(List 3.2 之后使用)
- 本质是一个链表,每个节点是一个压缩列表
- 通过控制每个压缩列表的大小或元素个数规避连锁更新
listpack:压缩列表的替代(Redis 7.0 废弃 ziplist 后使用)
- 不记录前一个节点的长度,只记录当前节点长度
- 新增元素不影响其他节点的长度字段,从根本上避免连锁更新
Redis持久化
Redis如何实现数据不丢失
Redis数据持久化机制会把数据存储到磁盘,Redis重启时能从磁盘中恢复原有数据。
三种持久化方式:
AOF日志
以⽇志的形式记录服务器所处理的每⼀个写操作,redis服务器启动之初,会读取该⽇志来重新构建数据库,以保证启动后的数据库是完整的。
AOF是写后日志:先执行命令把数据写入内存,然后才记录日志,mysql是写前日志。
写后日志的好处:
- 避免出现错误命令
- 不会阻塞当前的写操作
AOF的三种写回测策略
AOF 磁盘重写机制
作用:命令越执行越多,AOF 文件体积越来越大;为避免日志文件过大,Redis 提供 AOF 重写机制。
核心思路:直接扫描数据库中所有键值对数据,为每个键值对生成一条写操作命令,写入新的 AOF 文件;重写完成后替换现有 AOF 日志。
执行过程(一处拷贝,两处日志):
- 与 RDB 创建过程类似,创建子进程完成重写
- 子进程遍历所有数据库,把每个键值对以插入操作形式写入日志文件,并执行写盘
- 用子进程可避免阻塞主线程,减少对 Redis 整体性能的影响
四个触发时机:
- 执行
bgrewriteaof命令 - 主从复制完成 RDB 文件解析和加载(无论成功与否)
- AOF 重写被设置为待调度执行
- AOF 被启用,且 AOF 文件大小比例超出阈值、绝对值也超出阈值
执行前提:以上四个时机下都不能有正在执行的 RDB 子进程或 AOF 重写子进程,否则 AOF 重写无法执行。
RDB快照
记录某一个瞬间的内存数据,记录的是实际数据,每次执行快照,都是把内存中的所有数据都记录到磁盘中,通常设置至少五分钟保存一次快照。
Redis 提供了两个命令来⽣成 RDB ⽂件:
save 命令:就会在主线程⽣成 RDB ⽂件
bgsave 命令:会创建⼀个⼦进程来⽣成 RDB ⽂件
混合持久化
工作机制(作用于 AOF 重写过程):
- fork 出的重写子进程先将与主线程共享的内存数据以 RDB 方式写入 AOF 文件(前半部分)
- 重写期间主线程处理的操作命令记录在重写缓冲区
- 重写缓冲区的增量命令以 AOF 方式写入 AOF 文件(后半部分)
- 写入完成后通知主进程,用新文件替换旧 AOF 文件
文件结构:前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
优点:
- 重启加载时前半部分是 RDB 内容,加载速度快
- 加载完 RDB 后继续加载后半部分 AOF(重写期间主线程处理的命令),数据丢失更少
缺点:
- AOF 文件可读性变差
- 兼容性差:Redis 4.0 之前的版本不支持
Redis集群
Redis 如何实现高可用
1. 主从复制
主从库模式保证数据副本一致,采用读写分离:
- 读操作:主库、从库都可以接收
- 写操作:先到主库执行,主库再将写操作同步给从库
全量复制(主从第一次复制,三个阶段):
- 从库与主库建立连接,告知即将同步,主库确认回复后开始同步
- 主库收到 psync 命令后,用FULLRESYNC响应,带上两个参数:主库runID和当前复制进度offset
- 主库把第二阶段执行过程中新收到的写命令再发送给从库
增量复制(从库宕机重连后的同步):
- 断连期间主库把写操作命令写入repl_backlog_buffer
- 从库重连后发送 psync 命令,并带上自己的slave_repl_offset
- 主库比较 master_repl_offset 与 slave_repl_offset 的差距:
- 差距> repl_backlog_buffer(断连过久,前面的数据被覆盖)→ 无法增量复制,只能全量复制
- 差距< repl_backlog_buffer→ 把差距部分发送给从库执行
同步三种模式:全量复制、基于长连接的命令传播(正常运行阶段)、增量复制(断开重连后)。
全量同步用 RDB 而不用 AOF 的原因:
- RDB 是压缩的二进制数据(对不同数据类型做了针对性优化),文件小;AOF 记录每次写命令,写操作越多文件越大,且包含对同一 key 的多次冗余操作
- AOF 需要选择刷盘策略,选择不当会严重影响性能;RDB 只在定时备份和主从全量同步时才生成快照
2. 哨兵模式
背景:主从服务器故障宕机时需手动恢复,哨兵模式解决这一问题,监控主从服务器并提供故障转移。
- 监控:监控主从服务器是否正常
- 通知:出现问题时通知相关人员
- 故障迁移:自动主从切换
- 统一配置管理:获取主从地址
3. 切片集群
背景:数据量大到一台服务器无法承载时使用。
作用:把数据分布到不同服务器上,降低系统对单主节点的依赖,提高 Redis 读写性能。
什么是Redis主从复制
将主服务器的数据复制到从服务器
- 主服务器上可以进⾏读写操作,当发⽣写操作时⾃动将写操作同步给从服务器
- 从服务器⼀般只读,并接受主服务器同步过来写操作命令,然后执⾏这条命令。
主从复制的同步过程
1. 建立连接:从节点向主节点发送 SYNC 命令请求建立连接;初次同步时,主节点会创建专门的后台线程进行数据传输。
2. 主节点创建 RDB 快照:初次同步时,主节点执行 BGSAVE 命令创建 RDB 快照,把当前内存数据保存到文件。
3. 主节点发送 RDB 文件和 AOF 缓冲区内容:RDB 快照创建后发送给从节点;同时把 AOF 缓冲区中的写命令也发送给从节点,确保从节点收到 RDB 后,通过执行 AOF 缓冲区写命令把数据更新至主节点执行 BGSAVE 时的状态。
4. 从节点载入 RDB 文件并执行 AOF 缓冲区命令:先载入 RDB 文件,将自身数据状态更新为主节点执行 BGSAVE 时的状态;再执行 AOF 缓冲区中的写命令,追赶主节点最新状态。
5. 增量复制:初次同步完成后转入增量复制阶段,主节点实时将写入命令发送给从节点,从节点接收并执行,保持数据一致。
6. 心跳和命令传播:主从节点间维持心跳连接以检测对方存活状态;主节点通过命令传播机制把写入命令实时同步给从节点。
Redis哨兵机制是什么
Redis的哨兵⽤于监控和管理Redis实例,它可以实现⾃动故障转移和⾼可⽤性。
哨兵系统由⼀组独⽴运⾏的进程组成,这些进程定期检查Redis主节点和从节点的状态,并在主节点宕机时选择⼀个从节点升级为新的主节点。
如何避免主从数据的不⼀致
- 持久化和复制配置
- 保证⽹络稳定性
- 监控和报警
哨兵机制的工作原理
Redis 切片集群(Redis Cluster)工作原理
背景:缓存数据量大到一台服务器无法承载时使用,将数据分布在不同服务器上,降低系统对单主节点的依赖,提高读写性能。
1. 数据分片:整个数据集划分为16384 个槽(slot),每个槽对应一个整数值,每个节点负责管理其中一部分槽的数据。
2. 节点间通信:节点之间通过gossip 协议相互通信,分享节点信息和集群状态;使用 PING、PONG 等消息保持通信,并通过定期集群信息交换保持一致。
3. 数据分布:新增键值对时,根据键的CRC16 哈希值确定它属于哪个槽,再存储到负责该槽的节点上,确保数据在集群中均匀分布。
4. 故障检测:使用哨兵机制检测节点健康状态;节点不可用时,其他节点发起故障检测流程,重新分配槽到可用节点,并选举新的主节点。
5. 客户端路由:客户端通过计算键的 CRC16 哈希值确定所属槽,直接把请求发送到负责该槽的节点,无需中间代理。
6. 数据复制:使用主从复制机制,每个槽有一个主节点和若干从节点;数据在主节点写入后异步复制到从节点,保证数据持久性和高可用性。
什么是集群脑裂(Split Brain)
定义:主从集群中出现两个主节点的现象,会导致数据丢失。
发生过程:
- 主节点网络突然出现问题,与所有从节点失联,但主节点与客户端的网络仍然正常
- 客户端不知道集群内部出问题,仍继续向失联的主节点写数据,这些数据被缓存到缓冲区
- 哨兵发现主节点失联,在从节点中选举出新的主节点→ 此时集群同时存在两个主节点,即"脑裂"
后果:脑裂会导致集群数据丢失,本质是"旧主节点与集群失联期间接收的写数据"在恢复后被全量同步清空。
Redis过期删除
使⽤ EXPIRE 命令或 SET 命令的 EX 选项来设置过期时间
集群脑裂带来数据丢失怎么办
1、设置主节点连接的从节点中⾄少有 N 个从节点,并且主节点进⾏数据复制时的 ACK 消息延迟不能超过 T 秒。
2、当主节点发现从节点下线或者通信超时的总数量⼩于阈值时,那么禁⽌主节点进⾏写数据,直接把错误返回给客户
端。,
等到新主节点上线时,就只有新主节点能接收和处理客户端请求。
此时,新写的数据会被直接写到新主节点中。⽽原主节点会被哨兵降为从节点,即使它的数据被清空了,也不会有新数据丢失。
过期删除策略有哪些
定时删除:设置 key 的过期时间时,同时创建⼀个定时事件,当时间到达时,由事件处理器⾃动执⾏ key 的删除操作。
但删除过期 key 会占⽤⼀部分 CPU 时间,对CPU不友好。
惰性删除: 不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
但是可能会导致过期key⻓期不被删除,浪费内存空间。
定期删除:每隔⼀段时间「随机」从数据库中取出⼀定数量的 key 进⾏检查,并删除其中的过期key。但是效果也没有两者好,且难以确定删除操作执⾏的时⻓和频率。
Redis过期删除策略是什么?
Redis 过期删除 = 惰性 + 定期
- 惰性:每次读写 key 前查一下。过期→删掉(
lazyfree_lazy_expire决定同步 / 异步),返回 null;没过期→正常返回。 - 定期:定时随机抽 20 个 key,删过期的;若过期比例 >25% 就继续抽,否则停,等下一轮。
- 为什么配合:惰性省 CPU、保证读不到过期数据;定期防过期 key 堆内存。
Redis内存淘汰
Redis通过参数 maxmemory 来设定最⼤运⾏内存。当系统内存不⾜时,Redis会根据配置的淘汰策略来删除⼀些键以释放内存。
内存淘汰策略
Redis的LRU算法和LFU算法有什么区别
LRU关注的是最近的访问情况,认为最近被访问的对象更可能在未来被再次访问。
记录最后一次访问时间,随机取五个值,淘汰最久没用过的那个。LFU关注的是使⽤频率,认为使⽤频率较低的对象在未来仍然会较少被访问。
维护计数,访问则增加计数,淘汰选择计数最低的淘汰。
Redis缓存
如何保证数据库和缓存的一致性
Cache Aside(旁路缓存)= 读查缓存 + 写更库
- 写策略:先更新数据库 → 再删除缓存
- 读策略:命中缓存→直接返回;未命中→查数据库→写入缓存→返回
两个变体:
- 删缓存版(默认):一致性有保障,代价是每次更新都删缓存,命中率下降
- 更新缓存版(对命中率要求高时):改为更新库 + 更新缓存;但并发更新会不一致,两个解法兜底:
- 加分布式锁:同一时间只允许一个请求更新缓存;
- 更新后给缓存加短过期时间(TTL)兜底。
如何保证删除缓存操作一定能成功?
方式一:重试机制 + 消息队列
- 删缓存失败 → 从消息队列重新读取数据 → 再次删除(重试)
- 删除成功 → 把该消息从队列移除,避免重复操作;失败 →继续重试
- 重试超过一定次数仍失败 →向业务层发送报错
方式二:订阅 Binlog(更可靠、与业务解耦)
- 删除服务伪装成 MySQL 从节点,向主节点发送 dump 请求;
- 主节点收到后推送 Binlog,删除服务解析字节流为结构化数据,再执行缓存删除。