编程导航面试通关营话题讨论

面试通关营

127 参与
分享

第五期面试通关特训营来了!30 天集中特训,每天 3 道精选高频题,带你吃透八股文高频考点,克服面试障碍,拿下高薪 offer。

🌟 关于面试通关营:https://mianshiya.com/traininfo

💎 报名链接:https://mianshiya.com/getoffer

第五期特训营开营时间:4 月 17 日 - 5 月 16 日

沉浸式备战春招

本次特训营我们直接加码升级为春招特别版!给所有入营的候选人们增加了面试求职所需的实用福利:

  • 入营即可免费领取 2w+ 字保姆级写简历指南
  • 群内获取实时更新的春招企业招聘表
  • 统一的结营考试,检验学习成果,帮助你查漏补缺

特训营奖励

👑 全勤奖:成功完成全部 30 天打卡的学员,将获得 1 个月老鱼简历会员和 60 元编程导航优惠券

💎 特别奖:面试鸭站长请吃饭,结营后选择一位特别优秀努力的学员,线下进一步交流沟通

🧧 优质题解奖:写高质量题解,获官方认证,拿红包奖励!

这里是特训营讨论交流区,可以分享你的入营目标、实用的学习技巧、每日学习总结,或是简单的心得体会,和其他学员共同进步。等到 30 天集训结束后再回看,这一路的成长都将有迹可循~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

Day01总结 MySql索引类型有哪些? 首先要明白索引是什么 索引是一种用于加速数据查询的数据结构(记这点就可以) 建立在表的一列或多列上 本质是额外的数据结构 查询时可以减少扫描的数据量 索引就是 MySQL 的“目录”,用来加速查询、保证唯一、优化排序和连接,但会增加存储和维护成本。 为什么从这三个方面回答及在突然被问到时候怎么快速回忆(数据结构、存储方式、索引性质,快速回顾就从逻辑,物理,现实使用这三个方面来展开说明) 面试官考察是否具备系统化知识架构能力(简单点:回答要体系化一些) 第一维度:数据结构角度 为什么首先要讲这个? 这是最底层、最本质的分类。索引之所以快,是因为它利用了特定的数据结构来减少磁盘 I/O 次数。不同的数据结构决定了索引的适用场景(如:适合等值查询还是范围查询)。 1.B+ 树索引 为什么是它? 因为它解决了 B 树“非叶子节点也存数据导致树矮胖”的问题。B+ 树的非叶子节点只存键值,不存数据,使得单个页能容纳更多键值,树的高度更矮(通常 3 层就能存几百万数据)。树矮意味着查询时需要的磁盘 I/O 次数少( O(logn) )。 链表设计:叶子节点之间通过双向链表连接。这解释了为什么 B+ 树既适合等值查询(快速定位),又适合范围查询(顺序扫描链表)。 2.哈希索引:利用哈希表,直接定位。时间复杂度是 O(1)O(1) ,但因为是“散列”的,无法进行排序和范围查询。 3.全文索引:本质是倒排索引。这与 B+ 树的“正排索引”(ID -> 内容)相反,它是“词 -> ID 列表”。这是搜索引擎的底层原理,用于解决模糊匹配和大数据量文本检索问题。 总结:考察的是对算法与数据结构的理解,知道“索引的性能瓶颈在于磁盘 I/O”。(如果问到优化,可以从这方面去想) 2. 第二维度:存储方式角度 为什么紧接着讲这个? 这主要针对 InnoDB 引擎(目前 MySQL 最常用的引擎)。这一层考察的是你对物理存储的理解,特别是聚簇索引和回表的概念。 核心逻辑: 聚簇索引:数据和索引绑在一起。 主键就是聚簇索引:InnoDB 中,数据行是按照主键顺序物理存储在磁盘上的。这非常高效,因为查到了索引就查到了数据(一次 I/O)。 为什么只能有一个? 因为数据行在物理磁盘上只能按一种顺序排列。 非聚簇索引:索引和数据分开存。 回表:当你用非主键索引(如普通索引)查询时,数据库先在二级索引中找到主键 ID,然后再拿着这个 ID 去聚簇索引中查找完整的数据行。这个过程叫“回表”。 覆盖索引:如果非聚簇索引的叶子节点已经包含了你要查询的所有字段(比如 select id, name from user where name='xxx',且 name 是索引),就不需要回表了。这是这一层最重要的优化思想。 总结:这一层考察的是对InnoDB 引擎内部机制的掌握,特别是“数据是怎么在磁盘上放的”以及“回表带来的性能损耗”。 3. 第三维度:索引性质角度 为什么最后讲这个? 这是最上层、最贴近业务开发的分类。面试官想知道在实际写代码、建表时,会不会用对索引类型。 核心逻辑: 主键索引:唯一性、非空。作为聚簇索引的锚点。 唯一索引:业务上的唯一约束(如身份证号、邮箱)。它允许有 NULL,但主键不允许。 普通索引:纯粹为了加速查询,没有约束。 联合索引:考察最左前缀原则 原理:联合索引 (a, b, c) 在 B+ 树中是先按 a 排序,a 相同再按 b 排序,以此类推。如果查询条件跳过 a 直接用 b,索引就失效了(因为树的结构决定了你必须从最左边开始找)。 为什么要强调列顺序? 这是面试必问,考察是否具备索引设计能力。 还有全文索引和空间索引 总结:这一层考察的是工程实践能力,即如何根据业务需求选择合适的索引类型,以及如何设计高效的联合索引。 回答每个点时,主动引出相关的高频考点 讲 B+ 树时,主动提“为什么不用红黑树”(红黑树太高,I/O 太多)。 讲非聚簇索引时,主动提“什么是回表”和“如何避免回表”(覆盖索引)。 讲联合索引时,主动提“最左前缀原则”和“索引下推”(ICP)。 面试是有时间的,尽量让大部分时间让自己处在一个高效回答期,尽可能压缩面试官对自己不熟悉领域的考察,问道时候,尽量去联想一些体系化的关键词,然后展开回答

Day 1 Q1:为什么 MySQL 选择使用 B+ 树作为索引结构? 答:因为mysql是存储在磁盘的,而树结构的IO是最低的。 Q2:MySQL 索引的最左前缀匹配原则是什么? 答:最左前缀的规则是,从左到右依次按顺序匹配索引,直到遇到范围查询停止。 Q3:MySQL 三层 B+ 树能存多少数据? 答:应该是能存储大约2000万条数据

特训营刷题知识整理(Redis)-未完成

