一条SQL查询语句是如何执行的?
- 连接器:首先需要和SQL客户端建立TCP连接,连接器负责身份认证和权限读取;
- 查询缓存:MySQL服务器拿到查询请求后,会会先看缓存,看之前是不是很执行过该语句;但是MySQL8.0已经删除了这一步,因为缓存命中率极差(只要表有了一点改动就要删除相关所有缓存);
- 分析器:分析器分为词法分析和语法分析,简单来说词法分析就是看用了哪些词,比如什么SELECT,WHERE;而语法分析则是构建一颗抽象语法树,检查语句是否合法;
- 优化器: 优化器的职能是指定查询的具体执行计划:比如如果有多个索引,用哪一个;多表JOIN的时候决定JION的顺序等;
- 执行器: 执行器阶段就是执行语句,通过存储引擎的接口进行查询操作;
事务的四大特性有哪些?
四大特性就是ACID:
- A:原子性;一个事务中的语句的执行要么全部成功,要么全部失败。不会出现成功一半的情况;
- C:一致性;事务执行之后,数据库必须从一个一致性状态转移到另一个一致性状态;
- I:隔离性;事务与事务之间互相隔离,一个事务的中间状态对其他事务不可见;为的就是解决脏读,不可重复读,幻读这些数据竞态问题;
- D:持久性:事务一旦提交,对数据库的修改是永久性的,掉电也不影响结果;
数据库的事务隔离级别有哪些?
数据库的事务隔离是为了解决以下三个问题:
- 脏读:事务A读取了事务B没有提交的数据,之后事务B进行了回滚,事务A读到了不存在的数据;
- 不可重复读:事务A读取某数据后,事务B修改了该数据,事务A再次读取发现两次结果不一样;强调的是已有数据的修改;
- 幻读: 事务A执行两次相同的范围查询,事务B提交了插入或删除,导致事务A的行数不一致,出现“幻影行”;强调的是行数增减;
事务隔离等级有四个:
- 读未提交:事务可以读取其他事务未提交的修改,没有任何隔离;
- 读已提交:事务只能读取其他事务已经提交的数据,解决了脏读;但不可重复读和幻读问题仍然存在;
- 可重复读:在同一个事务内,多次读取同一行数据的结果始终一致;即在事务开始和结束之间,其他事务对数据的修改不可见。但没有解决幻读;
- 串行化:拒绝并发就不会有数据竞态了,只在非常严格的时候出现;
Inno DB虽然是可重复读的隔离等级,但是通过机制的特别设计解决了幻读:
- 快照读(普通 SELECT):只在Select第一次执行的时候创建一个ReadView快照,之后的所有查询都在快照中进行,直接解决了不可重复读的问题;
- 当前读(SELECT … FOR UPDATE / INSERT / UPDATE / DELETE):通过 Next-Key Lock(临键锁) 解决。Next-Key Lock = Record Lock(行锁)+ Gap Lock(间隙锁),它会锁住查询条件命中的记录以及记录之间的间隙,阻止其他事务在这个范围内插入新行。
如果事务中先用快照读读了一次(建立了 Read View),然后另一个事务插入并提交了新行,接着当前事务用当前读(比如 SELECT … FOR UPDATE)再读一次,就可能看到新插入的行
MySQL的执行引擎有哪些?
常见的执行引擎有三个:
- InnoDB:实际生产的唯一选择,支持事务,支持ACID特性,支持行级锁,支持MVCC;对并发访问和数据安全都有很好的实现;
- MyISAM:旧引擎,不支持事务、不支持行级锁(只有表级锁)、不支持外键、不支持崩溃安全恢复,并发性能很差;
- Memory:使用内存,数据在掉电之后就会消失,不常用;
MySQL为什么使用B+树来作索引
数据存储在磁盘中,而磁盘的IO速度非常慢,索引就是为了降低查询时的磁盘IO次数而设计的;
B+树的特性非常好:
- 层高低,每次查询的IO在2-3次:InnoDB的一页是16KB,而且所有的数据都放在叶子节点中,非叶子节点只存key,扇出非常大:一个非叶子节点能放1170的分支,两层就能放130万以上的记录;基本IO只需要两次就能查到数据;而与之相对的AVL树因为每个节点只存一个key,100w条记录需要20层,就是20次IO;跳表也是层高太高;
- 支持范围查询:B+树的叶子节点之间有链表(InnoDB是双向链表)进行连接,可以非常快速地进行范围查询;而B树没有,范围查询就要多次遍历,IO次数不可控;全表查询B+树只用走底层叶子链表一次,而B树要遍历全表;哈希则是做不了范围查询;
- 查询性能稳定:B+树的所有非叶子节点都不存数据,每次查询都要走到叶子层,路径长度一致,性能可预测;而B树因为把数据放在非叶子节点中,在降低了每层容纳分支的同时也导致了查询可能提前退出,IO次数不可控,对于优化来说是个大问题;
说一下索引失效的场景?
联合索引的基本规则就是最左前缀原则:如果建了一个联合索引 (a, b, c),B+ 树是按照 a → b → c 的顺序排列的。查询时必须从最左列开始连续匹配,才能走索引,如果使用索引时跳过了最左边的 a 则直接无法使用索引;
- 对索引列使用函数或表达式: B+ 树中存储的是列的原始值,对列做函数运算或算术运算后,得到的值和 B+ 树中的排列顺序不一致,优化器无法利用索引的有序性进行二分查找。MySQL 8.0 引入了函数索引(Functional Index),可以直接对表达式建索引来解决这个问题,但需要显式创建。
- 隐式类型转换:字符串列和数字比较时,MySQL 会把字符串转为数字,相当于对索引列施加了一个隐式的 CAST() 函数,回到了"对索引列使用函数"的场景。
- 隐式字符集转换: 两张表 JOIN 时,如果关联字段的字符集不一致(比如一个 utf8、一个 utf8mb4),MySQL 会在查询时对其中一个字段做 CONVERT() 转换。
- LIKE 以通配符开头:B+ 树是按照字符串从左到右排序的。张% 可以利用有序性定位到"张"开头的范围;%三 相当于不知道开头是什么,只能全索引扫描或全表扫描。
- OR 连接非索引列:OR 表示两个条件满足其一即可。即使 a 走了索引找到一批行,b 没有索引还是得全表扫描,合起来还不如直接全表扫描一次,优化器就放弃索引了。
- 使用非等值查询:当使用!=或<>操作符时,索引可能不会被使用,特别是当非等值条件在WHERE子句的开始部分时。
什么是慢查询?原因是什么?可以怎么优化?
- 慢查询就是查询时间超过指定阈值的SQL语句;
- 常见原因有:
- 没有使用索引或索引失效:如果没有建索引或者索引失效,就会走全表扫描;
- JOIN查询设计不合理:关联字段没有索引,一次性JOIN太多表,驱动表选择不当等;
- 排序和分组效率低:ORDER BY 和 GROUP BY的字段没有索引时,MySQL需要用filesort或临时表进行额外排序,数据量大的时候就很慢;
- 查询数据量大:查询的数据量很大时也会导致慢查询;
- 锁等待:可能不一定是自身慢,也有可能是被其他事务阻塞了;
- 优化手段:
- 索引优化:针对Where,Group by, Order by等建立合适的索引;
- JOIN优化:控制JOIN表的数量,如果要多表关联更建议在应用层做聚合;
- 表结构和架构层面:避免单表数据过大,分库分页;读写分离:把读请求放到从库;加入Redis缓存;
undo log、redo log、binlog 有什么用?
- undo log: 回滚日志,是 InnoDB 引擎层的日志,记录数据被修改之前的旧值;保证事务的原子性:如果事务执行过程中需要回滚,undo log可以把数据恢复到修改之前的状态;而且undo log还构成了数据的版本链,有其他事务需要读取历史版本时,就沿着版本链找到对自己可见的,实现了多版本读;
- redo log: 重做日志,是 InnoDB 引擎层的日志,记录的是"对某个数据页做了什么修改"。保证持久性:InnoDB 更新数据时,不会立即把修改写回磁盘数据文件,而是先修改 Buffer Pool 中的内存页,然后把修改操作记录到 redo log 并刷盘。后续由后台线程异步地把脏页刷回磁盘。如果脏页还没来得及刷盘数据库就崩溃了,重启时 InnoDB 会读取 redo log,重放 这些修改操作,把数据恢复到崩溃前的状态
- binlog: 归档日志,是Server 层的日志,记录的是"对数据库执行了什么逻辑操作"。主要用于主从复制和数据恢复,可以通过重放 binlog 恢复到任意时间点。比如误删数据后,先恢复到最近的全量备份,再用 binlog 恢复到误删前的那一刻。
MySQL和Redis的区别是什么
MySQL是关系型数据库,核心定位是持久化存储。Redis是基于内存的键值型NoSQL数据库,核心定位是高速缓存和特定数据结构的高性能操作;
- 存储介质的不同: MySQL数据存储在磁盘上,而Redis的数据存储在内存中,通过哈希表直接定位Key不需要SQL解析;单次操作延迟在微妙级,QPS能轻松上10w+;
- 线程模型不同: MySQL是多线程架构,而Redis的核心命令处理是单线程的;
- 数据模型: MySQL是结构化数据,预定义表结构,数据按行列组织;Redis是半结构化,甚至可以说是无结构化的,每个key对应一个value;
Redis有什么优缺点?为什么用Redis查询会比较快
优点:
- 极致的性能:读写操作在微秒级完成,单机QPS能到10w以上;
- 丰富的数据结构:redis支持多种数据结构,包括string,hash,list,set,sortedset(zset);
- 原子性操作:redis的命令是串行执行的,所以天然原子;
- 天然支持高可用和水平拓展:主从复制(Replication) 实现数据备份和读写分离;哨兵(Sentinel) 实现自动故障转移;Redis Cluster 通过 16384 个哈希槽实现数据分片和在线扩缩容。
缺点:
- 内存成本高:数据全部在内存中,而内存的价格远高于磁盘。存储大量数据的成本非常高,不适合存放冷数据或大体量的全量数据。
- 事务能力弱:MULTI/EXEC 不支持回滚,某条命令失败后其余命令照样执行。没有隔离级别,也没有 MVCC。不适合需要严格事务保障的场景。
- 不支持复杂查询:没有 SQL,不支持 JOIN、子查询、聚合函数等。只能通过 key 精确定位或在特定数据结构内做有限的查询。
为什么Redis这么快:
- 基于内存操作:这是最根本的原因。内存的随机访问延迟大约在 100 纳秒级别,而磁盘的随机 I/O(即使是 SSD)在几十微秒到毫秒级别,差了两到三个数量级。Redis 所有的数据读写都在内存中完成,从根源上消除了磁盘 I/O 这个最大的性能瓶颈。
- 单线程模型:避免了锁竞争和上下文切换,多线程操作共享数据需要加锁,锁的获取和释放本身有开销,还可能导致线程阻塞和死锁。单线程完全不需要。多线程在 CPU 核心之间切换时需要保存和恢复寄存器、程序计数器等状态,每次切换大约耗时几微秒。单线程没有这个开销。
- I/O 多路复用:Redis 使用 epoll(Linux)等 I/O 多路复用机制,一个线程可以同时监听成千上万个客户端连接的读写事件。有事件就绪就立即处理,没有就绪就不阻塞。
- 高效的底层数据结构:Redis 对每种数据类型在不同数据规模下选用了不同的底层编码,在内存占用和操作效率之间取得了精细的平衡。
Redis的数据类型有哪些?
首先是五种基础数据类型:
- string:最基本的数据类型,一个key对应一个value;value可以是字符串,整数或浮点数;
- hash:一个key对应一组field-value键值对,类似于Python的字典;
- List:有序的字符串列表,按插入顺序排列,支持从两端推入和弹出;
- set:无序的字符串集合,元素不可重复,支持交集、并集、差集运算。
- zset:Sorted Set ,和 Set 一样元素不可重复,但每个元素关联一个 score(分数),按 score 从小到大排序。score 相同时按字典序排列。 然后是四种特殊的数据类型:
- Bitmap:不是独立的数据类型,底层就是 String,但 Redis 提供了按位(bit) 操作的命令。
- HyperLogLog:一种基数估算的概率型数据结构,用极小的固定内存(每个 key 最多 12KB)估算一个集合中不重复元素的数量,标准误差约 0.81%。
- GEO:底层基于 Sorted Set 实现,把经纬度通过 GeoHash 编码为一个 52 位整数作为 score。
- Stream:专为消息队列设计的数据结构,借鉴了 Kafka 的设计思想,支持消息持久化、消费者组(Consumer Group)、消息确认(ACK)、消息回溯等。
Redis是单线程的还是多线程的,为什么?
Redis 的核心命令处理是单线程的,但辅助功能,如网络 I/O、持久化、异步删除等方面使用了多线程。
- Redis 的操作本质上是内存中的数据结构操作——哈希表查找、跳表遍历、链表增删——计算量极小,单核 CPU 就绑绑有余。
- 单线程避免了并发复杂性, 如果命令执行用多线程,对共享数据结构的操作就需要加锁。锁带来的问题很多,诸如锁本身的开销,死锁风险等;
- 单线程 + I/O 多路复用已经足够快:单线程配合 epoll 可以同时处理成千上万个连接。一个连接有数据到达就处理,没有就跳过,不会阻塞。单机 QPS 轻松 10 万+,对绝大多数业务场景来说已经远超需求。
- 原子性天然保证:单线程串行执行意味着每条命令天然是原子的,不需要额外的锁或事务机制。
说一说Redis持久化机制有哪些
Redis 提供了三种持久化方案:
- RDB:RDB 是对 Redis 某一时刻的内存数据做一次全量快照,生成一个二进制压缩文件。恢复时直接把文件加载到内存即可。
- AOF:AOF 以追加写的方式记录 Redis 执行的每一条写命令(以 RESP 协议格式),相当于一份操作日志。恢复时重放(replay)这些命令即可重建数据。
- 混合持久化:混合持久化结合了 RDB 和 AOF 的优点:在 AOF 重写时,不再生成纯命令格式的新 AOF 文件,而是先把当前内存数据以 RDB 格式写入新 AOF 文件的前半部分,再把重写期间产生的增量写命令以 AOF 格式追加到后半部分。
介绍一下Redis缓存雪崩和缓存穿透,如何解决这些问题?
- 缓存雪崩(Cache Avalanche):
- 大量缓存 key 在同一时间集中过期,或者 Redis 服务整体宕机,导致海量请求瞬间全部打到数据库,数据库压力骤增甚至被打垮。
解决方案:
- 过期时间加随机值:给每个 key 的 TTL 加上一个随机偏移量,让过期时间分散开来,避免集中失效。
- 多级缓存架构:在 Redis 之前再加一层本地缓存。即使 Redis 大面积失效,本地缓存还能扛住一部分请求,不至于全部打到数据库。
- 缓存预热:系统上线或大促之前,提前把热点数据批量加载到 Redis 中,避免上线后短时间内大量缓存 miss。
- Redis 高可用架构:通过哨兵或 Redis Cluster 实现主从自动切换,避免单点故障导致整个缓存层不可用。
- 服务降级和限流:在缓存层全面失效的极端情况下,启用熔断降级(如 Sentinel、Hystrix),对数据库的请求做限流,只放过一部分请求,其余快速返回默认值或错误提示。
- 缓存击穿(Cache Breakdown):
- 某个热点 key恰好在某一刻过期,在缓存重建完成之前,大量并发请求同时发现缓存未命中,全部涌入数据库查询同一条数据,造成数据库瞬间压力飙升。
解决方案:
- 互斥锁:当缓存未命中时,不是所有请求都去查数据库,而是用分布式锁(如 Redis 的 SETNX)让只有一个请求去查库并重建缓存,其他请求要么等待重试、要么返回旧数据或默认值。
- 逻辑过期:不给 key 设置真正的 TTL(让它永不过期),而是在 value 中存一个逻辑过期时间。读取时判断是否逻辑过期:未过期 → 直接返回, 已过期 → 先返回旧数据,同时开一个异步线程去查库更新缓存
- 热点 key 永不过期 + 主动更新:对于已知的热点 key,干脆不设 TTL,让它永远存在于缓存中。当底层数据变化时,通过业务逻辑或消息队列主动更新缓存。
- 缓存穿透(Cache Penetration):
- 请求查询的数据在 Redis 和数据库中都不存在。每次请求都先查 Redis 未命中,再查数据库也查不到,而且因为数据不存在所以也无法回填缓存,导致每次请求都会打到数据库。
解决方案:
- 缓存空值 / 空对象:查询数据库发现数据不存在时,在 Redis 中缓存一个空值(比如 “"、null、或约定的空标记),并设置一个较短的 TTL(如 2 ~ 5 分钟)。下次同样的请求进来,Redis 直接返回空值,不会再打到数据库。
- 布隆过滤器:在 Redis 之前加一层布隆过滤器,预先把所有合法的 key 加载进去。请求进来时先经过布隆过滤器:如果布隆过滤器判断 key 不存在 → 直接拒绝,不查 Redis 也不查数据库, 如果布隆过滤器判断 key 可能存在 → 继续查 Redis 和数据库
- 参数校验 / 接口层拦截: 在业务层做前置校验,对明显非法的请求直接拦截。比如 id 不可能为负数、格式不符合规范的直接返回错误,不要让这些请求进入缓存和数据库查询链路。
如何保证数据库和缓存的一致性
只要同时使用数据库和缓存两份存储,就不可能做到强一致性。我们追求的是最终一致性——允许短暂的不一致窗口,但最终缓存和数据库的数据会达成一致
1. Cache Aside(旁路缓存)
- 读流程:先读缓存,命中直接返回;未命中则查数据库,把结果写入缓存,再返回。
- 写流程:先更新数据库,再删除缓存。
- 为什么是"删除缓存"而不是"更新缓存”:两个原因。第一,并发写场景下,两个线程同时更新,由于网络延迟可能导致缓存更新顺序和数据库不一致。第二,缓存的值可能是多表关联计算的结果,每次写都重新计算很浪费,不如删掉等下次读的时候再重建,符合懒加载的思想。
- 为什么是"先更新数据库"而不是"先删除缓存":如果先删缓存再更新数据库,在这两步之间有一个时间窗口,读请求进来发现缓存为空,就去数据库读到旧值回填缓存,之后数据库才被更新为新值,导致缓存长期保存旧数据。而先更新数据库再删缓存就不存在这个问题。
- Cache Aside 可能的不一致场景:一个读操作没命中缓存去查数据库,此时来了一个写操作更新了数据库并删除了缓存,然后读操作才把之前查到的旧值写入缓存。
- 延迟双删可以作为 Cache Aside 的额外加固,尤其适合读写分离架构下主从同步有延迟的场景: 更新数据库 → 删除缓存 → 延迟 N 毫秒 → 再删一次缓存。 第二次延迟删除是为了清理在更新数据库期间可能被其他读请求回填的旧缓存。
2. Read/Write Through(读写穿透)
- 核心思路是把缓存和数据库的同步操作交给缓存层代理,应用只跟缓存打交道,把后端看作一个单一的存储。缓存层更新完数据库后返回。
- Read Through:读请求缓存未命中时,由缓存服务自己去加载数据库数据并回填,对调用方完全透明。和 Cache Aside 的区别在于,Cache Aside 是调用方负责查库回填,Read Through 是缓存层自己负责。
- Write Through:写请求如果命中了缓存,先更新缓存,再由缓存同步更新数据库;如果没命中缓存,直接更新数据库。整个过程对调用方来说只需要写一次。
- 这种模式的一致性比 Cache Aside 好一些,因为缓存和数据库的更新由同一个组件控制。但实际落地中用得不多,因为 Redis 本身不提供自动回写数据库的能力,需要额外的缓存框架支持
3. Write Behind / Write Back(异步回写)
- 更新数据时只更新缓存,更新完立即返回,不更新数据库。缓存会异步地批量将数据刷回数据库。
- 优点:写操作极快,因为只写内存,而且批量异步刷盘可以合并多次写操作,I/O 效率很高。Linux 操作系统的 Page Cache、CPU 的 Write Back Cache 都是这个思路。
- 缺点:第一,数据丢失风险——缓存中的数据还没来得及刷回数据库,如果 Redis 宕机数据就丢了。第二,一致性窗口较长——异步刷盘之前数据库中是旧值,如果有其他系统直接读数据库就会拿到旧数据。第三,实现复杂度高,需要自己维护写队列和刷盘逻辑。