Redis 笔记

扫码查看

Redis 学习笔记

1. 认识Redis

1.1 启动Redis 的三种方式

1. 默认配置启动: redis-server/redis
2. 运行配置: redis-server加上要修改配置名和值(可以是多对),没有设置的配置将使用默认配置
    redis-server --configkey1 configvalue1 --configkey2 configvalue2...
    此种配置使用键值对的方式进行配置(适用于配置量较小的场景)
    eg: redis-server --port 6380
3. 配置文件启动: redis-server /opt/redis/redis.conf   这里将配置项存放在redis.conf文件中

1.2 Redis 重要的配置

port    端口
logfile 日志文件
dir    Redis 工作目录
daemonize   是否以守护进程的方式启动Redis

注意:

Redis目录下都会有一个redis.conf配置文件,里面就是Redis的默认配置,通常来讲我们会在一台机器上启动多个Redis,并且将配置集中管理在指定目录下,而且配置不是完全手写的,而是将redis.conf作为模板进行修改。

通过配置文件启动的方式提供了更大的灵活性,所以大部分生产环境下会使用这种方式启动Redis

1.3 Redis 命令行客户端

redis-cli 是Redis命令行客户端, redis-cli可以使用两种方式连接Redis服务器

1.3.1 交互模式

通过 redis-cli -h{host} -p{port}的方式链接到Redis服务

redis-cli -h 127.0.0.1 -p 6379

1.3.2 命令方式

使用redis-cli -h{host} -p{port} {command} 就可以直接得到命令的返回结果

redis-cli -h 127.0.0.1 -p 6379 get hello
"world"

这里需要注意:

  1. 如果没有-h参数,那么默认连接127.0.0.1;如果没有-p,那么默认6379端口,也就是说如果-h和-p都没写就是连接127.0.0.1:6379这个Redis实例。

1.4 停止Redis服务

redis-cli shutdown   // Redis内置命令停止Redis服务

注意:

1)Redis关闭的过程:断开与客户端的连接、持久化文件生成,是一种相对优雅的关闭方式。

2)除了可以通过shutdown命令关闭Redis服务以外,还可以通过kill进程号的方式关闭掉Redis,但是不要粗暴地使用kill-9强制杀死Redis服务,不但不会做持久化操作,还会造成缓冲区等资源不能被优雅关闭,极端情况会造成AOF和复制丢失数据的情况。

3)shutdown还有一个参数,代表是否在关闭Redis前,生成持久化文件:

redis-cli shutdown nosave|save  // 生成或不成成持久文件

2. API的理解和使用

2.1 预备

2.1.1 全局命令

keys *  //查看所有键
dbsize  // 返回键总数, dbsize命令在计算键总数时不会遍历所有键,而是直接获取Redis内置的键总数变量,所以它的时间复                    杂度为O(n),当Redis保存了大量键时,线上环境禁止使用
exists key // 判断键是否存在, 如果键存在则返回1,不存在则返回0:
del key [key .....]   // 删除键 , del 是一个通用命令,无论是什么数据结构类型,del命令都可以将其删除,同时del命令可以                   支持删除多个键: del a b c  多个键以空格隔开
expire key seconds  // 键过期, Redis支持对键添加过期时间,当超过过期时间后,会自动删除键。expire hello 10
ttl key // 返回键key 剩余的过期时间,有三种结果:1. 大于0的数,返回过期时间,-1, 键没设置过期时间,-2 键不存在
type key // 返回键的数据结构类型,如果键不存在则返回none

2.1.2 数据结构和内部编码

Redis 有5种基本数据类型:string(字符串),hash(哈希), list(列表), set(集合),zset(有序集合)

通过命令: object encoding key 可返回每种数据的编码内部实现

2.1.3 单线程架构

Redis使用了单线程架构和I/O多路复用模型来实现高性能的内存数据库服务

Redis的单线程架构,决定了,Redis同一时间只能执行同一条命令,所有到Redis的命令都会排队执行,不会出现并发问题

为什么Redis的单线程架构还会如此快?

1, 纯内存访问,Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,这是Redis达到每秒万级别访问的重要基础。
2, 非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间
3, 单线程避免了线程切换和竞态产生的消耗。// 通常来说,锁和线程切换是服务端的性能杀手

2.2 字符串

​ 字符串类型是Redis最基础的数据结构。首先键都是字符串类型,而且其他几种数据结构都是在字符串类型基础上构建的,所以字符串类型能为其他四种数据结构的学习奠定基础。如图2-7所示,字符串类型的值实际可以是字符串(简单的字符串、复杂的字符串(例如JSON、XML))、数字(整数、浮点数),甚至是二进制(图片、音频、视频),但是值最大不能超过512MB。

2.2.1 命令

一: 常用命令:

1) 设置键:

set key value  // set hello world

set命令有几个选项:

ex seconds:为键设置秒级过期时间。
px milliseconds:为键设置毫秒级过期时间。
nx:键必须不存在,才可以设置成功,用于添加。   // 如果键存在则会返回nil
xx:与nx相反,键必须存在,才可以设置成功,用于更新。 // 如果键不存在则返回nil
eg:
    set 3 4 ex 5
    set 4 5 px 5
    set 1 2 nx
    set 2 3 xx
    

除了set选项,Redis还提供了setex和setnx两个命令: 作用和 .xx .nx 相同

setnx hello redis  // 如果键存在则返回0
setxx hello redis  // 如果键不存在则返回0

2) 获取值

get key // 如果键不存在则返回nil
eg:
    get hello

3) 批量设置值

mset key1 value1 key2 value2 key3 value3 key4 value4
eg:
mset a 1 b 2 c 3 d 4

4) 批量获取值

mget key1 key2 key3 key4     // 如果有些键不存在则返回nil, 结果按照传入键的顺序返回
eg:
mget a b c d

注意:

学会使用批量操作,有助于提高业务处理效率,但是要注意的是每次批量操作所发送的命令数不是无节制的,如果数量过多可能造成Redis阻塞或者网络拥塞。

5) 计数

incr key // 对值为整数的值进行递增,如果值不是整数则返回错误,是整数则返回递增结果,如果键不存在则
                    把该键值设置为0同时进行递增,返回的结果为1,(此时将这对键值存入Redis)
incr命令用于对值做自增操作,返回结果分为三种情况:
·值不是整数,返回错误。
·值是整数,返回自增后的结果
·键不存在,按照值为0自增,返回结果为1

除了incr命令,Redis提供了decr(自减)、incrby(自增指定数字)、decrby(自减指定数字)incrbyfloat(自增浮点数):

很多存储系统和编程语言内部使用CAS机制实现计数功能,会有一定的CPU开销,但在Redis中完全不存在这个问题,因为Redis是单线程架构,任何命令到了Redis服务端都要顺序执行。

二: 不常用命令

1) 追加值

append key value    // append可以向字符串尾部追加值
eg:
set a 3
append a hello

2) 字符串长度

strlen key // 获取字符串长度
eg:
strlen a // 6
set b "世界你好"
strlen b // 12   因为一个汉字占用3个字符

3) 设置值并返回原值

getset key  // 获取原始值并重新设置值
getset a  "世界"  // 返回3hello

4) 设置指定位置的字符

setrange key offeset value
eg:
setrange a 0 w
get a // whello

5) 获取部分字符串

getrange key start end
eg:
getrange a 0 2 // whe

​ Redis 字符串操作的时间复杂度表:

2.2.2 内部编码

字符串类型的内部编码有三种:

int: 8字节的长整形

embstr: 小于等于39个字节的字符串

raw: 大于39个字节的字符串

Redis会根据当前值的类型和长度决定使用哪种内部编码实现。

2.2.3 典型使用场景

1) 缓存功能

图2-1是比较典型的缓存使用场景,其中Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。

开发提示:

与MySQL等关系型数据库不同的是,Redis没有命令空间,而且也没有对键名有强制要求(除了不能使用一些特殊字符)。但设计合理的键名,有利于防止键冲突和项目的可维护性,比较推荐的方式是使用“业务名:对象名:id:[属性]”作为键名(也可以不是分号)例如MySQL的数据库名为vs,用户表名为user,那么对应的键可以用"vs:user:1","vs:user:1:name"来表示,如果当前Redis只被一个业务使用,甚至可以去掉“vs:

2) 计数

​ 许多应用都会使用Redis作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。

​ 实际上一个真实的计数系统要考虑的问题会很多:防作弊、按照不同维度计数,数据持久化到底层数据源等。

3) 共享session

​ 如图2-2所示,一个分布式Web服务将用户的Session信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。

​ 图2-2 Session 分散管理

​ 为了解决这个问题,可以使用Redis将用户的Session进行集中管理,如图2-3所示,在这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都直接从Redis中集中获取

​ 图2-3Redis集中管理Session

4) 限速

很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次

set key  value ex 60 nx              //设置60s的过期时间,在此期间不能重新发送验证码(nx)

2.3 哈希

​ 几乎所有的编程语言都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组。在Redis中,哈希类型是指键值本身又是一个键值对结构,形如value={{field1,value1},...{fieldN,valueN}}Redis键值对和哈希类型二者的关系可以用图2-4来表示

​ 图2-4 字符串和哈希类型对比

2.3.1 命令

1) 设置值

hset key field value        // hset user:1 name tom      设置成功会返回1,反之返回0
hsetnx       // 命令和setnx命令类似, 只不过作用域由键变为field
eg:
hsetnx user:1 name tom // return 0
hsetnx user:1 name1 tom  // return 1

2) 获取值

hget key field      // hget user:1 name //return "tom" 如果不存在则返回nil

3) 删除

hdel会删除一个或多个field,返回结果为成功删除field的个数

hdel key field1 field2 ...        //       hdel user:1 name name1 // return 2

4) 计算field个数

hlen key  // hlen user:1 //return 0

5) 批量设置或获取field-value

hmget key field [field ...]
hmset key field value [field value ...]
eg:
 hmset user:1 name tom name1 tony name3 sed
hmget user:1 name name1 name3 // return "tom" "tony" "sed"

6) 判断field是否存在

kexists key field         //hexists user:1 name      //return 1 存在返回1 不存在返回0

7) 获取所有field

hkeys key //hkeys user:1  // "name" "name1"

8) 获取所有value

hvals key  // hvals user:1 // "tom" "tom1"

9) 获取所有的field-value

hgetall key       // hgetall user:1  // return "name" "tom" "name1" "tom1"

开发提示:

在使用hgetall时,如果哈希元素个数比较多,会存在阻塞Redis的可能。如果开发人员只需要获取部分field,可以使用hmget,如果一定要获取全部field-value,可以使用hscan命令,该命令会渐进式遍历哈希类型

10) hincrby hincrbyfloat

hincrby key field
hincrbyfloat key field

hincrby和hincrbyfloat,就像incrby和incrbyfloat命令一样,但是它们的作用域是filed

11) 计算value的字符串长度

hstrlen key field  // jsrtlen user:1 name   // return 3

​ 表2-1 哈希类型命令的时间复杂度

2.3.2 内部编码

哈希类型的内部编码有两种:

​ ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀

​ hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)

演示ziplist:

hmset hashkey f1 v1 f2 v2
object encoding hashkey  // "ziplist"

演示hashtable:

1) 当有value大于64字节,内部编码会由ziplist变为hashtable:

hset hashkey f3 "one string is bigger than 64 byte... 忽略 ..."
object encoding hashkey  // "hashtable"

2)当field个数超过512,内部编码也会由ziplist变为hashtable:

hmset hashkey f1 v1 f2 v2 f3 v3 ... 忽略 ... f513 v513
object encoding hashkey  // "hashtable"

2.3.3 使用场景

图2-6为关系型数据表记录的两条用户信息,用户的属性作为表的列,每条用户信息作为行

​ 关系型数据库表保存用户信息

​ 使用哈希类型缓存用户信息

​ 相比于使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操作上会更加便捷。可以将每个用户的id定义为键后缀,多对field-value对应每个用户的属性

​ 注意哈希类型和关系型数据库有两点不同之处:

1. 哈希类型是稀疏的,而关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的field,而关系型数据库一旦添加新的列,所有行都要为其设置值(即使为NULL)
2. 关系型数据库可以做复杂的关系查询,而Redis去模拟关系型复杂查询开发困难,维护成本高


稀疏性: 在数据库中,稀疏数据是指在二维表中含有大量空值的数据;即稀疏数据是指,在数据集中绝大多数数值缺失或者为零的数据。稀疏数据绝对不是无用数据,只不过是信息不完全,通过适当的手段是可以挖掘出大量有用信息。

​ 图2-8 关系型数据库稀疏性

总结:

目前为止,我们已经能够用三种方法缓存用户信息,下面给出三种方案的实现方法和优缺点分析

1) 原生字符串类型:每个属性一个键

set user:1:name tom
set user:1:age 23
set user:1:city beijing
优点:
    简单直观,每个属性都支持更新操作
缺点:
    占用过多的键,内存占用量较大,同时用户信息内聚性比较差,所以此种方案一般不会在生产环境使用。

2) 序列化字符串类型:将用户信息序列化后用一个键保存。

set user:1 serialize(userInfo)
优点:
    简化编程,如果合理的使用序列化可以提高内存的使用效率。
缺点:
    序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到    Redis中。

3) 哈希类型:每个用户属性使用一对field-value,但是只用一个键保存。

hmset user:1 name tom age 23 city beijing
优点:
    简单直观,如果使用合理可以减少内存空间的使用
缺点:
    要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存。

2.4 列表

列表(list)类型用来存储多个有序的字符串,列表中的每个字符串称为元素(element),一个列表最多可以存储2 32 -1个元素。在Redis中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。

列表类型有两个特点:

第一、列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表
第二、列表中的元素可以是重复的

2.4.1 命令

​ 列表操作

1) 添加操作

1.1 从右边插入元素

rpush key value [value ...]           // rpush listkey c b a  插入成功返回列表长度

1.2 从左边插入元素

lpush key value [value...]          // lpush listkey a b c 插入成功返回列表长度

1.3 向某个元素前或者后插入元素

linsert key before|after pivot value             //linsert listkey before b java 插入成功返回列表的长度

2) 查找

2.1 获取指定范围内的元素列表

lrange key start end    // lrange listkey 0 -1  查找整个列表数据,如果成功,返回所有值

lrange操作会获取列表指定索引范围所有的元素。索引下标有两个特点:

第一,索引下标从左到右分别是0到N-1,但是从右到左分别是-1到-N。
第二,lrange中的end选项包含了自身,这个和很多编程语言不包含end不太相同
lrange listkey 1 3   // 获取第2到4个元素

2.2 获取列表指定索引下标的元素

lindex key index // lindex listkey 0

2.3 获取列表长度

llen key  // llen listkey

3) 删除

3.1 从列表左侧弹出元素

lpop key     // lpop listkey  弹出成功返回被弹出的元素

3.2 从列表右侧弹出元素

rpop key   // rpop listkey

3.3 删除指定元素

lrem key count value   //

lrem命令会从列表中找到等于value的元素进行删除,根据count的不同分为三种情况:

count>0,从左到右,删除最多count个元素。
count<0,从右到左,删除最多count绝对值个元素。
count=0,删除所有。
删除成功返回删除的个数

3.4 按照索引范围修剪列表

ltrim key start end  //ltrim listkey 1 3  只保留2-4之间的元素

4) 修改

修改指定索引下标的元素::

lset key index newValue            // lset listkey 2 python

5) 阻塞操作

阻塞式弹出如下:

blpop key [key ...] timeout  // key[key...]:多个列表的键。timeout 阻塞时间(单位:秒)。
brpop key [key ...] timeout  //

blpop和brpop是lpop和rpop的阻塞版本,它们除了弹出方向不同,使用方法基本相同

如果timeout=3,那么客户端要等到3秒后返回,如果timeout=0,那么客户端一直阻塞等下去:

brpop listkey  3 // 如果有元素则会立马弹出元素,并且返回"listkey", "value",没有元素的话则阻塞至少3s
brpop listkey 0  // 如果有元素立马弹出,并返回值,如果没有元素则一直阻塞,直到放入元素
brpop listkey listkey1 3 //

注意:

如果是多个键,那么brpop会从左至右遍历键,如果遇到一个键里面有元素则立马返回
如果多个客户端对同一个键执行brpop,那么最先执行brpop命令的客户端可以获取到弹出的值。

​ 列表命令时间复杂度:

2.4.2 内部编码

列表类型的内部编码有两种:

​ ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时(默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使用。

​ linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现

​ quicklist结合了ziplist和linkedlist两者的优势,为列表类型提供了一种更为优秀的内部编码实现,

2.4.3 使用场景

1) 消息队列

​ 如图所示,Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。

2) 文章列表

​ 每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。

3) 实际上列表的使用场景很多,在选择时可以参考以下口诀:

lpush+lpop=Stack(栈)
lpush+rpop=Queue(队列)
lpush+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息队列)

2.5 集合

​ 集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。如图2-22所示,集合user:1:follow包含着 "it"、 "music"、"his"、"sports"四个元素,一个集合最多可以存储2 32 -1个元素。Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题。

2.5.1 命令

1) 集合内操作

1.1 添加元素

sadd key element [element ...] // sadd myset name age // 添加成功返回成功个数 2
如果添加的元素在Redis中本来就存在那么不会添加成功,因为集合是没有重复元素的

1.2 删除元素

srem key value[value...]  // srem myset name age // 返回删除成功的个数

1.3 计算元素个数

scard key       // scard myset
scard 命令时间复杂度为O(1), 它不会遍历集合,而是直接调用Redis 内部命令

1.4 判断元素是否在集合中

sismember key value     //如果返回1则表示在,0表示不在

1.5 随机在集合中返回指定个数的元素

srandmember key count // srandmember myset 5  随机取5个元素
注意:
1. count 为可选参数,如果不填,那么默认值为1
2. 如果count 参数值大于集合中所有元素的和那么会返回所有元素

1.6 从集合中随机弹出元素

spop key count // spop myset 5 随机弹出5个元素
注意:
1. count 为可选参数,如果不填,那么默认值为1
2. 如果count 参数值大于集合中所有元素的和那么会弹出所有元素

1.7 获取所有元素

smembers key // smembers myset

2) 集合间操作

2.1 求多个集合的交集

sinter key [key ...]

2.2 求多个集合的并集

suinon key [key ...]

2.3 求多个集合的差集

sdiff key [key ...]

2.4 将交集,并集, 差集的结果保存

sinterstore destination key [key ...]
suionstore destination key [key ...]
sdiffstore destination key [key ...]

​ 集合间的运算在元素较多的情况下会比较耗时,所以Redis提供了上面三个命令(原命令+store)将集合间交集、并集、差集的结果保存在destination key中,例如下面操作将user:1:follow和user:2:follow两个集合的交集结果保存在user:1_2:inter中,user:1_2:inter本身也是集合类型:

sinterstore user:1_2:inter user:1:follow user:2:follow

​ 集合常用命令时间复杂度

2.5.2 内部编码

集合类型的内部编码有两种:

​ intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用

​ hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使
用hashtable作为集合的内部实现

2.5.3 使用场景

​ 集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。例如一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益

应用场景:

sadd=Tagging(标签)
spop/srandmember=Random item(生成随机数,比如抽奖)
sadd+sinter=Social Graph(社交需求)

2.6 有序集合

​ 有序集合相对于哈希、列表、集合来说会有一点点陌生,但既然叫有序集合,那么它和集合必然有着联系,它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据。如图2-24所示,该有序集合包含kris、mike、frank、tim、martin、tom,它们的分数分别是1、91、200、220、250、251,有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。

开发提示:

​ 有序集合中的元素不能重复,但是score可以重复,就和一个班里的同学学号不能重复,但是考试成绩可以相同

下图给出了列表、集合、有序集合三者的异同点:

​ 给出了列表、集合和有序集合三者的异同点

2.6.1 命令

1) 集合内

1.1 添加成员

zadd key score member [score member ...]  // zadd zset 1 name1 2 name2 3 name3  添加了3个成员

1.1.1 zadd 命令选项:

nx:member必须不存在,才可以设置成功,用于添加。
xx:member必须存在,才可以设置成功,用于更新。
ch:返回此次操作后,有序集合元素和分数发生变化的个数
incr:对score做增加,相当于后面介绍的zincrby。
eg:
zadd zset nx 1 name1 2 name2 3 name3
zadd zset xx 2 name1 3 name2 4 name3
zadd zset xx ch 2 name1 3 name2 4 name3
有序集合相比集合提供了排序字段,但是也产生了代价,zadd的时间
复杂度为O(log(n)),sadd的时间复杂度为O(1)

1.2 计算成员个数

zcard key  // zcard zset

1.3 计算某个成员的分数

zscore key member   // zscore zset name1

1.4 计算成员的排名

zrank key member  // 由低到高
zrevrank key member // 由高到低
最低位为0

1.5 删除成员

zrem key member [member ...]

1.6 增加成员的分数

zincrby key increment member   // zincrby zset 100 name1  返回增加后的分数

1.7 返回指定排名范围的成员

zrange key start end [withscores]
zrevrange key start end [withscores]

