在线测试Redis

官方命令查询

Redis读的速度是10万次/s,写的速度是8万次/s。

所有操作都是原子性。单个操作是原子性的,多个操作也支持事务。

CAP定理

传统数据库遵循ACID规则,Nosql(Not Only SQL)关系型数据库遵循CAP规则,一般为分布式。

一致性(Consistence)、可用性(Availability)、分区容错(Partition Tolerance),分布式系统最多满足三个中的两个。

  • 舍弃P(选择C/A):单点的传统关系型数据库 DBMS(MySQL/Oracle),但如果采用集群就必须考虑P了;
  • 舍弃A(选择C/P):是分布式系统要保证P,而且保证一致性,如 ZooKeeper / Redis / MongoDB / HBase;
  • 舍弃C(选择A/P):是分布式系统要保证P,而且保证可用性,如 CoachDB / Cassandra / DynamoDB。

五种数据类型

键的类型只能为字符串,值常见有五种数据类型:字符串、散列表、列表、集合、有序集合。

redis 源码文件 src/server.h 中对于5种结构的定义:

1
2
3
4
5
6
/* The actual Redis Object */
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */

Redis 对象由 redisObject 结构体表示,从 src/server.h可以看到该结构的定义如下:

1
2
3
4
5
6
7
typedef struct redisObject {
unsigned type:4; //对象类型
unsigned encoding:4; //对象编码方式
unsigned lru:LRU_BITS; //过期设置
int refcount; //引用计数
void *ptr; //内存指针
} robj;

redisObject 明确了对象类型、对象编码方式、过期设置、引用计数、内存指针等,从而完整表示一个 key-value 键值对。

由于 Redis 是基于内存的,在实现这5种数据类型时在底层创建了多种数据结构,在对象底层选择采用哪种结构来实现,需要根据对象大小以及单个元素大小来进行确定,从而提高空间使用率和效率。

string字符串

底层原理

适用于简单key-value存储、setnx key value实现分布式锁、计数器(原子性)、分布式全局唯一ID。

底层C 语言中 Stringchar[] 数组表示,源码中用 SDS (simple dynamic string)封装 char[] ,这是是 Redis 存储的最小单元一个SDS最大可以存储512M信息

图片

  1. 字符串长度

    用一个 len 字段记录当前字符串的长度。想要获取长度只需要获取 len 字段即可。

    1
    2
    3
    4
    5
    6
    7
    //有四种sdshdr: sdshdr8,sdshdr16,sdshdr32,sdshdr64
    struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
    };
  2. 空间预分配:

    SDS 修改及空间扩充时,除了分配所必须的空间外,还会额外分配未使用的空间。

    具体分配规则是这样的:SDS 修改后,len 长度小于 1M,那么将会额外分配与 len 相同长度的未使用空间。如果修改后长度大于 1M,那么将分配1M的使用空间。但是有大小限制,redis 中的字符串最大值是 512m

    redis 中采用这种冗余的预处理机制来扩容主要是为了防止频繁的内存申请,内存的分配是很浪费时间的。

  3. 惰性空间释放

    SDS惰性释放空间的。SDS 缩短时,并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,不用新申请空间,减少了内存的分配。

  4. 二进制安全

    SDS 类型是二进制安全的。意思是 redisstring 可以包含任何数据。比如 jpg图片或者序列化对象 。二进制数据并不是规则的字符串格式,可能会包含一些特殊的字符,比如 '\0' 等。C 中字符串遇到 '\0' 会结束,那 '\0' 之后的数据就读取不上了。但在 SDS 中,是根据 len 长度来判断字符串结束的,二进制安全的问题就解决了。

操作命令

1
2
3
4
5
6
set string1 "hello" //赋值
get string1 //读值
strlen string1 // 获取字符串的长度
getrange string1 1 2 //获取子字符串,前后都是闭
setrange string1 1 ddddddd // 从index开始覆盖字符串
append string1 ttt // 追加字符串 append string xxx

当字符串是整数的时候,也可以将它当成计数器(原子性的)。

list列表

底层原理

list 的底层是一个双向链表,所以可以使用这个链表来实现 queue 或者 stack 的功能。查看源码底层 adlist.h 会发现底层就是个 双端链表,该链表最大长度为 2^32 - 1 ,即40亿左右。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* Node, List, and Iterator are the only data structures used currently. */

typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;

typedef struct listIter {
listNode *next;
int direction;
} listIter;

typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned long len;
} list;

操作命令

