校招笔记(六)_计算机基础_Redis
我的校招记录:校招笔记(零)_写在前面 ,以下是校招笔记总目录。
备注 | ||
---|---|---|
算法能力(“刷题”) | 这部分就是耗时间多练习,Leetcode-Top100 是很好的选择。 | 补充练习:codeTop |
计算机基础(上)(“八股”) | 校招笔记(一)__Java_Java入门 | C++后端后续更新 |
校招笔记(一)__Java_面对对象 | ||
校招笔记(一)__Java_集合 | ||
校招笔记(一)__Java_多线程 | ||
校招笔记(一)__Java_锁 | ||
校招笔记(一)__Java_JVM | ||
计算机基础(下)(“八股”) | 校招笔记(二)__计算机基础_Linux&Git | |
校招笔记(三)__计算机基础_计算机网络 | ||
校招笔记(四)__计算机基础_操作系统 | ||
校招笔记(五)__计算机基础_MySQL | ||
校招笔记(六)__计算机基础_Redis | ||
校招笔记(七)__计算机基础_数据结构 | ||
校招笔记(八)__计算机基础_场景&智力题 | ||
校招笔记(九)__计算机基础_相关补充 | ||
项目&实习 | 主要是怎么准备项目,后续更新 |
六、Redis
6.1 Redis基本
1.什么是 Redis?
Redis 是一个开源(BSD 许可)、基于内存(读写快)、支持多种数据结构的存储系统,可以作为数据库、缓存和消息中间件。
- 支持的数据结构有5种:字符串(String)、哈希(hash)、列表(list)、集合(set)、有序集合(sorted set)。
1.1 有MySQL不就够用了吗?为什么要用Redis这种新的数据库?
主要是因为 Redis 具备高性能和高并发两种特性。
- 高性能:除了第一次读取硬盘比较慢,后面加载到缓存,读取速度都相关快,性能高;
- 高并发:直接操作缓存能够承受的并发请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分热点数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
1.2 C++ / JAVA 中的Map也是一种缓存型数据结构,为什么不用Map,而选择Redis做缓存?
缓存分为本地缓存和分布式缓存 。
- 本地缓存不具一致性。以Java为例 ,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着jvm的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性;
- Redis分布式缓存具有一致性。 使用redis或memcached之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性;
- Redis可以使用更大内存作为缓存。 Redis可以使用几十G内存作为缓存,Map不行,比如JVM最多使用几个G ;
- Redis可以持久化。 Redis可以实现持久化,而Map是内存对象,程序重启就没了;
- Redis可以处理百万级别并发;
- Redis有丰富的API & 缓存过期等机制。
2. 【重点】redis的数据类型,以及每种数据类型的使用场景?
数据类型 | 使用场景 |
---|---|
String | 存储key-value键值对,注意redis中String可修改。统计在线人数;也可以存储视频、图片等 |
hash | 购物车:hset [key] [field] [value] 命令, 存放键值对,一般可以用来存某个对象的基本属性信息,例如,用户信息,商品信息等 |
set | 全局去重,JVM自带的set不适合分布式集群情况 |
zset | 排行榜,比如微信运动排行榜 |
list | 分页功能,lrange做基于redis的分页功能,性能很好;模仿一个消息队列 |
3.说一下 Redis有什么优点和缺点 ?
优点 | 缺点 |
---|---|
速度快:因为在内存中 | 存储有限:因为Redis是内存数据库,大小和机器本身内存有关 |
支持多种数据结构: String,List,Set,Hash,Sorted Set等 | 完成重同步耗费CPU资源和带宽 |
持久化存储:RDB和AOF | 当Redis重启后通过把硬盘文件重新加载到内存,速度比较慢,这个时候redis做不了其它事。 |
高可用:内置 Redis Sentinel (哨兵),实现主从故障自动转移。 内置 Redis Cluster ,提供集群方案。 | |
丰富特性:Key过期、计数、分布式锁 |
4. Redis的数据结构?key是怎么存储的?
-
概述
Redis底层采用数组, key就是对应数组的索引 ,采用Hash(key)映射到数组上。解决冲突采用链地址法。
具体可看参考下文。
-
底层存储原理
redis 中以
redisDb
作为整个缓存存储的核心,保存着我们客户端需要的缓存数据。其结构如下:
1
2
3
4
5
6
7
8
9typedef struct redisDb {
dict *dict; // 最重要--字典类型,保存数据库的键值对
dict *expires; // 重要--字典类型,保存过期的时间
dict *blocking_keys; // 和ready_key 实现BLPOP等阻塞命令
dict *ready_keys; // 同上
dict *watched_keys; // 实现watch命令,记录正在被watch的key
int id; // 数据库id,默认16个,支持单个
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;现在我们来查看,dict 的结构。
-
dict的结构
1
2
3
4
5
6
7
8
9
10/* 字典 : 每个字典使用两个哈希表,用于实现渐进式 rehash */
typedef struct dict {
// type存储了hash函数,key和value的复制函数等,比较以及销毁函数
dictType *type;
// privdata保存一些私有数据,决定了*type保存的函数,实现了【多态】
void *privdata;
dictht ht[2]; // 哈希表(2 个), 正常使用ht[0],rehash就会扩容使用ht[1]
int rehashidx; // 记录 rehash 进度的标志,值为 -1 表示 rehash 未进
int iterators; // 当前正在运作的安全迭代器数量
} dict;上述
dictht
就是个hash表,包含:1
2
3
4
5
6
7
8
9
10typedef struct dictht {
// 哈希表节点指针数组(俗称桶,bucket)
dictEntry **table;
// 指针数组的大小
unsigned long size;
// 指针数组的长度掩码,用于计算索引值,其实永远都是size-1
unsigned long sizemask;
// 哈希表现有的节点数量
unsigned long used;
} dictht;-
dictEntry 指针数组(table)。key 的哈希值最终映射到这个数组的某个位置上(对应一个 bucket)。如果多个 key 映射到同一个位置,就发生了冲突,那么就拉出一个 dictEntry 链表。
1
2
3
4
5
6
7
8
9
10// 哈希表节点dictEntry
typedef struct dictEntry {
void *key; // redis的键
union {
void *val; // 存储了对应string/set/list/hash/zset的数据
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next; //链表后续节点
}dictEntry; -
size:标识 dictEntry 指针数组的长度。它总是 2 的指数次幂。
上面
dictEntry
的value 最终指向了redisObject
对象,我们来观察下其结构。 -
-
Redis Object
1
2
3
4
5
6
7typedef struct redisObject {
unsigned type:4; // 类型 ,比如string,set等,才能确定是哪种数据结构使用什么API操作
unsigned encoding:4; // encoding 表示 ptr 指向的具体数据结构,这个对象使用什么数据结构实现
unsigned lru:REDIS_LRU_BITS; // 对象最后一次被访问的时
int refcount; // 引用计数
void *ptr; // 指向底层数据结构的指针
robj;
-
4.1 String、list、hash、set、zset的底层结构是什么?
版本:redis 3.0.6中版本各种数据结构的实现
-
String
- embstr和raw都是由SDS动态字符串构成的 ,底层结构应该都是char数组吧 ;
- int ,就是指int类型。
-
list
-
hash
-
set
intset是集合键的底层实现方式之一,是int类型数组。
-
zest
4.2 讲讲redis的hash表扩容方式?
-
扩容条件
- 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 (保存的key超过哈希表大小);
- 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;
-
渐进式rehash
-
新建一个哈希表大小,为
2^N
次方,并分配内存,此时字典同时持有:ht[0] 和 ht[1] 两个哈希表同hashmap:哈希表掩码sizemask为size-1,当size满足2的n次方时,计算每个key的索引值时只需要用key的hash值与掩码sizemask进行位与操作,替代求余操作,计算更快。
-
哈希表赋值给字典的ht[1],然后将rehashidx赋值为0,表示rehash工作开始
rehashidx也标识了,当前rehash进行到了哪个槽
-
在 rehash 进行期间,:每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] 。
当 rehash 工作完成之后, 程序将 rehashidx 属性的值**+1**
-
随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] 。此时
rehashidx=-1
,表示rehash完成。
采取分而治之的方式, 将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。
-
-
渐进式rehas优缺点
- 优点:避免redis阻塞
- 缺点:rehash需要分配一个新的hash表,会使得内存爆增,使得大量key被驱逐
4.3 rehash过程中增删查改怎么操作呢?
-
增加: 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作
-
删除(delete)、查找(find)、更新(update)等: 同时在ht[0] & ht[1]两个表进行。
比如:要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找。
5. 说说Redis有序集合zset的底层结构?
zset底层的存储结构包括ziplist或 skiplist & dic ,当满足以下两个条件的时候使用ziplist:
- 有序集合保存的元素数量小于128个
- 有序集合保存的所有元素的长度小于64字节
其余情况用skiplist。
-
ziplist是一个经过特殊编码的双向链表,以O(1)的时间复杂度在表的两端提供push和pop操作。
ziplist将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。
-
使用原因:一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来;这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。
-
具体结构
- entry:表示真正存放数据的数据项,长度不定。一个数据项(entry)也有它自己的内部结构。
-
-
跳表是在单链表上实现多级索引,可以实现 二分查找 的有序链表。
跳表插入、删除、查找元素的时间复杂度跟红黑树都是一样量级的,时间复杂度都是O(logn)。
-
主要形式
在单链表上进行多级索引。
-
构建过程
上面链表是如何构建的呢,请见下图。
⚠️ skiplist为了避免上下两层出现严格1:2数量对应关系后,新插入节点会打乱这种关系,而需要把新插入节点后所以节点都进行调整。
它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。
-
查找过程
zset :
<key> <score> <member>
在上图中,我们没有区分member和score,但是实际上链表是按score进行排序,查找也是在比较score。
以查找 和 插入23为例。
- 从最高层(第4)层开始查找,因为
7<23
,本应该继续往后查找,但是后继节点为null,所以往下一层进行查找 - 此时第3层,满足
7<23<37
,继续往下一层继续查找 - 此时第2层,
7<23 & 19<23
,往下第二层的下一个节点(19)查找;此时满足19<23<37
,继续往下一层 - 此时第1层,一直往后遍历到22,发现
22<23<26
:- 如果此时是查询23:返回null,不存在
- 此时是插入23:生成新节点 & 随机生成层数,(1)将新节点各层指针指向对应层的下一个节点(不存在则指向null)(2)将新节点节点各层前一个节点对应层数的指针指向新节点
- 从最高层(第4)层开始查找,因为
-
5.1 Redis为什么不用红黑树
参考 : 知乎回答
虽然跳表操作时间复杂度和红黑树相同 ,但是:
-
实现简单:跳表代码实现更易读
-
区间查找:跳表区间查找效率更高
6. Redis持久化方式有哪些?以及有什么区别?
Redis
提供两种持久化机制 RDB
和 AOF
机制。
-
各自优点
RDB AOF 【方便】只有一个文件 dump.rdb
,方便持久化【数据安全】 AOF 持久化有 always
,每进行一次命令操作就记录到 AOF 文件中一次。【容灾性好】一个文件可以保存到安全的磁盘 【性能】最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化 【启动效率高】相对于数据集大时,比 AOF 的启动效率更高 -
各自缺点
RDB AOF 【安全性低】 RDB
是间隔一段时间进行持久化【启动效率低】数据集大的时候,比 RDB 启动效率低。 【恢复慢】 AOF
文件比RDB
文件大,且恢复速度慢。
6.1 AOF 重写了解吗?
AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任伺读入、分析或者写入操作。
具体过程如下:
- 在执行
BGREWRITEAOF
命令,开始重写; - Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令;
- 当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾 ;
- 最后,服务器用新的AOF文件替换旧的 AOF文件,以此来完成AOF文件重写操作。
7. Redis持久化有两种,那应该怎么选择呢?
- 如果Redis中的数据完全丢弃也没有关系(如Redis完全用作DB层数据的cache),那么无论是单机,还是主从架构,都可以不进行任何持久化;
- 单机环境: 如果可以接受十几分钟或更多的数据丢失,选择RDB对Redis的性能更加有利;如果只能接受秒级别的数据丢失,应该选择AOF;
- 主从架构:
- master:完全关闭持久化(包括RDB和AOF),这样可以让master的性能达到最好;
- slave:关闭RDB,开启AOF(如果对数据安全要求不高,开启RDB关闭AOF也可以),并定时对持久化文件进行备份(如备份到其他文件夹,并标记好备份的时间);然后关闭AOF的自动重写,然后添加定时任务,在每天Redis闲时(如凌晨12点)调bgrewriteaof。
8. (不太理解)pipeline有什么好处,为什么要用 pipeline?
-
使用 pipeline(管道)的好处在于可以将多次 I/O 往返的时间缩短为一次,但是要求管道中执行的指令间没有因果关系;
-
用 pipeline 的原因在于可以实现请求/响应服务器的功能,当客户端尚未读取旧响应时,它也可以处理新的请求。如果客户端存在多个命令发送到服务器时,那么客户端无需等待服务端的每次响应才能执行下个命令,只需最后一步从服务端读取回复即可。
9.怎么使用 Redis实现消息队列? 如何实现延时队列?
-
消息队列:一般使用
list
结构作为队列,rpush
生产消息,lpop
消费消息。当lpop
没有消息的时候,要适当sleep
一会再重试; -
延时队列: :使用
sortedset
,拿时间戳作为score
,消息内容作为key
调用zadd
来生产消息,消费者用zrangebyscore
指令获取符合条件的数据轮询进行处理。什么是延时队列?
当用户发送一个消息请求给服务器后台的时候,服务器会检测这条消息是否需要进行延时处理:
- 如果需要就放入到延时队列中,由延时任务检测器进行检测和处理;
- 如果不需要进行延时处理的任务,服务器会立马对消息进行处理,并把处理后的结果返会给用户。
【举个例子】
- 点外卖时,下单后不会立即安排配送,而是等待一段时间让商户接单才正式安排配送,否则超时取消
6.2 Redis单线程模型
1.为什么 Redis 使用单线程模型?单线程模型效率也能那么高?
-
采用单线程,避免了不要的上下文切换和竞争条件;
-
其次 CPU 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存或者网络带宽。
【效率高的原因】
. 1. C语言实现,效率高
-
纯内存操作
-
基于非阻塞的IO复用模型机制(可能会跟自己挖坑)
-
单线程的话就能避免多线程的频繁上下文切换问题(为什么单线程效率高)
-
丰富的数据结构(全程采用hash结构,读取速度非常快,对数据存储进行了一些优化,比如zset压缩表,跳表等)
2.(新,易忘)说说 Redis 的单线程模型 ?
这问题是因为前面回答问题的时候提到了 Redis 是基于非阻塞的IO复用模型。如果这个问题回答不上来,就相当于前面的回答是给自己挖坑了。
redis 内部使⽤⽂件事件处理器 file event handler ,这个⽂件事件处理器是单线程的,所以redis 才叫做单线程的模型。它采⽤ IO 多路复⽤机制一个线程同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进⾏处理。
⽂件事件处理器的结构包含 4 个部分:
- 多个 socket
- IO 多路复⽤程序
- ⽂件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
(1) IO 多路复⽤程序会监听多个 socket,(2)会将 socket 产⽣的事件放⼊队列中排队,(3)事件分派器每次从队列中取出⼀个事件,(4)把该事件交给对应的事件处理器进⾏处理。
3. 你说Redis是单线程的,那如何处理高并发?比如1000个并发请求同时发生?
-
Redis采用了IO多路复用机制,使其在网络IO操作中能并发处理大量的客户端请求。
详见上一个问题。
-
Redis可以采用主从架构,master负责写,slave负责读。
4.说说你对Redis事务的理解 ?
Redis 中的事务是一组命令的集合,是 Redis 的最小执行单位。
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的 Redis会将一个事务中的所有命令序列化,然后按顺序执行。
-
需要注意的地方
-
Redis 事务不支持回滚:不像 MySQL 的事务一样,要么都执行要么都不执行;
因为回滚需要增加很多工作,而不支持回滚则可以保持简单、快速的特性。
-
Redis 服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断,直到事务命令全部执行完毕才会执行其他客户端的命令。
-
5.为什么Redis的操作是原子性的,怎么保证原子性的?
- 原子性。 因为Redis是单线程的, Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
- 事务性。 Redis中的事务其实是要保证批量操作的原子性。
6.3 Redis缓存
1.为什么要用缓存 ?怎么提高缓存命中率?
-
为什么用缓存?
把热点数据存入内存中,提高读写性能。
-
提高命中率?
- 增加缓存空间
- 提升缓存更新频率
- 提前加载数据到缓存中
2.缓存雪崩、缓存穿透、缓存击透、缓存预热、缓存更新、缓存降级等?
-
缓存雪崩
简而言之:Redis 挂掉了,请求全部走数据库 。
-
例如: 对缓存数据设置相同的过期时间,导致某段时间内缓存失效,请求全部走数据库;
- key过期解决: 在缓存的时候给过期时间加上一个随机值,这样就会大幅度的减少缓存在同一时间过期。
更通用情况的做法:
- 事发前:实现 Redis 的高可用 (主从架构 + Redis Cluster),尽量避免 Redis 挂掉这种情况发生;
- 事发中:万一 Redis 真的挂了,我们可以设置本地缓存 (ehcache)+ 限流 (hystrix),尽量避免我们的数据库被干掉;
- 事发后:redis 持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据。
-
-
缓存穿透
查询一个一定不存在的数据 ,导致每次请求都要到数据库去查询,失去了缓存的意义 。
- 解决1: 使用布隆过滤器 (BloomFilter) 提前拦截,不合法就不让这个请求到数据库层;
- 解决2:当我们从数据库找不到的时候,我们也将这个空对象设置到缓存里边去,下次再请求的时候,就可以从缓存里边获取了。
-
缓存击穿
在平常高并发的系统中,大量的请求同时查询一个key时,此时这个高热key正好失效了,就会导致大量的请求都打到数据库上面去。这种现象我们称为缓存击穿。
-
解决1:使用互斥锁(mutex key)。 是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据就行。
如果是单机,可以用synchronized或者lock来处理,如果是【淘特】分布式环境可以用分布式锁就可以了。
-
解决2: key永不过期。 把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建。
-
-
缓存预热
系统上线后,将相关的缓存数据直接加载到缓存系统。
这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。
-
缓存更新
LRU
(访问时间最旧淘汰)/LFU
(把频次低的淘汰掉)- 超时剔除:设置key过期时间
- 主动更新:开发设置生命周期
-
缓存降级
降级的情况,就是缓存失效或者缓存服务挂掉的情况下,我们也不去访问数据库。我们直接访问内存部分数据缓存或者直接返回默认数据。
对于应用的首页,一般是访问量非常大的地方,首页里面往往包含了部分推荐商品的展示信息。这些推荐商品都会放到缓存中进行存储,同时我们为了避免缓存的异常情况,对热点商品数据也存储到了内存中。同时内存中还保留了一些默认的商品信息。
如下图所示:
3. Redis 设置key过期后如何处理?Redis缓存刷新策略(内存淘汰机制)有哪些?
-
Redis 设置过期时间
Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置⼀个过期时间。
- 如我们⼀般项⽬中的 token 或者⼀些登录信息,尤其是短信验证码都是有时间限制的,过期后基本不会使用
-
过期后采用什么策略进行删除?
- 定期删除:redis默认是每隔 100ms 就随机抽取⼀些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这⾥是随机抽取的。为什么要随机呢?你想⼀想假如 redis 存了⼏⼗万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
-
惰性删除:定期删除可能会导致很多过期 key 到了时间并没有被删除掉,所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存⾥,除⾮你的系统去查⼀下那个 key,才会被redis给删除掉。
- 内存淘汰策略 :如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没⾛惰性删除,此时会怎么样?如果大量过期key堆积在内存⾥,导致redis内存块耗尽了。所以有内存淘汰策略。
- volatile-lru:从已设置过期时间的数据中挑选最近最少使⽤的数据淘汰
-
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
-
volatile-random:从已设置过期时间的数据中任意选择数据淘汰
-
allkeys-lru:当内存不⾜以容纳新写⼊数据时,在键空间中,移除最近最少使⽤的key(这个是最常⽤的)
-
allkeys-random:从数据集中任意选择数据淘汰
-
no-eviction:禁⽌驱逐数据,也就是说当内存不⾜以容纳新写⼊数据时,新写⼊操作会报错。这个应该没⼈使⽤吧!
-
4. Redis报内存不足怎么处理?
- 增加 Redis 可用内存:
- 修改件
redis.conf
的maxmemory
参数; - 使用分布式集群,提高存储量;
- 修改件
- 设置缓存淘汰策略:提高内存的使用效率;
5. 【重点】缓存和数据库谁先更新呢? (保持缓存和数据库一致性)
-
对于读(查询)操作
一般我们对读操作的时候有这么一个固定的套路:
- 如果我们的数据在缓存里边有,那么就直接取缓存的;
- 如果缓存里没有我们想要的数据,我们会先去查询数据库,然后将数据库查出来的数据写到缓存中;
- 最后将数据返回给请求。
不用更新(写)数据库,只用更新(写)缓存。
-
对于写操作导致双写问题
写操作会更新数据库,那么缓存也要进行更新,此时会发生数据库和缓存不一致的问题。
键的过期时间:能保证缓存和数据库的数据最终是一致的。
因为只要缓存数据过期了,就会被删除。随后读的时候,因为缓存里没有,就可以查数据库的数据,然后将数据库查出来的数据重新写入到缓存中。
除了设置过期时间,我们还需要做更多的措施来尽量避免数据库与缓存处于不一致的情况发生。直接看结论:
不考虑更新缓存而是直接删除缓存,因为更新逻辑较为复杂涉及到其它数据,更新cache消耗会比较大。
- 先删除缓存,再更新数据库
- 在高并发下可能会导致数据长时间不一致
- 采用异步更新缓存的策略,不会导致数据不一致,但在数据库更新完成之前,都需要到数据库层面去读取数据,读的效率不太好——保证了数据的一致性,适用于对一致性要求高的业务
- 先更新数据库,再删除缓存 (Cache Aside Pattern 设计模式)
- 在高并发下不会导致数据长时间不一致
- 在更新数据库期间,cache中的旧数据会被读取,可能会有一段时间的数据不一致,但读的效率很好。——保证了数据读取的效率,如果业务对一致性要求不是很高,这种方案最合适
- 先删除缓存,再更新数据库
-
先删除缓存,再更新数据库
⚠️ 只有读才会更新缓存!!
-
正常情况
- A线程进行写操作,先淘汰缓存,再更新数据库
- B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取更新后的新数据 ,并更新缓存
-
高并发异常
- A线程进行写操作,先淘汰缓存,但由于网络原因等未及时更新数据库
- B线程读取缓存失败,去读取数据库的是旧值 ,并将旧数据放入缓存
- A线程再更新数据库成功(同步情况下写操作不更新redis而读操作更新redis),此时缓存(旧)和数据库(新)不一致
而且没有设置键过期,会保持很长时间的数据不一致。
-
解决方案
- 异步更新缓存 :B线程读操作不更新缓存,而是由A线程写操作更新数据库成功后,通过binlog异步更新缓存
- 延时双删: A线程休眠M秒(确保事务都已提交),再更新数据库成功后,再次删除缓存。其它线程进行读操作时,缓存中无数据,从数据库中读取的是更新后的新数据,又再次一致了。
-
-
先更新数据库,再删除缓存
-
正常情况
- A线程进行写操作,先更新数据库,再删除缓存
- B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取更新后的新数据 ,并更新缓存
-
高并发异常
-
A线程进行写操作,先更新数据库,但未来得及删除缓存
-
B线程进行读操作,读取缓存的旧数据(背错一次),此时数据不一致
-
A线程再删缓存
但其它线程进行读数据的时候更新缓存,更新缓存又一致了,不一致的时间很短。
但是还可能会考虑:3. A线程删除缓存失败 ,此后读取的一直都是旧数据了。
-
-
解决方案
- 消息队列进行删除补偿。如果Redis删除发现报错,将Redis的key作为消息发送到消息队列中,系统收到消息队列再次对Redis进行删除操作。
-
6.4 集群相关
1. Redis的同步机制了解是什么?
Redis主从复制可以根据是否是全量分为:全量同步和增量同步。
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。
-
全量同步
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份:
1)从服务器连接主服务器,发送
SYNC
命令;
2)主服务器接收到SYNC命名后,开始执行BGSAVE
命令(1)生成RDB文件 (2)并使用缓冲区记录此后执行的所有写命令;
3)主服务器BGSAVE
执行完后,向所有从服务器发送RDB快照文件,并在发送期间继续记录被执行的写命令;
4)从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
5)主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令; -
增量同步
Slave初始化后开始正常工作时:主服务器发生的写操作同步到从服务器的过程。
- 主服务器每执行一个写命令就会向从服务器发送相同的写命令;
- 从服务器接收并执行收到的写命令。
2.【新补充】 Redis集群架构模式有哪几种?集群的原理是什么?
-
1. 单机模式
QPS(每秒查询速度)大约在几万左右。
安装一个 Redis,启动起来,业务调用即可。
- 优点: 部署简单;成本低;高性能
- 缺点: 单节点宕机风险 ; 单机高性能受限于 CPU 的处理能力
-
2. 主从复制
Redis 的复制(Replication)功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品。
- 被复制的服务器为主服务器(Master),而通过复制创建出来的复制品则为从服务器(Slave)。
主要优缺点:
- 优点: Master/Slave 角色方便水平扩展,降低 Master 读压力,转交给 Slave 节点;
- 缺点: 可靠性保证不是很好,主节点故障便无法提供写入服务;没有解决主节点写的压力 ;主节点宕机,需要人为干预。
-
3. 哨兵模式
Redis 2.8版本后引入了哨兵的概念。
主从模式中,当主节点宕机之后,从节点是可以作为主节点顶上来继续提供服务,但是需要修改应用方的主节点地址,还需要命令所有从节点去复制新的主节点数据,整个过程需要人工干预。
为此,引入了哨兵(Sentinel)这个概念,在主从复制的基础上,哨兵实现了自动化故障恢复。哨兵模式由两部分组成,哨兵节点和数据节点:
-
哨兵节点:哨兵节点是特殊的 Redis 节点,不存储数据;
-
数据节点:主节点和从节点都是数据节点。
哨兵工作原理:
- 每个 Sentinel 以每秒一次的频率向它所知的 Master,Slave 以及其他 Sentinel 节点发送一个
PING
命令; - 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过配置文件
own-after-milliseconds
选项所指定的值,则这个实例会被 Sentinel 标记为主观下线; - 如果一个 Master 被标记为主观下线,那么正在监视这个 Master 的所有 Sentinel 要以每秒一次的频率确认 Master 是否真的进入主观下线状态;
- 当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认 Master 的确进入了主观下线状态,则 Master 会被标记为客观下线;
- 如果 Master 处于 ODOWN 状态,则投票自动选出新的主节点;将剩余的从节点指向新的主节点继续进行数据复制;
- 若没有足够数量的 Sentinel 同意 Master 已经下线,Master 的客观下线状态就会被移除。若 Master 重新向 Sentinel 的 PING 命令返回有效回复,Master 的主观下线状态就会被移除。
哨兵模式优缺点:
- 优点:(1)主从自动切换,更加健壮
- 缺点: (1)主从切换需要时间还是会丢失数据;(2)没有解决主节点写压力 (3)动态扩容复杂
-
-
4. 集群模式
Redis 3.0 版本引入了Redis Cluster集群模式。
- 如上图所示:该集群中包含 6 个 Redis 节点,3 主 3 从,分别为 M1,M2,M3,S1,S2,S3。除了主从 Redis 节点之间进行数据复制外,所有 Redis 节点之间采用 Gossip 协议进行通信,交换维护节点元数据信息
Redis Cluster 采用无中心结构,每个节点都可以保存数据和整个集群状态,每个节点都和其他所有节点连接。
- Cluster 一般由多个节点组成,节点数量至少为 6 个才能保证组成完整高可用的集群,其中3个为主节点,3个为从节点;
4.1 Redis 集群分片概念
单机、主从、哨兵的模式数据都是存储在一个master节点上,其他节点进行数据的复制。
集群模式就是把数据进行分片存储,当一个分片数据达到上限的时候,还可以分成多个分片。
Redis Cluster 采用虚拟哈希槽分区,所有的键根据哈希函数映射到 0 ~ 16383 整数槽内,计算公式:
HASH_SLOT = CRC16(key) % 16384
每一个主节点负责维护一部分槽以及槽所映射的键值数据。
-
举例说明:
有 3 个节点的集群环境如下
- 节点 A 哈希槽范围为 0 ~ 5500;
- 节点 B 哈希槽范围为 5501 ~ 11000;
- 节点 C 哈希槽范围为 11001 ~ 16383。
增加数据: (1)根据上述公式计算新增的key存储 ,映射到相应节点(假设为B)
增加节点: (1)从各个节点拿出一部分哈希槽分配到新增的D节点上即可
删除节点: (1)删除A节点,只需将A节点的哈希槽移动到其它节点接口
4.2 Reids集群的主从模式
Redis Cluster 为了保证数据的高可用性,加入了主从模式,一个主节点对应一个或多个从节点,主节点提供数据存取,从节点复制主节点数据备份,当这个主节点挂掉后,就会通过这个主节点的从节点选取一个来充当主节点,从而保证集群的高可用。
4.3 优缺点总结
- 优点: (1)无中心结构 ,多节点存储数据;(2)节点动态删除、移动数据分布方便;(3)部分节点不可用,集群依旧可用(哈希槽 + 从节点备份并故障晋升主节点);
- 缺点: (1)异步复制,无法保证数据一致性(2)集群搭建复杂(3)
mget
,pipeline
等命令。它们需要把请求分散到多个节点执行、再聚合。节点越多,性能越低
3.说说 Redis哈希槽的概念?什么情况下会导致整个集群不可用?
Redis 没有使用哈希一致性算法,而是使用哈希槽。Redis 中的哈希槽一共有 16384 个,计算给定密钥的哈希槽,我们只需要对密钥的 CRC16 取摸 16384。
假设集群中有 A、B、C 三个集群节点,不存在复制模式下,每个集群的节点包含的哈希槽如下:
-
节点 A 包含从 0 到 5500 的哈希槽;
-
节点 B 包含从 5501 到 11000 的哈希槽;
-
节点 C 包含从 11001 到 16383 的哈希槽;
这时,如果节点 B 出现故障,整个集群就会出现缺少 5501 到 11000 的哈希槽范围而不可用。
4. Redis 常见性能问题和解决方案有哪些?
Redis 常见性能问题和解决方案如下:
- Master不做持久化, Slave 做 AOF:Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件;如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次;
- 同局域网:为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网内;
- 尽量避免在压力很大的主库上增加从库;
- 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <-Slave3….;这样的结构方便解决单点故障问题,实现 Slave 对 Master 的替换。如果 Master 挂了,可以立刻启用 Slave1 做 Master,其他不变
6.5 Redis Key相关
1.假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?
我们可以使用 keys 命令和 scan 命令,但是会发现使用 scan 更好。
-
keys命令
虽然可以查询但不太推荐:
- 时间长且会导致线程阻塞: 时间长是因为O(N)遍历 ; 阻塞是因为Redis单线程,要等遍历完,这使得Redis要等keys执行完毕才能恢复生产(在生成环境中这是不被允许的)。
- 没有分页功能: 一次查找所有的结果
-
scan命令
推荐:
-
不会阻塞,但查找出的元素可能重复,需要客户端去重下
为什么不会阻塞?
因为 scan 是通过游标方式查询的 ,查询过程中会把游标返回给客户端,单次返回空值且游标不为 0,则说明遍历还没结束,客户端继续遍历查询。
-
2.如果有大量的 key 需要设置同一时间过期,一般需要注意什么?
如果有大量的 key 在同一时间过期,那么可能同一秒都从数据库获取数据,给数据库造成很大的压力,导致缓存雪崩。
- 解决方案: 最好给数据的过期时间加一个随机值,让过期时间更加分散
3.什么是 bigkey?会存在什么影响?
bigkey 是指键值占用内存空间非常大的 key。例如一个字符串 a 存储了 200M 的数据。
bigkey 的主要影响有:
-
网络阻塞:获取 bigkey 时,传输的数据量比较大,会增加带宽的压力;
-
超时阻塞:因为 bigkey 占用的空间比较大,所以操作起来效率会比较低,导致出现阻塞的可能性增加。
4. Redis如何解决 key冲突?
Redis 如果 key 相同,后一个 key 会覆盖前一个 key。
如果要解决 key 冲突,最好给 key 取好名区分开,可以按业务名和参数区分开取名,避免重复 key 导致的冲突。
5. 如何解决Redis的并发竞争Key问题 ?
多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同。
-
解决方案:分布式锁(zookeeper 和 Redis 都可以实现分布式锁)。
-
zookeeper分布式锁:(1)每个客户端对某个方法加锁时,在zookeeper上的 与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点;(2)判断是否获取锁的方式很简单,只需要判断有 序节点中序号最小的一个;(3)当释放锁的时候,只需将这个瞬时节点删除即可。
同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
-
6. Redis删除key的底层原理实现?
Redis在启动的时候,会注册两种事件:
- 时间事件: Redis处理后台操作的一类事件,比如客户端超时、删除过期key
- 文件事件: redis注册的回调函数是serverCron,在定时任务(惰性删除)回调函数中,通过调用databasesCron清理部分过期key
定时删除
对于每一个设置了过期时间的key都会创建一个定时器,一旦到达过期时间就立即删除:
- 缺点:占用了大量的CPU资源去处理过期的数据,会影响Redis的吞吐量和响应时间。
惰性删除
每次访问key的时候,都会调用expireIfNeeded
函数判断key是否过期,如果是,清理key:
- 缺点:大量的过期key没有被再次访问,因此不会被清除,导致占用了大量的内存。
定期删除
每隔一段时间,扫描Redis中过期key字典,并清除部分过期的key:
- 缺点:折中方案
Redis单线程清理key的时机
Redis是以单线程运行的,在清理key是不能占用过多的时间和CPU,需要在尽量不影响正常的服务情况下,进行过期key的清理。
-
以随机删除为例
-
server.hz配置了serverCron任务的执行周期,默认是10,即CPU空闲时每秒执行十次;
-
每次清理过期key的时间不能超过CPU时间的25% ;
-
如果是快速清理模式(在beforeSleep函数调用),则一次清理的最大时间是1ms;
-
依次遍历所有的DB;
-
从db的过期列表中随机取20个key,判断是否过期,如果过期,则清理;
-
如果有5个以上的key过期,则重复步骤5,否则继续处理下一个db ;
-
在清理过程中,如果达到CPU的25%时间,退出清理过程。
-
-
Redis4.0使用BIO处理
Redis4.0以前,删除指令是del,del会直接释放对象的内存,但是,如果删除的key是一个非常大的对象,那么删除操作就会导致单线程卡顿,Redis的响应就慢了。
- 在Redis4.0版本引入了unlink指令,能对删除操作进行“懒”处理,将删除操作丢给后台线程,由后台线程BIO来异步回收内存。
内存淘汰策略
Redis的内存淘汰策略,是指内存达到maxmemory极限时,使用某种算法来决定清理掉哪些数据,以保证新数据的存入。