有序集合是按照分值排名的,zrange是从低到高返回,zrevrange反之。下面代码返回排名最低的是三个成员,如果加上withscores选项,同时会返回成员的分数:

zrange user:ranking 0 2 withscores
zrevrange user:ranking 0 2 withscores

1.8 返回指定分数范围内的成员

zrangebyscore key min max [withscores] [limit offset count]
zrevrangebyscore key max min [withscores] [limit offset count]

其中zrangebyscore按照分数从低到高返回,zrevrangebyscore反之。例如下面操作从低到高返回200到221分的成员,withscores选项会同时返回每个成员的分数。[limit offset count]选项可以限制输出的起始位置和个数:

zrangebyscore zset 1 inf withscores limit 0 2   // 查找1 +oo 大 起始位置为0 数量为2条记录

同时min和max还支持开区间(小括号)和闭区间(中括号),-inf和+inf分别代表无限小和无限大:

1.9 返回指定分数范围内的成员个数

zcount key min max   // zcount user:ranking 200 221  // 2

1.10 删除指定排名内的升序元素

zremrangebyrank key start end  // zremrangebyrank user:ranking 0 2

1.11 删除指定分数范围的成员

zremrangebyscore key min max // zremrangebyscore user:ranking (250 +inf

2) 集合间的操作

2.1 交集

zinterstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]
destination:交集计算结果保存到这个键
numkeys:需要做交集计算键的个数
key[key...]:需要做交集计算的键
weights weight[weight...]:每个键的权重,在做交集计算时,每个键中的每个member会将自己分数乘以这个权           重,每个键的权重默认是1
aggregate sum|min|max:计算成员交集后,分值可以按照sum(和)、min(最小值)、max(最大值)做汇总,默认值是sum
eg:
zinterstore user:ranking:1_inter_2 2 user:ranking:1 user:ranking:2
zinterstore user:ranking:1_inter_2 2 user:ranking:1 user:ranking:2 weights 1 0.5 aggregate max

2.2 并集

该命令的所有参数和zinterstore是一致的,只不过是做并集计算

zunionstore destination numkeys key [key ...] [weights weight [weight ...]] [aggregate sum|min|max]

​ 有序集合命令的时间复杂度

2.6.2 内部编码

有序集合类型的内部编码有两种:

​ ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist-entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用

​ skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降

2.6.3 使用场景

​ 有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。本节使用赞数这个维度,记录每天用户上传视频的排行榜。主要需要实现以下4个功能。

1) 添加用户赞数

例如用户mike上传了一个视频,并获得了3个赞,可以使用有序集合的zadd和zincrby功能:

zadd user:ranking:2016_03_15 3 mike

如果之后再获得一个赞,可以使用zincrby:

zincrby user:ranking:2016_03_15 1 mike

2) 取消用户赞数

zrem user:ranking:2016_03_15 mike

3)展示获取赞数最多的十个用户

zrevrangebyrank user:ranking:2016_03_15 0 9

4)展示用户信息以及用户分数

此功能将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户的分数和排名可以使用zscore和zrank两个功能:
hgetall user:info:tom
zscore user:ranking:2016_03_15 mike
zrank user:ranking:2016_03_15 mike

2.7 键管理

2.7.1 单个键管理

1) 键重命名

rename key newkey  // rename python Python
renamenx key newkey // rename 命令会覆盖redis 中同名的键,renamenx 保证newkey 不在Redis中才会             成功执行
注意:
    1. 由于重命名键期间会执行del命令删除旧的键,如果键对应的值比较大,会存在阻塞Redis的可能性,这点不要忽视。
    2. 如果rename和renamenx中的key和newkey如果是相同的在Redis3.2 后不会出现问题,3.2 之前的版本会出现错误

2) 随机返回一个键

randomkey

3) 键过期

除了expire、ttl命令以外,Redis还提供了expireat、pexpire、pexpireat、pttl、persist等一系列命令

expire key seconds:键在seconds秒后过期
expireat key timestamp:键在秒级时间戳timestamp后过期
pexpire key milliseconds:键在milliseconds毫秒后过期
pexpireat key milliseconds-timestamp键在毫秒级时间戳timestamp后过
persist key 键过期时间清除
setex key seconds value // 添加键同时添加过期时间
ttl 查询秒级过期时间
pttl 查询毫秒级过期时间
ttl 和 pttl 都有三种返回结果: 大于0表示,键剩余过期时间,-1表示键没有设置过期时间,-2 表示键不存在

注意:
    1. 如果设置过期时间的键不存在,返回结果为0
    2. 如果过期时间为负值,键会立即被删除,犹如使用del命令一样
    3. 对于字符串类型键,执行set命令会去掉过期时间,这个问题很容易在开发中被忽视。
    4. Redis不支持二级数据结构(例如哈希、列表)内部元素的过期功能
    5. setex 命令作为set+expire的组合,不但是原子执行,同时减少了一次网络通讯的时间

4) 键迁移

​ 迁移键功能非常重要,因为有时候我们只想把部分数据由一个Redis迁移到另一个Redis(例如从生产环境迁移到测试环境),Redis发展历程中提供了move、dump+restore、migrate三组迁移键的方法,它们的实现方式以及使用的场景不太相同,下面分别介绍

4.1 move

move key db

​ 如图所示,move命令用于在Redis内部进行数据迁移,Redis内部可以有多个数据库,由于多个数据库功能后面会进行介绍,这里只需要知道Redis内部可以有多个数据库,彼此在数据上是相互隔离的,move key db就是把指定的键从源数据库移动到目标数据库中,不建议在生产环境使用(不太重要)

​ move命令在Redis内部数据库之间迁移数据

4.2 dump+restore

dump key
restore key ttl value

dump+restore可以实现在不同的Redis实例之间进行数据迁移的功能,整个迁移的过程分为两步:

①在源Redis上,dump命令会将键值序列化,格式采用的是RDB格式

②在目标Redis上,restore命令将上面序列化的值进行复原,其中ttl参数代表过期时间,如果ttl=0代表没 有过期时间

整个过程如图所示:

​ dump+restore命令在Redis实例之间迁移数据

​ 有关dump+restore有两点需要注意:第一,整个迁移过程并非原子性的,而是通过客户端分步完成的。第二,迁移过程是开启了两个客户端连接,所以dump的结果不是在源Redis和目标Redis之间进行传输,下面用一个例子演示完整过程。

第一步: 在源Redis上执行dump

redis-source> set hello world
OK
redis-source> dump hello
"\x00\x05world\x06\x00\x8f<T\x04%\xfcNQ"

第二步: 在目标Redis上执行restore

redis-target> get hello
(nil)
redis-target> restore hello 0 "\x00\x05world\x06\x00\x8f<T\x04%\xfcNQ"
OK
redis-target> get hello
"world"

经过上面两步就完成了数据的迁移,前提是两个Redis 客户端建立了链接

4.3 migrate

migrate host port key|"" destination-db timeout [copy] [replace] [keys key [key ...]]

​ migrate命令也是用于在Redis实例间进行数据迁移的,实际上migrate命令就是将dump、restore、del三个命令进行组合,从而简化了操作流程。migrate命令具有原子性,而且从Redis3.0.6版本以后已经支持迁移多个键的功能,有效地提高了迁移效率

​ 整个过程如图所示,实现过程和dump+restore基本类似,但是有3点不太相同:

​ 第一,整个过程是原子执行的,不需要在多个Redis实例上开启客户端的,只需要在源Redis上执行migrate命令即可。

​ 二,migrate命令的数据传输直接在源Redis和目标Redis上完成的。

​ 第三,目标Redis完成restore后会发送OK给源Redis,源Redis接收后会根据migrate对应的选项来决定是否在源Redis上删除对应的键。

​ migrate命令在Redis实例之间原子性的迁移数据

migrate 参数说明:

host:目标Redis的IP地址
port:目标Redis的端口
key|"": 在Redis3.0.6版本之前,migrate只支持迁移一个键,所以此处是要迁移的键,但Redis3.0.6版本之后支持            迁移多个键,如果当前需要迁移多个键,此处为空字符串""
destination-db: 目标Redis的数据库索引,例如要迁移到0号数据库,这里就写0
timeout:迁移的超时时间(单位为毫秒)
[copy]:如果添加此选项,迁移后并不删除源键
[replace]:如果添加此选项,migrate不管目标Redis是否存在该键都会正常迁移进行数据覆盖
[keys key[key...]]:迁移多个键,例如要迁移key1、key2、key3,此处填写“keys key1 key2 key3”

演示migrate:

情况1:源Redis有键hello,目标Redis没有

migrate 127.0.0.1 6380 hello 0 1000  // 0代表0号库, 1000超时

情况2:源Redis和目标Redis都有键hello

migrate 127.0.0.1 6379 hello 0 1000 replace // 必须加replace 才能迁移成功

情况3: 源Redis需要迁移的键在目标Redis部分有,

migrate 127.0.0.1 6380 "" 0 50 copy keys python hello //目标有python键,但是没有hello,那么hello 会迁移成功,但是,还是会报和情况2一样的错误

情况4: 源Redis 没有需要迁移的键,那么会提示: nokey

情况5: 需要保留源Redis迁移的数据需要加copy参数

move、dump+restore、migrate三种迁移方式的异同点: 综合来看建议使用migrate

​ move、dump+restore、migrate三个命令比较

2.7.2 遍历键

Redis提供了两个命令遍历所有的键,分别是keys和scan:

1) 全量遍历键

keys pattern
pattern 有以下几种情况
1. * 代表匹配任意字符
2. . 匹配一个字符
3. [] 代表匹配部分字符,例如[1,3]代表匹配1,3,[1-10]代表匹配1到10的任意数字
4. \x 用来做转义,例如要匹配星号,问好需要进行转义

eg1: 下面操作匹配以j,r开头,紧跟edis字符串的所有键:

127.0.0.1:6379> keys [j,r]edis
1) "jedis"
2) "redis"

eg2: 下面操作会匹配到hello和hill这两个键:

127.0.0.1:6379> keys hll*
1) "hill"
2) "hello"

​ 当需要遍历所有键时(例如检测过期或闲置时间、寻找大对象等),keys是一个很有帮助的命令,例如想删除所有以video字符串开头的键,可以执行如下操作:

redis-cli keys video* | xargs redis-cli del

注意:

    如果Redis包含了大量的键,执行keys命令很可能会造成Redis阻塞,所以一般建议不要在生产环境下使用keys命令。但有时候确实有遍历键的需求该怎么办,可以在以下三种情况使用:
    1. 在一个不对外提供服务的Redis从节点上执行,这样不会阻塞到客户端的请求,但是会影响到主从复制,有关主从复制我们将在第6章进行详细介绍。
    2. 如果确认键值总数确实比较少,可以执行该命令
    3. 使用scan命令渐进式的遍历所有键,可以有效防止阻塞。

2) 渐进式遍历

​ Redis从2.8版本后,提供了一个新的命令scan,它能有效的解决keys命令存在的问题。和keys命令执行时会遍历所有键不同,scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是O(1),但是要真正实现keys的功能,需要执行多次scan。Redis存储键值对实际使用的是hashtable的数据结构,其简化模型如图所示

​ hashtable示意图

每次执行scan,可以想象成只扫描一个字典中的一部分键,直到将字典中的所有键遍历完毕。scan的使用方法如下:

scan cursor [match pattern] [count number]
cursor是必需参数,实际上cursor是一个游标,第一次遍历从0开始,每次scan遍历完都会返回当前游标的值,直到游标值为0,表示遍历结束
match pattern是可选参数,它的作用的是做模式的匹配,这点和keys的模式匹配很像
count number是可选参数,它的作用是表明每次要遍历的键个数,默认值是10,此参数可以适当增大

​ 现有一个Redis有26个键(英文26个字母),现在要遍历所有的键,使用scan命令效果的操作如下。第一次执行scan0,返回结果分为两个部分:第一个部分6就是下次scan需要的cursor,第二个部分是10个键:

127.0.0.1:6379> scan 0
1) "6"
2) 1) "w"
2) "i"
3) "e"
4) "x"
5) "j"
6) "q"
7) "y"
8) "u"
9) "b"
10) "o"
127.0.0.1:6379> scan 6
1) "11"
2) 1) "h"
2) "n"
3) "m"
4) "t"
5) "c"
6) "d"
7) "g"
8) "p"
9) "z"
10) "a"
127.0.0.1:6379> scan 11
1) "0"
2) 1) "s"
2) "f"
3) "r"
4) "v"
5) "k"
6) "l"
注意:
除了scan以外,Redis提供了面向哈希类型、集合类型、有序集合的扫描遍历命令,解决诸如hgetall、smembers、zrange可能产生的阻塞问题,对应的命令分别是hscan、sscan、zscan,它们的用法和scan基本类似
eg:
SADD language a b c d e f g h i j k l m n o p
sscan language 0 // 遍历集合
渐进式遍历可以有效的解决keys命令可能产生的阻塞问题,但是scan并非完美无瑕,如果在scan的过程中如果有键的变化(增加、删除、修改),那么遍历效果可能会碰到如下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键,这些是我们在开发时需要考虑的。

2.7.3 数据库管理

Redis提供了几个面向Redis数据库的操作,它们分别是dbsize、select、flushdb/flushall命令

1) 切换数据库

select dbIndex

​ 许多关系型数据库,例如MySQL支持在一个实例下有多个数据库存在的,但是与关系型数据库用字符来区分不同数据库名不同,Redis只是用数字作为多个数据库的实现。Redis默认配置中是有16个数据库:

​ 假设databases=16,select0操作将切换到第一个数据库,select15选择最后一个数据库,但是0号数据库和15号数据库之间的数据没有任何关联,甚至可以存在相同的键:

127.0.0.1:6379> set hello world # 默认进到 0 号数据库
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> select 15   # 切换到 15 号数据库
OK
127.0.0.1:6379[15]> get hello # 因为 15 号数据库和 0 号数据库是隔离的,所以 get hello 为空
(nil)

如图所示表现了该过程:

​ 使用select命令切换数据库

对于Redis多库的使用注意事项:
    Redis3.0中已经逐渐弱化这个功能,例如Redis的分布式实现RedisCluster只允许使用0号数据库,只不过为了向下兼容老版本的数据库功能,该功能没有完全废弃掉,下面分析一下为什么要废弃掉这个“优秀”的功能
呢?总结起来有三点:
1. Redis是单线程的。如果使用多个数据库,那么这些数据库仍然是使用一个CPU,彼此之间还是会受到影响的。
2. 多数据库的使用方式,会让调试和运维不同业务的数据库变的困难,假如有一个慢查询存在,依然会影响其他数据库,这样会使得别的业务方定位问题非常的困难。
3. 部分Redis的客户端根本就不支持这种方式。即使支持,在开发的时候来回切换数字形式的数据库,很容易弄乱。
建议:
如果要使用多个数据库功能,完全可以在一台机器上部署多个Redis实例,彼此用端口来做区分,因为现代计算机或者服务器通常是有多个CPU的。这样既保证了业务之间不会受到影响,又合理地使用了CPU资
源。

2) flushdb / flushall

​ flushdb/flushall命令用于清除数据库,两者的区别的是flushdb只清除当前数据库,flushall会清除所有数据库。

flushdb/flushall命令可以非常方便的清理数据,但是也带来两个问题:
1. flushdb/flushall命令会将所有数据清除,一旦误操作后果不堪设想
2. 如果当前数据库键值数量比较多,flushdb/flushall存在阻塞Redis的可能性

2.8 本章重点回顾

1)Redis提供5种数据结构,每种数据结构都有多种内部编码实现。

2)纯内存存储、IO多路复用技术、单线程架构是造就Redis高性能的三个因素。

3)由于Redis的单线程架构,所以需要每个命令能被快速执行完,否则会存在阻塞Redis的可能,理解Redis单线程命令处理机制是开发和运维Redis的核心之一。
4)批量操作(例如mget、mset、hmset等)能够有效提高命令执行的效率,但要注意每次批量操作的个数和字节数。
5)了解每个命令的时间复杂度在开发中至关重要,例如在使用keys、hgetall、smembers、zrange等时间复杂度较高的命令时,需要考虑数据规模对于Redis的影响。
6)persist命令可以删除任意类型键的过期时间,但是set命令也会删除字符串类型键的过期时间,这在开发时容易被忽视。
7)move、dump+restore、migrate是Redis发展过程中三种迁移键的方式,其中move命令基本废弃,migrate命令用原子性的方式实现了dump+restore,并且支持批量操作,是Redis Cluster实现水平扩容的重要工
具。
8)scan命令可以解决keys命令可能带来的阻塞问题,同时Redis还提供了hscan、sscan、zscan渐进式地遍历hash、set、zset。

3. 小功能大用处

3.1 慢查询分析

​ 许多存储系统(例如MySQL)提供慢查询日志帮助开发和运维人员定位系统存在的慢操作。所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阀值,就将这条命令的相关信息(例如:发生时间,耗时,命令的详细信息)记录下来,Redis也提供了类似的功能。

如图所示: Redis客户端执行一条命令分为如下4个部分

​ 一条客户端命令的生命周期

1. 发送命令,2. 命令排队,3. 命令执行,4. 返回结果
注意:
慢查询只统计第三步,命令执行的时间,所以没有慢查询不代表客户端没有超时

3.1.1 慢查询的两个参数配置

对于慢查询功能,需要明确两件事:

1. 预设阀值怎么设置?
2. 慢查询记录存放在哪?

Redis提供了slowlog-log-slower-than和slowlog-max-len配置来解决这两个问题

slowlog-log-slower-than  就是预设阀值,默认值为10000微妙=10ms, 如果超过这个值就被记录在慢查询                                                       日志中
此外设置阀值的时候如果 slowlog-log-slower-than=0则会记录所有命令,如果小于0,对任何命令都不会记录

slowlog-max-len  // Redis使用了一个列表来存储慢查询日志,slowlog-max-len就是列表的最大长度, 当长度                                      超过了设置的最大值,则把最先存入的删除

Redis 中有两种修改配置的方法:

1. 使用config set 命令动态修改
eg:
config set slowlog-log-slower-than 20000
config set slowlog-max-len 1000
config rewrite
其中的config rewrite 命令是将配置持久化到本地的配置文件
2. 修改配置文件

Redis 虽然慢查询日志存放在列表中,但是并没有暴露这个列表的键,而是通过一组命令来实现对慢查询日志的访问和管理:

1. 获取慢查询日志
slowlog get [n]  // n 为可选参数,指定返回条数
慢查询日志有四个参数:分别是慢查询日志的标识id、发生时间戳、命令耗时、执行命令和参数
2. 获取慢查询日志列表当前的长度
slowlog len
3. 慢查询日志重置
slowlog reset

3.1.2 最佳实践

慢查询功能可以有效地帮助我们找到Redis可能存在的瓶颈,但在实际使用过程中要注意以下几点:

1. slowlog-max-len配置建议:线上建议调大慢查询列表,记录慢查询时Redis会对长命令做截断操作,并不会占用大量内存。增大慢查询列表可以减缓慢查询被剔除的可能,例如线上可设置为1000以上。

2. slowlog-log-slower-than配置建议:默认值超过10毫秒判定为慢查询,需要根据Redis并发量调整该值。由于Redis采用单线程响应命令,对于高流量的场景,如果命令执行时间在1毫秒以上,那么Redis最多可支撑OPS不到1000。因此对于高OPS场景的Redis建议设置为1毫秒。
3. 慢查询只记录命令执行时间,并不包括命令排队和网络传输时间。因此客户端执行命令的时间会大于命令实际执行时间。因为命令执行排队机制,慢查询会导致其他命令级联阻塞,因此当客户端出现请求超时,需要检查该时间点是否有对应的慢查询,从而分析出是否为慢查询导致的命令级联阻塞
4. 由于慢查询日志是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令,为了防止这种情况发生,可以定期执行slow get命令将慢查询日志持久化到其他存储中(例如MySQL),然后可以制作可视化界面进行查询,第13章介绍的Redis私有云CacheCloud提供了这样的功能,好的工具可以让问题排查事半功倍

3.2 Redis Shell

3.2.1 redis-cli 详解

重要命令解释:

1. -r 选项代表将命令执行多次
    eg: redis-cli -r 3 ping   // 执行三次ping
2. -i 选项代表每隔几秒执行一次命令,但是-i选项必须和-r选项一起使用
    eg: redis-cli -r -i 1 ping   // 每隔1秒执行一次ping命令,一共执行5次
3. -x 选项代表从标准输入(stdin)读取数据作为redis-cli的最后一个参数
    eg: $ echo "world" | redis-cli -x set hello  // 将字符串world作为set hello的值