字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边)或者尾部(右边)。

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
# list头部添加字符串元素
lpush listname value [value2 ...]
# list尾部添加字符串元素
rpush listname value
# 移除list中count个 值为value的 字符串
#(count:正数从左往右搜,负数从右往左搜,0全部删除)
lrem listname count value
# 获取list长度
llen listname
# 获取指定下标index的值(从0开始)
lindex listname index
# 获取idx1到idx2的值,前后都闭,负数表示从右往左数
lrange listname idx1 idx2
# 获取全部值
lrange listname 0 -1
# 修改下标idx的值为value
lset listname idx value
# 插入数据,分布式下下标不能代表物理位置顺序
# 所以插入需指定将被插入值(value2)插在具体值(value1)前或后
linsert listname before value1 value2
linsert listname after value1 value2
# 只保留[idx1,idx2]区间内的值
ltrim listname idx1 idx2
# 移出并获取列表的最后一个元素
# 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
brpop list1 list2 timeout

常用命令组合:

  • lpush + lpop = stack 先进后出的栈
  • lpush + rpop = queue 先进先出的队列
  • lpush + ltrim = capped collection 有限集合
  • lpush + brpop = message queue 消息队列

set集合

集合里不会有重复元素

底层原理

底层使用了 intsethashtable两种数据结构存储的。intset可以理解为数组,hashtable就是普通的哈希表(key为set的值,value为null)。

使用 intset存储必须满足下面两个条件,否则使用 hashtable ,条件如下:

  • 集合对象保存的所有元素都是整数值
  • 集合对象保存的元素数量不超过512个

操作命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 添加元素
sadd setname value [value2 ...]
# 读取全部元素
smembers setname
# 读取set长度
scard setname
# 获取count个随机的元素,默认count为1
srandmember setname [count]
# 删除元素
srem setname value
# 删除并返回集合中的一个随机元素
spop setname
# 判断集合中是否存在元素
sismember setname value
# 取出set1和set2的交集并存到set3中
sinterstore set3 set1 set2

zset有序集合

底层原理

  • 有序,无重复元素
  • 每个元素都会关联一个 double 类型的分数,通过分数来为集合中的成员进行排序。
  • 成员是唯一的,但分数(score)却可以重复。
  • 复杂度
    • 添加和删除,需要修改skiplist,O(log(n))
    • 查找,使用hash,O(1) 
    • 其他的range操作,一般为O(log(n))
    • 如果是小于64的时候,因为是采用了ziplist的设计,其时间复杂度为O(n)
  • 元素小于 128 并且元素长度小于 64Byte 时才会选择ziplist实现,一般使用skiplist跳表实现。

操作命令

1
2
3
4
5
6
7
8
# 添加元素,并关联分数
zadd zsetname score value
# 显示分数范围内成员
zrangebyscore zsetname scoremin scoremax [withscores]
# 显示全部成员
zrange zsetname 0 -1 [withscores]
# 删除元素
zrem zsetname value

hash哈希表

底层原理

哈希函数 + 数组 + 链表

hash非常适用于将一些相关的数据存储在一起,比如用户的购物车。

  • dictEntry

    真正的 hash 数据节点,包括 keyvaluenext 节点:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    typedef struct dictEntry {
    void *key;
    union {
    void *val;
    uint64_t u64;
    int64_t s64;
    double d;
    } v;
    struct dictEntry *next;
    } dictEntry;
  • dictht

    1
    2
    3
    4
    5
    6
    typedef struct dictht {
    dictEntry **table; /*指针数组,用于存储键值对*/
    unsigned long size; /*table数组的大小*/
    unsigned long sizemask; /*掩码 = size - 1 */
    unsigned long used; /*table数组已存元素个数,包含next单链表的数据*/
    } dictht;
  • dict

    1
    2
    3
    4
    5
    6
    7
    typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2]; //两个 Hash 表
    long rehashidx; /* 如果为-1说明当前没有扩容,如果 不为 -1 则记录扩容位置。 */
    unsigned long iterators; /* number of iterators currently running */
    } dict;

渐进式扩容

为什么 dictht ht[2] 是两个呢?目的是在扩容的同时不影响前端的CRUD,慢慢的把数据从 ht[0] 转移到 ht[1] 中,同时 rehashindex来记录转移的情况,当全部转移完成,将 ht[1] 改成 ht[0] 使用。

rehashidx = -1 说明当前没有扩容,rehashidx != -1 则表示扩容到数组中的第几个了。如果当前内存不够,或者操作系统繁忙,扩容的过程可以随时停止。