[TOC] # Redis ## 测试环境的搭建 > Win11 + WSL Ubuntu + redis7.0.15 **reidis安装** ```bash # 1. 更新系统源,安装基础依赖 sudo apt update && sudo apt upgrade -y sudo apt install -y wget curl net-tools # 2. 安装 Redis 稳定版APT 官方源) sudo apt install redis-server -y # 3. 验证安装查看版本,确认安装成功) redis-cli --version ``` **redis.conf配置** ```bash # 1. 备份原始配置防止改错回滚) sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.bak # 2. 批量修改核心配置无需手动编辑) sudo sed -i 's/bind 127.0.0.1 -::1/bind 0.0.0.0/g' /etc/redis/redis.conf # 允许所有IP访问 sudo sed -i 's/protected-mode yes/protected-mode no/g' /etc/redis/redis.conf # 关闭保护模式 sudo sed -i 's/dir \.\//dir \/var\/lib\/redis/g' /etc/redis/redis.conf # 数据目录 sudo sed -i 's/# maxmemory <bytes>/maxmemory 256MB/g' /etc/redis/redis.conf # 内存限制 sudo sed -i 's/# maxmemory-policy noeviction/maxmemory-policy allkeys-lru/g' /etc/redis/redis.conf # 内存淘汰策略 sudo sed -i 's/daemonize yes/daemonize no/g' /etc/redis/redis.conf # 关闭后台运行适配systemd) # 3. 修复配置文件+数据目录权限解决WSL权限bug) sudo mkdir -p /var/lib/redis /var/log/redis sudo chown -R redis:redis /etc/redis/redis.conf /var/lib/redis /var/log/redis sudo chmod 644 /etc/redis/redis.conf sudo chmod 770 /var/lib/redis /var/log/redis ``` **redis.service服务配置** ```bash # 1. 备份原始服务配置 sudo cp /usr/lib/systemd/system/redis-server.service /usr/lib/systemd/system/redis-server.service.bak # 2. 批量修改服务配置解决超时/notify模式bug) sudo sed -i 's/Type=notify/Type=simple/g' /usr/lib/systemd/system/redis-server.service # 替换启动模式 sudo sed -i '/\[Service\]/a TimeoutStartSec=300' /usr/lib/systemd/system/redis-server.service # 延长超时时间 sudo sed -i 's/--supervised systemd//g' /usr/lib/systemd/system/redis-server.service # 删除WSL不兼容参数 sudo sed -i 's/ExecStart=.*/ExecStart=\/usr\/bin\/redis-server \/etc\/redis\/redis.conf --daemonize no/g' /usr/lib/systemd/system/redis-server.service # 统一启动命令 # 3. 重新加载systemd配置,清除失败状态 sudo systemctl daemon-reload sudo systemctl reset-failed redis-server ``` **验证** ```bash # 1. 启动 Redis 并设置开机自启 sudo systemctl start redis-server sudo systemctl enable redis-server # 2. 验证服务状态显示 active (running) 即为成功) sudo systemctl status redis-server # 3. 验证 Redis 功能返回 PONG 即为正常) redis-cli ping # 4. 验证核心配置生效输出对应值即为配置成功) redis-cli CONFIG GET bind # 输出 "0.0.0.0" redis-cli CONFIG GET maxmemory # 输出 "268435456"256MB) redis-cli CONFIG GET protected-mode # 输出 "no" # 5. 验证端口监听显示 0.0.0.0:6379 即为外网访问生效) sudo netstat -tulpn | grep 6379 ``` <img src="https://oss.luhuhu.cn/202601280944822.png" alt="image-20260128094447738" style="zoom: 80%;" /> ## Redis的应用场景 | 场景名称 | 技术点 | 缺陷 | 方案 | | ------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------ | | 缓存热点数据 | 1. 数据结构:String、Hash<br />2. 核心特性:过期策略、内存淘汰策略<br />3. 命令:`SET/GET`、`HGET/HMSET`、`EXPIRE`<br />4. 性能优化:Pipeline、批量操作 | 1. 缓存穿透<br />2. 缓存击穿<br />3. 缓存雪崩<br />4. 数据一致性问题<br />5. 热key导致节点压力 | 见[异常处理](#Redis异常) | | 存储Session | 1. 数据结构:String(序列化Session)、Hash<br />2. 核心特性:过期策略<br />3. 核心命令:`HMSET/HMGETALL`、`EXPIRE`、`DEL`<br />4. 集群特性:RedisCluster保证Session全局可访问 | 1. 单点故障导致Session不可用<br />2. 序列化/反序列化的性能损耗<br />3. 过期时间内的内存占用<br />4. 集群场景数据分片不均 | 见[高可用](Redis高可用) | | 分布式锁 | 1. `SETNX`+`EXPIRE`、`DEL`、`lua`脚本<br />2. 进阶:Redis Redlock算法<br />3. 特性:原子性、过期自动释放 | 1. `SETNX`+`EXPIRE`非原子,加锁成功设置过期失败导致死锁<br />2. 主从切换导致锁丢失 <br />3. 锁超时释放<br />4. 不可冲入、不可阻塞 | 见[分布式锁](#分布式锁) | | 排行榜 | 1. 数据结构:Sorted Set,score排序<br />2. 核心命令:`ZADD`、`ZRANGE/ZREVEAGE`、`ZSCORE`、`ZINCRBY`;<br />3. 扩展:Bitmap辅助统计 | 1. 数据量大情况下SortedSet性能下降<br />2. 实时性要求高,频繁`ZADD`、`ZINCRBY`导致redis节点压力大<br />3. 无法直接实现分组排行榜、分页查询<br />4. 分数相同排序不可定制 | | | 简单消息队列 | 1. 数据结构:List、Stream<br />2. 核心命令:`LPUSH/RPOP`、`BRPOP`;`XADD`、`XREADGROUP`、`XACK`<br />3. 特性:`Pub/Sub`,广播模式 | 1. List队列:消费者宕机未处理消息丢失;不支持重复消费、消费组<br />2. Stream队列:数据挤压导致内存占用过高;消费组配置复杂,运维成本高<br />3. Pub/Sub:无持久化、消费者理线丢失消息 | [消息队列](#消息队列) | | 计数/限流 | 1. 计数核心:String(INCR/DECR)、Hash(HINCRYBY)<br />2. 限流核心:固定窗口(INCR+EXPIRE)、滑动窗口(SortedSet记录时间戳)<br />3. 特性:院子命令保证计数准确 | 1. 高并发`INCR`导致节点CPU飙升、计数溢出String最大数值限制<br />2. 固定窗口临界问题、滑动窗口大数据量下SortedSet性能下降<br />3. 计数/限流无持久化,redis宕机数据丢失 | | ## Redis特性 ### 数据类型 数据类型在底层会根据数据量大小做编码切换 **基础类型** | 名称 | 说明 | 底层核心 | 场景 | | ------ | ----------------------------------- | ----------------- | ---------------------------- | | String | 基本类型,存文本、数字、二进制 | SDS | 缓存会话、页面数据、计数器 | | Hash | 键值对集合,适合存对象 | 哈希表+ziplist | | | List | 有序字符串列表,底层双向链表 | 快速列表quicklist | 消息队列 | | Set | 无序不重复集合,查找去重效率高 | 哈希表+整数集合 | 标签等需要去重的集合运算场景 | | Zset | 类似Set,多一个权重值,底层跳表实现 | 哈希表+**跳表** | 排行榜、积分榜 | **后续新增的高级类型** | 名称 | 说明 | 底层核心 | 场景 | | ----------- | ------------------------------------------------------------ | ------------------- | -------------------------------- | | BitMap | 位存储,空间利用率极高 | SDS | 签到等简单标识信息 | | HyperLogLog | 概率性数据结构,用于估算技术,固定大小 | 基数估算算法+字符串 | 网站UV,对精度要求不高但数据量大 | | GEO | 地理位置信息,支持经纬度存储和空间查询,底层Zset | | 附近的人、配送距离 | | Stream | 消息队列专用,比List多两个特性:自动生辰给全局唯一消息ID;相比Pub/Sub可以消息持久话 | | 消息队列 | **数据类型的优化:** 1. 内存紧凑存储,根据**数量大小使用不同的数据结构**; 2. 渐进式rehash:哈希扩容,拆分多次迁移,避免单次rehash阻塞主线程 3. 跳表优化:zset用[跳表](#跳表)而非[红黑树](#红黑树) ### 底层原理 redis的核心是 **单线程事件驱动模型** + **高效内存数据结构** + **按需持久化机制** * 单线程 > 单线程无并发问题 ,避免了上下文切换、锁竞争的性的性能消耗,保证命令执行的**原子性** > > 单线程下能保证效率的原因 > 所有操作基于**内存**,CPU直接通过总线访问内存,无协议、磁盘定位等开销; > 核心操作都是O(1)、O(lgN)的高效算法 如哈希、**[跳表](#跳表)**; > 采用非阻塞IO+事件驱动,避免网络、IO等待 * 事件驱动模型(Reactor) > redis将所有操作抽象为事件 > > * 文件事件:处理套接字的连接、读、写操作,依赖操作系统的**IO多路复用**,避免单线程阻塞 > * 事件事件:处理定时/周期任务 > > ```mermaid > graph TD > A[aeEventLoop 事件循环] --> B[文件事件File Event] > A --> C[时间事件Time Event] > B --> D[套接字操作连接读写] > C --> E[定时任务过期键清理持久化] > B --> F[aeFileEvent 事件处理器] > F --> G[aeAcceptHandler处理新连接] > F --> H[aeReadHandler处理读请求] > F --> I[aeWriteHandler处理写响应] > ``` #### 多路复用 > redis核心架构:命令执行是单线程的,写入场景的单线程有很多的有点,但是读取的场景并没有单线程的需求,当某个读请求卡住会因为这个读取而阻塞其他请求,所以我们想要保证单线程执行写入的同时,"多线程"同时监听多个客户端的套接字连接,全程不阻塞其他套接字的处理 > > 多路复用在网络IO层 | 多路复用函数 | 操作系统 | 核心特点 | 性能 | | ------------ | -------------------------- | ----------------------------------------------------- | ---------------------------------------------- | | epoll | Linux(2.6 及以上) | 事件驱动、高效、支持海量文件描述符(万级以上) | 最高(Redis 首选,生产环境主流) | | kqueue | macOS、FreeBSD | 功能与 `epoll` 类似,事件驱动、高效 | 次高(类 Unix 系统的最优选择) | | select | 所有操作系统(兼容性最好) | 轮询模式、低效、支持的文件描述符数量有限(默认 1024) | 最低(仅用于兼容老旧系统,**不推荐高并发场景** | **多路复用相当于队列?将请求排队,先把请求存起来不阻塞他们?** | 对比维度 | Redis多路复用 | 消息队列 | | ------------ | ------------------------------------------------ | ------------------------------------- | | 工作层级 | **网络 IO 层**(操作系统 / Redis 底层) | 业务逻辑层(应用层) | | 处理对象 | 未完成 IO 的请求(数据未传输) | 已完成 IO 的请求(数据已到达服务端) | | 核心目标 | 解决单线程 IO 阻塞,提升**连接并发能力** | 解决业务并发冲突,保证**执行一致性** | | 排队逻辑 | 无显式排队,仅处理「就绪的请求」,无序但不阻塞 | 显式排队,所有请求按序执行,严格串行 | | 是否产生延迟 | 几乎无延迟(仅唤醒 / 处理就绪 IO) | 有明显延迟(请求入队等待消费) | | 一依赖 | 操作系统原生支持(epoll/kqueue),Redis 封装使用 | 独立的中间件,需单独部署维护 | | 与redis关系 | Redis**底层核心机制**,必须依赖 | 与 Redis 无关,是业务层的并发控制方案 | 多路复用并不是让请求排队,而是请求IO就绪了自己来找我,**多路复用是不让IO卡线程,消息队列是不让业务卡业务** **医院场景示例** > ### 多路复用:医院的「大门分诊台」(网络 IO 层) > > - **工作对象**:刚到医院门口、还没进入诊疗区的病人(还未完成网络 IO 的请求,数据还没传到 Redis); > - **核心工作**:判断「哪个病人已经走到分诊台(IO 就绪)」,让他进入诊疗区,**避免工作人员跑到大门口挨个等病人(阻塞 IO)**; > - **排队逻辑**:病人不用排队,只要走到分诊台(IO 就绪),就会被依次接待,未到的病人不会占用工作人员时间; > - **核心目的**:让工作人员能高效接待「同时到达的大量病人」,不被某个走得慢的病人堵在大门口。 > > ### 消息队列:医院的「诊疗区叫号机」(业务逻辑层) > > - **工作对象**:已经进入诊疗区、挂完号等待看病的病人(已完成网络 IO,请求已经到达 Redis / 业务服务端); > - **核心工作**:让病人按号排队,**避免多个病人同时挤到医生诊室(业务并发冲突)**; > - **排队逻辑**:病人必须按顺序排队,叫到号才能进入诊室执行业务(看病),全程串行; > - **核心目的**:让医生能有序处理病人,避免并发冲突,保证业务执行的一致性。 > > ### 二者的配合(如果医院同时有分诊台 + 叫号机) > > 病人先经过**分诊台(多路复用)** 进入诊疗区,再通过**叫号机(消息队列)** 排队看病,二者分工明确,缺一不可,这也是实际生产中「Redis 多路复用 + 业务层 MQ」的真实配合逻辑。 ## Redis高可用 > Reid部署方案的演进,核心是为了解决单点Redis的两个核心问题: > > * 可用性问题:单点redis可靠性差,宕机后服务不可用 > * 扩展性问题:单节点的内存QPS瓶颈,无法支撑大规模业务的高并发、大数据量需求 ### 持久化机制 Redis的持久化核心是将**内存写入磁盘**,宕机时避免数据丢失 #### RDB快照持久化 **原理** 1. 主线程`fork`子进程(写实复制[COW](#COW)),子线程遍历内存数据,形成二进制RDB文件 2. 主线程继续处理请求,修改数据会单独 复制一份副本,不影响子进程快照 **同步策略:触发** 手动`save/bgsave`、**配置定时策略**、主从复制主节点自动触发 **优缺点** * 优点 1. 文件体积小,相对AOF文件RDB的二进制压缩文件小很多 2. RDB是数据的完整快照,只需要加载RDB到内存,和AOF相比无需 执行命令重放,恢复速度远快于AOF 3. 快照由子进程完成,父进程只在`fork`期间短暂阻塞,适合高并发 4. RDB完整快照适合 定期备份场景、数据归档 * 缺点 1. RDB是定时快照,快照期间宕机会丢失两个快照间隙的数据 2. fork阻塞问题:当写入数据量大的时候,`fork`子进程耗时越长,阻塞时间越长,影响redis可用性 > RDB因其原理导致只适合用于对恢复速度要求高 ,数据丢失容忍度高的场景。如定时备份、容灾等场景 #### AOF追加日志 **原理** 1. 不保存数据本身,记录所有写命令到AOF文件(类似MySQL binlog的`statement`模式),重启时重放AOF存的写命令恢复数据 2. AOF重写:`fork`子进程遍历内存数据,生成最终状态命令集,多次INCR合并压缩文件体积 **核心同步刷盘策略** * always:每次写命令同步刷盘,最安全性能,IO开销巨大redis性能急剧下降一般用于数据持久性要求极高的场景如金融交易、核心账务数据 * everysec:每秒刷盘,平衡性能和可靠性,最多丢掉1s数据 * no:由操作系统决定刷盘时机,性能最好,但是可靠性差 **优缺点** * 优点 1. 安全性可控,`always`模式下可以实现数据领丢失,可靠性远高于RDB 2. 数据恢复完整,AOF记录所有写命令,重启时重放可完整恢复数据 3. 文件可读性高:AOF文件是明文redis敏玲,可直接查看,便于排查数据问题 4. 无fork阻塞风险,追加命令时无需`fork`子进程,仅在AOF重写是fork,对主进程阻塞影响小于 RDB * 缺点 1. 文件体积大,存的是全部命令追加,文件远大于RDB,占用更多磁盘空间(**[AOF重写机制](#AOF重写机制)**) ##### AOF重写机制 为避免对同一条记录多次SET情况导致AOF文件爆炸,优化出一个AOF重写机制(**默认不开启**,也就是会记录所有SET),最终只保留该数据的最新一条有效SET,丢弃冗余SET. AOF重写机制并不是**修改**原AOF文件,而是生成一份**全新的精简AOF文件**流程如下: 1. Redis主进程`fork`一个子进程,负责生成新的AOF 2. 子进程遍历Redis中所有key,为每个key生成最终的有效命令,写入新AOF 3. 在子进程中生成新AOF期间,主进程继续处理正常写请求,同时 将这些新的命令追加到**AOF重写缓冲区** 4. 子进程完成新AOF文件后,主进程会将AOF重写缓冲区所有命令追加到新AOF中 1. 最后用新AOF替代旧AOF 除此之外AOF重写还有两个重要优化: * 合并批量命令,如对一个列表执行100次LPUSH,重写会合并成一条LPUSH命令 * 忽略无效命令:对不存在的key执行DEL或对非列表执行LPUSH等无效 命令会被直接忽略过滤 **触发方式** * 手动触发 ```bash 127.0.0.1:6379> BGREWRITEAOF Background append only file rewriting started ``` * 自动触发 修改配置文件,当增长率100%时,redis自动执行`BGREWRITEAOF` ```xml # AOF 文件最小重写大小(默认 64MB),小于该大小不会触发重写 auto-aof-rewrite-min-size 64mb # AOF 文件增长率(默认 100%),即当前 AOF 文件大小 ÷ 上一次重写后的 AOF 文件大小 ≥ 100% 时触发 auto-aof-rewrite-percentage 100 ``` #### 混合持久化 **绝大多数生产环境优选** > 由于RDB和AOF各自的短板,redis4.0版本引入了**混合持久化**,结合RDB和AOF的优点: > AOF文件的前半部分是RDB格式的完整快照,后半部分是AOF的增量写命令。 > > 在这种模式下,redis重启先加载RDB格式完整快照恢复大部分数据,再重放AOF格式的增量命令,兼顾了RDB的快速恢复和AOF的数据完整性 **开启混合配置** ```xml # 先开启 AOF(混合持久化依赖 AOF) appendonly yes # 开启混合持久化(Redis 4.0+ 支持,默认 yes,推荐开启) aof-use-rdb-preamble yes # 其他配置(沿用 AOF 和 RDB 的核心配置) appendfsync everysec auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb ``` **核心流程** 混合持久化的核心是AOF重写成RDB+AOF混合格式的文件 1. Redis 执行 `BGREWRITEAOF` 命令,主进程 `fork()` 子进程。 2. 子进程遍历 Redis 内存中的所有数据,将数据以 RDB 格式写入临时文件(前半部分)。 3. 父进程将重写期间接收的写命令,追加到「AOF 重写缓冲区」。 4. 子进程完成 RDB 格式数据写入后,通知父进程。 5. 父进程将「AOF 重写缓冲区」中的命令,以 AOF 格式追加到临时文件末尾(后半部分)。 6. 父进程用临时混合文件替换原有的 AOF 文件,重写完成。 ### 高可用方案 #### 主从架构 > 主从架构是**最基础**的高可用和数据备份方案,采用一主多从结构,核心是**数据复制**,主节点处理所有请求(也可以做**读写分离**只负责写入),从节点 通过复制主节点数据形成副本,实现数据备份和分担主节点的读压力 > > 纯主从架构中,主节点宕机redis服务会处于可读不可写状态,直到主节点恢复 * 主节点:核心节点,处理所有`SET/HSET`等命令,同时接受从节点复制请求,同步数据给从节点 * 从节点:只读节点,仅复制主节点,提供读服务,分担主节点读压力 **核心工作流程** * 全量复制(首次同步/大故障后同步) 1. 从节点向主节点发送同步请求 2. 主节点接收请求后,执行`bgsave`, `fork`子进程复制RDB快照 ,同时将快照创建期间接受的写命令缓存到**复制积压缓冲区**(repl_backlog_buffer) 3. 从节点获取到RDB文件,加载RDB文件 4. 主节点将复制积压缓冲区的增量写命令发送给 从节点 5. 从节点执行增量命令 * 增量同步 主节点后续执行的所有写命令都会通过复制积压缓冲区(repl_backlog_buffer),试试发送给从节点 * 主节点每执行一个写,就将命令追加到复制积压缓冲区 * 从节点定期向主节点发送心跳,同时携带同步的偏移量 * 主节点根据偏移量,将复制积压缓冲区中未同步命令发送给从节点 * 从节点 执行,保持和主节点的数据一致性,实现增量同步 > **级联复制**,主从复制在一主多从情况下,为了缓解主节点的复制压力,可以让从节点作为其他从节点的复制结点,形成主->从->从的链式结构 > ##### 复制积压缓冲区和AOF的区别 复制积压缓冲区是专服务于**主从同步**的,AOF是服务于**redis磁盘持久化**的,当开启混合持久化模式时同步依然是走先同步AOF(RDB+AOF),之后在同步复制积压缓冲区给从库(即便AOF中可能和复制积压缓冲区中有重复的部分),实现同步。 **整个同步流程** > **阶段 1**:从库发起同步请求,主库初始化同步 > > 1. 从库执行`slaveof 主库IP 端口`,向主库发送**同步请求**,携带自身标识; > 2. 主库接收到请求后,标记该从库为「待同步节点」,**初始化复制积压缓冲区**(若未初始化),同时记录「主库运行 ID(runid)」和「当前主库偏移量(master_repl_offset=0)」。 > > 阶段 2:主库 fork 子进程,生成「RDB+AOF 混合文件」(全量数据准备) > > 1. 主库执行`fork`系统调用创建子进程(利用 COW 机制,不阻塞主进程),**子进程负责遍历主库内存全量数据,生成 RDB 二进制快照**; > 2. 主进程继续处理客户端写命令,此时会做3 个关键操作(核心:同一份命令多端写入): > - 写入**AOF 缓冲区**(最终刷盘到混合格式的 AOF 文件,持久化落地); > - 写入**复制积压缓冲区**(内存环形缓冲区,供主从同步使用); > - 写入**复制客户端缓冲区**(实时推送给从库,若从库还未准备好,先暂存); > 3. 子进程生成 RDB 完成后,主库会将**fork 期间主进程产生的所有增量写命令**,以**AOF 明文格式**拼接在 RDB 文件后,生成 **「RDB 全量 + AOF 增量」的混合文件 **(这就是混合持久化的产物,和 AOF 重写的文件格式完全一致)。 > > **阶段 3**:主库传输混合文件,从库加载全量数据(全量同步核心) > > 1. 主库通过 TCP 长连接,将**混合文件完整传输给从库**; > > 2. 从库接收混合文件后, > > 先清空自身内存数据 > > ,执行两步加载: > > - 第一步:加载**混合文件的 RDB 部分**,快速恢复主库 fork 瞬间的全量数据(RDB 加载速度远快于纯 AOF); > - 第二步:加载**混合文件的 AOF 部分**,重放主库 fork 期间的增量命令,恢复到主库「当前最新数据状态」; > > > > 3. 加载完成后,从库记录**主库 runid**和**自身偏移量(slave_repl_offset)**,并向主库发送「加载完成确认」。 > > **阶段 4**:进入常态增量同步,依赖复制积压缓冲区(核心阶段,长期运行) > > 这是主从同步的**常态阶段**,全量同步完成后永久运行,**复制积压缓冲区是核心载体**,AOF 仅做自身持久化,流程如下: > > 1. 主库处理客户端 > > 任意写命令 > > (SET/HSET/DEL 等),执行后做 > > 3 个必选操作: > > - ✅ 更新自身内存数据; > - ✅ 将命令写入**AOF 缓冲区**(按混合格式刷盘,保障主库自身宕机不丢数据); > - ✅ 将命令写入**复制积压缓冲区**,同时**主库偏移量(master_repl_offset)自增**(每 1 字节命令 + 1); > > 2. 主库通过长连接,**将该写命令实时推送给从库**; > > 3. 从库接收命令后,**立即在本地执行**,更新自身内存数据,同时**从库偏移量(slave_repl_offset)自增**(与主库偏移量保持一致); > > 4. 从库以**1 秒为间隔**,向主库发送**心跳包(PING)**,心跳包中携带**自身当前偏移量**; > > 5. 主库接收心跳包后, > > 对比主从偏移量: > > - 若两者一致:回复 PONG,同步状态正常; > - 若从库偏移量 < 主库偏移量:说明从库漏同步了命令,主库从**复制积压缓冲区**中提取「从库偏移量→主库偏移量」之间的所有增量命令,推送给从库,从库执行后补全偏移量。 > > > > **阶段 5**:短暂断连后恢复,走「部分重同步」(复制积压缓冲区的核心价值) > > 若主从网络短暂波动导致断连,**只要复制积压缓冲区未被覆盖**,就不会触发全量同步,流程如下: > > 1. 主从断连期间,主库继续处理写命令,**正常写入 AOF 和复制积压缓冲区**,主库偏移量持续自增; > 2. 从库重连主库后,向主库发送**重同步请求**,携带「之前记录的主库 runid」+「自身断连前的偏移量」; > 3. 主库验证: > - 若**runid 一致**(主库未重启)+ **从库偏移量在复制积压缓冲区的有效范围内**(未被新命令覆盖):触发**部分重同步**; > 4. 主库从复制积压缓冲区中,提取「从库偏移量到当前主库偏移量」的所有增量命令,一次性推送给从库; > 5. 从库执行所有增量命令,更新自身偏移量,**快速恢复与主库的偏移量一致**,回到「阶段 4 的常态增量同步」。 > > **阶段 6**:极端情况触发「全量重同步」(兜底机制) > > 若从库断连时间过长,**复制积压缓冲区中的增量命令被新命令覆盖**,或主库重启(runid 变化),主库会拒绝部分重同步,**重新回到阶段 2**,再次生成「RDB+AOF 混合文件」,走全量同步流程,完成后回到阶段 4。 | 对比维度 | 复制积压缓冲区 | AOF | | -------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | 核心用途 | 服务于「**主从增量同步 / 部分重同步**」,仅用于主从之间的数据补全 | 服务于「Redis 数据持久化」,防止 Redis 宕机后内存数据丢失,用于重启恢复数据 | | 存储形式 | 「内存级」环形缓冲区,数据存储在内存中,断电即失 | 「磁盘级」日志文件,数据存储在磁盘上,断电后数据不会丢失 | | 存储内容 | 仅存储「最新的写命令」(二进制格式,精简),仅保留近期命令,用于补全从库同步缺口 | (未开启混合持久化)存储「所有写命令」(明文 Redis 命令格式),按执行顺序完整追加,用于完整恢复内存数据 | | 生命周期 | 随Redis主进程启动创建,关闭而销毁;<br />环形特性,写满后会覆盖旧命令;<br />从库长期断连,主库会释放对应的缓冲区资源; | 随Redis启动(开启AOF),文件永久保存在磁盘;<br />无限追加;<br /> | | 作用对象 | 仅作用域主从同步,与客户端无关 | 仅作用域redis本身,用于redis持久化,与主从架构无关 | | 配置参数 | 1. repl-backlog-size:缓冲区大小<br />2.repl-bakclog-ttl:断连后缓冲区保留时间 | 1. appendonly yes:开启AOF<br />2. appendfsync:刷盘策略<br />3. aotu-aof-rewrite-*:自动重写配置 | ##### **增量/全量复制判断逻辑,缓冲区实现原理** Redis主从复制是根据**复制积压缓冲区**的偏移量来判断**增量复制**和**全量复制**的。 **核心结论** 1. 增量复制的前提:从节点的偏移量在主节点复制积压缓冲区有效范围内,切主从节点的ID匹配,此时触发**增量复制** 2. 全量复制触发场景 * 从节点首次复制 * 主从运行ID不匹配(主节点重启、故障恢复后runid会变更) * 从节点的偏移量已不在主节点的复制积压缓冲区(指针被覆盖) **复制积压缓冲区原理** 复制积压缓冲区的底层是一个**固定大小**的连续字节**环形**数组。 包含数据主体和3个管理标记,3个标记如下 * 主库偏移量,标记主库当前写入进度 * 起始偏移量,标记缓冲区最久的数据,用于判定增量同步可行性 * 缓冲区长度,管理环形覆盖逻辑,限定缓冲区最大内存 ##### 异常场景实例 纯主从架构,当主节点宕机会发生什么? > 主节点宕机,redis进入 可读不可写状态 > > 1. 场景一:修复主节点。主节点刷新**runid**,期间从节点会携带两个信息(旧runid和偏移量)持续尝试重连主节点,主节点验证runid不匹配,直接触发**全量复制**。即主从架构 **主节点宕机重启后 所有从节点都会执行一次全量复制** 会造成主节点网络带宽被大量占用(向多个从节点传输RDB快照,可以通过**级联同步规避**);从节点服务阻塞直到完成同步 > > 2. 场景二:等不了修复了,需要手动选择一个从节点作为新的主节点。 > 为了业务不中断,选择一个从节点作为新的主节点。 > 从节点如何变成主节点: > > 1. 选择一个健康、数据同步完整度最高的从节点,解除从节点身份 > > ```bash > # 核心命令:取消从节点身份,变为可写主节点 > 127.0.0.1:6380> SLAVEOF NO ONE > OK > > # 验证:查看节点角色,确认已变为master(可写) > 127.0.0.1:6380> INFO replication > # Replication > role:master # 角色为master > connected_slaves:0 # 暂无从节点 > master_runid:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 新生成的runid(晋升后自动生成) > ``` > > 2. 逐个登录其他从节点,设置主节点指向 > > ```bash > # 核心命令:指向新主节点的IP和端口 > 127.0.0.1:6381> SLAVEOF 192.168.1.101 6380 # 新主节点IP:端口 > OK > > # 若新主节点有密码认证,需配置认证密码 > 127.0.0.1:6381> CONFIG SET masterauth 123456 > OK > ``` > > 3. 主从全量同步,切换业务读写地址,完成切换 > > 那么当原主节点恢复时会发生什么? > > 1. 原主节点恢复后任然保持主节点和原有数据,形成**双主并存并互不感知的情形** > 2. 数据必然存在不一致:原主节点宕机前未同步给从节点的偏移量、新主节点在宕机期间接收到的新写入数据,这两部分数据互为缺失,而**Redis无自动合并能力** > 3. 这种局面会导致业务读写混乱、数据丢失/错乱,**必须人工介入处理** > > 处置方式:核心是保留新主节点,将原主节点降级或直接废弃,处置步骤 > > 1. 隔离原主节点,防止后续新的写入 > 2. 处理数据差异:如果原主节点存储的非核心数据可以直接废弃;如果是核心数据就只能通过**人工甄别**迁移到新主节点上(**高危操作**) > 3. 原主节点降级或废弃 > 4. 验证业务状态 ##### 主从架构的缺陷 1. 主节点宕机后无自动故障转移,依赖人工介入 2. 主节点宕机恢复后的双主冲突,数据不一致且不具备合并能力 3. 主节点重启或新选举主节点,必然触发全量复制(runid的更新),引发性能风暴 4. 单主节点性能瓶颈、可靠性健壮性不佳 5. 无统一的节点状态感知监控,各节点的状态(同步进度、偏移量、节点存活)相互独立,无法全局感知管理 #### 哨兵模式 > 哨兵模式是Redis官方提供的企业级高可用方案,基于**主从架构**扩展,核心是 **自动化故障检测和故障转移** ```mermaid flowchart TB subgraph 哨兵集群 A[sentinel1]---B[sentinel2] end 哨兵集群 ---> 数据节点 subgraph 数据节点 C[master] --> D[slave1] --> E[slave2] end 数据节点 --> 客户端 ``` **哨兵节点** 1. 监控:实时监控数据节点健康状态,定时发送心跳请求 2. 通知:当节点出现故障时候,通过配置的脚本通知运维人员 3. 故障转移:主节点宕机后,自动选举从节点晋升为主节点,更新其他从节点和客户端配置 4. 配置提供者:客户端通过哨兵获取当前主节点地址,无需硬编码主节点IP:PORT 5. 哨兵节点通常部署奇数个,避免选举脑裂,单哨兵存在故障风险 **启动哨兵节点** ```bash # 格式:redis-sentinel <哨兵配置文件路径> redis-sentinel /usr/local/redis/conf/sentinel.conf # 或等价于(以哨兵模式启动 Redis) redis-server /usr/local/redis/conf/sentinel.conf --sentinel ``` ##### 工作原理 * 监听 1. 哨兵节点启动后,通过配置文件指定监控的主节点 2. 定期PING,判断主节点存活 3. 主节点超时未响应30s,尚明将主节点标记为`主观下线` 4. 集群哨兵间通信,交换割接点对主节点状态判断 5. 超过半数哨兵判断主观下线,则将主节点标记为`客观下线`,触发故障转移流程 * 故障转移 1. 选举master哨兵:集群 通过Raft选举出`master sentinel`,仅由MS执行故障转移 2. 选举新主节点:`mater sentinel`遍历数据节点,根据优先级、复制偏移量、运行规则等,选举 出最优从节点未主节点 3. 晋升主节点:`master sentiel`向选中的节点发送`SLAVEOF NO ONE` 晋升为主节点 4. 更新其他从节点:`master sentiel`向其他数据节点发送`SLAVE OF newip:port`,设置新主节点(**会全量同步?**) 5. 哨兵集群更新配置将心的主节点作为 后续监控主节点 6. 通知客户端:通过配置脚本通知客户端更新新主节点地址 7. 旧主节点恢复(可选):旧主节点宕机恢复后,会被哨兵自动配置为新主节点的从节点,复制新主节点数据 **核心配置(sentinel.conf)** ```bash # 格式:sentinel monitor <监控名称> <主节点IP> <主节点端口> <法定票数> # 含义:监控一个名为 mymaster 的主节点,法定票数为 2(需至少 2 个哨兵认为主节点下线,才触发故障转移) sentinel monitor mymaster 192.168.1.100 6379 2 # 主节点密码(若主节点有认证,必须配置) sentinel auth-pass mymaster 123456 # 主节点主观下线超时时间(默认 30000 毫秒 = 30 秒) sentinel down-after-milliseconds mymaster 30000 # 故障转移超时时间(默认 180000 毫秒 = 3 分钟) sentinel failover-timeout mymaster 180000 # 故障转移时,同时同步的从节点数量(默认 1,减少对新主节点的压力) sentinel parallel-syncs mymaster 1 # 故障通知脚本(可选,节点故障时执行,用于告警) sentinel notification-script mymaster /usr/local/redis/sentinel/notify.sh ``` ##### 优劣分析 **优点** * 自动化故障转移 * 基于主从架构 * 高可靠:哨兵集群 * 无需修改客户端配置(硬编码) **缺点** * 仅解决了高可用,未解决扩展性,哨兵模式依旧是一主多从,主节点性能瓶颈依然存在 * 写操作几种在单主节点,无法实现写入的负载均衡 * 故障转移期间,Redis服务可能有短暂的抖动,对实时性高的业务会有影响 * 配置和运维复杂度高于 主从架构 #### 集群部署 RedisCluster是Redis3.0+ 官方提供的**分布式集群方案**,核心是**分片存储**+**分布式高可用** ```mermaid flowchart TB subgraph Redis集群 A[master1:0-5460]---B[master2:5461-10922] --- C[master3:10923-16383] A --> D[slave1] B --> E[slave2] C --> F[slave3] end Redis集群 --> 客户端 ``` **核心概念** 集群采用五中心架构,所有节点地位平等 * 槽位:将数据空间划分为16384个槽位,每个键值对通过`CRC16(key) % 16384`计算,映射到其中一个槽位 * 主节点与槽位:每个主节点负责一部分槽位 * 主从节点对应:每个主节点至少配备1个从节点 * 集群通信:所有结点通过`Gossip`协议定期交换集群信息(节点状态、槽位分配、故障信息),保障集群的一致性 **节点要求** * 集群节点最少为3个主节点+3个从节点 * 每个节点需开启集群模式 ##### 工作原理 1. 数据分片与存储 1. 客户端发送 `SET key value` 命令,先通过 `CRC16(key) % 16384` 计算出 `key` 对应的槽位(如槽位 1000)。 2. 客户端(或节点)查询集群槽位分配信息,找到负责槽位 1000 的主节点(如主节点 1)。 3. 客户端将命令发送到该主节点,主节点执行命令,将数据存储在本地。 4. 主节点将数据同步给自身的从节点,形成数据副本。 2. 故障检测与故障转移 1. 集群中每个节点定期向其他节点发送 `PING` 命令,判断节点是否存活。 2. 若某个主节点在超时时间内(默认 15 秒)未返回响应,发送 `PING` 的节点将其标记为「主观下线」。 3. 若集群中超过半数的主节点都将该主节点标记为「主观下线」,则将其标记为「客观下线」。 4. 该主节点的从节点通过「Raft 算法」选举出一个新主节点,晋升为主节点,接管原主节点的槽位。 5. 集群通过 Gossip 协议更新槽位分配信息,客户端后续请求将发送到新主节点。 3. 客户端重定向 若客户端将命令发送到了不负责对应槽位的节点,该节点会返回 `MOVED` 重定向响应,告知客户端正确的主节点地址,客户端后续将命令发送到正确节点。 **核心配置**(redis.conf) ```conf # 开启 Redis 集群模式(默认 no,改为 yes) cluster-enabled yes # 集群配置文件名称(自动生成,无需手动编辑,记录集群槽位、节点信息等) cluster-config-file nodes-6379.conf # 集群节点超时时间(默认 15000 毫秒 = 15 秒,节点超时未响应则标记为下线) cluster-node-timeout 15000 # 开启持久化(推荐,避免集群重启后数据丢失) appendonly yes appendfsync everysec # 其他基础配置(端口、密码等) port 6379 requirepass 123456 masterauth 123456 ``` **集群的创建** Redis提供了`redis-cli --cluster`可快速搭建集群 1. 准备6个redis节点,分别配置不同端口,开启集群模式 2. 启动6个节点 3. 执行集群搭建命令,自动分配槽位和主从关系 ```bash # 格式:redis-cli --cluster create <节点1IP:端口> <节点2IP:端口> ... --cluster-replicas 1 # --cluster-replicas 1:表示每个主节点对应 1 个从节点 redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 --cluster-replicas 1 -a 123456 ``` 4. 验证集群状态 ```bash redis-cli -c -p 6379 -a 123456 127.0.0.1:6379> CLUSTER INFO # 查看集群整体信息 127.0.0.1:6379> CLUSTER NODES # 查看集群所有节点信息 ``` ##### 优劣分析 **优点** 1. 水平扩展:通过数据分片,将数据分散到多个主节点,增加主节点扩展内存和QPS 2. 分布式高可用:每个节点都有从节点,无需哨兵,主从自行切换 3. 无中心架构:所有结点地位平等,无单点故障风险 4. 支持读写分离:从节点可以提供读服务,分担主节点读压力 **缺点** 1. 配置和运维复杂度高:集群的搭建、扩缩容、故障排查难度远高于主从和哨兵 2. 不支持跨槽位操作:跨槽位命令需要客户端拆分命令分别执行 3. 数据迁移有开销:扩缩容期间会占用网络带宽和节点资源,会影响服务性能 4. 不分功能受限:如事务、Lua脚本等在集群模式下有一定限制 ##### 异常场景 1. 因为集群部署是数据分片的,那么范围查询跨分片时会怎么样?如何解决 范围查询在单节点内不受影响; 范围查询超过单节点会直接失效或额外处理; > 解决方案: > > 1. 使用哈希标签,强制相关key落入同槽位 > 2. 客户端/**中间件**(Twemproxy、Codis):分布式场景下通用解决方案,客户端或中间件一次连接集群所有节点,在每个节点上执行`SCAN`,最后将所有结果聚合返回完整数据 > 3. 业务侧重新设计数据结构(**业务侧让步**) 2. 如果分片节点和它相关的从节点都故障宕机了,会如何?只是分片主节点宕机 主从切换过程中是不是会丢数据? 分片节点和相关的从节点同时故障宕机,会导致该分片相关的所有业务中断,只能人工介入修复;分片节点宕机,主从切换必然会丢失一部分未同步的数据,**复制积压缓冲区原理**中的数据 **CRC算法** ##### Gossip协议 ##### 读写分离中间件 Codis、Twemproxy **异地多活** ##### 分布式存储 MinIO、Ceph #### 云厂商Redis托管 花钱买服务,相当于把Redis这块业务卖给云厂商,由他们去负责细节的实现,只管使用(**财力雄厚方案**) #### 心跳模式 > 节点间的心跳是双向、定时的,核心目的是检测**节点的存活** 和 **节点状态/偏移量**,不同架构下的心跳略有差异,单核心一致 1. 主从架构下的心跳模式 从节点主动发起,主节点被动响应 slave -> master,发送PING包,携带3个信息(自身节点ID+已同步的偏移量+主节点runid) master响应,收到PING后返回PONG包(包含主节点当前的偏移量+自身状态) slave接受到PONG后,判断主节点存活,更新自身记录的主节点偏移量,为后续增量同步做准备 若PING超时(默认60s),判定主节点宕机,停止增量同步,进入重连流程 主节点也会主动发送增量数据包,不属于心跳,是数据同步,和心跳并行互不干扰 2. 哨兵模式下的心跳模式 哨兵模式的心跳由哨兵节点(sentinel)发起,数据节点(主/从)被动响应,核心是为了监控数据节点的健康状态 sentinel -> master/slave,发送PING master/slave 响应PONG(自身状态+是否可读+是否主节点) sentinel在30s内未收到PONG响应,则将该节点标记为主观下线 哨兵集群间通过心跳同步节点状态,超过半数哨兵未收到PONG则标记为客观下线,触发故障转移 * ## Redis异常 ### 缓存异常 | 异常名称 | 描述 | | -------- | ------------------------------------------------------------ | | 缓存穿透 | **高危!**通常是恶意攻击。靶数据不存在,缓存、数据库中均没有 | | 缓存击穿 | 某热点key过期瞬间,大量请求打到数据库。常见于秒杀、抢车票情形 | | 缓存雪崩 | 大批量key同时过期,或redis服务宕机,请求全打到数据库 | #### 缓存穿透、击穿、雪崩的应对策略 **穿透** 1. 入口校验 参数和方法性校验,如数据类型、数值大小(ID < 0)等直接返回 2. **[布隆过滤器](#布隆过滤器)** ```java public User getUser(Long userId) { String key = "user:" + userId; // 1. 先查布隆过滤器 if (!bloomFilter.mightContain("user_bloom", String.valueOf(userId))) { return null; // 布隆过滤器说不存在,直接返回 } // 2. 查缓存 User user = cache.get(key); if (user != null) { return user; } // 3. 查数据库 user = userDao.findById(userId); if (user != null) { cache.set(key, user); } return user; } ``` 3. 缓存空值 已经打到数据库返回`null`的数据也在内存中缓存一个空标识,设置较短过期时间,防止同一个不存在的key反复穿透 **击穿** 1. 互斥锁:缓存失效时加锁限制只让一个请求数据库重建缓存,其他请求等待。分布式环境用redis `SETNX`实现[分布式锁](#分布式锁) 2. 热点数据定时刷新 **雪崩** 1. 大量key同时过期情形:过期时间随机分布,避免集体失效 2. redis宕机情形 * Redis[高可用](#高可用方案)部署:主从架构+哨兵模式、集群部署 * 本地缓存:本地缓存**Caffeine/GuavaCache**,转移部分压力到JVM缓存 * 服务熔断:Sentinel/Hystrix,监听压力过大直接熔断,避免系统崩溃 * 缓存预热:系统启动主动加载热点数据到缓存,避免冷启动大量请求穿透 ### 一致性问题 双写一致性问题,即不同数源之间同步间隙产生的不一致情形 ### 布隆过滤器 布隆过滤器(BloomFilter)是一种**概率型**数据结构,不存在一定,存在不一定。 **原理** > 布隆过滤器由一个位数组和k个独立的哈希函数组成。添加元素通过k哈希 函数算出k个位置,置1;查询时计算同样k位置,全1则可能存在,存在1个0则一定不存在。**误判发生在不同元素哈希冲突时** > > 布隆过滤器**不支持删除**,只能新增。想要删除某个元素,只能把整个过滤器重建 > 位数组的某个位可能是多个元素共同设置的,导致其中一个删除时会将另一个误判为不存在,所以不支持删除 **适用场景**:大数量,允许小概率误判允许一定的不可靠性),只判断存在性 * 爬虫url去重 * 垃圾邮件过滤 * 推荐系统去重 * 分布式缓存 **RedisBloom模块详解** ```bash # 创建过滤器时指定误判率和预期容量 BF.RESERVE myfilter 0.001 10000000 # 误判率 0.1%,容量 1000 万 # 批量添加 BF.MADD myfilter item1 item2 item3 # 批量查询 BF.MEXISTS myfilter item1 item2 item3 # 查看过滤器信息 BF.INFO myfilter ``` **Java Redisson操作** ```java RBloomFilter<String> bloomFilter = redisson.getBloomFilter("user_bloom"); // 初始化,预期插入 5000 万,误判率 3% bloomFilter.tryInit(50000000L, 0.03); bloomFilter.add("user:10086"); boolean exists = bloomFilter.contains("user:10086"); ``` **误判率和参数选择** 误判率取决于三个因素:位数组大小m,哈希函数个数k,已插入元素数量n 理论上最优哈希函数个数 `k = (m/n) & ln2`,大约是0.7倍的位数组和元素数量之比 实际工程中一般这样估算: 1. 误判率1%,每个元素约10bit 2. 误判率0.1%,每个元素约15bit 3. 误判率0.01%,每个元素约20bit 降低误判率通过增大位数组,增加哈希函数来降低,但是**哈希冲突不可避免**无法降低误判率到0),其实是用**空间换可靠性** **布隆过滤器的变种** * counting bloom fileter:每个位置 不是0/1,而是计数器,可以删除,代价是空间翻好几倍 * cuckoo filter:支持删除,空叫效率和原版差不多,Facebook在用 * scalable bloom filter:支持动态扩容,元素超了自动加一层过滤 > RedisBloom 模块只支持cuckoo filter,用CF.ADD/CF.EXISTS操作 **Redis实现布隆过滤器的方式** * *位图手动实现* (**不推荐使用,自己实现自己选哈希、计算数组大小、写代码逻辑容易出错)** `SETBIT/GETBIT`自己管理哈希函数和**位数组** * 官方RedisBloom 由一个**位数组**和**k个哈希函数**组成 ### 倾斜 数据分布/访问负载不均,导致单点压力过大 ## 分布式锁和消息队列 ### 分布式锁 > 多台应用服务器、多Redis节点(集群/主从)下,并发更新缓存需要用**分布式锁**,保证跨应用、跨节点的并发安全 **基础方案**:SETNX + EXPIRE 分布式锁的基础实现,核心是利用SETNX的原子性,存在**缺陷** ```java // 加锁 public boolean tryLock(String lockKey, String requestId, int expireSeconds) { // 1. SETNX 加锁 Long result = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId); if (result != null && result == 1) { // 2. EXPIRE 设置过期时间,防止死锁(加锁成功后进程宕机,锁无法释放) redisTemplate.expire(lockKey, expireSeconds, TimeUnit.SECONDS); return true; } return false; } // 解锁 public void unlock(String lockKey, String requestId) { // 校验请求ID,防止误删其他线程的锁(重要) if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } } // 缓存更新逻辑(伪代码) public void updateCache(String cacheKey, Object newData) { String lockKey = "lock:" + cacheKey; // 锁key与缓存key绑定 String requestId = UUID.randomUUID().toString(); // 唯一标识,用于解锁校验 try { // 尝试加锁,超时时间30秒 boolean locked = tryLock(lockKey, requestId, 30); if (!locked) { // 加锁失败,重试或直接返回(根据业务场景) return; } // 加锁成功,安全更新缓存 redisTemplate.opsForValue().set(cacheKey, newData); } finally { // 解锁,释放资源 unlock(lockKey, requestId); } } ``` > SETNX 和 EXPIRE是两个独立的命令,两者一起运行的时候没有原子性,存在SETNX 加锁成功后服务宕机EXPIRE无法执行,造成死锁 **优化方案** SET key value NX EX,原子加锁 redis支持`SET`命令组合参数,将加锁和设置 过期时间合并为一个原子命令,解决SETNX 和EXPIRE的缺陷 ```bash SET lock:user:1001 uuid-12345 NX EX 30 ``` ```java // 原子加锁(推荐) public boolean tryLockAtomic(String lockKey, String requestId, int expireSeconds) { // setIfAbsent 重载方法,直接实现 NX + EX 原子操作 Boolean result = redisTemplate.opsForValue().setIfAbsent( lockKey, requestId, expireSeconds, TimeUnit.SECONDS ); // 避免空指针(RedisTemplate 集群环境下可能返回 null) return Boolean.TRUE.equals(result); } // 原子解锁(必须用 Lua 脚本,保证「校验+删除」原子性) public void unlockAtomic(String lockKey, String requestId) { String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redisTemplate.execute( new DefaultRedisScript<>(luaScript, Long.class), Collections.singletonList(lockKey), requestId ); } ``` **进阶方案**:Redisson分布式锁 #### Redisson **Redisson**,封装了完善的分布式锁(可重入、可阻塞、自动续期),支持单机、集群、哨兵等多种部署模式 * 自动实现了`SET NX EX`原子加锁 * 内置**WatchDog**机制,自动给未执行完的业务续期所时间,避免锁在业务执行期间过期 * 支持可重入锁、公平锁、读写锁等锁类型 * 解锁使用`Lua`脚本,保证原子性 **示例** ```java // 1. 注入 RedissonClient(提前配置好连接) @Autowired private RedissonClient redissonClient; // 2. 缓存更新加锁逻辑 public void updateCacheWithRedisson(String cacheKey, Object newData) { String lockKey = "lock:" + cacheKey; // 获取可重入锁 RLock lock = redissonClient.getLock(lockKey); try { // 尝试加锁,最多等待10秒,锁自动过期30秒 boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS); if (!locked) { throw new RuntimeException("获取分布式锁失败,无法更新缓存"); } // 加锁成功,安全更新缓存 redisTemplate.opsForValue().set(cacheKey, newData); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("加锁过程被中断", e); } finally { // 解锁(只有当前线程持有锁时才会释放) if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } ``` **分布式锁的兜底**RedLock > **为了解决主从架构中主节点加锁成功后宕机,从节点未同步到锁信息而造成锁丢失的情形,抽象出一层RedLock(多台redis) 专用于 分布式锁的申请、释放** Redis主从模式,存在主节点加锁后,同步期间宕机导致锁丢失的问题 1. 建立多个单独的主节点Redis(**专注于分布式锁,不涉及业务数据,不需要同步**) 2. 加锁时对所有节点发送`SET NX EX`超过半数成功才认为加锁成功 3. 解锁时发送所有节点发起释放锁命令,超过半数认为删除锁成功 ```mermaid graph TD A[1.业务发起写入请求] --> B[2.客户端向RedLock申请分布式锁] B --> C[3.超过半数RedLock Redis加锁成功,返回客户端加锁成功] C --> D[4.执行业务redis写入操作] D --> E[5.业务写入结束,向RedLock申请释放分布式锁] E --> F[6.超过半数RedLock redis释放锁成功,返回客户端解锁成功] ``` #### 单机场景(本地锁) **synchronized/ReentrantLock** ### 消息队列 **给我一个选择分布式锁,不选消息队列的理由?** > 分布式锁这么繁琐,人生苦短我选MQ! 分布式锁和消息队列都是并发控制的核心方案,适用场景的核心差异在于请求**是否需要实时执行、能否接受串行化的性能损耗、业务是否试单节点操作** **消息队列(MQ)vs分布式锁** 在大多数生产环境,**消息队列**的优先级都高于分布式锁,以下环境优先使用MQ: 1. 非实时性要求并发更新场景,允许请求有短暂延迟的,如库存扣减、缓存更新、订单状态同步、积分变更 2. 高并发峰值场景,如秒杀、大促、活动引流,需要削峰填谷,避免请求直接打垮redis/MySQL 3. 单节点业务操作,业务操作之恶换手机一个核心资源,串行处理无冲突 4. 需要失败重试,数据可靠性要求高的 单体MQ不适合的场景:(**但是我可以联合分布式事务避免原子性问题**) 1. 强实时性要求:请求需要立即执行立即返回结果,不允许等待耗时的 2. 多资源组合,涉及多个资源、多个库,如同时更新余额、库存、订单状态,需要通过锁保证操作的原子性 3. 大量查询极少数写入场景,串行会 导致查询延迟过高,影响用户体验 4. 没有MQ部署的轻量化应用(**没有你用个屁**) | 维度 | 消息队列 | 分布式锁 | | -------- | ----------------------------------------------------- | ----------------------------------------------------------- | | 核心思路 | 排队串行,消除并发竞争 | 几所互斥,控制并发竞争 | | 实时性 | 低,排队等待消费存在延迟 | 高,拿到锁立即执行,无额外延迟 | | 性能 | 串行处理,但消费端**TPS**有限,可以通过消费端分片提升 | 并行处理,拿到锁的请求同时执行TPS高 | | 复杂度 | 简单,生产/消费端简单开发,无需处理锁逻辑 | 高,需要封装加解锁、处理锁丢失/死锁/续期,RedLock等复杂技术 | | 适用操作 | 单业务操作,支持串行处理 | 多业务组合操作(事务型操作),需要并行执行业务 | | 峰值处理 | 串行处理无惧峰值 | 高并发情形锁竞争激烈,易导致请求重试/超时 | | 失败恢复 | 天然支持(消息持久化、失败重试、死信队列) | 徐手动实现(获取锁失败重试、执行失败回滚) | | 部署成本 | 部署 MQ(RocketMQ/Kafka/RabbitMQ) | 需部署redis,主从、集群、RedLock等 | **总结** > 在符合MQ适用场景的前提下,MQ+事务的方案 整体是优于分布式锁方案的,MQ更稳定、更少踩坑、可维护性更强,是生产环境高并发核心业务的优选。如果一定要说一个分布式锁优于MQ+事务的场景,那么就是单纯的查询业务,极少的写入需求场景 #### 消息队列的缺陷 消息队列保证**串行执行**,不保证**执行结果的原子性**,原子性保障需要通过**数据库事务、分布式事务**等其他技术实现 > **消息队列无法保障执行结果的原子性的原因** > > 1. 消息队列只负责投递消息,不负责监督操作执行结果 > 消息队列的职责是: > 保证消息可靠投递不丢失、不重复; > 保证消息按顺序被消费串行执行; > 消息队列不关心消费端的内部操作及执行结果; > 2. 多操作原子性,一来底层资源的事务支持,与消息包无关 > 如更新用户余额和扣减库存的两个操作,本质是对数据库的操作,他们的原子性应该有存储的事务机制来保障,与MQ的投递无关 > 如果是同库操作,通过数据库本地事务保障执行的原子性 > 如果是不同库,通过**[分布式事务](#分布式事务)**保障执行的原子性 > 3. 消息重试会导致重复执行,破幻原子性 > MQ的失败重试机制当执行失败时会重新投递消息包 > * 假设操作1执行成功,操作2执行失败,MQ重新投递消息,会导致操作以被重复执行 > * 即使做了幂等性处理,也只能保证操作1不被重复执行,依然无法解决操作1成功操作2失败的中间状态,无法保障原子性 | 概念 | 核心含义 | 保障主体 | 失败场景 | | -------- | ---------------------------------------------- | -------- | ----------------------------- | | 串行执行 | 多个操作顺序执行,不被其他请求打断 | 消息队列 | 操作1执行成功,操作2执行失败 | | 原子性 | 多个操作构成一个整体,要么全部成功要么全部失败 | **事务** | 操作1成功,操作2失败回滚操作1 | #### 如何解决消息队列的缺陷(执行一致性问题) MQ+(分布式)事务,兼顾串行化和原子性 **场景** * 简单场景(单库) 1. 把操作1操作2打成一个消息包,发送到MQ 2. 消费端接受消息,开启数据库**本地事务** 3. 串行执行操作1、操作2 4. 执行成功COMMIT,向MQ返回ACK,消息处理成功 5. 任意操作失败,回滚事务`ROLLBACK`,不向MQ返回ACK,MQ会重新投递消息,直到成功或进入**死信** * 复杂场景(多库):MQ+分布式事务 1. 把操作1操作2打成一个消息包,发送到MQ 2. 消费端接收消息,开启**Seata全局事务** 3. 串行调用操作1操作2,每个服务内部开启**数据库本地事务** 4. Seata会自动记录操作前快照、操作后快照,实现自动回滚 5. 如果都执行成功,则全局 提交事务,MQ返回ACK 6. 如果任意操作失败,全局回滚 事务,不向MQ返回ACK,等待重新投递直到成功或进入死信 #### 分布式事务 Seata、TCC、SAGA ## 术语 ### COW copy-on-write 写时复制 是Linux系统的一种内存管理机制,Redis深度依赖COW实现 **RDB持久化**、**AOF重写**、**主从复制**等核心功能 COW**核心逻辑**:当多个进程共享同一块内存数据时,只有 当某个进程对数据执行**写入**操作时,才会为该进程复制一份新的内存副本,供其单独修改,未执行写入操作时仍共享内存数据 **Linux底层实现** > Linux通过页表、内存也管理内存,COW基于这两个核心结构实现 > > * 内存共享:父进程创建子进程(如RDB持久化`fork`子进程)时,系统不会立即复制父进程的所有内存数据,而是父子共享同一套内存,仅在页表中标记内存页为**只读** > * 写时触发:当父进程对共享内存页执行**写入**操作时,系统检测到只读内存被写入,立即为该内存页复制一份新的物理副本,更新父进程页表指向该副本,允许父进程修改副本,而子进程页表仍然指向只读的原始页 > * 独立修改:父子进程后续内存操作相互隔离,子进程始终能访问到`fork`创建瞬间的原始内存数据,父进程则操作新的内存副本 > > 疑问? 那么当父进程写入之后,子进程访问的还是原副本 什么时候会合并?会存在子进程读取到fork之前的数据,新的进程访问到父进程写入完成后的数据 导致两个进程间数据不一致的情况吗 #### **Redis为什么需要COW** Redis是单进程单线程内存数据库,所有正常的读写请求都在主进程处理,而**RDB持久化**、**AOF重写**、**主从同步**等操作,需要读取全量内存写入磁盘/同步从库 直接让主进程执行会有以下问题 1. 阻塞主进程:全量读、写会导致主进程无法处理正常请求,阻塞业务服务 2. 数据一致性:操作过程中如果主进程修改数据,会导致持久化/同步数据不完整、不一致 `fork`子进程+COW机制,解决了阻塞主进程和不一致问题(读快照) #### COW的隐患 高写入场景下,会带来内存占用飙升、磁盘I/O压力增大,是Redis运维中需要关注的重点 1. fork后,redis主进程大量写操作,会 触发大量内存也的COW复制,导致内存占用迅速飙升,内存不足会触发内存Swap,redis性能会急剧下降 2. fork子进程曹总,本身就会造成短暂阻塞,当redis占用大时,fork操作耗时越长,会短暂阻塞主进程 3. 子进程的磁盘I/O竞争,fork进程持久化/同步时,会 大量读写磁盘,会造成磁盘带宽占用急剧上升,印象redis主进程**AOF追加写**(AOF默认每秒刷盘),严重甚至会阻塞主进程 **针对以上隐患的优化** * 系统层面 1. 关闭透明大表 2. 保证足够的物理内存(预留内存) 3. 降低fork频率 * redis层面 1. 合理设置`maxmemory`,避免redis内存占用过高 2. 选择核实的内存淘汰策略,高写入场景使用`allkeys-lru`/`volatile-lru`等淘汰策略,及时释放无用内存 3. 主从架构优化:关闭主库的RDB/AOF操作,在从库上做持久化操作,主库只负责读写请求 4. 监控fork耗时:监听fork耗时,超过阈值告警 ### 跳表 跳表的本质是**多层链表**,底层链表保存所有元素,上层是下层的子表,通过分层索引查找优化。 Redis的跳表笔常规的跳表多一个**回退指针**、并且score允许重复 > 为了删除节点时可以快速定位 到前驱节点,不需要 重新遍历 **随机层级的概率算法** > 层数n: 0.25 ^ (n-1)*0.75 > > ```c > #define ZSKIPLIST_MAXLEVEL 32 > #define ZSKIPLIST_P 0.25 > > int zslRandomLevel(void) { > static const int threshold = ZSKIPLIST_P*RAND_MAX; > int level = 1; > while (random() < threshold) > level += 1; > return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; > } > ``` **跳表和红黑树的比较** 相比较红黑树,多层链表 1. 实现更简单 2. 范围查询效率高 3. 并发友好,只需要锁相关节点,红黑树旋转会锁一片节点 4. 内存占用可控,跳表每个节点平均1.33个指针,红黑树每个节点3个指针 ### 红黑树 ![RBTree](https://pic.code-nav.cn/post_picture/1866300517913518082/6IjTKuJyxPgmEiJn.webp) 红黑树是**平衡二叉查找树** > 二叉查找树**BST** > > 左子树所有节点值 < 根节点值,右侧所有节点值 > 根节点值; > > 中序遍历可以得到有序序列 > > 理想情况下查询/插入/删除效率O(lgN),如果插入有序数据则会退化为单链表 红黑树是一种近似平衡的二叉查找树,不追求绝对平衡(左右树高差不超过1),通过严格颜色规则维护平衡 **特性** 1. 每个节点要么红色要么黑色 2. 根节点必须黑色 3. 所有叶子结点必须黑色 4. 红色结点子结点必须黑色 5. 从任意结点到其他所有叶子结点路径包含的黑色节点数相同 **核心操作**:自平衡操作 * 旋转:左旋、右旋,调整节点的子树结构,不改变二叉查找树的有序性 * 变色:修改节点的颜色,满足颜色规则 **性能** * 查询/插入/删除的平均和最坏时间复杂度均为O(lgN) * 旋转操作的次数少,维护成本低于**AVL树** > AVL树严格平衡,每个节点维护平衡因子,旋转次数更多,适合查询效率要求极高,吸入操作少的场景(如数据库索引辅助结构) ### SDS ### TPS ## 版本演变 ### Redis4.0 引入混合持久化 结合RDB和AOF,RDB存储全量数据,AOF存储增量命令。兼顾RDB的快速回复和AOF的数据完整性 ### Redis6.0+ 多线程IO优化 核心命令执行仍单线程,仅将**网络读取**和**响应写入**拆分为多线程,解决单线程高并发情形下**网络带宽**的瓶颈