4. -c 选项是连接Redis Cluster节点时需要使用的,-c选项可以防止moved和ask异常
5. -a 如果Redis配置了密码,可以用-a(auth)选项,有了这个选项就不需要手动输入auth
6. --scan和--pattern   --scan选项和--pattern选项用于扫描指定模式的键,相当于使用scan命
7. --slave 选项是把当前客户端模拟成当前Redis节点的从节点,可以用来获取当前Redis节点的更新操作
8. --rdb 选项会请求Redis实例生成并发送RDB持久化文件,保存在本地。可使用它做持久化文件的定期备份。
9. --pipe  选项用于将命令封装成Redis通信协议定义的数据格式,批量发送给Redis执行
10. --bigkeys  选项使用scan命令对Redis的键进行采样,从中找到内存占用比较大的键值,这些键可能是系统的      瓶颈。
11. --eval  选项用于执行指定Lua脚本
12. --latency  latency有三个选项,分别是--latency、--latency-history、--latency-dist。它们都可以检测网络     延迟
    1) --latency  该选项可以测试客户端到目标Redis的网络延迟,如现在有客户端a和客户端b 两个跨地区机房链        接那么:
        客户端B:
            redis-cli -h {machineB} --latency
            min: 0, max: 1, avg: 0.07 (4211 samples)
        客户端A:
            redis-cli -h {machineB} --latency
            min: 0, max: 2, avg: 1.04 (2096 samples)
        可以看到客户端A由于距离Redis比较远,平均网络延迟会稍微高一些
    2) --latency-history
        --latency的执行结果只有一条,如果想以分时段的形式了解延迟信息,可以使用--latency-history选项:
        redis-cli -h 10.10.xx.xx --latency-history
        min: 0, max: 1, avg: 0.28 (1330 samples) -- 15.01 seconds range ...
        min: 0, max: 1, avg: 0.05 (1364 samples) -- 15.01 seconds range
        可以看到延时信息每15秒输出一次,可以通过-i参数控制间隔时间。
    3) --latency-dist
        该选项会使用统计图表的形式从控制台输出延迟统计信息。
13. --stat 选项可以实时获取Redis的重要统计信息
14. --raw和--no-raw  --no-raw选项是要求命令的返回结果必须是原始的格式,--raw恰恰相反,返回格式化后的      结果。
        $redis-cli get hello
        "\xe4\xbd\xa0\xe5\xa5\xbd"
        $redis-cli --no-raw get hello
         "\xe4\xbd\xa0\xe5\xa5\xbd"
         $redis-cli --raw get hello 你好

3.2.2 redis-server 详解

​ redis-server除了启动Redis外,还有一个--test-memory选项。redis-server--test-memory可以用来检测当前操作系统能否稳定地分配指定容量的内存给Redis,通过这种检测可以有效避免因为内存问题造成Redis崩溃

redis-server --test-memory 1024  // 检测当前操作系统能否提供1G的内存给Redis:

整个内存检测的时间比较长。当输出passed this test时说明内存检测完毕,最后会提示--test-memory只是简单检测,如果有质疑可以使用更加专业的内存检测工具
提示:
通常无需每次开启Redis实例时都执行--test-memory选项,该功能更偏向于调试和测试,例如,想快速占满机器内存做一些极端条件的测试,这个功能是一个不错的选择

3.2.3 redis-benchmark详解

​ redis-benchmark可以为Redis做基准性能测试,它提供了很多选项帮助开发和运维人员测试Redis的相关性能,下面分别介绍这些选项

1. -c     -c(clients)选项代表客户端的并发数量(默认是50)
2. -n     -n(num)选项代表客户端请求总量(默认是100000)
    例如redis-benchmark-c100-n20000代表100各个客户端同时请求Redis,一共执行20000次。redis-                      benchmark会对各类数据结构的命令进行测试,并给出性能指标:
    redis-benchmark -c100 -n20000
      20000 requests completed in 0.14 seconds
      100 parallel clients
      3 bytes payload
      keep alive: 1
    93.93% <= 1 milliseconds
    100.00% <= 1 milliseconds
    140845.06 requests per second

    性能解读: 一共执行了20000次get操作,在0.14秒完成,每个请求数据量是3个字节,93.93%的命令执行时间小     于1毫秒,Redis每秒可以处理140845.06次get请求
3. -q 选项仅仅显示redis-benchmark的requests per second信息
4. -r   在一个空的Redis上执行了redis-benchmark会发现只有3个键如果想向Redis插入更多的键,可以执行使          用-r(random)选项,可以向Redis插入更多随机的键。
        127.0.0.1:6379> dbsize
        (integer) 3
        127.0.0.1:6379> keys *
        1) "counter:__rand_int__"
        2) "mylist"
        3) "key:__rand_int__"

        $redis-benchmark -c 100 -n 20000 -r 10000
        -r选项会在key、counter键上加一个12位的后缀,-r10000代表只对后四位做随机处理(-r不是随机数的个          数)
        127.0.0.1:6379> dbsize
        (integer) 18641
        127.0.0.1:6379> scan 0
        1) "14336"
        2) 1) "key:000000004580"
        2) "key:000000004519"
        ...
        10) "key:000000002113"
5. -P -P选项代表每个请求pipeline的数据量(默认为1)
6. -k<boolean>   -k选项代表客户端是否使用keepalive,1为使用,0为不使用,默认值为1
7. -t 选项可以对指定命令进行基准测试
        redis-benchmark -t get,set -q
        SET: 150375.94 requests per second
        GET: 151975.69 requests per second
8. --csv  --csv选项会将结果按照csv格式输出,便于后续处理,如导出到Excel...
        redis-benchmark -t get,set --csv
        "SET","81300.81"
        "GET","79051.38"

3.3 Pipeline

3.3.1 Pipeline概念

​ Redis客户端执行一条命令分为如下四个过程:
​ 1)发送命令
​ 2)命令排队
​ 3)命令执行
​ 4)返回结果
​ 其中1)+4)称为Round Trip Time(RTT,往返时间)。
​ Redis提供了批量操作命令(例如mget、mset等),有效地节约RTT。但大部分命令是不支持批量操作的,例如要执行n次hgetall命令,并没有mhgetall命令存在,需要消耗n次RTT。Redis的客户端和服务端可能部署在不同的机器上。例如客户端在北京,Redis服务端在上海,两地直线距离约为1300公里,那么1次RTT时间=1300×2/(300000×2/3)=13毫秒(光在真空中传输速度为每秒30万公里,这里假设光纤为光速的2/3),那么客户端在1秒内大约只能执行80次左右的命令,这个和Redis的高并发高吞吐特性背道而驰。
​ Pipeline(流水线)机制能改善上面这类问题,它能将一组Redis命令进行组装,通过一次RTT传输给Redis,再将这组Redis命令的执行结果按顺序返回给客户端,下图为没有使用Pipeline执行了n条命令,整个过程需要n次RTT。

​ 没有Pipeline执行n次命令模型

下图为使用Pipeline执行了n次命令,整个过程需要1次RTT

​ 使用Pipeline执行n条命令模型

​ Pipeline并不是什么新的技术或机制,很多技术上都使用过。而且RTT在不同网络环境下会有不同,例如同机房和同机器会比较快,跨机房跨地区会比较慢。Redis命令真正执行的时间通常在微秒级别,所以才会有Redis性能瓶颈是网络这样的说法。

​ redis-cli的--pipe选项实际上就是使用Pipeline机制

3.3.2 性能测试

​ 下表给出了在不同网络环境下非Pipeline和Pipeline执行10000次set操作的效果,可以得到如下两个结论:

​ 1) Pipeline执行速度一般比逐条执行要快

​ 2) 客户端和服务端的网络延时越大,Pipeline的效果越明显

​ 在不同网络下,10000条set非Pipeline和Pipeline的执行时间对比

3.3.3 原生批量命令与Pipeline对比

​ 可以使用Pipeline模拟出批量操作的效果,但是在使用时要注意它与原生批量命令的区别,具体包含以下几点:

​ 1) 原生批量命令是原子的,Pipeline是非原子的

​ 2) 原生批量命令是一个命令对应多个key,Pipeline支持多个命令

​ 3) 原生批量命令是Redis服务端支持实现的,而Pipeline需要服务端和客户端的共同实现

3.3.4 最佳实践

​ Pipeline虽然好用,但是每次Pipeline组装的命令个数不能没有节制,否则一次组装Pipeline数据量过大,一方面会增加客户端的等待时间,另一方面会造成一定的网络阻塞,可以将一次包含大量命令的Pipeline拆分成多次较小的Pipeline来完成。

​ Pipeline只能操作一个Redis实例,但是即使在分布式Redis场景中,也可以作为批量操作的重要优化手段

3.4 事务与Lua

3.4.1 事务

事务概念: 事务表示一组动作,要么全部执行,要么全部不执行

​ Redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和exec两个命令之间。multi命令代表事务开始,exec命令代表事务结束,它们之间的命令是原子顺序执行的

举例说明:

127.0.0.1:6379> multi   // 事务开始
OK
127.0.0.1:6379> sadd user:a:follow user:b
QUEUED
127.0.0.1:6379> sadd user:b:fans user:a
QUEUED
127.0.0.1:6379> sismember user:a:follow user:b   // 另一个客户端,可看到集合中没有该成员
(integer) 0
127.0.0.1:6379> exec  // 事务执行
1) (integer) 1

127.0.0.1:6379> sismember user:a:follow user:b   // 事务执行后,成功添加成员,此时成员存在
(integer) 1

​ 如果要停止事务的执行,可以使用discard命令代替exec命令即可

127.0.0.1:6379> discard
OK
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 0

如果事务出现错误Redis处理机制

1) 命令错误

下面操作错将set写成了sett,属于语法错误,会造成整个事务无法执行,key和counter的值未发生变化:

127.0.0.1:6388> mget key counter
1) "hello"
2) "100"
127.0.0.1:6388> multi
OK
127.0.0.1:6388> sett key world
(error) ERR unknown command 'sett'
127.0.0.1:6388> incr counter
QUEUED
127.0.0.1:6388> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6388> mget key counter
1) "hello"
2) "100"

2) 运行时出错

​ 例如用户B在添加粉丝列表时,误把sadd命令写成了zadd命令,这种就是运行时命令,因为语法是正确的

127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd user:a:follow user:b
QUEUED
127.0.0.1:6379> zadd user:b:fans 1 user:a
QUEUED
127.0.0.1:6379> exec
1) (integer) 1
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 1

可以看到Redis并不支持回滚功能,user:a:follow user:b命令已经执行成功

注意:

​ 有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修改过,才执行事务,否则不执行(类似乐观锁)。Redis提供了watch命令来解决这类问题 下表展示了两个客户端执行命令的时序

代码演示如下:

#T1 :客户端 1
127.0.0.1:6379>
OK
#T2 :客户端 1
127.0.0.1:6379>
OK
#T3 :客户端 1
127.0.0.1:6379>
OK
#T4 :客户端 2
127.0.0.1:6379>
(integer) 11
#T5 :客户端 1
127.0.0.1:6379> append key jedis
QUEUED
#T6 :客户端 1
127.0.0.1:6379> exec
(nil)
#T7 :客户端 1
127.0.0.1:6379> get key
"javapython"

​ Redis提供了简单的事务,之所以说它简单,主要是因为它不支持事务中的回滚特性,同时无法实现命令之间的逻辑关系计算

3.4.2 Lua (暂略)

3.5 Bitmaps

3.5.1 数据结构模型

Bitmaps 概念:

​ 多开发语言都提供了操作位的功能,合理地使用位能够有效地提高内存使用率和开发效率。Redis提供了Bitmaps这个“数据结构”可以实现对位的操作

​ Bitmaps本身不是一种数据结构,实际上它就是字符串, 但是对它可以实现对字符串的位进行操作

​ Bitmaps单独提供了一套命令,所以在Redis中使用Bitmaps和使用字符串的方法不太相同。可以把Bitmaps想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在Bitmaps中叫做偏移量。

3.5.2 命令

1) 设置值

setbit key offset value

​ 设置键的第offset个位的值(从0算起),假设现在有20个用户,userid=0,5,11,15,19的用户对网站进行了访问,那么当前Bitmaps初始化结果如图所示:

​ setbit使用

​ 很多应用的用户id以一个指定数字(例如10000)开头,直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费,通常的做法是每次做setbit操作时将用户id减去这个指定数字。在第一次初始化Bitmaps时,假如偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成Redis的阻塞

2) 获取值

gitbit key offset

​ 获取键的第offset位的值(从0开始算),下面操作获取id=8的用户是否在2016-04-05这天访问过,返回0说明没有访问过:

127.0.0.1:6379> getbit unique:users:2016-04-05 8
(integer) 0
127.0.0.1:6379> getbit unique:users:2016-04-05 1000000
(integer) 0

由于offset=1000000根本就不存在,所以返回结果也是0:

3) 获取Bitmaps指定范围值为1的个数

bitcount [start][end]

下面操作计算2016-04-05这天的独立访问用户数量:

127.0.0.1:6379> bitcount unique:users:2016-04-05
(integer) 5

​ [start]和[end]代表起始和结束字节数,下面操作计算用户id在第1个字节到第3个字节之间的独立访问用户数,对应的用户id是11,15,19。

127.0.0.1:6379> bitcount unique:users:2016-04-05 1 3
(integer) 3

4) Bitmaps间的运算

bitop op destkey key[key....]

​ bitop是一个复合操作,它可以做多个Bitmaps的and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中。假设2016-04-04访问网站的userid=1,2,5,9,如图所示。

​ 2016-04-04访问网站的用户Bitmaps

eg:

下面操作计算出2016-04-04和2016-04-03两天都访问过网站的用户数量如图所示

127.0.0.1:6379> bitop and unique:users:and:2016-04-04_03 unique: users:2016-04-03
unique:users:2016-04-03
(integer) 2
127.0.0.1:6379> bitcount unique:users:and:2016-04-04_03
(integer) 2

​ 如果想算出2016-04-04和2016-04-03任意一天都访问过网站的用户数量(例如月活跃就是类似这种),可以使用or求并集,具体命令如下

127.0.0.1:6379> bitop or unique:users:or:2016-04-04_03 unique:
users:2016-04-03 unique:users:2016-04-03
(integer) 2
127.0.0.1:6379> bitcount unique:users:or:2016-04-04_03
(integer) 6

5) 计算Bitmaps中第一个值为targetBit的偏移量

bitpos key targetBit [start] [end]

​ 下面操作计算2016-04-04当前访问网站的最小用户id:

127.0.0.1:6379> bitpos unique:users:2016-04-04 1
(integer) 1

除此之外,bitpos还可以指定start和end,分别代表起始字节和结束字节:

127.0.0.1:6379> bitpos unique:users:2016-04-04 1 0 1
(integer) 0

3.5.3 Bitmaps 分析

​ 假设网站有1亿用户,每天独立访问的用户有5千万,如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表如下:

​ set和Bitmaps存储一天活跃用户的对比

随着时间推移Bitmaps 节省内存非常客观

​ 但Bitmaps并不是万金油,假如该网站每天的独立访问用户很少,例如只有10万(大量的僵尸用户),那么两者的对比如下:

​ set和Bitmaps存储一天活跃用户的对比(独立用户比较少)

3.6 HyperLogLog(详细使用暂略)

​ HyperLogLog并不是一种新的数据结构(实际类型为字符串类型),而是一种基数算法,通过HyperLogLog可以利用极小的内存空间完成独立总数的统计,数据集可以是IP、Email、ID等

3.7 发布订阅(Redis 的消息队列)

​ Redis提供了基于“发布/订阅”模式的消息机制,此种模式下,消息发布者和订阅者不进行直接通信,发布者客户端向指定的频道(channel)发布消息,订阅该频道的每个客户端都可以收到该消息,如图所示。Redis提供了若干命令支持该功能,在实际应用开发时,能够为此类问题提供实现方法。

3.7.1 命令

​ Redis主要提供了发布消息、订阅频道、取消订阅以及按照模式订阅和取消订阅等命令

1) 发布消息

publish channel message

​ 下面操作会向channel:sports频道发布一条消息“Tim won thechampionship”,返回结果为订阅者个数,因为此时没有订阅,所以返回结果为0:

客户端A: 
127.0.0.1:6379> publish channel:sports "Tim won the championship"
(integer) 0

2) 订阅消息

subscribe channel [channel ...]

​ 订阅者可以订阅一个或多个频道,下面操作为当前客户端订阅了channel:sports频道:

客户端B;
127.0.0.1:6379> subscribe channel:sports
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel:sports"
3) (integer) 1
// 由于订阅之前客户端发送的消息,所以订阅后收不到消息

此时客户端A再次发布一条消息:

客户端A
127.0.0.1:6379> publish channel:sports "James lost the championship"
(integer) 1

当前订阅者客户端B:收到消息

客户端B
127.0.0.1:6379> subscribe channel:sports
Reading messages... (press Ctrl-C to quit)
...
1) "message"
2) "channel:sports"
3) "James lost the championship"

如果有多个客户端同时订阅了channel:sports, 整个过程如下:

​ 多个客户端同时订阅频道channel:sports

有关订阅命令有两点需要注意:

1. 客户端在执行订阅命令之后进入了订阅状态,只能接收subscribe、psubscribe、unsubscribe、punsubscribe的四个命令。
2. 新开启的订阅客户端,无法收到该频道之前的消息,因为Redis不会对发布的消息进行持久化

3) 取消订阅

unsubscribe [channel [channel ...]]

​ 客户端可以通过unsubscribe命令取消对指定频道的订阅,取消成功后,不会再收到该频道的发布消息:

127.0.0.1:6379> unsubscribe channel:sports
1) "unsubscribe"
2) "channel:sports"
3) (integer) 0

4) 按照模式订阅和取消订阅

psubscribe pattern [pattern...]
punsubscribe [pattern [pattern ...]]

​ 除了subcribe和unsubscribe命令,Redis命令还支持glob风格的订阅命令psubscribe和取消订阅命令punsubscribe,例如下面操作订阅以it开头的所有频道:

127.0.0.1:6379> psubscribe it*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "it*"

5) 查询订阅

5.1 查看活跃的频道

pubsub channels [pattern]

​ 所谓活跃的频道是指当前频道至少有一个订阅者,其中[pattern]是可以指定具体的模式:

127.0.0.1:6379> pubsub channels
1) "channel:sports"
2) "channel:it"
3) "channel:travel"
127.0.0.1:6379> pubsub channels channel:*r*
1) "channel:sports"
2) "channel:travel"

5.2 查看频道订阅数

pubsub numsub [channel ...]
127.0.0.1:6379> pubsub numsub channel:sports
1) "channel:sports"
2) (integer) 2

5.3 查看模式订阅数

pubsub numpat
127.0.0.1:6379> pubsub numpat
(integer) 1

3.7.2 使用场景

​ 聊天室、公告牌、服务之间利用消息解耦都可以使用发布订阅模式,下面以简单的服务解耦进行说明。如图所示,图中有两套业务,上面为视频管理系统,负责管理视频信息;下面为视频服务面向客户,用户可以通过各种客户端(手机、浏览器、接口)获取到视频信息

​ 发布订阅用于视频信息变化通知

3.8 GEO(暂略)

​ Redis3.2版本提供了GEO(地理信息定位)功能,支持存储地理位置信息用来实现诸如附近位置、摇一摇这类依赖于地理位置信息的功能,对于需要实现这些功能的开发者来说是一大福音。GEO功能是Redis的另一位作者Matt Stancliff [1] 借鉴NoSQL数据库Ardb [2] 实现的,Ardb的作者来自中国,它
提供了优秀的GEO功能。

4. 客户端

4.1 客户端通信协议

略........................

4.2 Python客户端redis-py

1) 安装

1. pip3 install redis
2. easy_install redis
3. 源码编译安装

2) 基本使用方法:

2.1 导入依赖库

import redis

2.2 生成客户端连接:需要Redis的实例IP和端口两个参数

client = redis.StrictRedis(host='127.0.0.1', port=6379)

2.3 执行命令:redis-py的API保留了Redis API的原始风格,所以使用起来不会有不习惯的感觉

下面代码给出redis-py操作Redis五种数据结构的示例:

#1.string
# 输出结果: True
client.set("hello","world")
# 输出结果: world
client.get("hello")
# 输出结果: 1
client.incr("counter")
#2.hash
client.hset("myhash","f1","v1")
client.hset("myhash","f2","v2")
# 输出结果: {'f1': 'v1', 'f2': 'v2'}
client.hgetall("myhash")
#3.list
client.rpush("mylist","1")
client.rpush("mylist","2")
client.rpush("mylist","3")
# 输出结果: ['1', '2', '3']
client.lrange("mylist", 0, -1)
#4.set
client.sadd("myset","a")
client.sadd("myset","b")
client.sadd("myset","a")
# 输出结果: set(['a', 'b'])
client.smembers("myset")
#5.zset
client.zadd("myzset","99","tom")
client.zadd("myzset","66","peter")
client.zadd("myzset","33","james")
# 输出结果: [('james', 33.0), ('peter', 66.0), ('tom', 99.0)]
client.zrange("myzset", 0, -1, withscores=True)

redis-py支持Redis的Pipeline功能,下面用一个简单的示例进行说明:

1) 连接客户端:

import redis
client = redis.StrictRedis(host='127.0.0.1', port=6379)

2) 生成Pipeline:注意client.pipeline包含了一个参数,如果transaction=False代表不使用事务:

pipeline = client.pipeline(transaction=False)

3) 将命令封装到Pipeline中,此时命令并没有真正执行:

pipeline.set("hello","world")
pipeline.incr("counter")

4) 执行Pipeline:

#[True, 3]
result = pipeline.execute()

4.3 客户端管理

Redis提供了客户端相关API对其状态进行监控和管理

4.3.1 客户端API

1) client list

​ client list命令能列出与Redis服务端相连的所有客户端连接信息,例如下面代码是在一个Redis实例上执行client list的结果:

127.0.0.1:6379> client list
id=254487 addr=10.2.xx.234:60240 fd=1311 name= age=8888581 idle=8888581 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
id=300210 addr=10.2.xx.215:61972 fd=3342 name= age=8054103 idle=8054103 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
id=5448879 addr=10.16.xx.105:51157 fd=233 name= age=411281 idle=331077 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=ttl
id=2232080 addr=10.16.xx.55:32886 fd=946 name= age=603382 idle=331060 flags=N
db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get
id=7125108 addr=10.10.xx.103:33403 fd=139 name= age=241 idle=1 flags=N db=0
sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=del
id=7125109 addr=10.10.xx.101:58658 fd=140 name= age=241 idle=1 flags=N db=0
sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=del
...

1.1 标识: id, addr, fd, name

id:     客户端连接的唯一标识,这个id是随着Redis的连接自增的,重启Redis后会重置为0。
addr:       客户端连接的ip和端口。
fd:     socket的文件描述符,与lsof命令结果中的fd是同一个,如果fd=-1代表当前客户端不是外部客户端,而是         Redis内部的伪装客户端。
name:       客户端的名字,后面的client setName和client getName两个命令会对其进行说明。

2) 输入缓冲区:qbuf、qbuf-free

​ Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis从会输入缓冲区拉取命令并执行,输入缓冲区为客户端发送命令到Redis执行命令提供了缓冲功能,如图所示

​ client list中qbuf和qbuf-free分别代表这个缓冲区的总容量和剩余容量,Redis没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭。

2.1 输入缓冲使用不当会产生两个问题:

1. 一旦某个客户端的输入缓冲区超过1G,客户端将会被关闭
2. 输入缓冲区不受maxmemory控制,假设一个Redis实例设置了maxmemory为4G,已经存储了2G数据,但是如果此时输入缓冲区使用了3G,已经超过maxmemory限制,可能会产生数据丢失、键值淘汰、OOM等情况如下图所示

​ 输入缓冲区超过了maxmemory

2.2 造成输入缓冲区过大的原因:

1. 主要是因为Redis的处理速度跟不上输入缓冲区的输入速度,并且每次进入输入缓冲区的命令包含了大量bigkey,从而造成了输入缓冲区过大的情况
2. 还有一种情况就是Redis发生了阻塞,短期内不能处理命令,造成客户端输入的命令积压在了输入缓冲区,造成了输入缓冲区过大。

2.3 监控输入缓冲区的方法:

1. 通过定期执行client list命令,收集qbuf和qbuf-free找到异常的连接记录并分析,最终找到可能出问题的客户端。
2. 通过info命令的info clients模块,找到最大的输入缓冲区,例如下面命client_biggest_input_buf代表最大的输入缓冲区,例如可以设置超过10M就进行报警
127.0.0.1:6379> info clients
# Clients
connected_clients:1414
client_longest_output_list:0
client_biggest_input_buf:2097152
blocked_clients:0

2.4 对比以上两种方法:

​ 对比client list和info clients监控输入缓冲区的优劣势

2.5 输入缓冲区问题出现概率比较低,但是也要做好防范,在开发中要减少bigkey、减少Redis阻塞、合理的监控报警

3) 输出缓冲区:obl、oll、omem

3.1 Redis为每个客户端分配了输出缓冲区,它的作用是保存命令执行的结果返回给客户端,为Redis和客户端交互返回结果提供缓冲,如图所示

​ 客户端输出缓冲区模型

3.2 与输入缓冲区不同的是,输出缓冲区的容量可以通过参数client-output-buffer-limit来进行设置,并且输出缓冲区做得更加细致,按照客户端的不同分为三种:普通客户端、发布订阅客户端、slave客户端,如图所示

​ 三种不同类型客户端的输出缓冲区

3.3 对应的配置规则是:

client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
·<class>:客户端类型,分为三种。a)normal:普通客户端;b)
slave:slave客户端,用于复制;c)pubsub:发布订阅客户端。
·<hard limit>:如果客户端使用的输出缓冲区大于<hard limit>,客户端
会被立即关闭。
·<soft limit>和<soft seconds>:如果客户端使用的输出缓冲区超过了<soft
limit>并且持续了<soft limit>秒,客户端会被立即关闭。

3.4 Redis 的默认配置是:

client-output-buffer-limit normal 0 0 0
client-output-buffer-limit slave 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

3.5 和输入缓冲区相同的是,输出缓冲区也不会受到maxmemory的限制,如果使用不当同样会造成maxmemory用满产生的数据丢失、键值淘汰、OOM等情况。

3.6 实际上输出缓冲区由两部分组成:固定缓冲区(16KB)和动态缓冲区,其中固定缓冲区返回比较小的执行结果,而动态缓冲区返回比较大的结果,例如大的字符串、hgetall、smembers命令的结果等

3.7 固定缓冲区使用的是字节数组,动态缓冲区使用的是列表。当固定缓冲区存满后会将Redis新的返回结果存放在动态缓冲区的队列中,队列中的每个对象就是每个返回结果,如图所示

​ 输出缓冲区两个组成部分:固定缓冲区和动态缓冲区

3.8 client list中的obl代表固定缓冲区的长度,oll代表动态缓冲区列表的长度,omem代表使用的字节数。例如下面代表当前客户端的固定缓冲区的长度为0,动态缓冲区有4869个对象,两个部分共使用了133081288字节=126M内存:

id=7 addr=127.0.0.1:56358 fd=6 name= age=91 idle=0 flags=O db=0 sub=0 psub=0 multi=-1
qbuf=0 qbuf-free=0 obl=0 oll=4869 omem=133081288 events=rw cmd=monitor

3.9 监控输出缓冲区的方法依然有两种:

1. 通过定期执行client list命令,收集obl、oll、omem找到异常的连接记录并分析,最终找到可能出问题的客户端。
2. 通过info命令的info clients模块,找到输出缓冲区列表最大对象数如:
127.0.0.1:6379> info clients
# Clients
connected_clients:502
client_longest_output_list:4869
client_biggest_input_buf:0
blocked_clients:0
// 其中,client_longest_output_list代表输出缓冲区列表最大对象数,这两种统计方法的优劣势和输入缓冲区是一样的

3.10 预防输出缓冲区异常

1. 进行上述监控,设置阀值,超过阀值及时处理。
2. 限制普通客户端输出缓冲区的,把错误扼杀在摇篮中,例如可以进行如下设置:
client-output-buffer-limit normal 20mb 10mb 120
3. 适当增大slave的输出缓冲区的内存,如果master节点写入较大,slave客户端的输出缓冲区可能会比较大,一旦slave客户端连接因为输出缓冲区溢出被kill,会造成复制重连
4. 限制容易让输出缓冲区增大的命令,例如,高并发下的monitor命令就是一个危险的命令
5. 及时监控系统内存,一旦发现内存抖动频繁,可能就是输出缓冲区过大

4) 客户端的存活状态

4.1 client list中的age和idle分别代表当前客户端已经连接的时间和最近一次的空闲时间:

id=2232080 addr=10.16.xx.55:32886 fd=946 name= age=603382 idle=331060 flags=N db=0
sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=get

上面这条记录代表当期客户端连接Redis的时间为603382秒,其中空闲了331060秒:属于不太

正常的情况,当age=idle时说明一直处于空闲状态

5) 客户端的限制maxclients和timeout

5.1 Redis提供了maxclients参数来限制最大客户端连接数,一旦连接数超过maxclients,新的连接将被拒绝。maxclients默认值是10000,可以通过infoclients来查询当前Redis的连接数:

127.0.0.1:6379> info clients
# Clients
connected_clients:1414
...
// 当前连接数是1414

5.2 可以通过config set maxclients对最大客户端连接数进行动态设置

127.0.0.1:6379> config get maxclients
1) "maxclients"
2) "10000"
127.0.0.1:6379> config set maxclients 50
OK
127.0.0.1:6379> config get maxclients
1) "maxclients"
2) "50"

5.3 一般来说maxclients=10000在大部分场景下已经绝对够用,但是某些情况由于业务方使用不当(例如没有主动关闭连接)可能存在大量idle连接,无论是从网络连接的成本还是超过maxclients的后果来说都不是什么好事,因此Redis提供了timeout(单位为秒)参数来限制连接的最大空闲时间,一旦客户端连接的idle时间超过了timeout,连接将会被关闭,例如设置timeout为30秒:

#Redis 默认的 timeout 是 0 ,也就是不会检测客户端的空闲
127.0.0.1:6379> config set timeout 30
OK

6) 客户端的类型

6.1 client list中的flag是用于标识当前客户端的类型,例如flag=S代表当前客户端是slave客户端、flag=N代表当前是普通客户端,flag=O代表当前客户端正在执行monitor命令,下表列出了11种客户端类型。

​ 客户端类型

7) 其他

​ client list命令结果的全部属性

7.1 client setName和client getName

client setName xx   // 给客户端命名 client setName test_client
client getName          // 此时执行 client getName  // test_client
// 给客户端设置名字,这样比较容易标识出客户端的来源

7.2 client kill

client kill ip:port           //  client kill 127.0.0.1:52343
// 此命令用于杀掉指定IP地址和端口的客户端

7.3 client pause

client pause timeout( 毫秒 )   // 用于阻塞客户端timeout毫秒数

如下图所示,client pause命令用于阻塞客户端timeout毫秒数,在此期间客户端连接将被阻塞

​ client pause命令示意图

该命令可以在如下场景起到作用:

1. client pause只对普通和发布订阅客户端有效,对于主从复制(从节点内部伪装了一个客户端)是无效的,也就是此期间主从复制是正常进行的,所以此命令可以用来让主从复制保持一致
2. client pause可以用一种可控的方式将客户端连接从一个Redis节点切换到另一个Redis节点

// 需要注意的是在生产环境中,暂停客户端成本非常高

8) monitor

monitor命令用于监控Redis正在执行的命令,如下图所示,我们打开了两个redis-cli,一个执行set get ping命令,另一个执行monitor命令。可以看到monitor命令能够监听其他客户端正在执行的命令,并记录了详细的时间戳。

​ monitor命令演示

注意:

    monitor的作用很明显,如果开发和运维人员想监听Redis正在执行的命令,就可以用monitor命令,但事实并非如此美好,每个客户端都有自己的输出缓冲区,既然monitor能监听到所有的命令,一旦Redis的并发量过大,monitor客户端的输出缓冲会暴涨,可能瞬间会占用大量内存,下图展示了monitor命令造成大量内存使用。

​ 高并发下monitor命令使用大量输出缓冲区

4.3.2 客户端相关配置
1. timeout:检测客户端空闲连接的超时时间,一旦idle时间达到了timeout,客户端将会被关闭,如果设置为0就不进行检测
2. maxclients:客户端最大连接数,这个参数会受到操作系统设置的限制
3. tcp-keepalive:检测TCP连接活性的周期,默认值为0,也就是不进行检测,如果需要设置,建议为60,      那么Redis会每隔60秒对它创建的TCP连接进行活性检测,防止大量死连接占用系统资源
4. tcp-backlog:TCP三次握手后,会将接受的连接放入队列中,tcp-backlog就是队列的大小,它在          Redis中的默认值是511。通常来讲这个参数不需要调整,但是这个参数会受到操作系统的影响
    如在Linux操作系统中,如果/proc/sys/net/core/somaxconn小于tcp-backlog,那么在Redis启动     时会看到如下日志
    The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn      is set to the lower value of 128
    修改方法是:   echo 511 > /proc/sys/net/core/somaxconn
4.3.3客户端统计片段

下面是一次info clients 的执行结果

127.0.0.1:6379> info clients
# Clients
connected_clients:1414
client_longest_output_list:0
client_biggest_input_buf:2097152
blocked_clients:0

1)connected_clients:代表当前Redis节点的客户端连接数,需要重点监控,一旦超过maxclients,新的客户端连接将被拒绝。
2)client_longest_output_list:当前所有输出缓冲区中队列对象个数的最大值。
3)client_biggest_input_buf:当前所有输入缓冲区中占用的最大容量。
4)blocked_clients:正在执行阻塞命令(例如blpop、brpop、brpoplpush)的客户端个数。

下面是info stats 执行结果

# Stats
total_connections_received:80
...
rejected_connections:0

total_connections_received:Redis自启动以来处理的客户端连接数总数。
rejected_connections:Redis自启动以来拒绝的客户端连接数,需要重点监控。

4.5 客户端常见异常(暂略)

5. 持久化

5.1 RDB

​ RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。

5.1.1 触发机制

手动触发分别对应save和bgsave命令

  1. save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。运行save命令对应的Redis日志如下:
* DB saved on disk
  1. bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。运行bgsave命令对应的Redis日志如下:
* Background saving started by pid 3151
* DB saved on disk
* RDB: 0 MB of memory used by copy-on-write
* Background saving terminated with success

显然bgsave命令是针对save阻塞问题做的优化。因此Redis内部所有的涉及RDB的操作都采用bgsave的方式,而save命令已经废弃

  1. 除了执行命令手动触发之外,Redis内部还存在自动触发RDB的持久化机制:有如下场景
1) 使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
2) 如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点
3) 执行debug reload命令重新加载Redis时,也会自动触发save操作。
4) 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave

5.1.2 流程说明

bgsave是主流的触发RDB持久化方式,下面根据下图了解它的运作流程

​ bgsave命令的运作流程

1) 执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进程,如RDB/AOF子进程,如果存在bgsave命令直接返回。

2)父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒。

3) 父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。

4) 子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的时间,对应info统rdb_last_save_time 选项。

5) 进程发送信号给父进程表示完成,父进程更新统计信息,具体见info Persistence下的rdb_*相关选项

5.1.3 RDB文件的处理

  1. 保存: RDB文件保存在dir配置指定的目录下,文件名通过dbfilename配置指定。可以通过执行config set dir{newDir}和config setdbfilename{newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。
  2. Redis默认采用LZF算法对生成的RDB文件做压缩处理,压缩后的文件远远小于内存大小,默认开启,可以通过参数config setrdbcompression{yes|no}动态修改。
  3. 校验: 如果Redis加载损坏的RDB文件时拒绝启动,并打印如下日志:这时可以使用Redis提供的redis-check-dump工具检测RDB文件并获取对应的错误报告
# Short read or OOM loading DB. Unrecoverable error, aborting now.

提示:

1. 压缩RDB会消耗CPU,但可大幅降低文件的体积,方便保存到硬盘或通过网络发送给从节点,因此线上建议开启

5.1.4 RDB的优缺点

RDB的优点:

  1. RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份,全量复制等场景。比如每6小时执行bgsave备份,并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。
  2. Redis加载RDB恢复数据远远快于AOF的方式

RDB的缺点:

  1. RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。

  2. RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。

5.2 AOF

AOF(append only file)持久化:以独立日志的方式记录每次写命令,重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。理解掌握好AOF持久化机制对我们兼顾数据安全性和性能非常有帮助。

5.2.1 使用AOF

开启AOF功能需要设置配置:appendonly yes,默认不开启。AOF文件名通appendfilename 配置设置,默认文件名是appendonly.aof。保存路径同RDB持久化方式一致,通过dir配置指定。AOF的工作流程操作:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load),如图所示。

​ AOF工作流程

流程如下:

1)所有的写入命令会追加到aof_buf(缓冲区)中。
2)AOF缓冲区根据对应的策略向硬盘做同步操作。
3)随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
4)当Redis服务器重启时,可以加载AOF文件进行数据恢复。

5.2.2 命令写入

AOF命令写入的内容直接是文本协议格式。例如set hello world这条命令,在AOF缓冲区会追加如下文本:

*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
  1. AOF为什么直接采用文本协议格式?可能的理由如下:
1. 文本协议具有很好的兼容性。
2. 开启AOF后,所有写入命令都包含追加操作,直接采用协议格式,避免了二次处理开销。
3. 文本协议具有可读性,方便直接修改和处理。
  1. AOF为什么把命令追加到aof_buf中:
Redis使用单线程响应命令,如果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负载。先写入缓冲区aof_buf中,还有另一个好处,Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡。

5.2.3 文件同步

Redis提供了多种AOF缓冲区同步文件策略,由参数appendfsync控制,不同值的含义如下表所示。

系统调用write和fsync说明:

  1. write操作会触发延迟写(delayed write)机制。Linux在内核提供页缓冲区用来提高硬盘IO性能。write操作在写入系统缓冲区后直接返回。同步硬盘操作依赖于系统调度机制,例如:缓冲区页空间写满或达到特定时间周期。同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。
  2. fsync针对单个文件操作(比如AOF文件),做强制硬盘同步,fsync将阻塞直到写入硬盘完成后返回,保证了数据持久化。

配置为always时:

​ 每次写入都要同步AOF文件,在一般的SATA硬盘上,Redis只能支持大约几百TPS写入,显然跟Redis高性能特性背道而驰,不建议配置。

配置为no:

​ 由于操作系统每次同步AOF文件的周期不可控,而且会加大每次同步硬盘的数据量,虽然提升了性能,但数据安全性无法保证。

配置为everysec:

​ 是建议的同步策略,也是默认配置,做到兼顾性能和数据安全性。理论上只有在系统突然宕机的情况下丢失1秒的数据

5.2.4 重写机制

随着命令不断写入AOF,文件会越来越大,为了解决这个问题,Redis引入AOF重写机制压缩文件体积。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。

  1. 重写后的AOF文件为什么可以变小?有如下原因:
1) 进程内已经超时的数据不再写入文件。

2)旧的AOF文件含有无效命令,如del key1、hdel key2、srem keys、seta111、set a222等。重写      使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令。

3)多条写命令可以合并为一个,如:lpush list a、lpush list b、lpush listc可以转化为:lpush list a b c    为了防止单条命令过大造成客户端缓冲区溢出,对于list、set、hash、zset等类型操作,以64个元    素为界拆分为多条。
  1. AOF重写降低了文件占用空间,除此之外,另一个目的是:更小的AOF文件可以更快地被Redis加载。
  2. AOF重写过程可以手动触发和自动触发:
手动触发:直接调用bgrewriteaof命令。
自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机。
1) auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB。
2) auto-aof-rewrite-percentage:代表当前AOF文件空间(aof_current_size)和上一次重写后AOF文     件空间(aof_base_size)的比值。
3) 自动触发时机=aof_current_size>auto-aof-rewrite-min-
size&&(aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewrite-percentage
  1. 当触发AOF重写时,内部运行流程如下图:

​ AOF重写运作流程

1)执行AOF重写请求。
    如果当前进程正在执行AOF重写,请求不执行并返回如下响应:
    ERR Background append only file rewriting already in progress
    如果当前进程正在执行bgsave操作,重写命令延迟到bgsave完成之后再执行,返回如下响应:
    Background append only file rewriting scheduled
2)父进程执行fork创建子进程,开销等同于bgsave过程
3.1)主进程fork操作完成后,继续响应其他命令。所有修改命令依然写入AOF缓冲区并根                            appendfsync策略同步到硬盘,保证原有AOF机制正确性。
3.2)由于fork操作运用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然响应      命令,Redis使用“AOF重写缓冲区”保存这部分新数据,防止新AOF文件生成期间丢失这部分数         据。
4)子进程根据内存快照,按照命令合并规则写入到新的AOF文件。每次批量写入硬盘数据量由配置       aof-rewrite-incremental-fsync控制,默认为32MB,防止单次刷盘数据过多造成硬盘阻塞。
5.1)新AOF文件写入完成后,子进程发送信号给父进程,父进程更新统计信息,具体见info persistence   下的aof_*相关统计。
5.2)父进程把AOF重写缓冲区的数据写入到新的AOF文件。
5.3)使用新AOF文件替换老文件,完成AOF重写。

5.2.5 重启加载

AOF和RDB文件都可以用于服务器重启时的数据恢复。如图所示,表示Redis持久化文件加载流程。

​ Redis持久化文件加载流程

  1. 流程说明
1)AOF持久化开启且存在AOF文件时,优先加载AOF文件,打印如下日志:
    * DB loaded from append only file: 5.841 seconds
2)AOF关闭或者AOF文件不存在时,加载RDB文件,打印如下日志:
    * DB loaded from disk: 5.586 seconds
3)加载AOF/RDB文件成功后,Redis启动成功
4)AOF/RDB文件存在错误时,Redis启动失败并打印错误信息。

5.2.6 文件校验

加载损坏的AOF文件时会拒绝启动,并打印如下日志:

# Bad file format reading the append only file: make a backup of your AOF file,
then use ./redis-check-aof --fix <filename>