停止之后如果对该对象进行操作,那是什么样子的呢?

  1. 如果是新增,则直接新增到第二个数组,因为如果新增到第一个数组,以后还是要移过来,没必要浪费时间
  2. 如果是删除,更新,查询,则先查找第一个数组,如果没找到,则再查询第二个数组。

扩容之后的数组大小为大于 used*22的n次方的最小值,跟 Hashmap 类似。然后挨个遍历数组同时调整 rehashidx 的值,对每个 dictEntry[i] 再挨个遍历链表将数据 Hash 后重新映射到 dictht[1] 里面。并且 dictht[0].usedictht[1].use 是动态变化的。

为了避免数据迁移带来的巨大损耗,redis是新旧同时保留,然后在后台使用一个定时的任务,以及hash读写指令,将数据逐步转移到新的数据结构中。

操作命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 增加一个元素
hset mapname key value
# 增加多个元素
hmset mapnae key value key2 value2 ...
# 根据key获取value
hget mapname key
# 获取多个key的value
hmget mapname key key2 key3
# 获取全部k-v
hgetall mapname
# 获取全部key
hkeys mapname
# 获取全部value
hvals mapname
# 删除元素
hdel mapname key key key
# 判断元素是否存在
hexists mapname key

Bitmap

底层是基于字符串类型实现

想象成一个以比特位为单位的数组,数组的每个单元只能存储 01 ,数组的下标在 Bitmaps 中叫做偏移量 offsetBitMapoffset 值上限 2^32 - 1(42亿)。

应用例子:

  • 用户签到

key = 年份:用户id

offset = (今天是一年中的第几天) % (今年的天数)

  • 统计活跃用户

key = 日期

offset = 用户 id

不同offset设置为0 1

布隆过滤器

不存在的一定不存在,存在的不一定存在

原理:

当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点(有效降低冲突概率),把它们置为1。检索时,我们只要看看这些点是不是都是1就知道集合中有没有它了:如果这些点有任何一个为0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。

HyperLogLog

待补充

Redis持久化

持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。

Redis 提供了两种持久化方式:RDB (默认)和 AOF

RDB

半持久化/冷备

在指定的时间间隔内将内存中的数据集快照写入磁盘

实际操作过程是 fork 一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。(半持久化模式,更适合做冷备)

  • 优点:
    • 压缩后的二进制文件,适用于备份、全量复制,用于灾难恢复加载 RDB 恢复数据远快于 AOF 方式,适合大规模的数据恢复。
    • 如果业务对数据完整性和一致性要求不高,RDB 是很好的选择。数据恢复比 AOF。因为 RDB 是数据的内存映射,直接载入到内存,而 AOF 是命令,需要逐条执行。
    • 如果创建 RDB 文件时出现了错误,Redis不会将它用于替换原来的文件,所以出错时不会影响到之前保存的版本。
  • 缺点:
    • 数据的完整性和一致性不高RDB 是周期间隔性的快照文件,RDB 可能在最后一次备份时宕机了。
    • 备份时占用内存,因为 Redis 在备份时会独立 fork 一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件。所以要考虑到大概两倍的数据膨胀性。

功能核心函数 rdbSave(生成RDB文件)和 rdbLoad(从文件加载内存)两个函数。

手动触发以及CopyOnWrite

  1. SAVE 直接调用 rdbSave阻塞Redis 主进程,导致无法提供服务,直到 RDB 文件创建完毕。
  2. BGSAVEfork 出一个子进程,子进程负责调用 rdbSave ,在保存完成后向主进程发送信号告知完成。在 BGSAVE 执行期间仍可以继续处理客户端的请求
  3. Copy On Write 机制,备份的是开始那个时刻内存中的数据,只复制被修改内存页数据,不是全部内存数据。Copy On Write 时如果父子进程大量写操作会导致分页错误。

AOF

全持久化模式/热备

AOF就是将Redis服务端执行过的每一条命令都保存到一个文件,这样当Redis重启时只要按顺序回放这些命令就会恢复到原始状态。

每当执行服务器(定时)任务或者函数时,flushAppendOnlyFile 函数都会被调用,把每一次数据变化都写入到一个 append only file(aof) 里面。 由于该机制对日志文件的写入操作采用的是 append 模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。因为这个模式是 只追加 的方式,所以没有任何磁盘寻址的开销,所以很快,有点像 Mysql 中的 binlog

  • 优点:
    • AOF 是一秒一次去通过一个后台的线程 fsync 操作,数据丢失不用怕。(适合热备)
  • 缺点:
    • 对于相同数量的数据集而言,AOF 文件通常要大于 RDB 文件。恢复速度比RDB慢
    • 根据同步策略的不同,AOF 在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的。

