Redis入门指南 --day006

接上一章:Redis入门指南 --day005
管道

  1. 客户端和 Redis使用TCP协议连接。不论是客户端向Redis发送命令还是Redis向客户端返回命令的执行结果,都需要经过网络传输,这两个部分的总耗时称为往返时延。根据网络性能不同,往返时延也不同,大致来说到本地回环地址(loopback address)的往返时延在数量级上相当于Redis处理一条简单命令(如LPUSH list123)的时间。如果执行较多的命令,每个命令的往返时延累加起来对性能还是有一定影响的。
  2. 在执行多个命令时每条命令都需要等待上一条命令执行完(即收到Redis的返回结果)才能执行,即使命令不需要上一条命令的执行结果。如要获得post:1、post:2和post:3这3个键中的title字段,需要执行3条命令
  3. Redis的底层通信协议对管道(pipelining)提供了支持。通过管道可以一次性发送多条命令并在执行完后一次性将结果返回,当一组命令中每条命令都不依赖于之前命令的执行结果时就可以将这组命令一起通过管道发出。管道通过减少客户端与Redis的通信次数来实现降低往返时延累计值的目的,如图4-3所示。
    在这里插入图片描述
    节省空间 优化内存
    1 精简键名和键值:
    精简键名和键值是最直观的减少内存占用的方式,如将键名 very.important.person:20改成VIP:20。当然精简键名一定要把握好尺度,不能单纯为了节约空间而使用不易理解的键名(比如将VIP:20修改为 V:20,这样既不易维护,还容易造成命名冲突)。又比如一个存储用户性别的字符串类型键的取值是male和 female,我们可以将其修改成m和f来为每条记录节约几个字节的空间(更好的方法是使用0和1来表示性别)。
    2 内部编码优化:
    有时候仅凭精简键名和键值所减少的空间并不足以满足需求,这时就需要根据 Redis内部编码规则来节省更多的空间。Redis为每种数据类型都提供了两种内部编码方式,以散列类型为例,散列类型是通过散列表实现的,这样就可以实现O(1)时间复杂度的查找、赋值操作,然而当键中元素很少的时候,O(1)的操作并不会比O(n)有明显的性能提高,所以这种情况下Redis会采用一种更为紧凑但性能稍差(获取元素的时间复杂度为O(n))的内部编码方式。内部编码方式的选择对于开发者来说是透明的,Redis会根据实际情况自动调整。当键中元素变多时Redis会自动将该键的内部编码方式转换成散列表。如果想查看一个键的内部编码方式可以使用OBJECT ENCODING命令,例如:
127.0.0.1:6379> set foo 123
OK
127.0.0.1:6379> object encoding foo
"int"

Redis的每个键值都是使用一个 redisobject结构体保存的,redisObject 的定义如下:

typedef struct redis0bject{
    unsigned type:4;
    unsigned notused : 2;
    unsigned encoding : 4;
    unsigned lru: 22;
    int refcount;
    void *ptr;
} robj;
-- 其中 type字段表示的是键值的数据类型,取值可以是如下内容:
#define REDIS_STRING  0  
#define REDIS_LIST    1
#define REDIS_SET     2
#define REDIS_ZSET    3
#define REDIS_HASH    4
-- encoding字段表示的就是Redis键值的内部编码方式

在这里插入图片描述
1. 字符串类型
Redis使用一个sdshdr类型的变量来存储字符串,而redisobject的ptr字段指向的是该变量的地址。sdshdr的定义如下:

struct sdshdr {
    int len;
    int free;
    char buf[];
};  

其中len字段表示的是字符串的长度,free字段表示buf中的剩余空间,而buf字段存储的才是字符串的内容。所以当执行SET key foobar时,存储键值需要占用的空间是sizeof(redis0bject)+sizeof (sdshdr) + strlen (“foobar”)=30字节而当键值内容可以用一个64位有符号整数表示时,Redis会将键值转换成long类型来存储。如 SET key 123456,实际占用的空间是sizeof(redisObject)= 16字节,比存储"foobar"节省了一半的存储空间,如图4-5所示。
在这里插入图片描述
redis0bject中的refcount字段存储的是该键值被引用数量,即一个键值可以被多个键引用。Redis启动后会预先建立10000个分别存储从0到9999这些数字的redisObject类型变量作为共享对象,如果要设置的字符串键值在这10000个数字内(如SET key1 123)则可以直接引用共享对象而不用再建立一个redis0bject 了,也就是说存储键值占用的空间是0字节,如图4-6所示。
由此可见,使用字符串类型键存储对象ID这种小数字是非常节省存储空间的,Redis只需存储键名和一个对共享对象的引用即可。
在这里插入图片描述
注意!!! 当通过配置文件参数maxmemory 设置了Redis 可用的最大空间大小时,Redis不会使用共享对象,因为对于每一个键值都需要使用一个redis0bject来记录其LRU信息。
2. 散列类型
散列类型的内部编码方式可能是 REDIS_ENCODING_HT或REDIS_ENCODINGZIPLIST。
在配置文件中可以定义使用REDIS_ENCODING_ZIPLIST方式编码散列类型的时机:

hash-max-ziplist-entries 512
hash-max-ziplist-value 64

当散列类型键的字段个数少于hash-max-ziplist-entries参数值且每个字段名和字段值的长度都小于hash-max-ziplist-value参数值(单位为字节)时,Redis就会使用REDIS_ ENCODING_ZIPLIST来存储该键,否则就会使用REDIS_ENCODING_HT。转换过程是透明的,每当键值变更后Redis 都会自动判断是否满足条件来完成转换。
REDIS_ENCODING_HT编码即散列表,可以实现O(1)时间复杂度的赋值取值等操作,其字段和字段值都是使用redisobject存储的,所以前面讲到的字符串类型键值的优化方法同样适用于散列类型键的字段和字段值。
不宜将 hash-max-ziplist-entries 和hash-max-ziplist-value两个参数设置得很大。
3.列表类型
列表类型的内部编码方式可能是REDIS_ENCODING_LINKEDLIST 或REDIS ENCODINGZIPLIST。
同样在配置文件中可以定义使用REDIS_ENCODING_ZIPLIST方式编码的时机:

list-max-ziplist-entries 512
list-max-ziplist-value 64

REDIS_ENCODING_LINKEDLIST编码万式即双问链表,链表中的每个儿素是用redis0bject存储的,所以此种编码方式下元素值的优化方法与字符串类型的键值相同。
而使用REDIS_ENCODING_ZIPLIST编码方式时具体的表现和散列类型一样,由于REDIS ENCODING_ZIPLIST 编码方式同样支持倒序访问,所以采用此种编码方式时获取两端的数据依然较快。
Redis最新的开发版本新增了REDIS_ENCODING_QUICKLIST 编码方式,该编码方式是REDIS ENCODING_LINKEDLIST 和 REDIS_ENCODING_ZIPLIST的结合,其原理是将一个长列表分成若干个以链表形式组织的ziplist,从而达到减少空间占用的同时提升REDIS_ ENCODING_ZIPLIST编码的性能的效果。

4.集合类型
集合类型的内部编码方式可能是 REDIS_ENCODING_HT或REDIS_ENCODINGINTSET。当集合中的所有元素都是整数且元素的个数小于配置文件中的set-max-intset-entries参数指定值(默认是512)时 Redis会使用REDTS_ENCODINGINTSET 编码存储该集合,否则会使用REDIS_ENCODING_HT来存储。
REDIS_ ENCODING INTSET编码存储结构体intset的定义是:

typedef struct intset {
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

其中 contents存储的就是集合中的元素值,根据encoding 的不同,每个元素占用的字节大小不同。默认的encoding是 INTSET_ENC_INT16(即2个字节),当新增加的整数元素无法使用2个字节表示时,Redis会将该集合的 encoding 升级为INTSET_ENC_INT32(即4个字节)并调整之前所有元素的位置和长度,同样集合的encoding还可升级为INTSET_ENC_INT64(即8个字节)。
REDIS_ENCODING_INTSET编码以有序的方式存储元素(所以使用SMEMBERS命令获得的结果是有序的),使得可以使用二分算法查找元素。然而无论是添加还是删除元素,Redis都需要调整后面元素的内存位置,所以当集合中的元素太多时性能较差。
当新增加的元素不是整数或集合中的元素数量超过了set-max-intset-entries参数指定值时,Redis会自动将该集合的存储结构转换成REDIS ENCODING HT。

5. 有序集合类型
有序集合类型的内部编码方式可能是REDIS_ENCODING_SKIPLIST或 REDISENCODING ZIPLIST。同样在配置文件中可以定义使用REDIS_ENCODING_ZIPLIST 方式编码的时机:
zset-max-ziplist-entries 128
zset-max-ziplist-value64
具体规则和散列类型及列表类型一样,不再赘述。
当编码方式是 REDIS_ENCODING_SKIPLIST时,Redis 使用散列表和跳跃列表(skiplist)两种数据结构来存储有序集合类型键值,其中散列表用来存储元素值与元素分数的映射关系以实现O(1)时间复杂度的zSCORE等命令。跳跃列表用来存储元素的分数及其到元素值的映射以实现排序的功能。Redis’对跳跃列表的实现进行了几点修改,其中包括允许跳跃列表中的元素(即分数)相同,还有为跳跃链表每个节点增加了指向前一个元素的指针以实现倒序查找。
采用此种编码方式时,元素值是使用redisobject存储的,所以可以使用字符串类型键值的优化方式优化元素值,而元素的分数是使用double类型存储的。
使用REDIS_ENCODING_ZIPLIST编码时有序集合存储的方式按照“元素1的值,元素1的分数,元素2的值,元素2的分数”的顺序排列,并且分数是有序的。

Redis 持久化
Redis支持两种方式的持久化,一种是 RDB方式,另一种是AOF 方式。前者会根据指定的规则“定时”将内存中的数据存储在硬盘上,而后者在每次执行命令后将命令本身记录下来。两种持久化方式可以单独使用其中一种,但更多情况下是将二者结合使用。
2. RDB 方式
RDB方式的持久化是通过快照(snapshotting)完成的,当符合一定条件时Redis 会自动将内存中的所有数据生成一份副本并存储在硬盘上,这个过程即为“快照”。

Redis 会在以下几种情况下对数据进行快照:

  1. 根据配置规则进行自动快照;

  2. 用户执行SAVE或BGSAVE命令;

  3. 执行FLUSHALL命令;

  4. 执行复制时;

根据配置规则进行自动快照:
Redis允许用户自定义快照条件,当符合快照条件时,Redis 会自动执行快照操作。进行快照的条件可以由用户在配置文件中自定义,由两个参数构成:时间窗口M和改动的键的个数N。每当时间M内被更改的键的个数大于N时,即符合自动快照条件。例如Redis安装目录中包含的样例配置文件中预置的3个条件:

save 900  1
save 300  10
save 60   10000

每条快照条件占一行,并且以 save参数开头。同时可以存在多个条件,条件之间是“或”的关系。就这个例子而言,save 900 1的意思是在15分钟(900秒)内有一个或一个以上的键被更改则进行快照。同理,save 300 10表示在300秒内至少有10个键被修改则进行快照。
用户执行SAVE或BGSAVE命令
除了让 Redis自动进行快照外,当进行服务重启、手动迁移以及备份时我们也会需要手动执行快照操作。Redis提供了两个命令来完成这一任务。
1. SAVE命令:
当执行SAVE命令时,Redis同步地进行快照操作,在快照执行的过程中会阻塞所有来自客户端的请求。当数据库中的数据比较多时,这一过程会导致 Redis 较长时间不响应,所以要尽量避免在生产环境中使用这一命令。
2. BGSAVE命令:
需要手动执行快照时推荐使用 BGSAVE命令。BGSAVE命令可以在后台异步地进行快照操作,快照的同时服务器还可以继续响应来自客户端的请求。执行 BGSAVE后Redis 会立即返回OK表示开始执行快照操作,如果想知道快照是否完成,可以通过 LASTSAVE 命令获取最近一次成功执行快照的时间,返回结果是一个 Unix时间戳,如

执行FLUSHALL命令
当执行FLUSHAL命令时,Redis会清除数据库中的所有数据。需要注意的是,不论清空数据库的过程是否触发了自动快照条件,只要自动快照条件不为空,Redis就会执行一次快照操作。例如,当定义的快照条件为当1秒内修改10000个键时进行自动快照,而当数据库里只有一个键时,执行FLUSHALL命令也会触发快照,即使这一过程实际上只有一个键被修改了。
当没有定义自动快照条件时,执行FLUSHALL 则不会进行快照。

执行复制时
当设置了主从模式时,Redis会在复制初始化时进行自动快照,这里只需要了解当使用复制操作时,即使没有定义自动快照条件,并且没有手动执行过快照操作,也会生成RDB快照文件。

Redis 快照原理
理清Redis实现快照的过程对我们了解快照文件的特性有很大的帮助。Redis默认会将快照文件存储在Redis当前进程的工作目录中的dump.rdb文件中,可以通过配置dir和dbfilename 两个参数分别指定快照文件的存储路径和文件名。快照的过程如下。
(1) Redis 使用fork函数复制一份当前进程(父进程)的副本(子进程);
(2)父进程继续接收并处理客户端发来的命令,而子进程开始将内存中的数据写入硬盘中的临时文件;
(3)当子进程写入完所有数据后会用该临时文件替换旧的RDB 文件,至此一次快照操作完成。

通过上述过程可以发现Redis在进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候RDB 文件都是完整的。这使得我们可以通过定时备份RDB 文件来实现 Redis 数据库备份。RDB文件是经过压缩(可以配置rdbcompression参数以禁用压缩节省CPU占用)的二进制格式,所以占用的空间会小于内存中的数据大小,更加利于传输。
Redis启动后会读取RDB快照文件,将数据从硬盘载入到内存。根据数据量大小与结构和服务器性能不同,这个时间也不同。通常将一个记录1000万个字符串类型键、大小为1GB的快照文件载入到内存中需要花费20~30秒。
通过RDB方式实现持久化,一旦 Redis异常退出,就会丢失最后一次快照以后更改的所有数据。这就需要开发者根据具体的应用场合,通过组合设置自动快照条件的方式来将可能发生的数据损失控制在能够接受的范围。例如,使用Redis存储缓存数据时,丢失最近几秒的数据或者丢失最近更新的几十个键并不会有很大的影响。如果数据相对重要,希望将损失降到最小,则可以使用AOF 方式进行持久化。

AOF 方式
当使用Redis存储非临时数据时,一般需要打开AOF持久化来降低进程中止导致的数据丢失。AOF可以将Redis执行的每一条写命令追加到硬盘文件中,这一过程显然会降低Redis 的性能,但是大部分情况下这个影响是可以接受的,另外使用较快的硬盘可以提高AOF的性能。
开启 AOF
默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数启用:

appendonly yes

开启 AOF 持久化后每执行一条会更改Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的AOF 文件。AOF 文件的保存位置和RDB 文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof,可以通过appendfilename参数修改:

appendfilename appendonly.aof

AOF 实现
AOF 文件以纯文本的形式记录了Redis执行的写命令,例如在开启 AOF 持久化的情况下执行了如下4个命令:

# 开启 AOF  
redis-server /redis.windows.conf
# 执行命令
127.0.0.1:6379> set foo 111
OK
127.0.0.1:6379> set f00 222
OK
127.0.0.1:6379> set foo 222
OK
127.0.0.1:6379> set foo 333
OK
127.0.0.1:6379> get foo
"333"
# 查看 aof 文件
*2
$6
SELECT
$1
0
*3
$3
set
$3
foo
$3
111
*3
$3
set
$3
f00
$3
222
*3
$3
set
$3
foo
$3
222
*3
$3
set
$3
foo
$3
333

可见 AOF 文件的内容正是 Redis客户端向Redis 发送的原始通信协议的内容,从中可见Redis确实只记录了前4条命令。然而这时有一个问题是前set foo 前2条命令其实都是冗余的,因为这两条的执行结果会被第三条命令覆盖。随着执行的命令越来越多,AOF文件的大小也会越来越大,即使内存中实际的数据可能并没有多少。很自然地,我们希望Redis 可以自动优化AOF文件,就上例而言,就是将前两条无用的记录删除,只保留第三条。实际上Redis也正是这样做的,每当达到一定条件时 Redis就会自动重写AOF 文件,这个条件可以在配置文件中设置:

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

auto-aof-rewrite-percentage参数的意义是当目前的AOF 文件大小超过上一次重写时的AOF 文件大小的百分之多少时会再次进行重写,如果之前没有重写过,则以启动时的AOF 文件大小为依据。auto-aof-rewrite-min-size参数限制了允许重写的最小AOF 文件大小,通常在AOF 文件很小的情况下即使其中有很多冗余的命令我们也并不太关心。除了让Redis自动执行重写外我们还可以主动使用BGREWRITEAOF命令手动执行AOF重写。

同步硬盘数据
虽然每次执行更改数据库内容的操作时,AOF 都会将命令记录在AOF 文件中,但是事实上,由于操作系统的缓存机制,数据并没有真正地写入硬盘,而是进入了系统的硬盘缓存。在默认情况下系统每30秒会执行一次同步操作,以便将硬盘缓存中的内容真正地写入硬盘,在这30秒的过程中如果系统异常退出则会导致硬盘缓存中的数据丢失。一般来讲启用AOF 持久化的应用都无法容忍这样的损失,这就需要Redis在写入AOF 文件后主动要求系统将缓存内容同步到硬盘中。在 Redis 中我们可以通过 appendfsync参数设置同步的时机:

#appendfsync  always
appendfsync   everysec
#appendfsync  no

默认情况下Redis采用everysec规则,即每秒执行一次同步操作。always表示每次执行写入都会执行同步,这是最安全也是最慢的方式。no表示不主动进行同步操作,而是完全交由操作系统来做(即每30秒一次),这是最快但最不安全的方式。一般情况下使用默认值everysec就足够了,既兼顾了性能又保证了安全。
Redis 允许同时开启 AOF 和 RDB,既保证了数据安全又使得进行备份等操作十分容易。此时重新启动Redis后 Redis 会使用AOF 文件来恢复数据,因为AOF 方式的持久化可能丢失的数据更少。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值