对于错误格式的AOF文件,先进行备份,然后采用redis-check-aof--fix命令进行修复,修复后使用diff-u对比数据的差异,找出丢失的数据,有些可以人工修改补全。

AOF文件可能存在结尾不完整的情况,比如机器突然掉电导致AOF尾部文件命令写入不全。Redis为我们提供了aof-load-truncated配置来兼容这种情况,默认开启。加载AOF时,当遇到此问题时会忽略并继续启动,同时打印如下警告日志:

# !!! Warning: short read while loading the AOF file !!!
# !!! Truncating the AOF at offset 397856725 !!!
# AOF loaded anyway because aof-load-truncated is enabled

5.3 问题定位与优化

Redis持久化功能一直是影响Redis性能的高发地

5.3.1 fork操作

​ 当Redis做RDB或AOF重写时,一个必不可少的操作就是执行fork操作创建子进程,对于大多数操作系统来说fork是个重量级操作。虽然fork创建的子进程不需要拷贝父进程的物理内存空间,但是会复制父进程的空间内存页表。例如对于10GB的Redis进程,需要复制大约20MB的内存页表,因此fork操作耗时跟进程总内存量息息相关,如果使用虚拟化技术,特别是Xen虚拟机,fork操作会更耗时

  1. fork耗时问题定位:

    对于高流量的Redis实例OPS可达5万以上,如果fork操作耗时在秒级别将拖慢Redis几万条命令执行,对线上应用延迟影响非常明显。正常情况下fork耗时应该是每GB消耗20毫秒左右。可以在info stats统计中查latest_fork_usec指标获取最近一次fork操作耗时,单位微秒。

    改善fork操作耗时:

    1) 优先使用物理机或者高效支持fork操作的虚拟化技术,避免使用Xen。

    2) 控制Redis实例最大可用内存,fork耗时跟内存量成正比,线上建议每个Redis实例内存 控制在10GB以内。

    3)合理配置Linux内存分配策略,避免物理内存不足导致fork失败

    4) 降低fork操作的频率,如适度放宽AOF自动触发时机,避免不必要的全量复制等。

5.3.2 子进程开销监控和优化

​ 子进程负责AOF或者RDB文件的重写,它的运行过程主要涉及CPU、内存、硬盘三部分的消耗

  1. CPU
1. CPU开销分析。子进程负责把进程内的数据分批写入文件,这个过程属于CPU密集操作,通常子进程对单核CPU利用率接近90%.
2. CPU消耗优化。Redis是CPU密集型服务,不要做绑定单核CPU操作。由于子进程非常消耗CPU,会和父进程产生单核资源竞争
3. 不要和其他CPU密集型服务部署在一起,造成CPU过度竞争
4. 如果部署多个Redis实例,尽量保证同一时刻只有一个子进程执行重写工作
  1. 内存
1. 内存消耗分析。子进程通过fork操作产生,占用内存大小等同于父进程,理论上需要两倍的内存来完成持久化操作,但Linux有写时复制机制(copy-on-write)。父子进程会共享相同的物理内存页,当父进程处理写请求时会把要修改的页创建副本,而子进程在fork操作过程中共享整个父进程内存快照。
2. 内存消耗优化:
1)同CPU优化一样,如果部署多个Redis实例,尽量保证同一时刻只有一个子进程在工作。
2)避免在大量写入时做子进程重写操作,这样将导致父进程维护大量页副本,造成内存消耗。

  1. 硬盘
1. 硬盘开销分析。子进程主要职责是把AOF或者RDB文件写入硬盘持久化。势必造成硬盘写入压力。根据Redis重写AOF/RDB的数据量,结合系统工具如sar、iostat、iotop等,可分析出重写期间硬盘负载情况
2. 硬盘开销优化:
a)不要和其他高硬盘负载的服务部署在一起。如:存储服务、消息队列服务等。
b)AOF重写时会消耗大量硬盘IO,可以开启配置no-appendfsync-on-rewrite,默认关闭。表示在AOF重写期间不做fsync操作
c)当开启AOF功能的Redis用于高流量写入场景时,如果使用普通机械磁盘,写入吞吐一般在100MB/s左右,这时Redis实例的瓶颈主要在AOF同步硬盘上。
d)对于单机配置多个Redis实例的情况,可以配置不同实例分盘存储AOF文件,分摊硬盘写入压力

5.3.3 AOF追加阻塞

当开启AOF持久化时,常用的同步硬盘的策略是everysec,用于平衡性能和数据安全性。对于这种方式,Redis使用另一条线程每秒执行fsync同步硬盘。当系统硬盘资源繁忙时,会造成Redis主线程阻塞,如图所示

​ 使用everysec做刷盘策略的流程

  1. 阻塞流程分析:
1)主线程负责写入AOF缓冲区。
2)AOF线程负责每秒执行一次同步磁盘操作,并记录最近一次同步时间。
3)主线程负责对比上次AOF同步时间:
    ·如果距上次同步成功时间在2秒内,主线程直接返回
    ·如果距上次同步成功时间超过2秒,主线程将会阻塞,直到同步操作完成。
  1. 通过对AOF阻塞流程可以发现两个问题:
1)everysec配置最多可能丢失2秒数据,不是1秒。
2)如果系统fsync缓慢,将会导致Redis主线程阻塞影响效率。
  1. AOF阻塞问题定位:
1)发生AOF阻塞时,Redis输出如下日志,用于记录AOF fsync阻塞导致拖慢Redis服务的行为:
    Asynchronous AOF fsync is taking too long (disk is busy). Writing the AOF buffer
    without waiting for fsync to complete, this may slow down Redis
2)每当发生AOF追加阻塞事件发生时,在info Persistence统计中,aof_delayed_fsync指标会累加,查    看这个指标方便定位AOF阻塞问题。
3)AOF同步最多允许2秒的延迟,当延迟发生时说明硬盘存在高负载问题,可以通过监控工具如iotop,  定位消耗硬盘IO资源的进程。

5.4 多实例部署

​ Redis单线程架构导致无法充分利用CPU多核特性,通常的做法是在一台机器上部署多个Redis实例。当多个实例开启AOF重写后,彼此之间会产生对CPU和IO的竞争

​ 对于单机多Redis部署,如果同一时刻运行多个子进程,对当前系统影响将非常明显,因此需要采用一种措施,把子进程工作进行隔离。Redis在info Persistence中为我们提供了监控子进程运行状况的度量指标,如下表所示

​ 我们基于以上指标,可以通过外部程序轮询控制AOF重写操作的执行,整个过程如图所示。

​ 轮询控制AOF重写

流程说明:

1)外部程序定时轮询监控机器(machine)上所有Redis实例。
2)对于开启AOF的实例,查看(aof_current_size-aof_base_size)/aof_base_size确认增长率。
3)当增长率超过特定阈值(如100%),执行bgrewriteaof命令手动触发当前实例的AOF重写。
4)运行期间循环检查aof_rewrite_in_progress和aof_current_rewrite_time_sec指标,直到AOF重写结束
5)确认实例AOF重写完成后,再检查其他实例并重复2)~4)步操作。从而保证机器内每个Redis实例AOF重写串行化执行。

6. 复制

​ 在分布式系统中为了解决单点问题,通常会把数据复制多个副本部署到
其他机器,满足故障恢复和负载均衡等需求。Redis也是如此,它为我们提
供了复制功能,实现了相同数据的多个Redis副本。复制功能是高可用Redis
的基础

6.1 配置

6.1.1 建立复制

​ 参与复制的Redis实例划分为主节点(master)和从节点(slave)。默认情况下,Redis都是主节点。每个从节点只能有一个主节点,而主节点可以同时具有多个从节点。复制的数据流是单向的,只能由主节点复制到从节点。配置复制的方式有以下三种:

  1. 在配置文件中加入slaveof{masterHost}{masterPort}随Redis启动生效。
  2. 在redis-server启动命令后加入--slaveof{masterHost}{masterPort}生效。
  3. 直接使用命令:slaveof{masterHost}{masterPort}生效。

综上所述,slaveof命令在使用时,可以运行期动态配置,也可以提前写到配置文件中。例如本地启动两个端口为6379和6380的Redis节点,在127.0.0.1:6380执行如下命令:

127.0.0.1:6380>slaveof 127.0.0.1 6379

slaveof配置都是在从节点发起,这时6379作为主节点,6380作为从节点。复制关系建立后执行如下命令测试:

127.0.0.1:6379>set hello redis
OK
127.0.0.1:6379>get hello
"redis"
127.0.0.1:6380>get hello
"redis"

从运行结果中看到复制已经工作了,针对主节点6379的任何修改都可以同步到从节点6380中,复制过程如下图所示

​ Redis主从节点复制过程

​ slaveof本身是异步命令,执行slaveof命令时,节点只保存主节点信息后返回,后续复制流程在节点内部异步执行主从节点复制成功建立后,可以使用info replication命令查看复制相关状态如下所示。

1)主节点6379复制状态信息:
127.0.0.1:6379>info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6379,state=online,offset=43,lag=0
....
2)从节点6380复制状态信息:
127.0.0.1:6380>info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
...

6.1.2 断开复制

​ slaveof命令不但可以建立复制,还可以在从节点执行slaveof no one来断开与主节点复制关系。例如在6380节点上执行slaveof no one来断开复制,如图所示

​ 从节点执行slaveof no one命令断开与主节点的复制关系

  1. 断开复制主要流程
1)断开与主节点复制关系。
2)从节点晋升为主节点。

​ 注意:

从节点断开复制后并不会抛弃原有数据,只是无法再获取主节点上的数据变化。
  1. 切主操作:

    通过slaveof命令还可以实现切主操作,所谓切主是指把当前从节点对主节点的复制切换到另一个主节点。执行slaveof{newMasterIp}{newMasterPort}命令即可,如下图所示

    ​ 从节点通过slave of切换新的主节点

执行流程如下:

1)断开与旧主节点复制关系。
2)与新主节点建立复制关系。
3)删除从节点当前所有数据。
4)对新主节点进行复制操作。

注意:

切主后从节点会清空之前所有的数据,线上人工操作时小心slaveof在错误的节点上执行或者指向错误的主节点

6.1.3 安全性

​ 对于数据比较重要的节点,主节点会通过设置requirepass参数进行密码验证,这时所有的客户端访问必须使用auth命令实行校验。从节点与主节点的复制连接是通过一个特殊标识的客户端来完成,因此需要配置从节点的masterauth参数与主节点密码保持一致,这样从节点才可以正确地连接到主节点并发起复制流程

6.1.4 只读

​ 默认情况下,从节点使用slave-read-only=yes配置为只读模式。由于复制只能从主节点到从节点,对于从节点的任何修改主节点都无法感知,修改从节点会造成主从数据不一致。因此建议线上不要修改从节点的只读模式

6.1.5 传输延迟

​ 主从节点一般部署在不同机器上,复制时的网络延迟就成为需要考虑的问题,Redis为我们提供了repl-disable-tcp-nodelay参数用于控制是否关闭TCP_NODELAY,默认关闭,说明如下:

·当关闭时,主节点产生的命令数据无论大小都会及时地发送给从节点,这样主从之间延迟会变小,但增加了网络带宽的消耗。适用于主从之间的网络环境良好的场景,如同机架或同机房部署。

·当开启时,主节点会合并较小的TCP数据包从而节省带宽。默认发送时间间隔取决于Linux的内核,一般默认为40毫秒。这种配置节省了带宽但增大主从之间的延迟。适用于主从网络环境复杂或带宽紧张的场景,如跨机房部署。

部署建议:

​ 部署主从节点时需要考虑网络延迟、带宽使用率、防灾级别等因素,如要求低延迟时,建议同机架或同机房部署并关闭repl-disable-tcp-nodelay;如果考虑高容灾性,可以同城跨机房部署并开启repl-disable-tcp-nodelay。

6.2 拓扑

​ Redis的复制拓扑结构可以支持单层或多层复制关系,根据拓扑复杂性可以分为以下三种:一主一从、一主多从、树状主从结构,下面分别介绍。

  1. 一主一从结构

    一主一从结构是最简单的复制拓扑结构,用于主节点出现宕机时从节点提供故障转移支持(如图所示)。当应用写命令并发量较高且需要持久化时,可以只在从节点上开启AOF,这样既保证数据安全性同时也避免了持久化对主节点的性能干扰。但需要注意的是,当主节点关闭持久化功能时,如果主节点脱机要避免自动重启操作。因为主节点之前没有开启持久化功能自动重启后数据集为空,这时从节点如果继续复制主节点会导致从节点数据也被清空的情况,丧失了持久化的意义。安全的做法是在从节点上执行slaveof no one断开与主节点的复制关系,再重启主节点从而避免这一问题。

  2. 一主多从结构

    一主多从结构(又称为星形拓扑结构)使得应用端可以利用多个从节点实现读写分离(见下图)。对于读占比较大的场景,可以把读命令发送到从节点来分担主节点压力。同时在日常开发中如果需要执行一些比较耗时的读命令,如:keys、sort等,可以在其中一台从节点上执行,防止慢查询对主节点造成阻塞从而影响线上服务的稳定性。对于写并发量较高的场景,多个从节点会导致主节点写命令的多次发送从而过度消耗网络带宽,同时也加重了主节点的负载影响服务稳定性

    ​ 一主多从(星形)结构

    1. 树状主从结构

    树状主从结构(又称为树状拓扑结构)使得从节点不但可以复制主节点数据,同时可以作为其他从节点的主节点继续向下层复制。通过引入复制中间层,可以有效降低主节点负载和需要传送给从节点的数据量。如下图所示,数据写入节点A后会同步到B和C节点,B节点再把数据同步到D和E节点,数据实现了一层一层的向下复制。当主节点需要挂载多个从节点时为了避免对主节点的性能干扰,可以采用树状主从结构降低主节点压力。

    ​ 树状主从结构

6.3 原理

6.3.1 复制过程

在从节点执行slaveof命令后,复制过程便开始运作如下图所示:

​ 主从节点建立复制流程图

1)保存主节点(master)信息。
2)从节点(slave)内部通过每秒运行的定时任务维护复制相关逻辑,当定时任务发现存在新的主节点后,会尝试与该节点建立网络连接如下图所示:

​ 从节点与主节点建立网络连接

如果从节点无法建立连接,定时任务会无限重试直到连接成功或者执行slaveof no one取消复制

3)发送ping命令:

连接建立成功后从节点发送ping请求进行首次通信,ping请求主要目的如下:

1. 检测主从之间网络套接字是否可用。
2. 检测主节点当前是否可接受处理命令。
3. 如果发送ping命令后,从节点没有收到主节点的pong回复或者超时,比如网络超时或者主节点正在阻塞无法响应命令,从节点会断开复制连接,下次定时任务会发起重连如下图所示:

​ 从节点发送PING命令流程

4)权限验证:

如果主节点设置了requirepass参数,则需要密码验证,从节点必须配置masterauth参数保证与主节点相同的密码才能通过验证;如果验证失败复制将终止,从节点重新发起复制流程。

5)同步数据集:

主从复制连接正常通信后,对于首次建立复制的场景,主节点会把持有的数据全部发送给从节点,这部分操作是耗时最长的步骤。Redis在2.8版本以后采用新复制命令psync进行数据同步,原来的sync命令依然支持,保证新旧版本的兼容性。新版同步划分两种情况:全量同步和部分同步

6)命令持续复制:

当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来主节点会持续地把写命令发送给从节点,保证主从数据一致性。

6.3.2 数据同步

​ Redis在2.8及以上版本使用psync命令完成主从数据同步,同步过程分为:全量复制和部分复制

  1. 全量复制:

    一般用于初次复制场景,Redis早期支持的复制功能只有全量复制,它会把主节点全部数据一次性发送给从节点,当数据量较大时,会对主从节点和网络造成很大的开销。

  2. 部分复制:

    用于处理在主从复制中因网络闪断等原因造成的数据丢失场景,当从节点再次连上主节点后,如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。

  3. psync命令运行需要以下组件支持:
    ·主从节点各自复制偏移量。
    ·主节点复制积压缓冲区。
    ·主节点运行id

3.1 复制偏移量

1)参与复制的主从节点都会维护自身复制偏移量。主节点(master)在处理完写入命令后,会把命令的字节长度做累加记录,统计信息在inforelication中的master_repl_offset指标中

127.0.0.1:6379> info replication
# Replication
role:master
...
master_repl_offset:1055130

2) 从节点(slave)每秒钟上报自身的复制偏移量给主节点,因此主节点也会保存从节点的复制偏移量,统计指标如下:

127.0.0.1:6379> info replication
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=1055214,lag=1
...

3) 从节点在接收到主节点发送的命令后,也会累加记录自身的偏移量。统计信息在info relication中的slave_repl_offset指标中:

127.0.0.1:6380> info replication
# Replication
role:slave
...
slave_repl_offset:1055214

复制偏移量维护如图所示:

提示:

1. 通过对比主从节点的复制偏移量,可以判断主从节点数据是否一致。
可以通过主节点的统计信息,计算出master_repl_offset-slave_offset字节量,判断主从节点复制相差的数据量,根据这个差值判定当前复制的健康度。如果主从之间复制偏移量相差较大,则可能是网络延迟或命令阻塞等原因引起

3.2 复制挤压缓存区

复制积压缓冲区是保存在主节点上的一个固定长度的队列,默认大小为1MB,当主节点有连接的从节点(slave)时被创建,这时主节点(master)响应写命令时,不但会把命令发送给从节点,还会写入复制积压缓冲区,如图所示。

由于缓冲区本质上是先进先出的定长队列,所以能实现保存最近已复制数据的功能,用于部分复制和复制命令丢失的数据补救。复制缓冲区相关统计信息保存在主节点的inforeplication中:

127.0.0.1:6379> info replication
# Replication
role:master
...
repl_backlog_active:1           // 开启复制缓冲区
repl_backlog_size:1048576   // 缓冲区最大长度
repl_backlog_first_byte_offset:7479    // 起始偏移量,计算当前缓冲区可用范围
repl_backlog_histlen:1048576    // 已保存数据的有效长度

3.3 主节点运行ID

每个Redis节点启动后都会动态分配一个40位的十六进制字符串作为运行ID。运行ID的主要作用是用来唯一识别Redis节点,比如从节点保存主节点的运行ID识别自己正在复制的是哪个主节点。如果只使用ip+port的方式识别主节点,那么主节点重启变更了整体数据集(如替换RDB/AOF文件),从节点再基于偏移量复制数据将是不安全的,因此当运行ID变化后从节点将
做全量复制。可以运行info server命令查看当前节点的运行ID:

127.0.0.1:6379> info server
# Server
redis_version:3.0.7
...
run_id:545f7c76183d0798a327591395b030000ee6def9

注意:

Redis关闭再启动后,运行ID会随之改变

如何不改变运行ID的情况下重启?

当需要调优一些内存相关配置,例如:hash-max-ziplist-value等,这些配置需要Redis重新加载才能优化已存在的数据,这时可以使用debug reload命令重新加载RDB并保持运行ID不变,从而有效避免不必要的全量复制。

注意:

debug reload命令会阻塞当前Redis节点主线程,阻塞期间会生成本地RDB快照并清空数据之后再加载RDB文件。因此对于大数据量的主节点和无法容忍阻塞的应用场景,谨慎使用。

3.4 psync命令

  1. 从节点使用psync命令完成部分复制和全量复制功能,命令格式:
    psync{runId}{offset},参数含义如下:

    runId:从节点所复制主节点的运行id

    offset:当前从节点已复制的数据偏移量

    psync命令运行流程如图所示:

  1. 流程说明:
1)从节点(slave)发送psync命令给主节点,参数runId是当前从节点保存的主节点运行ID,如果没有则默认值为,参数offset是当前从节点保存的复制偏移量,如果是第一次参与复制则默认值为-1。
2)主节点(master)根据psync参数和自身数据情况决定响应结果:
·如果回复+FULLRESYNC{runId}{offset},那么从节点将触发全量复制流程。
·如果回复+CONTINUE,从节点将触发部分复制流程。
·如果回复+ERR,说明主节点版本低于Redis2.8,无法识别psync命令,从节点将发送旧版的sync命令触发全量复制流程。

6.3.3 全量复制

​ 全量复制是Redis最早支持的复制方式,也是主从第一次建立复制时必须经历的阶段。触发全量复制的命令是sync和psync,它们的对应版本如图

​ Redis版本复制命令差异

  1. 全量复制的基本流程,如下图所示:
1)发送psync命令进行数据同步,由于是第一次进行复制,从节点没有复制偏移量和主节点的运行ID,所以发送psync-1。
2)主节点根据psync-1解析出当前为全量复制,回复+FULLRESYNC响应。
3)从节点接收主节点的响应数据保存运行ID和偏移量offset,执行到当前步骤时从节点打印如下日志:
    Partial resynchronization not possible (no cached master)
    Full resync from master: 92d1cb14ff7ba97816216f7beb839efe036775b2:216789