AOF同步方式配置:

1
2
3
4
/* Append only defines */
#define AOF_FSYNC_NO 0 //让操作系统决定何时进行同步。性能最好
#define AOF_FSYNC_ALWAYS 1 //每秒钟同步一次,该策略为AOF的缺省策略,显示地将多个写命令同步到硬盘
#define AOF_FSYNC_EVERYSEC 2 //每次有数据修改发生时都会写入AOF文件。性能最差

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

AOF流程

  1. 命令的实时写入和磁盘同步。

    先写到buf中再同步到磁盘。

    Redis 在处理一条命令时,并不立即调用 writeAOF 文件,只是将数据写入到 AOF buffer(server.aof_buf)中。调用 write 和命令处理是分开的,Redis 只在每次进入 epoll_wait 之前做 write 操作。具体来说,写入AOF文件的步骤是在 void flushAppendOnlyFile(int force) 中。然后再根据 fsync 的频率 config 条件,调用 fsyncfdatasync 函数,将 AOF 文件同步到磁盘中。

  2. 重写

    aof 文件的重写,目的是为了减少 AOF 文件的大小,可以自动触发或者手动触发(BGREWRITEAOF),是 fork 出子进程然后在background进行操作,期间 Redis 服务仍可用。

    通过该功能,Redis服务器可以创建一个新的AOF文件替代现有的AOF文件,新旧两个AOF文件所保存的数据库状态完全相同,但新AOF文件不会包含任何浪费空间的冗余命令,所以新AOF会比旧AOF文件体积小很多。主要思想是,从数据库中读取出键现在的值,然后用一条命令去记录键值对,替代之前记录这个键值对的多条命令,这个就是AOF重写功能的实现原理。

    • 在重写期间,由于主进程依然在响应命令,为了保证最终备份的完整性;为了解决数据不一致的问题,Redis 服务器设置了一个aof_rewrite_buf,这个缓冲区在服务器创建子进程的时候开始使用,当 redis 主进程响应了一个操作的时候,它会同时将命令发送到 AOF 缓存区 aof_bufAOF 重写缓存区aof_rewrite_buf。从而防止新写的 file 丢失数据。
    • 当子进程完成 AOF rewrite 的工作后,会向父进程发送一个信号,父进程会调用一个信号处理函数,然后:
      • aof_rewrite_buf中的所有内容写到新的 AOF 的临时文件中,这时 AOF 文件所保存的数据库状态与当前数据库状态一致。
      • 对新的 AOF 文件进行改名,原子地覆盖现有 AOF 文件,完成新旧两个 AOF 文件的替换。
      • 重写是直接把 当前内存的数据生成对应命令,并不需要读取老的AOF文件进行分析、命令合并。
      • 无论是 RDB 还是 AOF 都是先写入一个临时文件,然后通过rename完成文件的替换工作。

关于 Fork 的建议:

1、降低fork的频率,比如可以手动来触发RDB生成快照、与AOF重写;

2、控制Redis最大使用内存,防止fork耗时过长;

3、配置牛逼点,合理配置Linux的内存分配策略,避免因为物理内存不足导致fork失败。

4、Redis在执行 BGSAVEBGREWRITEAOF命令时,哈希表的负载因子>=5,而未执行这两个命令时>=1。目的是尽量减少写操作,避免不必要的内存写入操作。

5、哈希表的扩展因子:哈希表已保存节点数量 / 哈希表大小。因子决定了是否扩展哈希表。

恢复

启动时会先检查 AOF (数据更完整)文件是否存在,如果不存在就尝试加载 RDB

单独用 RDB 会丢失很多数据。单独用 AOF ,数据恢复没 RDB 来的快,所以出现问题了第一时间用 RDB 恢复,然后 AOF 做数据补全才是王道。

存储结构

持久化存储的内容是 redis 通讯协议( RESP(Redis Serialization Protocol) )格式的命令文本存储。RESPredis 客户端和服务端之前使用的一种基于TCP的应用层协议通讯协议

RESP 的特点:实现简单、快速解析、可读性好