面试通关营打卡DAY7

# 快问快答版 ## Q1:MySQL中事务的隔离级别有哪些? MySQL InnoDB 引擎实现了 SQL 标准的四种隔离级别:读未提交、读已提交、可重复读和串行化。其中,MySQL 的默认级别是‘可重复读(Repeatable Read)’,它通过 MVCC 机制解决了不可重复读问题,并配合 Next-Key Lock 在当前读下解决了幻读问题,是并发度与数据一致性的最佳平衡点。 ## Q2 :MySQL 默认的事务隔离级别是什么?为什么选择这个级别? MySQL 默认选择可重复读(RR),主要是为了保证主从复制时的数据一致性。因为在 MySQL 早期的 Binlog 默认格式(Statement)下,读已提交(RC)级别因为没有间隙锁,会导致主库和从库执行 SQL 的顺序或结果不一致。为了避免这种主从数据偏差,MySQL 选择了 RR 作为默认级别。 ## Q3:数据库的脏读、不可重复读和幻读分别是什么? 脏读是读到了别人‘没提交’的数据;不可重复读是前后两次读取‘同一行’数据不一样,重点在于修改;幻读是前后两次查询‘数据条数’不一样,重点在于新增或删除。

面试通关营打卡day5 今天依旧八点下班,下周公司要开新项目了。毕设也要准备开题。。

