文章目录
- MySQL
- 基础概念
- 数据库三大范式是什么?
- 索引
- Redis
- 基本概念
- 1、为什么用Redis作为MySQL的缓存?
- 数据结构
- 2、Redis包含哪些数据类型?使用场景是什么?
- 3、五种场景的Redis数据类型底层都是怎么实现的?
- Redis线程网络模型
- 4、Redis是单线程的吗?
- 5、Redis网络IO处理模式是怎样的?
- Redis持久化
- 6、Redis如何实现数据不丢失?
- 7、RDB快照如何实现?
- 8、AOF日志如何实现?
- 9、混合持久化如何实现?
- Redis缓存设计
- 10、如何避免缓存穿透、缓存雪崩、缓存击穿、?
- 缓存穿透
- 缓存雪崩
- 缓存击穿
- 11、数据库与缓存如何保持一致性?
- 12、Redis的过期删除策略是什么?
- 13、Redis的内存淘汰策略有哪些?
- Redis主从、切片集群
- 14、Redis主从复制原理,有什么缺陷?
- 15、Redis主从模式下的哨兵机制是怎么实现的?
- 16、Redis的分片集群
- Redis实战
MySQL
基础概念
数据库三大范式是什么?
- 第一范式:强调列的原子性,即数据库表的每一列是不可分割的原子数据项。
- 第二范式:实体的属性完全依赖于主关键字。
- 第三范式:任何非主属性不依赖于其他非主属性。
索引
Redis
基本概念
1、为什么用Redis作为MySQL的缓存?
主要是因为Redis具备高性能和高并发两种特性。
- 高性能:从MySQL中访问数据,是从硬盘读取,而使用Redis是从内存中读取,效率更高。
- 高并发:单台Redis的QPS每秒处理请求的次数是MySQL的10倍,直接访问Redis能够承受的请求数是远远高于直接访问MySQL的。
数据结构
2、Redis包含哪些数据类型?使用场景是什么?
- String类型,应用场景包含:缓存对象、分布式锁、共享session信息。
- List类型,应用场景包含:消息队列。(但是有两个问题:1、是生产者需要自行实现全局唯一ID,2、是不能以消费形式消费数据)
- Hash类型:缓存对象、购物车等。
- Set类型:唯一性且需要聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
- ZSet类型:唯一性且有排序需求的场景,比如排行榜、电话和姓名排序。
- BitMap:二值状态统计的场景,比如签到、判断用户登录状态,连续签到用户总数等;
- HyperLogLog:海量数据基数统计的场景,比如百万级网页UV统计。
- GEO:存储地理位置信息的场景,比如滴滴叫车。
- Stream:消息队列,相比与基于List类型实现的消息队列,Stream可以自动生成全局唯一性消息ID ,并且支持以消费组的形式消费数据,每个订阅的消费组都会收到消息的发送。
3、五种场景的Redis数据类型底层都是怎么实现的?
-
string 底层是由SDS(简单动态字符串)实现的,它包含了字符串的长度len,以申请的空间数alloc,头类型flag和实际存放数据的字符数组buf,相比于C语言传统的字符数组,有如下优点:
- 可以在O(1)时间内获取字符串长度
- 支持动态扩容,并通过内存预分配机制,减少内存分配次数。
- 根据长度读取字符串内容,而非结束标志’/0’,因此是二进制安全 的。
-
list底层是由quicklist实现,quicklist中每个节点都是一个ZipList,采用ZipList,ZipList的空间利用率更高,采用多个小的ZipList连接,可以加快内存申请的效率,找到多个小的连续内存空间,要比找到一大块连续内存空间容易的多。
-
hash底层 默认由ZipList实现,高版本采用listpack,ZipList中相邻的两个entry分别保存field和value。但数据量较大时,会采用Dict实现。
-
Set底层基于Dict实现,filed存放Set的数据,而value存放null。当存放的数据都是整数类型时,采用整数集合IntSet实现。
-
ZSet有序表,基于Dict与skiplist实现。
Redis线程网络模型
4、Redis是单线程的吗?
Redis的核心功能,对于数据库键值的增删改查指令的解析、执行、结果发送是单线程执行的,因为Redis本身是对于内存的操作,执行效率很快,使用多线程要考虑线程安全,线程切换开销,反而影响其效率,但是对于一些非核心功能,如文件关闭、AOF刷盘、内存释放、网络IO请求处理用到了多线程以提高效率。
Redis的核心功能:接收客户端指令请求,解析请求,进行数据读写等操作,发送数据给客户端,这个过程是由一个线程(主线程)来完成的,因此可以说Redis的核心功能是单线程实现的。
但是Redis整体的实现不是单线程的。
- Redis在2.6版本会启动2个后台线程,分别处理关闭文件、AOF刷盘这两个任务。
- Redis在4.0版本之后,新增了一个新的后台线程,用来异步释放Redis内存,也就是lazyfree线程。
- Redis6.0版本以后,采用了多个I/O线程来处理网络请求。
之所以将关闭文件、AOF刷盘、释放内存这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果放在主线程完成,容易发生阻塞。
5、Redis网络IO处理模式是怎样的?
第一步:Redis初始化:
- 首先调用epoll_create()创建一个epoll对象,调用socket()创建一个服务端socket。
- 然后调用bind()绑定服务端端口号,调用listen()监听该socket。
- 调用epoll_ctl()将listen socket加入到epoll,同时注册连接事件处理函数。
第二步:事件循环函数:
初始化完成后,主线程进入到一个事件循环函数,主要会做以下事情:
- 首先,调用处理发送队列函数,查看是否有任务需要发送。如果有发送任务,通过write函数将客户端发送缓冲区里的数据发送处理,如果这一轮没有发送后,会注册写处理函数,等待epoll_wait发现后再次处理。
- 接着,调用epoll_wait函数,等待事件到来:
- 如果是连接事件,调用accept()获取已经连接的socket,调用epoll_ctl将已经连接的socket加入到epoll,注册读事件处理函数。
- 如果是读事件到来,则调用读事件处理函数,调用read()获取客户端发送的数据,解析命令,处理命令,将客户端对象添加到发送队列,将执行结果写到发送缓存区,等待发送。
- 如果是写事件到来,则会调用写事件处理函数,调用write()函数将客户端发送缓存区里的数据发送出去,如果这一轮数据,没有发送完,就会继续注册写事件处理函数,等待epoll_wait发现可写后再处理。
Redis持久化
6、Redis如何实现数据不丢失?
Redis的读写操作都是在内存中,所以Redis性能才会高,但是Redis宕机、重启,内存中的数据就会丢失,为了保证内存中的数据不丢失,Redis实现了数据持久化机制,将数据存储到磁盘,这样在Redis重启时就能够从磁盘中恢复原有的数据。
Redis共有以下三种数据持久化方式:
- RDB快照:将某一时刻的内存数据,以二进制的方式写入磁盘。
- AOF日志:每执行一条写操作命令,就把该命令以追加的方式写到一个文件里;
- 混合持久化方式,Redis4.0以后,集成了AOF和RDB各自的优点。
7、RDB快照如何实现?
Redis提供了两个命令来生成RDB文件,分别是save和bgsave。
- save命令在主线程执行生成RDB文件,会阻塞主线程,此期间,不可再执行Redis指令。
- bgsave命令,即后台保存,执行bgsave命令,会fork()创建子进程,复制父进程的页表,但是父子进程的页表指向的物理内存还是一个,此时如果执行读操作,父子进程是互不影响的,但是如果执行写操作则会为被修改的数据创建一份副本,然后bgsave子进程会把该副本写入到RDB文件。
极端情况下,如果所有的共享内存都被修改,则此时的内存占用是原先的 2 倍!!!。因此Redis在使用时应该预留内存。
8、AOF日志如何实现?
AOF概念:
AOF(append only file):Redis在执行一条写操作命令后,就会将该命令以追加的方式写入到一个文件里,然后Redis重启时,会读取文件记录的命令,逐一执行命令,来进行数据的恢复。
AOF触发重写:
AOF日志随着执行的写操作命令越来越多,文件大小越来越大,当大小超过所设定的阈值后,Redis会启用AOF重写机制,在没有使用重写机制前,假设前后执行了「set name xiaolin」和「set name xiaolincoding」这两个命令的话,就会将这两个命令记录到 AOF 文件。在使用重写机制后,就会读取 name 最新的 value(键值对) ,然后用一条 「set name xiaolincoding」命令记录到新的 AOF 文件,之前的第一个命令就没有必要记录了,因为它属于「历史」命令,没有作用了。
AOF重写过程:
Redis重写AOF过程是由后台子进程bgwriteaof
来完成的。子进程进程AOF重写期间,主进程可以继续出来命令请求,避免阻塞主进程。重写子进程对内存是只读的,把内存数据的键值对转换为一条命令,再将命令记录到重写日志(新的AOF文件)。
如果在重写AOF日志中,主进程修改了已经存在的数据,会发生写时复制。Redis设置了一个AOF重写缓冲区,该缓冲区在bgwriteaof
子进程之后开始使用,在重写AOF期间,当Redis执行完一个写命令之后,它会同时将这个写命令写入到AOF缓冲区和AOF重写缓冲区。当子进程完成AOF重写工作后,会向主进程发生一个信号。主进程收到信号后,会将AOF重写缓冲区中的所有内容追加到新的AOF文件中,使得新旧两个AOF文件所保存的数据库状态一致,新的AOF文件进行改名,覆盖现有的AOF文件。
9、混合持久化如何实现?
- RDB的优点是只保存内存中记录的数据,恢复快,文件小,但是有数据丢失风险,安全性不足。RDB的频率也不好把握,频率太低,丢失的数据越大,频率太高则会影响性能。
- AOF的优点是丢失数据少,较为安全,但是AOF文件通常比较大,而是数据恢复速度慢。
- Redis4.0提出了混合使用AOF和RDB,集成各自的优点。当开启了混合持久化时,在AOF重写日志时,fork出来的重写子进程会先将与主线程共享的内存数据以RDB方式写入到AOF文件,然后主线程处理操作的命令会被记录在重写缓冲区,重写缓冲区里的增量命令会以AOF方式写入到AOF文件,写入完成后,通知主进程将新的含有RDB格式和AOF格式的AOF文件替换旧的AOF文件。
也就是说,混合持久化时,AOF文件的前半部分是RDB格式的全量数据,后半部分是以AOF格式的增量数据
这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。
Redis缓存设计
10、如何避免缓存穿透、缓存雪崩、缓存击穿、?
缓存穿透
缓存穿透是指客户端请求的
数据在缓冲中和数据库中都不存在
,这样缓存永远不会生效,这些请求都会打到数据库中。
缓存穿透的发生一般有两种情况:
- 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
- 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
常见的方案有三种:
- 1、限制非法请求,在API入口处,判断请求参数是否合理,判断是否为恶意请求。
- 2、缓存空值,并设置一个较短的TTL。后序请求就会命中缓存,但同时为了避免大量这样的缓存,通过设置较短的TTL,自动清除他们。
- 3、使用布隆过滤器,判断数据是否存在。在写入数据到数据库时,使用布隆过滤器做个标记,用户请求过来时,业务判断缓存失效后,通过查询布隆过滤器快速判断数据是否存在。如果不存在,就不用查询数据库。
缓存雪崩
大量缓存数据在同一时间过期(失效)时
,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
对于缓存雪崩问题:我们可以采用以下方案解决:
- 1、将缓冲失效时间TTL随机打乱,降低缓存集体失效的概率。
- 2、利用Redis集群,提高服务的可用性
- 3、给缓存业务添加降级限流策略
- 4、给业务添加多级缓存。
缓存击穿
缓存击穿问题也称为
热点key
问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了
(热点key失效),无数的请求访问会在瞬间给数据库带来巨大的冲击,并且多个线程都会尝试进行缓存重建,导致服务器性能紧张。但实际上只需要一个线程重建缓存就足够了
缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。
常见的解决方法如下:
- 1、不给热点key设置TTL, 由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
- 2、给重建缓存的过程加锁,其他没有锁的线程阻塞,保证同一时间只有一个业务线程重建缓存。但是互斥锁导致多个线程等待,依然会造成性能影响。
- 3、基于逻辑过期策略,给缓存设置逻辑过期的字段
expire
,第一个发现缓存过期的线程拿到锁,开启一个新的线程,执行缓存重建过期,而自己直接返回过期的缓存。其余没有拿到锁的线程在缓存重建成功之前,都直接返回过期缓存,不阻塞。这种方式是弱一致性的,但是性能会比较好。
11、数据库与缓存如何保持一致性?
常见的缓存更新策略包含:
- Cache Aside (旁路缓存)策略
- Read/Write Through(读穿/写穿)策略
- Write Back(写回)策略
实际开发中Redis和MySQL的更新策略都是Cache Aside(旁路缓存)策略。
Cache Aside(旁路缓存)策略是最常用的,应用程序直接与数据库、缓存交互,并负责对缓存的维护,该策略又可以细分为读策略和写策略。
-
写策略:先更新数据库中的数据,再删除缓存中的数据。
-
读策略:如果读取数据直接命中了缓存,则直接返回数据,如果没有命中,则从数据库中读取数据,再将数据写入到缓存。
写数据时,先更新数据库,再更新缓存,并发时可能会有问题:
为了避免上边这种缓存与数据的不一致,写数据时,更新数据库后,执行了删除缓存。
那么为什么是先更新数据库,再删除缓存呢?
因为对缓存的操作要比对数据库的操作更快,先更新缓存再更新数据库,数据不一致的时间差更大,造成请求从数据库和缓存中结果不一致的概率更高!而先更新数据库,再删除缓存,删除缓存的时间更快,中间的时间差更小。
12、Redis的过期删除策略是什么?
每当我们对一个key设置了TTL时,Redis构造该key的数据时,会额外存储一个过期字典(expire dict),当我们查询一个key,Redis首先检查key是否存在于过期字典中。如果不存在,则正确读取,如果存在,则会检查key的过期时间与当前时间进行比对,检测key是否过期。
Redis采用的过期删除策略是惰性删除 + 定期删除
惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
-
优点:因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。
-
缺点:如果一个key已经过期,而这个key又依然保留在数据库,后续一直没访问,则浪费了内存空间。
为此Redis会使用定期删除策略,配合上边的惰性删除策略。
定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
定期删除有两种工作模式:
- 在Redis的initServer()函数中,会按照server.hz的频率来执行过期key的清理,模式为SLOW,默认每秒10次。
- Redis的每个事件循环前回调用beforeSleep()函数,执行过期key的清理,模式为FAST。
- SLOW执行低频但清理更加彻底。FAST模型执行高频,但执行时机较短。
13、Redis的内存淘汰策略有哪些?
内存淘汰策略执行的时机:
Redis会在处理客户端命令的方法processCommand()中,如果发现OOM,会触发内存淘汰机制。
Redis支持8种不同的策略进行key的淘汰,默认的淘汰策略是:
1、noeviction,不淘汰任何key,但是内存满的时候不允许写入新数据。
其他常见内存淘汰策略,通常分为对全体key处理和对设置了TTL的key处理。
2、包括按照TTL优先淘汰将要过期key。
3-4、对全体key,随机淘汰,对设置了TTL的key随机淘汰。
5-6、基于LRU算法对全体key或者设置了TTL的key进行淘汰。
7-8、基于LFU算法对全体key或者设置了TTL的key进行淘汰。
LRU(least recently used),最少最近使用。用当前时间减去最后一次访问时间,这个值越大,淘汰的优先级越高。
LRU是基于时间的,时间敏感,最近的,新出现的热点key,更有可能被常驻到内存中,而那些长时间没有使用的key则更有可能被淘汰掉。
LFU(least frequently used),最少频率使用。会统一每个key的访问频率,值越小,淘汰优先级越高。
LFU是频率敏感的,更适合保存包含有多个热点key,某个key上一次访问已经是昨天中午12:00,并且在昨天12:00的5分钟内,高频访问了200次,如果今天中午还有这样的需求,按照LFU的策略,昨天的热key会常驻在内存中。
Redis主从、切片集群
14、Redis主从复制原理,有什么缺陷?
Redis主从复制实现方案是采用多台Redis服务器,一主多从,且主从服务之间采用读写分离的模式,主服务器可以进行读写操作,当发生写操作时,自动将写操作同步给服务器,而从服务器一般是只读模式,并接收主服务器同步过来的写操作命令。
由于主从服务器之间的命令复制是异步进行的,所以无法实现主从数据的强一致性。
主从服务器数据同步的原理如下:
15、Redis主从模式下的哨兵机制是怎么实现的?
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复,哨兵的主要作用包括服务器状态监控、自动故障恢复、通知。
-
服务器状态监控
- 哨兵基于心跳机制监测服务器状态,每隔1秒向集群的每个实例发送ping命令;如果某个哨兵,发现实例未在规定时间内响应,则认为实例主观下线,当超过指定数量(一般大于哨兵总数一半)的哨兵都主观认为该实例下线,则该实例节点客观下线。
- 一旦监测到master节点客观下线,第一个发现master节点主观下线的哨兵需要在slave中选择一个作为新的master。 选举的依据:
- 判断slave节点与master节点断开的时间长短,超过指定值的slave直接排除。
- 判断slave节点的优先级值,值越小,优先级越高。
- 判断slave节点运行的id大小,id越小,优先级越高。
-
自动故障恢复
- 被选中的slave,执行slave of no one,成为新的master。
- 通知所有其他slave节点,执行slave of 新的master。
- 修改发生故障的master节点的配置,添加slave of 新的master。
16、Redis的分片集群
当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster )方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和节点之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。
这些哈希槽怎么被映射到具体的 Redis 节点上的呢?有两种方案:
- 平均分配: 在使用 cluster create 命令创建 Redis 集群时,Redis 会自动把所有哈希槽平均分布到集群节点上。比如集群中有 9 个节点,则每个节点上槽的个数为 16384/9 个。
- 手动分配: 可以使用 cluster meet 命令手动建立节点间的连接,组成集群,再使用 cluster addslots 命令,指定每个节点上的哈希槽个数。