协议如下:

  • 客户端以规定格式的形式发送命令给服务器;
  • 服务器在执行最后一条命令后,返回结果。
  1. 简单字符串 (Simple Strings), 以 “+”加号 开头

    格式: +字符串 \r\n 。字符串不能包含 CR或者 LF(不允许换行)

    1
    eg: "+OK\r\n"

    注意:为了发送二进制安全的字符串,一般推荐使用后面的 Bulk Strings 类型

  2. 错误( Errors), 以”-“减号 开头

    格式:- 错误前缀 错误信息 \r\n 。错误信息不能包含 CR或者 LF(不允许换行),Errors与Simple Strings很相似,不同的是Erros会被当作异常来看待

    1
    eg: "-Error unknow command 'foobar'\r\n"
  3. 整数型 (Integer), 以 “:” 冒号开头。

    格式 : 数字 \r\n

    1
    eg: ":1000\r\n"
  4. 大字符串类型 (Bulk Strings), 以 “$”美元符号开头,长度限制512M

    格式:$ 字符串的长度 \r\n 字符串 \r\n 字符串不能包含 CR或者 LF(不允许换行);

    1
    2
    3
    eg: **"$6\r\nfoobar\r\n"**    其中字符串为 foobar,而6就是foobar的字符长度
    "$0\r\n\r\n" 空字符串
    "$-1\r\n" null
  5. 数组类型 (Arrays),以 “*“星号开头。

    格式:* 数组元素个数 \r\n 其他所有类型 (结尾不需要\r\n)

    注意:只有元素个数后面的\r\n是属于该数组的,结尾的\r\n一般是元素的

    1
    2
    3
    4
    5
    eg: "*0\r\n"                                            空数组
    "*2\r\n2\r\nfoo\r\n2\r\nfoo\r\n3\r\nbar\r\n" 数组包含2个元素,分别是字符串foo和bar
    "*3\r\n:1\r\n:2\r\n:3\r\n" 数组包含3个整数:1、2、3
    "*5\r\n:1\r\n:2\r\n:3\r\n:4\r\n$6\r\nfoobar\r\n" 包含混合类型的数组
    "*-1\r\n" Null数组

总结

数据丢失分析

RDB保存的是一个时间点的快照,那么如果Redis出现了故障,丢失的就是从最后一次RDB执行的时间点到故障发生的时间间隔之内产生的数据。如果Redis数据量很大,QPS很高,那么执行一次RDB需要的时间会相应增加,发生故障时丢失的数据也会增多。

而AOF保存的是一条条命令,理论上可以做到发生故障时只丢失一条命令。但由于操作系统中执行写文件操作代价很大,Redis提供了配置参数,通过对安全性和性能的折中,我们可以设置不同的策略。

加载过程分析

RDB保存的是最终的数据,是一个最终状态,而AOF保存的是达到这个最终状态的过程。很明显,如果Redis有大量的修改操作,RDB中一个数据的最终态可能会需要大量的命令才能达到,这会造成AOF文件过大并且加载时速度过慢(Redis提供了一种AOF重写的策略来解决上述问题)

RDB只需要把相应数据加载到内存并生成相应的数据结构(有些结构如intset、ziplist,保存时直接按字符串保存,所以加载时速度会更快),而AOF文件的加载需要先创建一个伪客户端,然后把命令一条条发送给Redis服务端,服务端再完整执行一遍相应的命令。根据Redis作者做的测试,RDB10s~20s能加载1GB的文件,AOF的速度是RDB速度的一半(如果做了AOF重写会加快)

Redis架构模式/集群高可用

单机版

内存容量有限/处理能力有限/无法高可用。

主从复制

根据一个 Redis 服务器(master)来创建任意多个该服务器的复制品(slave)。网络正常,同步策略会让主从服务器数据一致。

持久化保证重启不会丢数据,主从复制则规避单点故障(某台服务器硬盘损坏)

主从复制具有高可用性且读写分离的特点, 会采用 增量同步全量同步 两种机制。

全量同步

  • 一般发生在Slave初始化阶段
  • 要将 Master 上的所有数据都复制一份

具体流程

  1. slave连接master,发送 psync 命令。
  2. master接收到 psync 命名后,开始执行 bgsave 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令。
  3. master发送快照文件到slave,并在发送期间使用缓冲区继续记录被执行的写命令。
  4. slave收到快照文件后丢弃所有旧数据,载入收到的快照
  5. master快照发送完毕后开始向slave发送缓冲区中的写命令
  6. slave完成对快照的载入,开始接收命令请求,并执行来自master缓冲区的写命令。(增量同步)