4)主节点执行bgsave保存RDB文件到本地
5)主节点发送RDB文件给从节点,从节点把接收的RDB文件保存在本地并直接作为从节点的数据文件,接收完RDB后从节点打印相关日志
    注意:
        1. 对于数据较大的主节点,比如生成的RDB文件超过6G以上要格外小心,传输文件十分耗时,取决于主从之间的带宽,如果总时间超过设置的repl-timeout配置的值(默认60s),从节点将放弃接收RDB文件并清理下载的临时文件,导致全量复制失败,如果文件实在太大,则适当的调整repl-timeout参数
        2. 关于无盘复制:为了降低主节点磁盘开销,Redis支持无盘复制,生成的RDB文件不保存到硬盘而是直接通过网络发送给从节点,通过repl-diskless-sync参数控制,默认关闭。无盘复制适用于主节点所在机器磁盘性能较差但网络带宽较充裕的场景。注意无盘复制目前依然处于试验阶段,线上使用需要做好充分测试
6)对于从节点开始接收RDB快照到接收完成期间,主节点仍然响应读写命令,因此主节点会把这期间写命令数据保存在复制客户端缓冲区内,当从节点加载完RDB文件后,主节点再把缓冲区内的数据发送给从节点,保证主从之间数据一致性。如果主节点创建和传输RDB的时间过长,对于高流量写入场景非常容易造成主节点复制客户端缓冲区溢出。默认配置为client-output-buffer-limit slave256MB64MB60,如果60秒内缓冲区消耗持续大于64MB或者直接超过256MB时,主节点将直接关闭复制客户端连接,造成全量同步失败。因此,运维人员需要根据主节点数据量和写命令并发量调整client-output-buffer-limit slave配置,避免全量复制期间客户端缓冲区溢出。
7)从节点接收完主节点传送来的全部数据后会清空自身旧数据
8)从节点清空数据后开始加载RDB文件,对于较大的RDB文件,这一步操作依然比较耗时
    注意:
    对于线上做读写分离的场景,从节点也负责响应读命令。如果此时从节点正出于全量复制阶段或者复制中断,那么从节点在响应读命令可能拿到过期或错误的数据。对于这种场景,Redis复制提供了slave-serve-stale-data参数,默认开启状态。如果开启则从节点依然响应所有命令。对于无法容忍不一致的应用场景可以设置no来关闭命令执行,此时从节点除了info和slaveof命令之外所有的命令只返回“SYNC with master in progress”信息。
    9)从节点成功加载完RDB后,如果当前节点开启了AOF持久化功能,它会立刻做bgrewriteaof操作,为了保证全量复制后AOF持久化文件立刻可用。

总结:

通过分析全量复制的所有流程,会发现全量复制是一个非常耗时费力的操作。它的时间开销主要包括:

·RDB文件网络传输时间。
·从节点清空数据时间。
·从节点加载RDB的时间。
·可能的AOF重写时间。

6.3.4 部分复制

​ 部分复制主要是Redis针对全量复制的过高开销做出的一种优化措施,使用psync{runId}{offset}命令实现。当从节点(slave)正在复制主节点(master)时,如果出现网络闪断或者命令丢失等异常情况时,从节点会向主节点要求补发丢失的命令数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点,这样就可以保持主从节点复制的一致性。补发的这部分数据一般远远小于全量数据,所以开销很小。部分复制的流程如图

​ 部分复制过程

流程说明:

1)当主从节点之间网络出现中断时,如果超过repl-timeout时间,主节点会认为从节点故障并中断复制连接
2)主从连接中断期间主节点依然响应命令,但因复制连接中断命令无法发送给从节点,不过主节点内部存在的复制积压缓冲区,依然可以保存最近一段时间的写命令数据,默认最大缓存1MB
3)当主从节点网络恢复后,从节点会再次连上主节点
4)当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们当作psync参数发送给主节点,要求进行部分复制操作。
5)主节点接到psync命令后首先核对参数runId是否与自身一致,如果一致,说明之前复制的是当前主节点;之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+CONTINUE响应,表示可以进行部分复制
6)主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态

6.3.5 心跳

主从节点在建立复制后,它们之间维护着长连接并彼此发送心跳命令,如图所示:

​ 主从心跳检测

主从心跳判断机制:

1)主从节点彼此都有心跳检测机制,各自模拟成对方的客户端进行通信,通过client list命令查看复制相关客户端信息,主节点的连接状态为flags=M,从节点连接状态为flags=S。
2)主节点默认每隔10秒对从节点发送ping命令,判断从节点的存活性和连接状态。可通过参数repl-ping-slave-period控制发送频率。
3)从节点在主线程中每隔1秒发送replconf ack{offset}命令,给主节点上报自身当前的复制偏移量。replconf命令主要作用如下:

·实时监测主从节点网络状态。
·上报自身复制偏移量,检查复制数据是否丢失,如果从节点数据丢失,再从主节点的复制缓冲区中拉取丢失数据。
·实现保证从节点的数量和延迟性功能,通过min-slaves-to-write、min-slaves-max-lag参数配置定义。

提示:

为了降低主从延迟,一般把Redis主从节点部署在相同的机房/同城机房,避免网络延迟和网络分区造成的心跳中断等情况。

6.3.6 异步复制

​ 主节点不但负责数据读写,还负责把写命令同步给从节点。写命令的发送过程是异步完成,也就是说主节点自身处理完写命令后直接返回给客户端,并不等待从节点复制完成,如图所示。

​ 主节点复制流程

1.主节点复制流程:

1)主节点6379接收处理命令。
2)命令处理完之后返回响应结果。
3)对于修改命令异步发送给6380从节点,从节点在主线程中执行复制的命令。

2.由于主从复制过程是异步的,就会造成从节点的数据相对主节点存在延迟。具体延迟多少字节,我们可以在主节点执行info replication命令查看相关指标:

slave0:ip=127.0.0.1,port=6380,state=online,offset=841,lag=1
master_repl_offset:841

6.4 开发与运维中的问题

6.4.1 读写分离

​ 对于读占比较高的场景,可以通过把一部分读流量分摊到从节点(slave)来减轻主节点(master)压力,同时需要注意永远只对主节点执行写操作,如下图所示:

​ Redis读写分离示意图

  1. 当使用从节点响应读请求时,业务端可能会遇到如下问题:
·复制数据延迟。
·读到过期数据。
·从节点故障。

1.1 数据延迟:

​ Redis复制数据的延迟由于异步复制特性是无法避免的,延迟取决于网络带宽和命令阻塞情况,比如刚在主节点写入数据后立刻在从节点上读取可能获取不到。需要业务场景允许短时间内的数据延迟。对于无法容忍大量延迟场景,可以编写外部监控程序监听主从节点的复制偏移量,当延迟较大时触发报警或者通知客户端避免读取延迟过高的从节点,实现逻辑如图所示:

流程说明如下:

1)监控程序(monitor)定期检查主从节点的偏移量,主节点偏移量在info replication的master_repl_offset指标记录,从节点偏移量可以查询主节点的slave0字段的offset指标,它们的差值就是主从节点延迟的字节量。
2)当延迟字节量过高时,比如超过10MB。监控程序触发报警并通知客户端从节点延迟过高。可以用Zookeeper的监听回调机制实现客户端通知。
3)客户端接到具体的从节点高延迟通知后,修改读命令路由到其他从节点或主节点上。当延迟恢复后,再次通知客户端,恢复从节点的读命令请求。

注意:

​ 这种方案的成本比较高,需要单独修改适配Redis的客户端类库。如果涉及多种语言成本将会扩大。客户端逻辑需要识别出读写请求并自动路由,还需要维护故障和恢复的通知。采用此方案视具体的业务而定,如果允许不一致性或对延迟不敏感的业务可以忽略,也可以采用Redis集群方案做水平扩展。

  1. 读到过期数据:

当主节点存储大量设置超时的数据时,如缓存数据,Redis内部需要维护过期数据删除策略删除策略主要有两种:惰性删除和定时删除

惰性删除:

​ 主节点每次处理读取命令时,都会检查键是否超时,如果超时则执行del命令删除键对象,之后del命令也会异步发送给从节点。需要注意的是为了保证复制的一致性,从节点自身永远不会主动删除超时数据,如图所示:

​ 主节点惰性删除过期键同步给从节点

定时删除:

Redis主节点在内部定时任务会循环采样一定数量的键,当发现采样的键过期时执行del命令,之后再同步给从节点,如图所示:

​ 主节点定时删除同步给从节点

  1. 从节点故障问题

对于从节点的故障问题,需要在客户端维护可用从节点列表,当从节点故障时立刻切换到其他从节点或主节点上。这个过程类似上文提到的针对延迟过高的监控处理

6.4.2 主从配置不一致

​ 主从配置不一致是一个容易忽视的问题。对于有些配置主从之间是可以不一致,比如:主节点关闭AOF在从节点开启。但对于内存相关的配置必须要一致,比如maxmemory,hash-max-ziplist-entries等参数。当配置的maxmemory从节点小于主节点,如果复制的数据量超过从节点maxmemory时,它会根据maxmemory-policy策略进行内存溢出控制,此时从节点数据已经丢失,但主从复制流程依然正常进行,复制偏移量也正常。修复这类问题也只能手动进行全量复制。当压缩列表相关参数不一致时,虽然主从节点存储的数据一致但实际内存占用情况差异会比较大

6.4.3 规避全量复制

·第一次建立复制:由于是第一次建立复制,从节点不包含任何主节点数据,因此必须进行全量复制才能完成数据同步。对于这种情况全量复制无法避免。当对数据量较大且流量较高的主节点添加从节点时,建议在低峰时进行操作,或者尽量规避使用大数据量的Redis节点。

·节点运行ID不匹配:当主从复制关系建立后,从节点会保存主节点的运行ID,如果此时主节点因故障重启,那么它的运行ID会改变,从节点发现主节点运行ID不匹配时,会认为自己复制的是一个新的主节点从而进行全量复制。对于这种情况应该从架构上规避,比如提供故障转移功能。当主节点发生故障后,手动提升从节点为主节点或者采用支持自动故障转移的哨兵或集群方案

·复制积压缓冲区不足:当主从节点网络中断后,从节点再次连上主节点时会发送psync{offset}{runId}命令请求部分复制,如果请求的偏移量不在主节点的积压缓冲区内,则无法提供给从节点数据,因此部分复制会退化为全量复制。针对这种情况需要根据网络中断时长,写命令数据量分析出合理的积压缓冲区大小。网络中断一般有闪断、机房割接、网络分区等情况。这时网络中断的时长一般在分钟级(net_break_time)。写命令数据量可以统
计高峰期主节点每秒info replication的master_repl_offset差值获取(write_size_per_minute)。积压缓冲区默认为1MB,对于大流量场景显然不够,这时需要增大积压缓冲区,保证repl_backlog_size>net_break_time*write_size_per_minute,从而避免因复制积压缓冲区不足造成的全量复制。

6.4.4 规避复制风暴

复制风暴是指大量从节点对同一主节点或者对同一台机器的多个主节点短时间内发起全量复制的过程。复制风暴对发起复制的主节点或者机器造成大量开销,导致CPU、内存、带宽消耗。因此我们应该分析出复制风暴发生的场景,提前采用合理的方式规避。规避方式有如下几个

  1. 单节点复制风暴

    ​ 单主节点复制风暴一般发生在主节点挂载多个从节点的场景。当主节点重启恢复后,从节点会发起全量复制流程,这时主节点就会为从节点创建RDB快照,如果在快照创建完毕之前,有多个从节点都尝试与主节点进行全量同步,那么其他从节点将共享这份RDB快照。这点Redis做了优化,有效避免了创建多个快照。但是,同时向多个从节点发送RDB快照,可能使主节点的网络带宽消耗严重,造成主节点的延迟变大,极端情况会发生主从节点连接断开,导致复制失败。

    ​ 解决方案首先可以减少主节点(master)挂载从节点(slave)的数量,或者采用树状复制结构,加入中间层从节点用来保护主节点,如图所示:

    ​ 采用树状结构降低多个从节点对主节点的消耗

  2. 但机器复制风暴

    ​ 由于Redis的单线程架构,通常单台机器会部署多个Redis实例。当一台机器(machine)上同时部署多个主节点(master)时如图所示:

    ​ 单机多实例部署

    如果这台机器出现故障或网络长时间中断,当它重启恢复后,会有大量从节点(slave)针对这台机器的主节点进行全量复制,会造成当前机器网络带宽耗尽

    避免方法如下:

    ·应该把主节点尽量分散在多台机器上,避免在单台机器上部署过多的主节点。
    ·当主节点所在机器故障后提供故障转移机制,避免机器恢复后进行密集的全量复制。
    

7. Redis 噩梦: 阻塞

7.1 发现阻塞

​ 当Redis 发生阻塞时,常见做法是在应用方加入异常统计并通过邮件/短信/微信报警,以便及时发现通知问题。

​ 自定义Appender收集Redis异常

7.2 内在原因

定位到具体的Redis节点异常后,首先应该排查是否是Redis自身原因导致,围绕以下几个方面排查:

·API或数据结构使用不合理。
·CPU饱和的问题。
·持久化相关的阻塞。

7.2.1 API或数据结构使用不合理

​ 通常Redis执行命令速度非常快,但也存在例外,如对一个包含上万个元素的hash结构执行hgetall操作,由于数据量比较大且命令算法复杂度是O(n),这条命令执行速度必然很慢。这个问题就是典型的不合理使用API和数据结构。对于高并发的场景我们应该尽量避免在大对象上执行算法复杂度超过O(n)的命令

  1. 如何发现慢查询
    Redis原生提供慢查询统计功能,执行slowlog get{n}命令可以获取最近的n条慢查询命令,默认对于执行超过10毫秒的命令都会记录到一个定长队列中,线上实例建议设置为1毫秒便于及时发现毫秒级以上的命令。如果命令执行时间在毫秒级,则实例实际OPS只有1000左右。慢查询队列长度默认128,可适当调大。
    慢查询本身只记录了命令执行时间,不包括数据网络传输时间和命令排队时间,因此客户端发生阻塞异常后,可能不是当前命令缓慢,而是在等待其他命令执行。需要重点比对异常和慢查询发生的时间点,确认是否有慢查询造成的命令阻塞排队。
  1. 应对慢查询
1)修改为低算法度的命令,如hgetall改为hmget等,禁用keys、sort等命令。
2)调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。大对象拆分过程需要视具体的业务决定,如用户好友集合存储在Redis中,有些热点用户会关注大量好友,这时可以按时间或其他维度拆分到多个集合中。
  1. 如何发现大对象
Redis本身提供发现大对象的工具,对应命令:redis-cli-h{ip}-p{port}bigkeys。内部原理采用分段进行scan操作,把历史扫描过的最大对象统计出来便于分析优化

7.2.2 CPU饱和

使用统计命令redis-cli-h{ip}-p{port}--stat获取当前Redis使用情况,该命令每秒输出一行统计信息

7.2.3 持久化阻塞

对于开启了持久化功能的Redis节点,需要排查是否是持久化导致的阻塞。持久化引起主线程阻塞的操作主要有:fork阻塞、AOF刷盘阻塞、HugePage写操作阻塞。

7.3 外在原因

排查Redis自身原因引起的阻塞原因之后,如果还没有定位问题,需要排查是否由外部原因引起。围绕以下三个方面进行排查

·CPU竞争
·内存交换
·网络问题

7.3.1 CPU竞争

cpu竞争问题如下:

进程竞争:

Redis是典型的CPU密集型应用,不建议和其他多核CPU密集型服务部署在一起。当其他进程过度消耗CPU时,将严重影响Redis吞吐量。可以通过top、sar等命令定位到CPU消耗的时间点和具体进程,这个问题比较容易发现,需要调整服务之间部署结构。

绑定CPU:

部署Redis时为了充分利用多核CPU,通常一台机器部署多个实例。常见的一种优化是把Redis进程绑定到CPU上,用于降低CPU频繁上下文切换的开销。这个优化技巧正常情况下没有问题,但是存在例外情况,如下图

​ Redis绑定CPU后父子进程使用一个CPU

当Redis父进程创建子进程进行RDB/AOF重写时,如果做了CPU绑定,会与父进程共享使用一个CPU。子进程重写时对单核CPU使用率通常在90%以上,父进程与子进程将产生激烈CPU竞争,极大影响Redis稳定性。因此对于开启了持久化或参与复制的主节点不建议绑定CPU。

7.3.2 内存交换

​ 内存交换(swap)对于Redis来说是非常致命的,Redis保证高性能的一个重要前提是所有的数据在内存中。如果操作系统把Redis使用的部分内存换出到硬盘,由于内存与硬盘读写速度差几个数量级,会导致发生交换后的Redis性能急剧下降。识别Redis内存交换的检查方法如下:

1.查询Redis进程号:

# redis-cli -p 6383 info server | grep process_id
process_id:4476

2.根据进程号查询内存交换信息:

# cat /proc/4476/smaps | grep Swap
Swap : 0 KB
Swap : 0 KB
Swap : 4 KB
Swap : 0 KB
Swap : 0 KB
......

如果交换量都是0KB或者个别的是4KB,则是正常现象,说明Redis进程内存没有被交换。预防内存交换的方法有:

·保证机器充足的可用内存。
·确保所有Redis实例设置最大可用内存(maxmemory),防止极端情况下Redis内存不可控的增长。
·降低系统使用swap优先级,如echo10>/proc/sys/vm/swappiness

7.3.3 网络问题

暂略

8. 理解内存

Redis所有的数据都存在内存中, 高效利用Redis内存首先需要理解Redis内存消耗在哪里,如何管理内存,最后才能考虑如何优化内存。

8.1 内存消耗

8.1.1 内存使用统计

通过执行info memory命令获取内存相关指标。读懂每个指标有助于分析Redis内存使用情况下表列出了内存统计指标和对应的解释

​ info memory详细解释

需要重点关注的指标有:used_memory_rss和used_memory以及它们的比值mem_fragmentation_ratio。

当mem_fragmentation_ratio>1时,说明used_memory_rss-used_memory多出的部分内存并没有用于数据存储,而是被内存碎片所消耗,如果两者相差很大,说明碎片率严重。
当mem_fragmentation_ratio<1时,这种情况一般出现在操作系统把Redis内存交换(Swap)到硬盘导致,出现这种情况时要格外关注,由于硬盘速度远远慢于内存,Redis性能会变得很差,甚至僵死

8.1.2 内存消耗划分

Redis进程内消耗主要包括:自身内存+对象内存+缓冲内存+内存碎片,其中Redis空进程自身内存消耗非常少,通常used_memory_rss在3MB左右,used_memory在800KB左右,一个空的Redis进程消耗内存可以忽略不计。

​ Redis内存消耗划分

  1. 对象内存

对象内存是Redis内存占用最大的一块,存储着用户所有的数据。Redis所有的数据都采用key-value数据类型,每次创建键值对时,至少创建两个类型对象:key对象和value对象。对象内存消耗可以简单理解为sizeof(keys)+sizeof(values)。键对象都是字符串,在使用Redis时很容易忽略键对内存消耗的影响,应当避免使用过长的键。values对象更复杂些, 主要包括5种基本类型, 字符串,列表, 哈希,集合,有序集合,其他数据类型都是建立在这5种数据结构实现的。

  1. 缓冲内存

缓冲内存主要包括:客户端缓冲、复制积压缓冲区、AOF缓冲区。

  1. 内存碎片

Redis默认的内存分配器采用jemalloc,可选的分配器还有:glibc、tcmalloc。内存分配器为了更好地管理和重复利用内存,分配内存策略一般采用固定范围的内存块进行分配。例如jemalloc在64位系统中将内存空间划分为:小、大、巨大三个范围。每个范围内又划分为多个小的内存块单位,如下所示:

·小:[8byte],[16byte,32byte,48byte,...,128byte],[192byte,256byte,...,512byte],[768byte,1024byte,...,3840byte]

·大:[4KB,8KB,12KB,...,4072KB]

·巨大:[4MB,8MB,12MB,...]

比如当保存5KB对象时jemalloc可能会采用8KB的块存储,而剩下的3KB空间变为了内存碎片不能再分配给其他对象存储。内存碎片问题虽然是所有内存服务的通病,但是jemalloc针对碎片化问题专门做了优化,一般不会存在过度碎片化的问题,正常的碎片率(mem_fragmentation_ratio)在1.03左右。但是当存储的数据长短差异较大时,以下场景容易出现高内存碎片问:

·频繁做更新操作,例如频繁对已存在的键执行append、setrange等更新操作。
·大量过期键删除,键对象过期删除后,释放的空间无法得到充分利用,导致碎片率上升。

出现高内存解决方法:

·数据对齐:在条件允许的情况下尽量做数据对齐,比如数据尽量采用数字类型或者固定长度字符串等,但是这要视具体的业务而定,有些场景无法做到。
·安全重启:重启节点可以做到内存碎片重新整理,因此可以利用高可用架构,如Sentinel或Cluster,将碎片率过高的主节点转换为从节点,进行安全重启。

8.1.3 子进程内存消耗