特训营刷题知识整理(MySQL)

[TOC] # MySQL ## 引擎 | 名称 | 说明 | | ------- | ------------------------------------------------------------ | | InnoDB | 默认引擎行锁,**锁索引记录**,支持事务、回滚,大多数时候可以用这个引擎; | | MyISAM | 老版本(5.5-)的默认引擎,表锁,不支持事务、回滚,适合大量读极少数写入场景; | | MEMORY | 表锁,基于内存,速度快但是无持久化(重启丢失),用来做临时表、会话缓存 | | Archive | 行锁,只支持INSERT/SELECT,不支持索引,**压缩率高**,日志、历史订单等场景常用 | ### InnoDB的组件 **内存组件**(性能核心) | 组件名称 | 作用 | | ------------------ | ------------------------------------------------------------ | | InnoDB Buffer Pool | 缓存池,缓存数据页、索引页、undo页、插入缓存,是减少磁盘IO的核心 | | redo log buffer | 缓存redo log(事务日志),减少redolog刷盘次数 | | 自适应哈希索引AHI | 缓存索引页构建哈希索引(自动为高频访问的索引也页生成哈希表) | | 索结构缓存LSC | 缓存行/表锁元数据,减少锁管理内存开销 | **磁盘组件**(数据持久化核心) | 组件名称 | 作用 | | ------------- | ---------------------------------------------------- | | 表空间 | 存储数据和索引的核心磁盘区域:系统表、独立表、临时表 | | redo Log File | 记录数据修改的物理日志 | | Undo Log | 记录 修改前状态,用于事务回滚 MVCC | | Binlog | 记录所有数据修改逻辑日志 ,主从复制、数据恢复 | | 双鞋缓冲区 | 解决数据页刷盘部分写失效问题,保障数据页的**完整性** | **功能组件**(调度/并发/事务控制) | 组件名称 | 作用 | | ---------- | -------------------------------------------- | | 后台线程BT | 异步处理刷盘、清洗、监控,避免阻塞用户线程 | | 事务管理器 | 控制事务的ACID特性,管理事务开始、提交、回滚 | | 锁管理器 | 管理行锁/表锁/意向锁,保障并发访问的一致性 | | MVCC | 读不加锁,提升并发性能,解决幻读问题 | ## 索引 分几个角度分类索引:数据结构、InnoDB存储方式、索引性质 **数据结构:** | 名称 | 说明 | | -------- | ------------------------------------------------------------ | | **B+树** | InnoDB和MyISAM**默认索引**方式,多层平衡树,叶子结点用双向链表串联,技能快速定位单条数据也能高效做范围查询 | | 哈希 | 通过hash函数直接算出数据位置,效率高,但是不支持范围查询和排序。内存引擎默认使用哈希索引 | | 全文索引 | 文本分词倒排索引,适合对TEXT类型做关键字搜索 | | 空间索引 | 基于R树实现,处理坐标等多为数组,支持区域查询、距离计算 | **InnoDB存储方式:** * 聚簇索引:主键索引,叶子结点存储真实数据行,可以单主键、联合主键,未设置主键引擎会自动生成隐藏6字节自增ID作为主键 * 非聚簇索引:二级索引,叶子结点存储索引值+主键值,不存储具体数据 **索引性质 :** * 主键索引:唯一非空 * 唯一索引:唯一允许空 * 普通索引:无唯一索引,用于加速查询 * 联合索引:多列组合形成索引,遵循最[左前缀原则](#最左前缀原则) * 全文索引:全文搜索 * 空间索引:GIS数据使用 ### 数据结构的比较 #### **B+树** 特性: 1. 矮多叉树**寻址磁盘IO少**; 2. 非叶子结点只存key和指针**节省空间**; 3. 叶子结点双向链表串联**适合范围查询**(MySQL对B+的优化:非叶子结点也有链表相连) #### B树 特性:节点包含数据行+子节点指针 #### B*树 特性:B+树的变性,**强化节点空间利用率,减少分裂次数** 1. 继承B+树特性,非叶子结点只存索引,叶子结点存数据行相互间有 双向链表 2. 增加兄弟节点指针,非叶子结点链表相连,用于放标节点间数据转移 3. B+树节点满会直接分裂,B*树会优先向**兄弟节点**借空间,借不到在分裂 4. 非叶子结点最小节点数从1/2提升到2/3,提高空间利用率 #### 选择B+树的原因 **B树**节点存储数据,导致节点存储效率低,树高会更高,磁盘 IO会更多,并且B树的范围查询性能差,节点之间独立,范围查询需要多次回溯、遍历不同分支,效率低。B树更适合**内存数据库**可以忽略IO开销的场景,在**查询单条数据性能优秀** **B+树**多叉树,非叶子结点只存索引,存储效率高,叶子结点双向链表串联,方便范围查询、寻址 **B*树**侧重的是空间利用率,相比B+树在时间和空间的选择上**倾向空间**,而这个空间利用率的提升并不是没有代价的,节点分裂先遍历兄弟节点、判断、移动数据**时间成本**远大于B+树直接分裂的空间成本;节点填充率也不是越高越好,节点填充率越高,节点的合并、数据迁移触发频率更高,填充率越低节点越松散,合并、迁移概率越低 综上,B+树与B树、B*树的比较,是性能、时间、空间上最合适的数据结构选择 #### 一条查询的流程 根据查询sql条件定位到`主键索引`、`二级索引`,从根向下一次顺序查询 1. 垂直查找:二分查找定位叶子结点 2. 业内查找:二分查找定位靶数据行 3. 水平查找:顺序遍历(链表)定位范围数据 ### 计算一棵B+树的数据存储量 **计算公式:** * 聚簇索引:单叶子结点存储数据行数 * 阶层m ^ (树高-1) * 非聚簇索引:(二级索引+主键索引)单叶子节点占用 * 阶层m ^ (树高-1) InnoDB默认**单页**大小为16KB,固定开销154KB,预留空间约15KB **示例:**聚簇索引假设存储表结构 ```mysql CREATE TABLE tablename ( id INT PRIMARY KEY, -- 4 字节 name VARCHAR(10), -- 最多 30 字节(10×3) age TINYINT, -- 1 字节 create_time DATETIME -- 8 字节 );单条数据43字节,每条记录需要固定开销27字节(事务字段6+回滚指针7+变长字段2+NULL标识位1+记录头11)合计单条数据 占用70字节 ``` **计算** 1. 叶子结点可存储数据量:15KB/70B = 214条数据 2. 非叶子结点存储量,取阶数m = 15KB / (主键大小BIGINT8B + 垂直指针8B)= 937 3. 三层B+树可存储数据量约为 937^2*214 = 1.8亿的数据量 **页的物理结构** | 部分名称 | 固定占用空间 | 核心作用 | 可变 | | ------------------------ | ------------ | ------------------------------------------------------------ | ---- | | File Header(文件头) | 38B | 标识页的类型、归属表空间、**水平指针**等,是页的「身份证」 | ❌ | | Page Header(页头) | 56B | 记录页的内部状态(如页中记录数、B + 树层级、空闲空间位置等) | ❌ | | Infimum + Supremum | 26B | 页内的「哨兵记录」,标记记录的最小 / 最大值,避免边界判断 | ❌ | | User Records(用户记录) | | 存储真正的业务数据(聚簇索引的行数据、二级索引的 `关键字+主键`),**数据水平指针**,非叶子结点的**垂直指针**(即索引字段本身) | ✅ | | Free Space(空闲空间) | | 页内未使用的空间,用于新增记录,空闲空间耗尽会触发页分裂 | ✅ | | Page Directory(页目录) | >26B | 存储用户记录的「槽位指针」,加速页内记录查找 | ✅ | | File Trailer(文件尾) | 8B | 校验页的完整性(防止页损坏) | ❌ | **不同数据类型主键占用量** | 主键类型 | 占用字节 | | ------------ | ----------------------- | | TINYINT | 1 | | SMALLINT | 2 | | MEDIUMINT | 4 | | BIGINT | 8 | | CHAR(n) utf8 | n*3(utf8),n\*4(utf8m64) | | VARCHAR(n) | 实际长度+1~2 | | DATE | 3 | | DATETIME | 8 | ### 页的指针们 * 垂直指针:位于数据行,核心路由指针,位于非叶子结点(即**索引信息**本身) * 页水平指针:位于页文件头,双向链表指针,用于高效范围(翻页)查询 * 数据水平指针:数据行,用于指向上一行和下一行 ### 索引的管理 索引并不是建好了就完了,后续要根据实际业务场景管理索引的**生命周期**(建、查、改、删) 索引并不是没有代价的,索引的本质是**用空间换时间**,每个索引都是一颗B+树,会占用磁盘空间,并且会**影响数据的写入效率**(写入操作要维护索引树) **建索引的注意事项:** 1. 不要大量建索引,占空间并且增加写入成本(写入频繁的表谨慎建索引) 2. 区分度高的字段适合做索引(SELECT COUNT(DISTINCT col) / count(1) )越接近1越好 3. 大字段不要建索引不得已情况下考虑**前缀索引** 4. 多字段使用联合索引 5. 排序、分组字段需要索引,不然会扫表重排 **[索引的优化](#索引的监听和优化)** ### 索引的失效 * 不符合[最左前缀原则](#最左前缀原则) * 索引列运算、函数嵌套 * OR关键字链接字段 > 两个索引用`or`相连,如果要走索引他会先走A索引的B+树,拿到主键id;再走B索引的B+树拿到主键id;两次查询的主键id去重合并;再回表走聚簇索引查业务数据。 > > 这个过程包含2次B+树查询+内存去重+批量回表,效率远低于全表扫描,MySQL的优化器会判定全表扫描更划算,放弃两个单列索引 > > 只有在**两个字段是联合索引**的情况下OR才会索引生效 * 数据类型的隐式转换,varchar = int 实际隐式的**调用了CAST函数** * 优化器判断问题、取反操作(使用!= 或 <> 、NOT IN 或者 NOT EXISTS、IS NOT NULL) 放弃索引的阈值:查询数量超过表15~30%,优化器会**放弃索引**选择扫全表 > B+树是`有序的单向区间索引`,优化器是`成本优先原则`,而!=是筛选出除了某个值之外的所有数据,B+树天生不擅长查`反向、离散`的全集 > > 假设强制走索引,取非值,首先要在索引查询<x;再索引查询>x;再合并去重;最后拿着所有id批量回表走聚簇索引。这个成本是2次完整的B+树索引+内存合并计算+回表聚簇索引,成本远高于1次磁盘全扫IO * ORDER BY 非主键、非覆盖索引,扫全表 ## 事务 **事务是什么** > **事务**是数据库操作的一个逻辑单元,由**一组**SQL语句组成,要么全部执行,要么全部回滚,保证数据的一致性和完整性 > > 事务的特性:原子性、一致性、隔离性、持久性 **ACID** ### 事务的隔离级别 | 名称 | 性能 | | -------------- | --------------------------------- | | 读未提交 | 性能最好,可靠性差 | | **读已提交RC** | oracle、postgreSQL、SQLServer默认 | | 可重复度RR | MySQL**默认级别** | | 串行读 | 无并发能力最安全 | RC和RR的区别: RC每次查询都会重新生成ReadView,别的事务提交了下次查询就能看到 RR每次查询不会重新生成ReadView,会复用事务内第一个生成的ReadView,即便别的事务提交了也看不到 **为什么MySQL默认隔离级别是RR,而现在主流大厂已倾向选读已提交?** 1. 选择RR作为默认是为了**兼容历史版本**,早期MySQL主从复制binlog的格式只有`statement`,RC在同事务中会出现前后不一致的情况,RR读快照可以保障事务内的数据一致性 2. RC对比RR在**并发能力**上有很大的优势。RR引入的间隙锁、临键锁会大大增加死锁的概率,并且快照读需要维护更多的undolog版本链,会加大内存消耗。而RC存在的问题可以通过更小的成本规避 > 1. binlog现在已经默认`row`,已不存在主从不一致问题 > 2. 大多数业务场景并不刚需统一事务内读取数据不变,即便有也可以通过`SELECT FOR UPDATE`手动加锁 ### 事务的核心组件 * UndoLog:回滚日志,存储于表空间中,原子性保障 记录物理变更的反向操作,支持**事务回滚**和**MVCC**的核心工具 每条数据都有两个隐藏字段`trx_id`记录最后修改这条数据的事务ID,`roll_point`指向rundolog中的上一个版本,多个版本会形成一条版本链 > undolog的持久化 > > undo不是直接刷盘,先写入内存Undo缓冲,在通过**redolog**间接**持久化**,最终刷入磁盘表空间 * RedoLog:重做日志,独立存储的物理日志文件先写日志在写磁盘,保障持久性。**WAL机制** 配置`innodb_flush_log_at_trx_commint` 1:每次刷盘,最安全,性能差; 0:每秒1次,宕机丢失 1秒数据; 2:写到操作系统缓存,服务器宕机会丢失数据库宕机不会丢 > **WAL**: Write-Ahead Logging,**先写日志在写数据**。 > > **执行流程** > > 1. 事务执行时,将行为写入**内存**redologbuffer,同时更新内存中BufferPool,生成**脏页** > 2. 事务提交时,InnoDB将redologbuffer的内容刷入磁盘redolog文件,只要日志落盘成功,事务持久化完成,即便此时宕机redolog日志已经存在 > 3. 后台刷新:内存中的脏页BufferPool,由后台线程异步将脏页刷到磁盘中 > 这也是为什么事务提交成功了但是磁盘的idb文件并没有实时更新的原因 > > redolog重写是如何定位断点的? > redolog的**循环写机制**,ib_logfile0和ib_logfile1组成环形结构,存在两个指针 > > * writepos:当前写(redolog)指针 > * checkpoint:脏页刷盘指针(**重写定位点**) > > 重启通过checkpoint定位断点 * [锁](#锁)+[MVCC](#MVCC):隔离性 * ### 事务的生命周期 假设有一个`update`操作 1. 开启事务,修改数据 查询目标行 -> 加排他锁 -> 加载所在页到BufferPool 内存中,修改内存中的数据(脏页) 2. 生成Undolog、Redolog undo表中记录旧值;记录事务id、指向undolog的指针 为支持后续ROLLBACK或一致性读 构造redolog,描述将某页某偏移量改为新值->写入redologbuffer 3. 执行commit 第一阶段(prepare):redolog写入磁盘记录为prepare;记录事务状态为TRX_PREPARE;binlog(如开启)写入文件 第二阶段(commit):事务SQL写入binlog(如开启)、调用fsync()刷盘​ -> InnoDB引擎提交,将redolog标记为commit -> 释放行锁 -> 返回成功给客户端 4. 后台异步刷脏页 提交完成后不会立刻将脏页写入磁盘,由后台现成逐步将脏页刷回数据文件 > 这就是为什么即使提交成功,磁盘的.ibd文件可能还没更新的原因 ```mermaid flowchart TD A["[ BEGIN ]"] --> B["执行 DML → 获取行锁"] B --> C["修改 Buffer Pool 中的数据页"] C --> D["生成 Undo Log(记录旧值)"] D --> E["生成 Redo Log(写入 redo log buffer)"] E --> F["COMMIT 触发"] F --> G["Phase 1: Prepare"] G --> H["→ 写 redo log 并 fsync(PREPARE 状态)"] H --> I["Phase 2: Commit"] I --> J["→ 写 binlog 并 fsync"] J --> K["→ 写 redo log commit 标记并 fsync"] K --> L["→ 释放锁"] L --> M["→ 返回客户端成功"] M --> N["[ 后台线程异步刷脏页到磁盘 ]"] N --> O["[ 宕机? → 启动时进行 Crash Recovery ]"] %% 样式优化(可选,让阶段块更醒目) style G fill:#e1f5fe,stroke:#01579b,stroke-width:2px style I fill:#f3e5f5,stroke:#4a148c,stroke-width:2px ``` ### **事务提交的两个阶段** **事务二阶段提交的原因** 因为redolog和binlog属于不同层,在redolog写入binlog过程中如果发生宕机,可能出现数据一致性问题,所以需要二阶段提交 * prepare:redolog写盘,记录stat 为`prepare` * commit:binlog写盘,redolog 记录stat为`commit` ### 事务的回滚和异常中断 1. 事务执行失败/取消,触发ROLLBACK执行,redolog只负责恢复**已提交**的修改,所以回滚操作redolog不参与,已存在的未提交redo后续会被覆盖掉无需管理 2. 事务异常终止 * 运行时异常(锁超时/主键冲突) Undolog 触发回滚,执行反向操作撤销所有修改; Redolog丢弃所有未提交记录 * MySQL宕机重启 重启后InnoDB会`先重做,后回滚` 1. 先重做redolog,恢复到宕机前状态,**重放所有Redolog**(定位Checkpoint LSN,逐行重放) 2. 执行undolog,回滚未提交事务(扫描事务表空间,筛选出所有未提交事务,根据事务ID找到所有undolog记录,执行undolog回滚,标记事务已终止,清理相关undolog) 3. 对已提交事务的redo重放完成持久化;未提交事务就不用管他后续会被新日志素钙 ### MVCC 多版本并发控制,让 **读写操作互不阻塞** 写入操作时,MySQL不会立即覆盖原有数据,而是生成新版本的记录,每个记录保留对应的版本号和时间戳,串联形成版本链 读操作,可以 根据事务启动时间去版本链上找到自己需要读取的版本数据 **ReadView可见性判断** 1. creator_trx_id:当前事务id 2. m_ids:活跃的事务ID(已启动,未提交) 3. min_trx_id:活跃id中最小值 4. max_trx_id:下一个将被分配的事务ID ### 快照读和当前读 * 快照读就是普通`SELECT`走MVCC,读的是历史快照 不加锁 * 当前读是`SELECT FOR UPDATE`,读最新版本,且加锁 ## 分析、优化 ### SQL查询分析、优化 **SQL分析** EXPLAIN `sql` 查看SQL执行效率 MySQL5.6+ 支持 `EXPLAIN FORMAT = JSON`:更详细的JSON格式信息 MySQL8新增 `EXPLAIN ANALYZE`:真正执行查询并给出实际执行时间、行数 ```mysql EXPLAIN SELECT id,name FROMT table; ``` | 字段名称 | 说明 | | -------- | ------------------------------------------------------------ | | type | 访问类型:性能从高到低列:const>eq ref>ref>range>index>ALL | | key | 索引字段,显示NULL 则没走索引 possible_keys只是候选 | | rows | 预估扫描行数,越小越好 | | extra | using index 覆盖索引;using filesort 说明排序无法用索引得额额外排序;using temporary说明用了临时表 | **SQL优化** // **TODO**细化实现 调优的核心:减少I/O,避免无效计算 步骤: 1. 定位慢SQL ```mysql SHOW VARIABLES LIKE '%slow%'; SHOW VARIABLES LIKE '%log_output%'; SHOW GLOBAL STATUS LIKE 'Slow_queries'; -- 1. 开启慢日志总开关 SET GLOBAL slow_query_log = ON; -- 2. 设置输出方式:同时写入文件+表(推荐,兼顾备份和查询) SET GLOBAL log_output = 'FILE,TABLE'; -- 3. 设置慢SQL阈值(1秒,可按需调整) SET GLOBAL long_query_time = 1; -- 4. 可选:记录管理类慢SQL(ALTER/CREATE等) SET GLOBAL log_slow_admin_statements = ON; -- !!!注意开启之后需要重新连接数据库开启新的session生效!!! -- 执行一条耗时超过1秒的SQL(SLEEP(2)强制耗时2秒) SELECT SLEEP(2), mysql.user.* FROM mysql.user; -- 查询慢sql SELECT start_time, query_time, LOCK_TIME, rows_examined, rows_sent, CONVERT(sql_text USING utf8mb4) AS sql_text -- 核心:BLOB转utf8mb4字符串 FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10; ``` 2. 分析执行计划 EXPAIN `slow sql` 3. 调优 * 索引优化 合理设计联合索引 ,避免/减少回表 * SQL查询优化 规范、优化业务`SQL`写法,如禁止SELECT * 、分页方式等 * 架构优化 数据库分库分表、读写分离、前置redis缓存/Caffeine本地缓存、消息队列 **// TODO** ### 索引的监听和优化 **工具** * MySQL自带工具 ```mysql -- 索引详情清单 SELECT TABLE_NAME 表名, INDEX_NAME 索引名, COLUMN_NAME 索引字段, SEQ_IN_INDEX 字段在索引中的顺序, INDEX_TYPE 索引类型, NON_UNIQUE 是否非唯一索引(0=唯一/主键,1=普通) FROM INFORMATION_SCHEMA.STATISTICS WHERE TABLE_SCHEMA = 'schema_name' AND TABLE_NAME = 'table_name' ORDER BY INDEX_NAME, SEQ_IN_INDEX; -- 碎片率、索引大小,根据碎片率划定阈值处理 SELECT TABLE_NAME 表名, DATA_LENGTH/1024/1024 数据大小_MB, INDEX_LENGTH/1024/1024 索引大小_MB, DATA_FREE/1024/1024 碎片大小_MB, CONCAT(ROUND(DATA_FREE/(DATA_LENGTH+INDEX_LENGTH)*100,2),'%') 碎片率 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'schema_name' AND TABLE_NAME = 'table_name'; -- 优化方式 -- 方式1:重建表+索引,清理碎片 OPTIMIZE TABLE 你的表名; -- 方式2:分析表,更新索引统计信息,轻量优化 ANALYZE TABLE 你的表名; -- 方式3:MySQL5.7+ 视图查询 -- 1. 排查僵尸索引 sys.schema_unused_indexes -- 2. 排查低效索引 SELECT OBJECT_NAME 表名, INDEX_NAME 索引名, COUNT_FETCH 使用次数, SUM_ROWS_EXAMINED 总扫描行数, SUM_ROWS_RETURNED 总返回行数, CONCAT(ROUND(SUM_ROWS_RETURNED/SUM_ROWS_EXAMINED*100,2),'%') 命中效率 FROM performance_schema.table_io_waits_summary_by_index_usage WHERE TABLE_SCHEMA = 'schema_name' AND TABLE_NAME = 'table_name' AND INDEX_NAME IS NOT NULL ORDER BY COUNT_FETCH ASC; -- 3. 排查冗余索引 sys.schema_redundant_indexes; ``` * 三方工具:**Prometheus + Grafana + mysqld_exporter** 实时监听数据库性能 //**TODO** ### 分页的优化 #### **分页的实现** 1. 基于**行号** LIMIT 偏移量,数量 = LIMIT 数量 OFFSET 偏移量 ```mysql SELECT * FROM T LIMIT 1000000,1000; SELECT * FROM T LIMIT 1000 OFFSET 1000000 ``` 2. 基于**主键**游标分页 ```mysql SELECT * FROM T WHERE id > (SELECT ID FROM TABLE WHERE NAME = 'X' ORDER BY ID LIMIT 1000000,1) LIMIT 1000 ``` 3. 两步式游标分页(**常用**) ```mysql SELECT t.* FROM TABLE t JOIN (SELECT ID FROM TABLE WHERE ID >1000000 LIMIT 1000)tmp ON t.id = tmp.id ``` #### **分页实现的优劣分析** 1. 基于**行号**,直接使用`LIMIT 偏移量, 数量`的写法会扫描前`偏移量`条数据丢弃,浪费大量 IO,只适合用于小表,小数量情形使用 > MySQL**不支持跳过偏移量**,都是需要遍历偏移量的,偏移量大的时候就造成性能浪费; > > OFFSET是基于**行数**的分页,在分页查询时有数据的变更(增、删、改)会出现翻页时数据 重复出现/数据丢失的情况 2. 基于**主键**,要求必须使用主键字段,必须唯一、有序、自增;分页需求是下一页**不支持跳表** > 非主键字段分页使用联合索引+主键,通过联合索引定位时间,在用主键筛选 > > ```mysql > idx_createtime_id(create_time,id) > SELECT ID FROM T WHERE CREATE_TIME > 'DATE' AND ID > 1000 LIMIT 1000 > ``` > > **不支持跳表怎么办** : > > 1. 我就不跳了,只做上一页下一页:grin: > 2. 先查询第N页的1st `id`,二次查询id> 1st`id`,虽然还是有一次 OFFSET 但是因为只查询1条数据开销较小,如果N特别大还是**不太行** > > 为什么不使用`BETWEEN AND `? 因为存在删除情况,id并不一定绝对连续,只适合在主键绝对连续的场景 3. 两步式和基础主键游标本质是同一套高性能逻辑的不同落地形式,当查询需要过滤ID 关联其他表时,两步式更优,因为前者的执行顺序是先关联再过滤,而后者是先过滤再关联 ```MYSQL -- 基础写法:需先查主表,再关联其他表(可能扫描更多数据) SELECT t.id,t.name,o.order_no FROM table t LEFT JOIN order o ON t.id=o.user_id WHERE t.id>1000 LIMIT 1000; -- 两步式JOIN:先精准过滤1000个ID,再关联(仅关联1000条数据) SELECT t.id,t.name,o.order_no FROM table t JOIN (SELECT id FROM table WHERE id>1000 LIMIT 1000) tmp ON t.id=tmp.id LEFT JOIN order o ON t.id=o.user_id; ``` #### **优化方案** * 子查询优化:先子查询二级索引快速定位id,再根据id聚簇索引获取数据 * 游标分页:每次查询都返回当前页最大id,下次查询以这个id作为起点,但是**只能连续翻页**不能跳页 * 搜索引擎:**Elasticsearch** // TODO #### 翻页跳表查询的实现 * 主键严格自增、连续 游标获取id,join查询详情 ```mysql -- 第一步:游标分页查id,无OFFSET,无IO浪费 SELECT id FROM table WHERE id>999000 LIMIT 1000; -- 第二步:JOIN查详情,高性能 SELECT t.* FROM table t JOIN (SELECT id FROM table WHERE id>999000 LIMIT 1000) tmp ON t.id=tmp.id; ``` * 主键非连续,存在断层 建分页锚点表,查询锚点表获取`maxid`,二次查询详情 ```mysql CREATE TABLE page_anchor ( page_num INT PRIMARY KEY COMMENT '页码', start_id BIGINT NOT NULL COMMENT '该页的起始主键ID' ); -- 用游标分页查每一页的起始id,插入锚点表 INSERT INTO page_anchor (page_num, start_id) SELECT @page := @page +1 AS page_num, id AS start_id FROM table, (SELECT @page :=0) t WHERE id>0 LIMIT 每页条数 OFFSET 0; -- 循环执行,直到所有数据处理完 -- 跳第1000页,先查锚点表的起始id(毫秒级,主键查询) SELECT start_id FROM page_anchor WHERE page_num=1000; -- 再用游标分页查详情,无OFFSET,无IO浪费 SELECT t.* FROM table t JOIN (SELECT id FROM table WHERE id>start_id LIMIT 1000) tmp ON t.id=tmp.id; ``` ## 日志 **binlog**:binlog主要职责是**主从复制**和**数据恢复** binlog的三种格式 * statement,记录原始SQL,日志量小,同步时会导致主从不一致情况,如`now()`的执行 * row格式记录行变更前后完整数据,日志量大但是安全 * mixed,自动判断,普通 用`statement`,有风险的用`row`,但是不是很智能 **redolog**:保障宕机后数据不丢 见[事务的核心组件](#事务的核心组件) **undolog**:支持事务回滚和MVCC的核心 见[事务的核心组件](#事务的核心组件) | 维度/名称 | binlog | redolog | undolog | | --------- | -------------- | ---------------- | -------------- | | 层级 | Server层 | InnoDB引擎 | InnoDB引擎 | | 日志类型 | 逻辑日志 | 物理日志 | 逻辑日志 | | 写入方式 | 追加写 | 循环写,固定大小 | 链式存储 | | 核心作用 | 主从、数据恢复 | 崩溃恢复 | 事务回滚,MVCC | | 事务相关 | 提交后写入 | 执行中持续写入 | 执行中写入 | ## 锁 **粒度区分** * 表锁:锁全表,适合写入情形极少场景 * 行锁:锁行 * 记录锁:锁行,根据**索引**锁定,无索引会锁全表 * 间隙锁:锁区间,禁止**INSERT**操作,但是不禁**UPDATE/DELETE** **插入意向锁**:等待间隙的提示锁,当对间隙操作等待间隙锁时会生成 * 临键锁:记录锁+间隙锁,**MVCC防止幻读的核心技术** 间隙锁只能锁已知范围区间,无法对nextkey进行闭合,所以需要临键锁锁定nextkey **模式区分** * 共享锁S:允许多事务同时读 * 排他锁X:独占锁,禁止其他读写`FOR UPDATE` **元数据锁** 分读锁(共享锁)、写锁(排他锁), 防止`DDL`和`DML`操作的冲突,对表结构的更改和对数据的操作(SELECT/INSERT/UPDATE/DELETE)相互排斥锁定,保障数据的一致性 **意向锁** 表锁,用于快速判断是否可以上锁的标记锁 **自增锁** 为了自增列在高并发`INSERT`情况下正常分配递增值 **谓词锁** 空间索引无NextKey这类的绝对排序概念,表锁 **乐观锁** 先操作,后校验,全程不使用数据库的锁机制 对冲突预期乐观,等到真正需要更新时检查数据是否 被更改 > 1. 在数据表中维护veresion > 2. 读取数据时读取version > 3. 事故更新数据只有获取的version和读取version一致才写入 > 4. 如果version不一致则,则说明数据已被其他书屋修改 **悲观锁** 直接加锁 对冲突预期悲观,假设冲突一定发生,在操作数据钱先获取锁 ## 异常情况 ### 脏读、不可重复读、幻读 | 名称 | 描述 | 方案 | | ---------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | 脏读 | 指读取到未提交的数据,可能因为事务回滚等丢失 | MVCC快照读,不会出现脏读情况 | | 不可重复读 | 单事务中对数据的更改,导致事务中前后读取不一致 | RR级别下,ReadView在第一次读取时就固定了,后续的查询不会重建ReadView | | 幻读 | 读取数据之后,对标有插入/删除操作,导致读取数量`count1`不一致 | 快照读不存在幻读情况;当前读可以用间隙锁锁住范围;<br />但是先快照读,再当前读可能会出现幻读情况 | ### 死锁 指两个或多个事务相互持有对方的锁,事务中有对对方锁定表的写入操作,相互等待对方锁的释放导致的死循环 InnoDB引擎默认开启自动检测``innodb_deadloc_detect,当检测到死锁引擎会自动选择一个代价小的事务回滚,释放它持有的锁; 锁等待时间`innodb_lock_timewait`默认50s,超过时间会自动放弃事务 **死锁的处置** * 自动处理:又Innodb引擎自动检测回滚事务、释放锁;锁等待超期回滚并释放锁 * 手动干预 ```mysql SHOW ENGINE INNODB STATUS -- 获取死锁日志片段 -- 1. 查看当前锁信息 -- MySQL8 之前 SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; SELECT * FROM INTOFMATION_SCHEMA.INNODB_LOCK_WAITS; -- MySQL8.0+ SELECT * FROM PERFORMANCE_SCHEMA.DATA_LOCKS; SELECT * FROM PERFORMANCE_SCHEMA.DATA_LOCK_waits; -- 2. 找到事务和线程的对应关系 SELECT TRIX_ID, TRX_STATE, TRX_STARTED, TRX_MYSQL_THREAD_ID, TRX_QUERY FROM INFORMATION_SCHEMA.INNODB_TRX -- 3. kill导致阻塞的线程 KILL <pid> ``` **死锁的规避** 1. 拆分大事务 2. 固定加锁顺序,避免循环依赖 3. **降低隔离级别** 4. 合理建索引 5. 调整锁等待时间 ### 主从延迟 主从延迟是**必然的**,无法完全消除,只能尽可能规避 **原因:**主库写binlog -> dump线程推送变更事件 -> 从库拉取数据 -> 从库I/O写relaylog -> SQL线程重放。在这个流程上任何一个环节的时延都可能导致主从延迟 **处置方案** * 关键业务强制走主库 * 延迟感知,写操作后记录时间戳,短期内读强制走住库,过了延迟窗口时间再走从库 * 二次查询,从库查询不到二次查询主库 * 缓存前置:写入主库的同时写入缓存 **并行复制优化** > 早期从库只有一个SQL线程**串行**重放,主库写得快从库跟不上。5.6开始引入并行复制,不同库的事务可以并发执行。5.7引入LOGICAL_CLOCK,可以同一group commit的事务可以并发。8.0引入WriteSet,只要更新的行不冲突就可以并发执行 | 复制模式 | 主库返回时机 | 性能 | 数据可靠性 | | ---------- | ------------------ | ---- | -------------------------- | | 异步复制 | 写完binlog立刻返回 | 最高 | 最差,主库挂了数据可能丢失 | | 同步复制 | 等待所有从库确认 | 最差 | 最高 | | 半同步复制 | 等至少N个从库确认 | 折中 | 折中 | ## 高可用 高可用的核心目标是**最大限度减少数据库故障时间**和**保证数据一致性和服务连续性** ### **主从复制** **主库**负责所有写入操作,将修改记录到**binlog**,[主从延迟](#主从延迟) **从库** * IO线程链接主库,拉取binlog写入本地relay log * SQL线程读取relaylog,重放SQL语句,同步主库数据 **流程** ```mermaid flowchart TD A[主库Master] --> B[写入数据并记录Binlog] B --> C[Binlog Dump线程] C --> D[从库IO线程] D --> E[从库RelayLog] E --> F[从库SQL线程] F --> G[从库执行RelayLog,同步数据] ``` ### **读写分离** 基于主从复制的性能扩展,主库负责写,从库负责读,分摊数据库压力,提升并发处理能力 ```mermaid graph TD A[应用程序/中间件] --> B{读写请求路由} B -->|写请求| C[Master] B -->|读请求| D[Slave1] B -->|读请求| E[Slave2] C --> F[同步数据到从库1/2] ``` ### 自动故障切换 | 方案核心 | 原理 | 优点 | 缺点 | | ---------------------------------- | ------------------------------------------------------------ | ---------------------------------------------- | ---------------------------------------------------- | | MHA(MySQL High Availability) | 监控主库状态,主库故障时自动选择最新的从库提升为主库,同步剩余 binlog,更新其他从库的主库地址 | 自动切换(10~30 秒)、数据丢失少、无需额外硬件 | 配置复杂、仅支持 MySQL 5.7 及以下(官方停更) | | Keepalived + VIP | 用 VRRP 协议实现 VIP(虚拟 IP)漂移,主库故障时 VIP 自动切换到从库 | 切换速度快(秒级)、实现简单 | 仅解决 IP 漂移,不保证数据一致性(需配合半同步复制) | | **MySQL MGR**(Group Replication) | 多节点组成集群,每个节点有完整数据副本,自动选主,故障时自动切换,支持读写分离 | 官方原生支持、强一致性、自动切换、易维护 | 对网络要求高、性能略低于主从复制 | ```mermaid graph TD A[MGR集群3节点] --> B{Primary} B -->|写请求| C[Primary] B -->|读请求| D[Secondary1] B -->|读请求| E[Secondary2] C --> F[事务广播到所有节点] F --> G[所有节点验证事务一致性] G --> H[多数节点确认后事务提交] ``` ### 分布式(分库分表+多集群) //**TODO** ### 主库master的选举策略 * 手动选举 无高可用工具应急 s1:停止所有从库复制进程 ```mysql -- 停止主从复制(IO线程+SQL线程) STOP SLAVE; -- 查看复制状态(确认停止成功) SHOW SLAVE STATUS\G; -- 成功标志:Slave_IO_Running: No,Slave_SQL_Running: No ``` s2:选举最优库: > **数据同步最完整**:`Seconds_Behind_Master = 0`(无延迟) > 延迟秒数最小; > > **复制位置最新**:`Exec_Master_Log_Pos` 最大(表示同步到原主库 binlog 的最新位置); > > **硬件性能最优**:CPU / 内存 / 磁盘 IO 最好的从库(保证新主库的业务承载能力); > > **业务负载最低**:当前读写压力最小的从库(减少切换后性能波动)。 s3:提升最优库为主库 ```mysql -- 重置主从复制关系(清除原relay log和复制配置) RESET MASTER; -- 重置binlog,生成新的binlog文件(供其他从库同步) RESET SLAVE ALL; -- 清除所有从库复制配置 -- 修改MySQL配置,开启读写(若从库配置了read_only=1) SET GLOBAL read_only = OFF; -- 临时生效,重启后失效 -- 永久生效需修改my.cnf:read_only = 0 ``` s4:其他从库指向新主库 ```mysql -- 清除原复制配置 RESET SLAVE ALL; -- 配置新主库信息(替换为新主库的IP、端口、账号、密码) CHANGE MASTER TO MASTER_HOST='新主库IP', MASTER_PORT=3306, MASTER_USER='复制账号', MASTER_PASSWORD='复制密码', MASTER_LOG_FILE='新主库的binlog文件名', -- 新主库执行RESET MASTER后的binlog名 MASTER_LOG_POS=154; -- 新主库binlog的起始位置(默认154) -- 启动复制进程 START SLAVE; -- 检查复制状态(确认同步成功) SHOW SLAVE STATUS\G; -- 成功标志:Slave_IO_Running: Yes,Slave_SQL_Running: Yes,Seconds_Behind_Master: 0 ``` s5:修改业务配置指向新主库(nacos更新配置) * 自动选举 **//TODO** * MGR * MHA * PorxySQL+Keepalived * 云托管 ## 配置项详解 //TODO ## 版本迭代区分 //TODO ## 术语定义 ### 最左前缀原则 SQL查询的索引**必须**从左到右匹配,如果第一个条件非索引字段会导致索引失效; 联合索引abc顺序优化器会自行优化; MySQL8有 **SkipScan**优化,特定场景下可以绕过最左匹配 > ### SkipScan 核心原理 > > 联合索引(如 idx (a,b,c))的 B + 树按最左列 a 排序;传统查询必须带 a 才能走索引,否则全表扫描。Skip Scan 的逻辑是: > > 1. 枚举最左列 a 的所有不同值(如 a=1、a=2); > 2. 对每个 a 值,在 (a,b,c) 索引上执行子范围扫描(如 a=1 AND b=xxx、a=2 AND b=xxx); > 3. 合并所有子范围结果,等效于 “跳过 a 列直接查 b/c”,但复用现有联合索引。 > > 本质是把一次全表扫描拆成多次小范围扫描,当最左列基数低时,效率显著高于全表扫描。 > > SkipScan 不是 “打破” 最左匹配,而是在低基数最左列场景下的**优化** —— 通过枚举最左列值,将无最左条件的查询拆成多次范围扫描,复用联合索引,避免全表扫描。核心是**最左列低基数 + 覆盖索引 + 单表查询**,高基数最左列时建议添加适配索引,而非依赖 SkipScan。 ### 回表 回表指的是二级索引需要二次通过主键索引查询获取其他字段,因为B+树非叶子节点只有索引和主键值。 **如何减少/避免回表** * 覆盖索引 条件项、查询项都在索引内,不需要回表 * 索引下推 MySQL5.6引入的优化,在联合索引场景中,条件中的字段为索引字段即便未生效,仍然可以在索引层进行过滤 * 避免/减少 `SELECT * `这种必然引起回表的查询

面试通关营打卡day4

今天加班到8点多,实习好累。。。上班好累。。。

面试通关营打卡day3

今天是完成最快的一天,第一天干了8h,第二天、第三天都用了5h左右就干完本日任务了。 PS:多亏了第一天的深度学习,今天的题目在第一天都见过。 ![day3.png](https://pic.code-nav.cn/post_picture/1977652382506422273/RboU6U0RjQwjV3qW.webp) 面试鸭真的是量大管饱。。三天就延伸出来这么多额外题目。。。 期待day4

面试通关营打卡day2 复习了昨日的三道题,由于day1开始的时候基础比较差,前前后后看了有8个小时。结果今天的三道题目,昨天探索的过程中已经接触过了,所以今天主要是强化巩固,以及规范回答流程。 今天多做了一道题:什么是mysql中的索引下推。 明天上午准备复习一下。

InnoDB B+ Tree 索引可视化网站

http://115.190.239.43:8889/ 面试通关营打卡第一天(打的昨天的卡) https://github.com/4iKZ/MySQL-InnoDB-B-Tree- ![image.png](https://pic.code-nav.cn/post_picture/1977652382506422273/fEaa4oDWsPULJMsL.webp)

下载 APP