增量同步

  • 发生在slave初始化后开始正常工作时
  • slave重做master中进行的指令
  • 指令放在环形队列中(如果slave一直没起来,指令会被覆盖),因为内存有限

主从同步策略

  1. 主从刚刚连接的时候,进行全量同步;全量同步结束后,进行增量同步。有需要slave 在任何时候都可以发起全量同步。总体策略是:先尝试增量同步,如不成功,再进行全量同步。
  2. slave在同步master数据时候如果slave丢失连接不用怕,slave在重新连接之后 丢失重补
  3. 一般通过主从来实现读写分离,但是如果master挂掉了,为保证Redis的高可用,引入 Sentinel哨兵 进行master的选择。

总结

复制是高可用Redis的基础,哨兵和集群都是在复制基础上实现高可用的。

优点:

  • 数据多机备份
  • 读操作负载均衡
  • 简单的故障恢复

缺点:

  • 无法保证高可用
  • master写操作无法负载均衡

哨兵

哨兵(sentinel)在分布式系统中监控redis主从服务器,并在主服务器下线时自动进行故障转移。

哨兵集群节点一般至少3个或以上,且为奇数。出于投票选举考虑。

功能:监控、消息通知、自动故障迁移

缺点:仍为主从模式,切换需要时间,会丢数据,没有解决master写的压力。

原理:心跳机制+raft投票算法

心跳:每个 sentinel 会向其它 sentinel 、master 、slave 定时发送消息,以确认对方是否活着,如果发现对方在指定时间内未回应,则暂时认为对方宕机。

raft算法

可视化

原理

集群

代理型

直连型

redis 3.0之后,集群采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。

数据的分区

把整个数据集按照分区规则映射到多个节点

常见哈希分区规则:

  1. 节点取余:hash(key) % N
  2. 一致性哈希:一致性哈希环
  3. 虚拟槽哈希:CRC16[key] & 16383

redis使用虚拟槽分区,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中(0~16383),整数定义为槽(slot)。

  1. 采用去中心化的思想,它使用虚拟槽slot分区覆盖到所有节点上,取数据一样的流程,节点之间使用轻量协议通信 Gossip 来减少带宽占用,所以性能很高
  2. 自动实现负载均衡与高可用,自动实现failover并且支持动态扩展,官方已经玩到可以1000个节点,实现的复杂度低。
  3. 每个 master 也需要配置主从,并且内部也是采用哨兵模式,如果有半数节点发现某个异常节点会共同决定更改异常节点的状态。
  4. 如果集群中的 master 没有 slave 节点,则 master 挂掉后整个集群就会进入fail状态,因为集群的 slot 映射不完整。如果集群超过半数以上的master挂掉,集群都会进入fail状态
  5. 官方推荐 集群部署至少要3台以上的master节点

特点

  1. 无中心架构(不存在哪个节点影响性能瓶颈),少了 proxy 层。
  2. 多节点分布:数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
  3. 可扩展性,可线性扩展到 1000 个节点,节点可动态添加或删除。
  4. 高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做备份数据副本
  5. 实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave到 Master 的角色提升。

缺点

  1. 资源隔离性较差,容易出现相互影响的情况。
  2. 数据通过异步复制,不保证数据的强一致性

Redis限流

setnx

比如我们需要在10秒内限定20个请求,那么我们在 setnx 的时候可以设置过期时间 10 ,当请求的setnx数量达到 20 时候即达到了限流效果。

缺点:比如当统计 1-10秒的时候,无法统计2-11秒之内,如果需要统计 N 秒内的 M 个请求,那么我们的Redis中需要保持N个key等等问题。

zset

限流涉及的最主要的就是滑动窗口,上面也提到 1-10 怎么变成 2-11 。其实也就是起始值和末端值都各 +1 即可。我们可以将请求打造成一个zset数组。

value是请求的UUID,score是当前时间戳。zset有序集合会按照score(时间)排序,range方法可以获取2个时间戳内有多少请求。

缺点:zset数据结构会越来越大。

漏桶算法

把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。

令牌桶算法

细节流程大致:

  1. 所有的请求在处理之前都需要拿到一个可用的令牌才会被处理。
  2. 根据限流大小,设置按照一定的速率往桶里添加令牌。
  3. 设置桶最大可容纳值,当桶满时新添加的令牌就被丢弃或者拒绝。
  4. 请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除。
  5. 令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流。

面试问题

Redis热点key的问题

Redis的跳跃表

Redis为什么这么快