一、基本概念

Redis为什么这么快

  1. 基于内存
  2. 单线程,IO多路复用
  3. 高级数据结构(如 SDS、Hash以及跳表等)

缓存三大问题以及解决方案?

缓存穿透(查询数据不存在)

解决办法

  1. 缓存空值
  2. 布隆过滤器做Key值校验

缓存击穿(缓存过期,伴随大量对该 key 的请求)

解决办法

  1. 互斥锁
  2. 热点数据永不过期
  3. 熔断降级

缓存雪崩(同一时间大批量的 key 过期)

解决办法

  1. 随机分散过期时间
  2. 热点数据永不过期

一致性解决办法

强一致性:串行化

弱一致性:延时双删

保证redis的高并发

主从加集群,读写分离

保证原子性

  1. 使用 incr、decr、setnx 等原子操作
  2. 使用锁
  3. 使用lua脚本

Redis 是如何实现字典的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
typedef struct dictEntry {
void *key; //键
union {
void *val; //值
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; //指向下一个节点,形成链表
} dictEntry;
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;

}dictht

typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];// 字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用。
long rehashidx; /*记录了 rehash 目前的进度 rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;

渐进式Hash

扩容

满足任一条件

  • 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1
  • 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5

缩容

  • 正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,则不进行缩容
  • 当哈希表的负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作。

步骤:

  1. 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表
  2. 字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始
  3. rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一
  4. redis周期函数中,如果发现有字典正在进行渐进式rehash操作,则会花费1毫秒的时间,帮助一起进行渐进式rehash操作
  5. ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成

影响:

  1. 读取、删除、更新会同时在俩张表上进行,先ht[0],后ht[1]
  2. 新增只会在ht[1]
  3. 在rehash期间,同时有两个hash表在使用,会使得redis内存使用量瞬间突增,在Redis 满容状态下由于Rehash会导致大量Key驱逐

拓展:redis的周期函数有哪些

Zset 为何不使用红黑树等平衡树?

  1. 跳跃表范围查询比平衡树操作简单
  2. 平衡树的删除和插入需要对子树进行相应的调整,而跳表只需要修改相邻的节点即可
  3. 跳表和平衡树的查询操作都是O(logN)的时间复杂度

什么是 RedisObject?

也就是我们常说的五种数据结构:字符串对象、列表对象、哈希对象、集合对象和有序集合对象等
这样做有两个好处
1)通过不同类型的对象,Redis 可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行该的命令。
2)可以针对不同的使用场景,为对象设置不同的实现,从而优化内存或查询速度。

Redis过期策略

采用惰性+定期删除策略,memcached只用了惰性删除

惰性删除

  1. 进行get或setnx等操作时,先检查key是否过期,过期则删除

定期删除

在配置文件中根据server.hz来配置,默认10,即每秒10次

  1. 遍历每个数据库,检查指定个key

持久化文件对过期策略的处理?

RDB:

  1. 持久化时检查是否过期,过期不持久化
  2. 恢复时同理判断

AOF

  1. 当key过期后,还没有被删除,此时进行执行持久化操作(该key是不会进入aof文件的,因为没有发生修改命令)
  2. 当key过期后,在发生删除操作时,程序会向aof文件追加一条del命令(在将来的以aof文件恢复数据的时候该过期的键就会被删掉)
  3. 重写时,会先判断key是否过期,已过期的key不会重写到aof文件

Redis 有哪些内存淘汰机制?

volatile

  1. volatile-lru:设置过期时间且最近最少使用淘汰
  2. volatile-ttl:设置过期时间且将要过期的淘汰
  3. volatile-random:设置过期时间的随机淘汰
  4. volatile-lfu:设置过期时间的且使用频率最低淘汰

allKeys

  1. allkeys-lru:最近最少使用
  2. allkeys-lfu: 使用频率最低
  3. allkeys-random: 随机

no-enviction

禁止驱逐数据,默认策略

Redis 有哪些持久化机制?

RDB

在指定的时间间隔内将内存中的数据集快照写入磁盘,默认的文件名为 dump.rdb,支持 同步(save 命令)、后台异步(bgsave)以及自动配置三种方式触发
优点:

  1. 文件紧凑,全量备份,非常适合用于进行备份和灾难恢复
  2. 支持异步
  3. 恢复大数据集时比AOF快

缺点:

  1. 在快照持久化期间修改的数据不会被保存,可能丢失数据

AOF

将每一个收到的写命令追加到文件中,三种触发方式:1)每修改同步 always 2)每秒同步 everysec 3)不同no:从不同步
优点

  1. 更好的保护数据不丢失
  2. 通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复
  3. 没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损
  4. 不会影响客户端的读写

缺点:

  1. AOF 日志文件通常比 RDB 数据快照文件更大
  2. 支持的写 QPS 会比RDB支持的写 QPS 低

结合使用

Redis从4.0版本开始引入RDB-AOF混合持久化模式,这种模式是基于AOF持久化模式构建而来,打开了服务器的AOF持久化功能,并且将aof-use-rdb-preamble 选项的值设置成了yes。
原理:
Redis服务器 在执行 AOF重写操作时,就会像执行BGSAVE命令那样,根据数据库当前的状态 生成出 相应的RDB数据,并将这些数据 写入 新建的AOF文件中,至于那些 在AOF重写开始之后 执行的Redis命令,则会继续以协议文本的方式 追加到 新AOF文件的末尾,即已有的RDB数据的后面

redis单线程IO多路复用

  1. IO 线程要么同时在读 socket,要么同时在写,不会同时读或写
  2. IO 线程只负责读写 socket 解析命令,不负责命令处理。

集群

Redis主从复制

全量同步和增量同步

全量同步

  1. slave发送SYNC命令
  2. master收到并执行 BGSAVE 命令生产 RDB 文件,并使用缓冲区记录此后执行的所有写命令;
  3. master 执行完 BGSAVE 后,向所有的 slave 发送快照文件,并在发送过程中继续记录执行的写命令;
  4. slave 收到快照后,丢弃所有的旧数据,载入收到的数据;
  5. master 快照发送完成后就会开始向 slave 发送缓冲区的写命令;
  6. slave 完成对快照的载入,并开始接受命令请求,执行来自 master 缓冲区的写命令;
  7. slave 完成上面的数据初始化后就可以开始接受用户的读请求了。
    也可以通过无盘复制来达到目的,由master直接开启一个socket将rdb文件发送给slave服务器

部分同步

从Redis 2.8开始,如果遭遇连接断开,重新连接之后可以从中断处继续进行复制,而不必重新同步。

它的工作原理是这样:
主服务器端为复制流维护一个内存缓冲区(in-memory backlog)。主从服务器都维护一个复制偏移量(replication offset)和master run id ,
当连接断开时,从服务器会重新连接上主服务器,然后请求继续复制,假如主从服务器的两个master run id相同,并且指定的偏移量在内存缓冲
区中还有效,复制就会从上次中断的点开始继续。如果其中一个条件不满足,就会进行完全重新同步(在2.8版本之前就是直接进行完全重新同步)。
因为主运行id不保存在磁盘中,如果从服务器重启了的话就只能进行完全同步了。
部分重新同步这个新特性内部使用PSYNC命令,旧的实现中使用SYNC命令。Redis2.8版本可以检测出它所连接的服务器是否支持PSYNC命令,不支持的
话使用SYNC命令。

增量同步

  1. 将master接到的写命令也发给slave

限制有N个以上从服务器才允许写入

从Redis 2.8版本开始,可以配置主服务器连接N个以上从服务器才允许对主服务器进行写操作。但是,因为Redis使用的是异步主从复制,
没办法确保从服务器确实收到了要写入的数据,所以还是有一定的数据丢失的可能性。

这一特性的工作原理如下:
1)从服务器每秒钟ping一次主服务器,确认处理的复制流数量。
2)主服务器记住每个从服务器最近一次ping的时间。
3)用户可以配置最少要有N个服务器有小于M秒的确认延迟。
4)如果有N个以上从服务器,并且确认延迟小于M秒,主服务器接受写操作。

还可以把这看做是CAP原则(一致性,可用性,分区容错性)不严格的一致性实现,虽然不能百分百确保一致性,但至少保证了丢失的数据不会超过M秒内的数据量。

如果条件不满足,主服务器会拒绝写操作并返回一个错误。
1)min-slaves-to-write(最小从服务器数)
2)min-slaves-max-lag(从服务器最大确认延迟)

一致性Hash

hash环,key 通过 hash 计算之后得到在 hash 环中的位置,然后顺时针方向找到第一个节点,这个节点就是存放 key 的节点。为了解决扩容和宕机问题。
因为普通Hash扩容或者宕机时,会影响每个节点上的Key,导致Key大面积失效,而用一致性Hash只会影响相邻的节点
同时还可以通过虚拟节点来解决数据倾斜的问题:就是在节点稀疏的 hash 环上对物理节点虚拟出一部分虚拟节点,key 会打到虚拟节点上面,而虚拟节点上的 key 实际也是映射到物理节点上的,这样就避免了数据倾斜导致单节点压力过大导致节点雪崩的问题。

Hash槽

槽的长度为16384,Redis 集群没有使用一致性hash, 而是引入了哈希槽slots的概念,jedis客户端jar包就是实现了一致性hash算法(客户端模式),或者在redis集群前面加上一层前置代理如Twemproxy也实现了hash一致性算法(代理模式)

在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 * 8 (8 bit) * 1024(1k) = 16K),也就是说使用2k的空间创建了16k的槽数。 虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 * 8 (8 bit) *1024(1k)=65K),也就是说需要需要8k的心跳包,作者认为这样做不太值得;并且一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择。

大Key优化

热key优化

请求到的分片过于集中,超过单台 Server 的性能极限
解决办法:
1)服务端缓存:即将热点数据缓存至服务端的内存中;
2)备份热点Key:即将热点Key+随机数,随机分配至 Redis 其它节点中

如何发现:

  1. 凭借业务经验,进行预估哪些是热key
  2. 在客户端进行收集
  3. 在Proxy层做收集
  4. 用redis自带命令:monitor命令和-hotkeys启动参数