子进程内存消耗主要指执行AOF/RDB重写时Redis创建的子进程内存消耗。Redis执行fork操作产生的子进程内存占用量对外表现为与父进程相同,理论上需要一倍的物理内存来完成重写操作。但Linux具有写时复制技术(copy-on-write),父子进程会共享相同的物理内存页,当父进程处理写请求时会对需要修改的页复制出一份副本完成写操作,而子进程依然读取fork时整个父进程的内存快照。

·Redis产生的子进程并不需要消耗1倍的父进程内存,实际消耗根据期间写入命令量决定,但是依然要预留出一些内存防止溢出。
·需要设置sysctl vm.overcommit_memory=1允许内核可以分配所有的物理内存,防止Redis进程执行fork时因系统剩余内存不足而失败。
·排查当前系统是否支持并开启THP,如果开启建议关闭,防止copy-on-write期间内存过度消耗。

8.2 内存管理

Redis主要通过控制内存上限和回收策略实现内存管理

8.2.1 设置内存上限

Redis使用maxmemory参数限制最大可用内存。限制内存的目的主要有:

·用于缓存场景,当超出内存上限maxmemory时使用LRU等删除策略释放空间。
·防止所用内存超过服务器物理内存。

需要注意的是 maxmemory 限制的是Redis 实际使用的内存,也就是used_memory统计的内存,由于内存碎片的存在,实际消耗的内存可能会高与想设置的maxmemory的值,因此需要根据实际需求适当的调大maxmemory的值,通过设置内存上限可以非常方便地实现一台服务器部署多个Redis进程的内存控比如一台24GB内存的服务器,为系统预留4GB内存,预留4GB空闲内存给其他进程或Redis fork进程,留给Redis16GB内存,这样可以部署4个 maxmemory=4GB的Redis进程

​ 服务器分配4个4GB的Redis进程

8.2.2 动态调整内存上限

Redis的内存上限可以通过config set maxmemory进行动态修改,即修改最大可用内存。

Redis-1>config set maxmemory 6GB
Redis-2>config set maxmemory 2GB

通过动态修改maxmemory,可以实现在当前服务器下动态伸缩Redis内存的目的

​ Redis实例之间调整max-memory伸缩内存

8.2.3 内存回收策略

Redis的内存回收机制主要体现在以下两个方面:

·删除到达过期时间的键对象。

·内存使用达到maxmemory上限时触发内存溢出控制策略。

  1. 删除过期键对象

Redis所有的键都可以设置过期属性,内部保存在过期字典中。由于进
程内保存大量的键,维护每个键精准的过期删除机制会导致消耗大量的
CPU,对于单线程的Redis来说成本过高,因此Redis采用惰性删除和定时任
务删除机制实现过期键的内存回收。

·惰性删除:惰性删除用于当客户端读取带有超时属性的键时,如果已经超过键设置的过期时间,会执行删除操作并返回空,这种策略是出于节省CPU成本考虑,不需要单独维护TTL链表来处理过期键的删除。但是单独用这种方式存在内存泄露的问题,当过期键一直没有访问将无法得到及时删除,从而导致内存不能及时释放。正因为如此,Redis还提供另一种定时任务删除机制作为惰性删除的补充。
·定时任务删除:Redis内部维护一个定时任务,默认每秒运行10次(通过配置hz控制)。定时任务中删除过期键逻辑采用了自适应算法,根据键的过期比例、使用快慢两种速率模式回收键,流程如图所示

​ 定时任务删除过期键逻辑

流程说明:

1)定时任务在每个数据库空间随机检查20个键,当发现过期时删除对应的键。
2)如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或运行超时为止,慢模式下超时时间为25毫秒。
3)如果之前回收键逻辑超时,则在Redis触发内部事件之前再次以快模式运行回收过期键任务,快模式下超时时间为1毫秒且2秒内只能运行1次。
4)快慢两种模式内部删除逻辑相同,只是执行的超时时间不同。
  1. 内存溢出控制

当Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制,Redis支持6种策略:

1)noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory,此时Redis只响应读操作
2)volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。
3)allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。
4)allkeys-random:随机删除所有键,直到腾出足够空间为止。
5)volatile-random:随机删除过期键,直到腾出足够空间为止。
6)volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。

内存溢出控制策略可以采用config set maxmemory-policy{policy}动态配置,当Redis因为内存溢出删除键时,可以通过执行info stats命令查看evicted_keys指标找出当前Redis服务器已剔除的键数量。

每次Redis执行命令时如果设置了maxmemory参数,都会尝试执行回收内存操作。当Redis一直工作在内存溢出(used_memory>maxmemory)的状态下且设置非noeviction策略时,会频繁地触发回收内存的操作,影响Redis服务器的性能

8.3 内存优化

8.3.1 redisObject 对象

Redis存储的所有值对象在内部定义为redisObject结构体,内部结构如图所示

​ redisObject内部结构

字段说明:

·type字段:表示当前对象使用的数据类型,Redis主要支持5种数据类型:string、hash、list、set、zset。可以使用type{key}命令查看对象所属类型,type命令返回的是值对象类型,键都是string类型。
·encoding字段:表示Redis内部编码类型,encoding在Redis内部使用,代表当前对象内部采用哪种数据结构实现。理解Redis内部编码方式对于优化内存非常重要,同一个对象采用不同的编码实现内存占用存在明显差异。
·lru字段:记录对象最后一次被访问的时间,当配置了maxmemory和maxmemory policy=volatile-lru或者allkeys-lru时,用于辅助LRU算法删除键数据。可以使用object idletime{key}命令在不更新lru字段情况下查看当前键的空闲时间。
·refcount字段:记录当前对象被引用的次数,用于通过引用次数回收内存,当refcount=0时,可以安全回收当前对象空间。使用object refcount{key}获取当前对象引用。当对象为整数且范围在[0-9999]时,Redis可以使用共享对象的方式来节省内存
·*ptr字段:与对象的数据内容相关,如果是整数,直接存储数据;否则表示指向数据的指针。Redis在3.0之后对值对象是字符串且长度<=39字节的数据,内部编码为embstr类型,字符串sds和redisObject一起分配,从而只要一次内存操作即可。

开发提示:

  1. 可以使用scan+object idletime命令批量查询哪些键长时间未被访问,找出长时间不访问的键进行清理,可降低内存占用。
  2. 高并发写入场景中,在条件允许的情况下,建议字符串长度控制在39字节以内,减少创建redisObject内存分配次数,从而提高性能。

8.3.2 缩减键值对象

降低Redis内存使用最直接的方式就是缩减键(key)和值(value)的长度。

·key长度:如在设计键时,在完整描述业务情况下,键值越短越好。如user:{uid}:friends:notify:{fid}可以简化为u:{uid}:fs:nt:{fid}。
·value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数组放入Redis。首先应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。

    值对象除了存储二进制数据之外,通常还会使用通用格式存储数据比如:json、xml等作为字符串存储在Redis中。这种方式优点是方便调试和跨语言,但是同样的数据相比字节数组所需的空间更大,在内存紧张的情况下,可以使用通用压缩算法压缩json、xml后再存入Redis,从而降低内存占用,例如使用GZIP压缩后的json可降低约60%的空间。

开发提示:

当频繁压缩解压json等文本数据时,开发人员需要考虑压缩速度和计算开销成本,这里推荐使用Google的Snappy压缩工具,在特定的压缩率情况下效率远远高于GZIP等传统压缩工具,且支持所有主流语言环境。

8.3.3 共享对象池

​ 共享对象池是指Redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。除了整数值对象,其他类型如list、hash、set、zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。

​ 整数对象池在Redis中通过变量REDIS_SHARED_INTEGERS定义,不能通过配置修改。可以通过object refcount命令查看对象引用数验证是否启用整数对象池技术,如下:

redis> set foo 100
OK
redis> object refcount foo
(integer) 2
redis> set bar 100
OK
redis> object refcount bar
(integer) 3

设置键foo等于100时,直接使用共享池内整数对象,因此引用数是2,再设置键bar等于100时,引用数又变为3,过程如图所示:

​ 整数对象池共享机制

注意:

1. 使用共享对象池后,相同的数据内存使用降低30%以上。可见当数据大量使用[0-9999]的整数时,共享对象池可以节约大量内存。需要注意的是对象池并不是只要存储[0-9999]的整数就可以工作。当设置maxmemory并启用LRU相关淘汰策略如:volatile-lru,allkeys-lru时,Redis禁止使用共享对象
池
2. 对于ziplist编码的值对象,即使内部数据为整数也无法使用共享对象

8.3.4 字符串优化

  1. 字符串结构:

Redis没有采用原生C语言的字符串类型而是自己实现了字符串结构,内部简单动态字符串(simple dynamic string,SDS)。结构如图所示

Redis自身实现的字符串结构有如下特点:

·O(1)时间复杂度获取:字符串长度、已用长度、未用长度。
·可用于保存字节数组,支持安全的二进制数据存储。
·内部实现空间预分配机制,降低内存再分配次数。
·惰性删除机制,字符串缩减后的空间不释放,作为预分配空间保留。
  1. 预分配机制

因为字符串(SDS)存在预分配机制,日常开发中要小心预分配带来的内存浪费,例如下表的测试用例

​ 字符串内存预分配测试

从测试数据可以看出,同样的数据追加后内存消耗非常严重:

三个阶段的流程如下:

1)阶段1插入新的字符串后,free字段保留空间为0,总占用空间=实际占用空间+1字节,最后1字节保存‘\0’标示结尾,这里忽略int类型len和free字段消耗的8字节。

​ 阶段1字符串对象内存占用

2)追加操作后字符串对象预分配了一倍容量作为预留空间,而且大量追加操作需要内存重新分配,造成内存碎片率(mem_fragmentation_ratio)上升,在阶段1原有字符串上追加60字节数据空间占用如图所示:

阶段2字符串内存占用

3)直接插入与阶段2相同数据的空间占用如图所示:

​ 阶段3字符串内存占用

字符串之所以采用预分配的方式是防止修改操作需要不断重分配内存和字节数据拷贝。但同样也会造成内存的浪费。字符串预分配每次并不都是翻倍扩容,空间预分配规则如下:

1)第一次创建len属性等于数据实际大小,free等于0,不做预分配。
2)修改后如果已有free空间不够且数据小于1M,每次预分配一倍容量。如原有len=60byte,free=0,再追加60byte,预分配120byte,总占用空间:60byte+60byte+120byte+1byte。
3)修改后如果已有free空间不够且数据大于1MB,每次预分配1MB数据。如原有len=30MB,free=0,当再追加100byte,预分配1MB,总占用空间:1MB+100byte+1MB+1byte。

开发提示:

尽量减少字符串频繁修改操作如append、setrange,改为直接使用set修改字符串,降低预分配带来的内存浪费和内存碎片化。

  1. 字符串重构

字符串重构:指不一定把每份数据作为字符串整体存储,像json这样的数据可以使用hash结构,使用二级结构存储也能帮我们节省内存。同时可以使用hmget、hmset命令支持字段的部分读取修改,而不用每次整体存取

8.4 编码优化

  1. 了解编码:

Redis对外提供了string、list、hash、set、zet等类型,但是Redis内部针对不同类型存在编码的概念,所谓编码就是具体使用哪种底层数据结构来实现。编码不同将直接影响数据的内存占用和读写效率。使用objectencoding{key}命令获取编码类型。

Redis针对每种数据类型(type)可以采用至少两种编码方式来实现,下表表示type和encoding的对应关系。

​ type和encoding对应关系表

  1. 控制编码类型

编码类型转换在Redis写入数据时自动完成,这个转换过程是不可逆的,转换规则只能从小内存编码向大内存编码转换。例如:

redis> lpush list:1 a b c d
(integer) 4  // 存储 4 个元素
redis> object encoding list:1
"ziplist"  // 采用 ziplist 压缩列表编码
redis> config set list-max-ziplist-entries 4
OK  // 设置列表类型 ziplist 编码最大允许 4 个元素
redis> lpush list:1 e
(integer) 5  // 写入第 5 个元素 e
redis> object encoding list:1
"linkedlist"  // 编码类型转换为链表
redis> rpop list:1
"a"  // 弹出元素 a
redis> llen list:1
(integer) 4  // 列表此时有 4 个元素
redis> object encoding list:1
"linkedlist"  // 编码类型依然为链表,未做编码回退

​ 以上命令体现了list类型编码的转换过程,其中Redis之所以不支持编码回退,主要是数据增删频繁时,数据向压缩编码转换非常消耗CPU,得不偿失。

各种编码应用场景:

​ hash、list、set、zset内部编码配置

编码转换机制如图所示:

  1. ziplist编码

ziplist编码主要目的是为了节约内存,因此所有数据都是采用线性连续的内存结构。ziplist编码是应用范围最广的一种,可以分别作为hash、list、zset类型的底层数据结构实现,结构如图:

​ ziplist内部结构

开发提示:

针对性能要求较高的场景使用ziplist,建议长度不要超过1000,每个元素大小控制在512字节以内。
  1. intset 编码

intset编码是集合(set)类型编码的一种,内部表现为存储有序、不重复的整数集。当集合只包含整数且长度不超过set-max-intset-entries配置时被启用。

开发提示:

1. 使用intset编码的集合时,尽量保持整数范围一致,如都在int-16范围内。防止个别大整数触发集合升级操作,产生内存浪费。
2. 当使用整数集合时尽量使用intset编码

8.3.6 控制键的数量

当使用Redis存储大量数据时,通常会存在大量键,过多的键同样会消耗大量内存。Redis本质是一个数据结构服务器,它为我们提供多种数据结构,如hash、list、set、zset等。使用Redis时不要进入一个误区,大量使用get/set这样的API,把Redis当成Memcached使用。对于存储相同的数据内容利用Redis的数据结构降低外层键的数量,也可以节省大量内存。如图8-所示,通过在客户端预估键规模,把大量键分组映射到多个hash结构中降低键的数量。

​ 客户端维护哈希分组降低键规模

hash结构降低键数量分析:

·根据键规模在客户端通过分组映射到一组hash对象中,如存在100万个键,可以映射到1000个hash中,每个hash保存1000个元素。
·hash的field可用于记录原始key字符串,方便哈希查找。
·hash的value保存原始值对象,确保不要超过hash-max-ziplist-value限制。

下表测试这种优化技巧:

通过测试数据说明:

·同样的数据使用ziplist编码的hash类型存储比string类型节约内存。
·节省内存量随着value空间的减少越来越明显。
·hash-ziplist类型比string类型写入耗时,但随着value空间的减少,耗时逐渐降低。

使用上面优化的关键点:

1)hash类型节省内存的原理是使用ziplist编码,如果使用hashtable编码方式反而会增加内存消耗。
2)ziplist长度需要控制在1000以内,否则由于存取操作时间复杂度在O(n)到O(n2)之间,长列表会导致CPU消耗严重,得不偿失。
3)ziplist适合存储小对象,对于大对象不但内存优化效果不明显还会增加命令操作耗时。
4)需要预估键的规模,从而确定每个hash结构需要存储的元素数量。
5)根据hash长度和元素大小,调整hash-max-ziplist-entries和hash-max-ziplist-value参数,确保hash类型使用ziplist编码。

开发提示:

使用ziplist+hash优化keys后,如果想使用超时删除功能,开发人员可以存储每个对象写入的时间,再通过定时任务使用hscan命令扫描数据,找出hash内超时的数据项删除即可。

9. 哨兵

​ Redis的主从复制模式下,一旦主节点由于故障不能提供服务,需要人工将从节点晋升为主节点,同时还要通知应用方更新主节点地址,对于很多应用场景这种故障处理的方式是无法接受的。但是Redis从2.8开始正式提供Redis Sentinel(哨兵)架构来解决这个问题

9.1 基本概念

相关名词解释:

​ Redis Sentinel相关名词解释

Redis Sentinel是Redis的高可用实现方案,在实际的生产环境中,对提高整个系统的高可用性是非常有帮助的

9.1.1 主从复制的问题

1. 主从复制中,一旦主节点出现故障需要手动将一个从节点晋升为主节点,同时需要修改应用发的主节点地址,还需要命令其他从节点去复制新的主节点,整个过程需要人工干预
2. ·主节点的写能力受到单机的限制。
3.·主节点的存储能力受到单机的限制。

9.1.2 高可用

主从模式下,一旦主节点不可用,必然会造成一定的写数据丢失和读数据错误,甚至可能造成应用方服务不可用。整个故障转义过程是人工干预的,实时性和准确性无法保证,整个流程如下:

1)如图所示,主节点发生故障后,客户端(client)连接主节点失败,两个从节点与主节点连接失败造成复制中断。

2)如图所示,如果主节点无法正常启动,需要选出一个从节点(slave-1),对其执行slaveof no one命令使其成为新的主节点。

3)如图所示,原来的从节点(slave-1)成为新的主节点后,更新应用方的主节点信息,重新启动应用方。

4)如图所示,客户端命令另一个从节点(slave-2)去复制新的主节点(new-master)

5)如图所示,待原来的主节点恢复后,让它去复制新的主节点。

9.1.3 Redis Sentinel的高可用性

​ 1.当主节点出现故障时,Redis Sentinel能自动完成故障发现和故障转移,并通知应用方,从而实现真正的高可用。

​ Redis Sentinel是一个分布式架构,其中包含若干个Sentinel节点和Redis数据节点,每个Sentinel节点会对数据节点和其余Sentinel节点进行监控,当它发现节点不可达时,会对节点做下线标识。如果被标识的是主节点,它还会和其他Sentinel节点进行“协商”,当大多数Sentinel节点都认为主节点不可达时,它们会选举出一个Sentinel节点来完成自动故障转移的工作,同时会将这个变化实时通知给Redis应用方。整个过程完全是自动的,不需要人工来介入,所以这套方案很有效地解决了Redis的高可用问题。

注意:

这里的分布式是指:Redis数据节点、Sentinel节点集合、客户端分布在多个物理节点的架构

​ 从逻辑架构上看,Sentinel节点集合会定期对所有节点进行监控,特别是对主节点的故障实现自动转移

2.以1个主节点、2个从节点、3个Sentinel节点组成的Redis Sentinel为例子进行说明,拓扑结构如图所示:

​ Redis Sentinel拓扑结构

整个故障转义的处理逻辑有以下4个步骤:

1)如图所示,主节点出现故障,此时两个从节点与主节点失去连接,主从复制失败

​ 主节点故障

2)如图所示,每个Sentinel节点通过定期监控发现主节点出现了故障。

​ Sentinel节点集合发现主节点故障

3)如图所示,多个Sentinel节点对主节点的故障达成一致,选举出sentinel-3节点作为领导者负责故障转移。

​ Redis Sentinel对主节点故障转移

4)如图所示,Sentinel领导者节点执行了故障转移

5)故障转移后整个Redis Sentinel的拓扑结构图

3.通过上面介绍的Redis Sentinel逻辑架构以及故障转移的处理,可以看出Redis Sentinel具有以下几个功能:

·监控:Sentinel节点会定期检测Redis数据节点、其余Sentinel节点是否可达。
·通知:Sentinel节点会将故障转移的结果通知给应用方。
·主节点故障转移:实现从节点晋升为主节点并维护后续正确的主从关系。
·配置提供者:在Redis Sentinel结构中,客户端在初始化的时候连接的是Sentinel节点集合,从中获取主节点信息。

4.Redis Sentinel包含了若个Sentinel节点,这样做也带来了两个好处:

·对于节点的故障判断是由多个Sentinel节点共同完成,这样可以有效地防止误判。
·Sentinel节点集合是由若干个Sentinel节点组成的,这样即使个别Sentinel节点不可用,整个Sentinel节点集合依然是健壮的

5.Sentinel节点本身就是独立的Redis节点,只不过它们有一些特殊,它们不存储数据,只支持部分命令。

9.2 安装和部署

9.2.1 部署拓扑结构

​ Redis Sentinel物理结构

9.2.2 部署Redis数据节点

1.启动主节点

配置:
redis-6379.conf
port 6379
daemonize yes
logfile "6379.log"
dbfilename "dump-6379.rdb"
dir "/opt/soft/redis/data/"
启动:
redis-server redis-6379.conf

确认是否启动。一般来说只需要ping命令检测一下就可以,确认Redis数据节点是否已经启动。

$ redis-cli -h 127.0.0.1 -p 6379 ping
PONG

2.配置两个从节点

redis-6380.conf
port 6380
daemonize yes
logfile "6380.log"
dbfilename "dump-6380.rdb"
dir "/opt/soft/redis/data/"
slaveof 127.0.0.1 6379

redis-6381.conf
port 6381
daemonize yes
logfile "6381.log"
dbfilename "dump-6381.rdb"
dir "/opt/soft/redis/data/"
slaveof 127.0.0.1 6379

启动两个从节点:
redis-server redis-6380.conf
redis-server redis-6381.conf

9.2.3 部署Sentinel节点

3个Sentinel节点的部署方法是完全一致的(端口不同),下面以sentinel-1节点的部署为例子进行说明:

1.配置Sentinel节点

redis-sentinel-26379.conf
port 26379
daemonize yes
logfile "26379.log"
dir /opt/soft/redis/data
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000

说明:

1)Sentinel节点的默认端口是26379。
2)sentinel monitor mymaster127.0.0.163792配置代表sentinel-1节点需要监控127.0.0.1:6379这个主节点,2代表判断主节点失败至少需要2个Sentinel节点同意,mymaster是主节点的别名

2.启动Sentinel节点

方法一:

redis-sentinel redis-sentinel-26379.conf

方式二:

redis-server redis-sentinel-26379.conf --sentinel

两种方式本质是一样的

3.确认:

Sentinel节点本质上是一个特殊的Redis节点,所以也可以通过info命令来查询它的相关信息,从下面info的Sentinel片段来看,Sentinel节点找到了主节点127.0.0.1:6379,发现了它的两个从节点,同时发现Redis Sentinel一共有3个Sentinel节点。
$ redis-cli -h 127.0.0.1 -p 26379 info Sentinel
# Sentinel
sentinel_masters:1
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3

当三个Sentinel节点都启动后,整个拓扑结构如图:

​ Redis Sentinel最终拓扑结构

建议:

1)生产环境中建议Redis Sentinel的所有节点应该分布在不同的物理机上
2)Redis Sentinel中的数据节点和普通的Redis数据节点在配置上没有任何区别,只不过是添加了一些Sentinel节点对它们进行监控。

9.2.4 配置优化

  1. 配置说明和优化
1) port和dir分别代表Sentinel节点的端口和工作目录
2) sentinel monitor
    sentinel monitor <master-name> <ip> <port> <quorum>
    Sentinel节点会定期监控主节点,所以从配置上必然也会有所体现,本配置说明Sentinel节点要监控的是一个名字叫做<master-name>,ip地址和端口为<ip><port>的主节点。<quorum>代表要判定主节点最终不可达所需要的票数。但实际上Sentinel节点会对所有节点进行监控,但是在Sentinel节点的配置中没有看到有关从节点和其余Sentinel节点的配置,那是因为Sentinel节点会从主节点中获取有关从节点以及其余Sentinel节点的相关信息
    对于<quorum> 一般建议设置为Sentinel节点的一半加1
    同时<quorum>还与Sentinel节点的领导者选举有关,至少要max(quorum,num(sentinels)/2+1)个Sentinel节点参与选举,才能选出领导者Sentinel,从而完成故障转移
3) sentinel down-after-milliseconds
    sentinel down-after-milliseconds <master-name> <times>
    每个Sentinel节点都要通过定期发送ping命令来判断Redis数据节点和其余Sentinel节点是否可达,如果超过了down-after-milliseconds配置的时间且没有有效的回复,则判定节点不可达
4) sentinel parallel-syncs
    sentinel parallel-syncs <master-name> <nums>
    当Sentinel节点集合对主节点故障判定达成一致时,Sentinel领导者节点会做故障转移操作,选出新的主节点,原来的从节点会向新的主节点发起复制操作,parallel-syncs就是用来限制在一次故障转移之后,每次向新的主节点发起复制操作的从节点个数。如果这个参数配置的比较大,那么多个从节点会向新的主节点同时发起复制操作,尽管复制操作通常不会阻塞主节点,但是同时向主节点发起复制,必然会对主节点所在的机器造成一定的网络和磁盘IO开销。
5) sentinel failover-timeout
    sentinel failover-timeout <master-name> <times>
    failover-timeout通常被解释成故障转移超时时间,但实际上它作用于故障转移的各个阶段:
    a)选出合适从节点。
    b)晋升选出的从节点为主节点。
    c)命令其余从节点复制新的主节点。
    d)等待原主节点恢复后命令它去复制新的主节点。
    failover-timeout的作用具体体现在四个方面:
    1)如果Redis Sentinel对一个主节点故障转移失败,那么下次再对该主节点做故障转移的起始时间是failover-timeout的2倍。
    2)在b)阶段时,如果Sentinel节点向a)阶段选出来的从节点执行slaveof no one一直失败(例如该从节点此时出现故障),当此过程超过failover-timeout时,则故障转移失败。
    3)在b)阶段如果执行成功,Sentinel节点还会执行info命令来确认a)阶段选出来的节点确实晋升为主节点,如果此过程执行时间超过failover-timeout时,则故障转移失败。
    4)如果c)阶段执行时间超过了failover-timeout(不包含复制时间),则故障转移失败。注意即使超过了这个时间,Sentinel节点也会最终配置从节点去同步最新的主节点。

6)sentinel auth-pass
    sentinel auth-pass <master-name> <password>
    如果Sentinel监控的主节点配置了密码,sentinel auth-pass配置通过添加主节点的密码,防止Sentinel节点对主节点无法监控。
7) sentinel notification-script
    sentinel notification-script <master-name> <script-path>
    sentinel notification-script的作用是在故障转移期间,当一些警告级别的Sentinel事件发生(指重要事件,例如-sdown:客观下线、-odown:主观下线)时,会触发对应路径的脚本,并向脚本发送相应的事件参数。
    例如在/opt/redis/scripts/下配置了notification.sh,该脚本会接收每个Sentinel节点传过来的事件参数,可以利用这些参数作为邮件或者短信报警依据:
    eg: sentinel notification-script mymaster /opt/redis/scripts/notification.sh
8) sentinel client-reconfig-script
    sentinel client-reconfig-script <master-name> <script-path>
    sentinel client-reconfig-script的作用是在故障转移结束后,会触发对应路径的脚本,并向脚本发送故障转移结果的相关参数

    当故障转移结束后每个Sentinel节点会将故障转移的结果发送给对应的脚本
    <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
    ·<master-name>:主节点名。
    ·<role>:Sentinel节点的角色,分别是leader和observer,leader代表当前Sentinel节点是领导者,是它进行的故障转移;observer是其余Sentinel节点。
    ·<from-ip>:原主节点的ip地址。
    ·<from-port>:原主节点的端口。
    ·<to-ip>:新主节点的ip地址。
    ·<to-port>:新主节点的端口。

    有关7和8的脚本需要注意:
    ·<script-path>必须有可执行权限
    ·<script-path>开头必须包含shell脚本头(例如#!/bin/sh),否则事件发生时Redis将无法执行脚本产生如下错误
    ·Redis规定脚本的最大执行时间不能超过60秒,超过后脚本将被杀掉。
    ·如果shell脚本以exit 1结束,那么脚本稍后重试执行。如果以exit 2或者更高的值结束,那么脚本不会重试。正常返回值是exit 0。
    ·如果需要运维的Redis Sentinel比较多,建议不要使用这种脚本的形式来进行通知,这样会增加部署的成本。
  1. 如何监控多个主节点

Redis Sentinel可以同时监控多个主节点,具体拓扑图类似于下图

配置方法也比较简单,只需要指定多个masterName来区分不同的主节点即可:

port 26379
daemonize yes
logfile "26379.log"
dir /opt/soft/redis/data
sentinel monitor mymaster1 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000

sentinel monitor mymaster2 127.0.0.1 6479 2
sentinel down-after-milliseconds mymaster 30000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 180000
  1. 调整配置

和普通的Redis数据节点一样,Sentinel节点也支持动态地设置参数:

sentinel set <param> <value>

下表是sentinel set命令支持的参数。

注意:

1)sentinel set命令只对当前Sentinel节点有效。
2)sentinel set命令如果执行成功会立即刷新配置文件,这点和Redis普通数据节点设置配置需要执行config rewrite刷新到配置文件不同。
3)建议所有Sentinel节点的配置尽可能一致,这样在故障发现和转移时比较容易达成一致。
4)表9中为sentinel set支持的参数,具体可以参考源码中的sentinel.c的sentinelSetCommand函数。
5)Sentinel对外不支持config命令。

9.2.5 部署技巧

1)Sentinel节点不应该部署在一台物理“机器”上。

​ 这里的物理机器是指的单台实际的物理机,非虚拟机器

2)部署至少三个且奇数个的Sentinel节点

​ 3个以上是通过增加Sentinel节点的个数提高对于故障判定的准确性,因为领导者选举需 要至少一半加1个节点,奇数个节点可以在满足该条件的基础上节省一个节点。

3)只有一套Sentinel,还是每个主节点配置一套Sentinel?

​ Sentinel节点集合可以只监控一个主节点,也可以监控多个主节点,也就意味着部署拓扑 可能是图1和图2两种情况

​ 一套Sentinel节点集合多个主节点

​ 每个主节点一套Sentinel

两种方案比较:

    方案一: 一套Sentinel,很明显这种方案在一定程度上降低了维护成本,因为只需要维护固定个数的Sentinel节点,集中对多个Redis数据节点进行管理就可以了。但是这同时也是它的缺点,如果这套Sentinel节点集合出现异常,可能会对多个Redis数据节点造成影响。还有如果监控的Redis数据节点较多,会造成Sentinel节点产生过多的网络连接,也会有一定的影响。
    方案二: 多套Sentinel,显然这种方案的优点和缺点和上面是相反的,每个Redis主节点都有自己的Sentinel节点集合,会造成资源浪费。但是优点也很明显,每套Redis Sentinel都是彼此隔离的。

提示:

如果Sentinel节点集合监控的是同一个业务的多个主节点集合,那么使用方案一、否则一般建议采用方案二。

9.3 API

1.sentinel masters

展示所有被监控的主节点状态以及相关的统计信息

2.sentinel master

展示指定的主节点状态以及相关的统计信息

127.0.0.1:26379> sentinel master mymaster-1
1) "name"
2) "mymaster-1"
3) "ip"
4) "127.0.0.1"
5) "port"
6) "6379"
......... 忽略 ............

3.sentinel slaves

展示指定的从节点状态以及相关的统计信息

4.sentinel sentinels

展示指定的Sentinel节点集合

5.sentinel get-master-addr-by-name

返回指定主节点的IP地址和端口

6.sentinel reset

当前Sentinel节点对符合(通配符风格)主节点的配置进行重置,包含清除主节点的相关状态(例如故障转移),重新发现从节点和Sentinel节点。

7.sentinel failover

对指定主节点进行强制故障转移(没有和其他Sentinel节点“协商”),当故障转移完成后,其他Sentinel节点按照故障转移的结果更新自身配置。

8.sentinel ckquorum

检测当前可达的Sentinel节点总数是否达到的个数。

9.sentinel flushconfig

将Sentinel节点的配置强制刷到磁盘上,这个命令Sentinel节点自身用得比较多,对于开发和运维人员只有当外部原因(例如磁盘损坏)造成配置文件损坏或者丢失时,这个命令是很有用的。

10.sentinel remove

取消当前Sentinel节点对于指定主节点的监控。

11.sentinel monitor

这个命令和配置文件中的含义是完全一样的,只不过是通过命令的形式来完成Sentinel节点对主节点的监控。

12.sentinel set

动态修改Sentinel节点配置选项

13.sentinel is-master-down-by-addr

Sentinel节点之间用来交换对主节点是否下线的判断

9.4 客户端连接

9.4.1 Redis Sentinel客户端基本实现原理:

1)遍历Sentinel节点集合获取一个可用的Sentinel节点,后面会介绍Sentinel节点之间可以共享数据,所以从任意一个Sentinel节点获取主节点信息都是可以的,如图9-1所示。
2)通过sentinel get-master-addr-by-name master-name这个API来获取对应主节点的相关信息,如图9-2所示。
3)验证当前获取的“主节点”是真正的主节点,这样做的目的是为了防止故障转移期间主节点的变化,如图9-3所示。
4)保持和Sentinel节点集合的“联系”,时刻获取关于主节点的相关“信息”,如图9-4所示。

​ 图9-1

​ 图9-2

​ 图9-3

​ 图9-4

9.4.2 Python 操作Redis Sentinel

9.5 实现原理

9.5.1 三个定时任务

1)每隔10秒,每个Sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构如图:

该定时任务作用:

·通过向主节点执行info命令,获取从节点的信息,这也是为什么Sentinel节点不需要显式配置监控从节点。
·当有新的从节点加入时都可以立刻感知出来。
·节点不可达或者故障转移后,可以通过info命令实时更新节点拓扑信息。

2)每隔2秒,每个Sentinel节点会向Redis数据节点的__sentinel__:hello频道上发送该Sentinel节点对于主节点的判断以及当前Sentinel节点的信息如下图:

该定时任务作用:

·发现新的Sentinel节点:通过订阅主节点的__sentinel__:hello了解其他的Sentinel节点信息,如果是新加入的Sentinel节点,将该Sentinel节点信息保存起来,并与该Sentinel节点创建连接。
·Sentinel节点之间交换主节点的状态,作为后面客观下线以及领导者选举的依据。

3)每隔1秒,每个Sentinel节点会向主节点、从节点、其余Sentinel节点发送一条ping命令做一次心跳检测,来确认这些节点当前是否可达。如图所示:

该定时任务作用:

通过上面的定时任务,Sentinel节点对主节点、从节点、其余Sentinel节点都建立起连接,实现了对每个节点的监控,这个定时任务是节点失败判定的重要依据。

9.5.2 主观下线和客观下线

1.主观下线:

每个Sentinel节点会每隔1秒对主节点、从节点、其他Sentinel节点发送ping命令做心跳检测,当这些节点超过down-after-milliseconds没有进行有效回复,Sentinel节点就会对该节点做失败判定,这个行为叫做主观下线,主观下线是当前节点的一家之言,存在误判的可能

2.客观下线:

当Sentinel主观下线的节点是主节点时,该Sentinel节点会通过sentinel is-master-down-by-addr命令向其他Sentinel节点询问对主节点的判断,当超过<quorum>个数,Sentinel节点认为主节点确实有问题,这时该Sentinel节点会做出客观下线的决定,这样客观下线的含义是比较明显了,也就是大部分Sentinel节点都对主节点的下线做了同意的判定,那么这个判定就是客观的

9.5.3 领导者的选举

选举大致流程如下:

1)每个在线的Sentinel节点都有资格成为领导者,当它确认主节点主观下线时候,会向其他Sentinel节点发送sentinel is-master-down-by-addr命令,要求将自己设置为领导者。
2)收到命令的Sentinel节点,如果没有同意过其他Sentinel节点的sentinelis-master-down-by-addr命令,将同意该请求,否则拒绝。
3)如果该Sentinel节点发现自己的票数已经大于等于max(quorum,num(sentinels)/2+1),那么它将成为领导者。
4)如果此过程没有选举出领导者,将进入下一次选举。

9.5.4 故障转移

领导者选举出的Sentinel节点负责故障转移,具体步骤如下:

a)过滤:“不健康”(主观下线、断线)、5秒内没有回复过Sentinel节点ping响应、与主节点失联超过down-after-milliseconds*10秒。
b)选择slave-priority(从节点优先级)最高的从节点列表,如果存在则返回,不存在则继续。
c)选择复制偏移量最大的从节点(复制的最完整),如果存在则返回,不存在则继续。
d)选择runid最小的从节点。

9.6 开发与运维中的问题(暂略)

9.6.1 故障转移日志分析

10. 集群

10.1 数据分布

10.1.1 数据分布理论

  1. 分布式数据库,就是按照分区规则把整个数据集映射到多个节点,如下图:

​ 分布式存储数据分区

  1. 分区规则有哈希分区和顺序分区,对比如下:

    ​ 哈希分区和顺序分区对比

常见的哈希分区规则有如下几种:

1) 节点取余分区:

    使用特定的数据,如Redis的键或用户ID,再根据节点数量N使用公式:hash(key)%N计算出哈希值,用来决定数据映射到哪一个节点上。这种方案存在一个问题:当节点数量变化时,如扩容或收缩节点,数据节点映射关系需要重新计算,会导致数据的重新迁移。
    这种方式的突出优点是简单性,常用于数据库的分库分表规则,一般采用预分区的方式,提前根据数据量规划好分区数,比如划分为512或1024张表,保证可支撑未来一段时间的数据量,再根据负载情况将表迁移到其他数据库中。扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况

​ 翻倍扩容迁移约50%数据

2)一致性哈希分区

    一致性哈希分区(Distributed Hash Table)实现思路是为系统中每个节点分配一个token,范围一般在0~2 32 ,这些token构成一个哈希环。数据读写执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于等于该哈希值的token节点
    这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。但一致性哈希分区存在几个问题:
    ·加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略这部分数据,因此一致性哈希常用于缓存场景。
    ·当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案。
    ·普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。

​ 一致性哈希数据分布

3)虚拟槽分区

    虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围一般远远大于节点数,比如Redis Cluster槽范围是0~16383。槽是集群内数据管理和迁移的基本单位。采用大范围槽的主要目的是为了方便数据拆分和集群扩展。每个节点会负责一定数量的槽,如下图所示。
    当前集群有5个节点,每个节点平均大约负责3276个槽。由于采用高质量的哈希算法,每个槽所映射的数据通常比较均匀,将数据平均划分到5个节点进行数据分区。Redis Cluster就是采用虚拟槽分区,

​ 槽集合与节点关系

​ 使用CRC16(key)&16383将键映射到槽上

Redis虚拟槽分区的特点:

·解耦数据和节点之间的关系,简化了节点扩容和收缩难度。
·节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。
·支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景。

10.1.2 集群功能限制

1)key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的key执行批量操作。对于映射为不同slot值的key由于执行mget、mget等操作可能存在于多个节点上因此不被支持。
2)key事务操作支持有限。同理只支持多key在同一节点上的事务操作,当多个key分布在不同的节点上时无法使用事务功能。
3)key作为数据分区的最小粒度,因此不能将一个大的键值对象如hash、list等映射到不同的节点。
4)不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模式下只能使用一个数据库空间,即db0。
5)复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。

10.2 集群搭建

10.2.1 准备节点

Redis集群一般由多个节点组成,节点数量至少为6个才能保证组成完整高可用的集群。每个节点需要开启配置cluster-enabled yes,让Redis运行在集群模式下。建议为集群内所有节点统一目录,一般划分三个目录:conf、data、log,分别存放配置、数据和日志相关文件。把6个节点配置统一放在conf目录下

1.新建目录:

/home/lmc/cluster-redis/data
/home/lmc/cluster-redis/log
/home/lmc/cluster-redis/conf

2.在conf目录下新建6个redis-server配置文件

port 6379
daemonize yes
logfile "/home/lmc/redis-cluster/log/6379.log"
dbfilename "dump-6379.rdb"
dir "/home/lmc/redis-cluster/data/"
# 开启集群模式
cluster-enabled yes
# 节点超时时间,单位毫秒
cluster-node-timeout 15000
# 集群内部配置文件
cluster-config-file "nodes-6379.conf"

每个配置文件参数都一样,除了端口号,和文件名不一样

3.启动服务

redis-server redis-6379.conf
redis-server redis-6380.conf
redis-server redis-6381.conf
redis-server redis-6382.conf
redis-server redis-6383.conf
redis-server redis-6384.conf

4.检查日志文件是否正确

cat log/redis-6379.log
* No cluster configuration found, I'm cfb28ef1deee4e0fa78da86abe5d24566744411e
# Server started, Redis version 3.0.7
* The server is now ready to accept connections on port 6379

以上代表启动成功,如果启动成功,没有集群配置文件则会自动生成文件名采用cluster-config-file参数项控制,建议采用node-{port}.conf格式定义,通过端口号区分不同节点

过程如下:

5.节点握手

127.0.0.1:6379>
cluster meet 127.0.0.1 6380
cluster meet 127.0.0.1 6381
cluster meet 127.0.0.1 6382
cluster meet 127.0.0.1 6383
cluster meet 127.0.0.1 6384

通过握手后,集群内的所有redis-server都会彼此感知:

6.分配槽:

redis-cli -h 127.0.0.1 -p 6379 cluster addslots {0..5461}
redis-cli -h 127.0.0.1 -p 6380 cluster addslots {5462..10922}
redis-cli -h 127.0.0.1 -p 6381 cluster addslots {10923..16383}

分配槽后集群即可进入在线状态,上面三个redis已经进入在线状态,另外三个则需要设置为从服务,作为备份redis

7.建立主从复制

127.0.0.1:6382>cluster replicate cfb28ef1deee4e0fa78da86abe5d24566744411e
OK
127.0.0.1:6383>cluster replicate 8e41673d59c9568aa9d29fb174ce733345b3e8f1
OK
127.0.0.1:6384>cluster replicate 40b8d09d44294d2e23c7c768efc8fcd153446746
OK

8.上面几个步骤,集群建立,如果需要redis-cli客户端进行测试,则需要加-c参数

redis-cli -c -p 6379
127.0.0.1:6379> set java python
OK

127.0.0.1:6380> get java
-> Redirected to slot [858] located at 127.0.0.1:6379
"python"

10.2.2 用redis-trib.rb搭建集群

注意自从redis5.0后redis-trib.rb将不再可用,可用redis-cli代替redis-trib.rb来创建集群

  1. 创建6个实例配置文件:
  2. 启动每一个实例
  3. 搭建集群:
redis-cli --cluster create 127.0.0.1:6379 127.0.0.1:6380 127.0.0.1:6381 127.0.0.1:6382 127.0.0.1:6383 127.0.0.1:6384 --cluster-replicas 1
  1. 检查槽分布:
redis-cli --cluster check 127.0.0.1 6379
12-27 11:18
查看更多