redis-3

redis学习第三章
事务

  1. 概述
    redis中的事务(transation)是一组命令的集合。事务同命令一样都是redis的最小执行单位,一个事务中的命令要么都执行,要么都不执行。
    事务命令:
    redis > multi//开启事务
    OK
    redis > sadd “user:1:following” 2
    QUEUED
    redis > sadd “user:2:followers” 1
    QUEUED
    redis > EXEC//执行事务
    1).(integer) 1
    2).(integer) 1

  2. 错误处理
    1).语法错误。
    语法错误指命令不存在或命令参数的个数不对。比如:
    redis > multi
    OK
    redis > SET key value
    QUEUED
    redis > SET key
    (error) …
    redis > ERRORCOMMAND key
    (error) …
    redis > EXEC
    (error) …
    跟在multi后面的三个命令,一个是正确的加入了事务,两个是错误的。而只要有一个是语法错误,redis会直接返回错误不会执行命令。
    注意:在redis 2.6.5之前的版本会忽略有语法错误的命令,相当于语法错误的命令没有加入到事务中去,所以,当执行exec命令时,其实事务队列中就一个set key value的命令,它会被执行,并返回结果OK。
    2).运行错误
    运行错误是指在命令执行时错线的错误,比如在散列类型的命令操作集合类型的键,这种错误redis在实际执行之前是无法发现的,所以在事务里这样的命令是会被redis接受并执行的(包括错误命令之后的命令),如:
    redis > multi
    OK
    redis > set key 1
    QUEUED
    redis > sadd key 2
    QUEUED
    redis > set key 3
    QUEUED
    rediis > exec
    1).OK
    2).(error)
    3).OK
    redis > get key
    “3”//会跳过错误执行第三行命令
    redis事务没有关系数据库事务提供的回滚功能。为此开发之必需在事务执行之后自己收拾剩下的烂摊子。

  3. WATCH 命令介绍

可以通过watch命令来实现incr函数以防竞态条件的出现,watch命令可以监控一个键或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行(exec返回nil),监控一直持续到exec命令(所以在multi-exec命令之后,可以修改键的值)。

提示:由于watch命令的作用只是当被监控的键值被修改后阻止之后一个事务的执行,而不能保证其他客户端不修改这一键值,所以我们需要在exec执行失败后重新执行整个函数。

执行exec后会取消对所有键的监控,不过如果不想执行事务,可以通过unwatch来取消对所有键的监控,例如:
对key的值为1时不进行事务操作,但是又不想watch影响下一个事务,使用unwatch来取消对该键的监控

  1. 过期时间

EXPIRE 命令可以设置一个键的过期时间,到时间后redis会自动删除它。

使用方法为 EXPIRE key seconds,其中seconds参数表示键的过期时间,单位是秒。例如:
//设置session:1在15分钟后被删除
redis > expire session:1 900
(integer) 1

expire 命令返回1表示设置成功,返回0表示键不存在或设置失败。

如果想知道某个键几秒后被删除,可以使用ttl命令
redis > ttl session:1
(integer) 800

随着时间的不同,ttl会不断减少,当键不存在时,ttl会返回 -2

如果没有为键设置过期时间,使用ttl会返回 -1(键默认永久存在)
注意: 在2.6版本中,无论键不存在还是键没有过期时间都会返回-1,直到2.8版本后两种情况才会分别返回-2和-1两种结果。

PERSIST 命令可以将键的过期时间恢复成永久的,如果过期时间被成功清除,则返回1 否则返回0

除了PERSIST命令之外,使用set或getset命令为键赋值也会同时清除键的过期时间。

使用expire会重新设置过期时间。
其他对键值的操作命令(INCR、LPUSH、HSET、ZREM等等)都不会影响键的过期时间。

PEXPIRE 命令可以更精确的显示时间,它的单位是毫秒。即PEXPIRE 1000 === EXPIRE 1。同时使用PTTL可以获得毫秒级的生存时间。
注意:如果使用watch命令监测一个拥有过期时间的键,则改建时间到期自动删除并不会被watch命令认为该键被改变了。

情况1:在watch之前设置键a的过期时间,multi事务可以有效执行(set命令)

情况2:在watch之后设置键a的过期时间,事务不会有效执行(set命令),返回nil

EXPIREATPEXPIREAT 命令,使用unix时间戳作为参数,两者的区别在一个一个是秒,一个是毫秒。
例如设置a 在2019年5月28日 0:0:0 到期
redis > expireat a 1558972800
(integer) 1

  1. 访问频率限制

通过expire可以来限制访问频率,
实现1:1分钟内访问100次
通过设置访问键的登录次数,生成键的时候同时生成过期时间60s,然后当键存在的时候执行插入操作,伪代码如下:

$isKeyExists = EXISTS rate.limiting:$IP
    if $isKeyExists is 1
       $times = INCR rate.limiting:$IP
       if $times > 100
       		print 禁止访问
       		exit
    else 
        MULTI
        INCR rate.limiting.$IP
        EXPIRE $keyName, 60
        EXEC

实现2:第一种方法有一个漏洞就是在前一分钟的最后一秒访问了99次,在第二分钟的第一秒访问了100次,那么其实用户是在一分钟内访问了199次,虽然比较极端,但是在特定场景下还是需要用粒度更小的方案,精确到每分钟100次。我们可以用列表的方式来记录。用户每访问一次,我们在时间戳里面记录一条访问时间,当总条数大于100时,我们判断列表中最早的时间是否是一分钟之内,如果是,则超过限制了,如果不是,则弹出最后一个元素,插入当前元素(该方法还是会出现竞态条件的情况),伪代码如下:

$listLength = LLEN rate.limiting.$IP
if $listLength < 100
    LPUSH rate.limit.$IP, now()\
else
   $time = LINDEX rate.limiting.$IP, -1
   if now() - $time < 60
       print 超过限制
   else 
       LPUSH rate.limiting.$IP,now()
       LTRIM rate.limiting.$IP, 0 , 99
  1. 实现缓存
    为了提高网站负载能力,常常需要将一些访问频率较高但是对cpu或io资源消耗较大的操作的结果缓存起来,并希望让这些缓存自动过期。但是如果不能设置一个合理的缓存过期时间,则会让缓存占满我们的内存或让我们的内存白白闲置。为此,我们可以限制redis能够使用的最大内存,并让redis按照一定的规则淘汰不需要的缓存建,这种方式在redis用作缓存系统时非常实用。

具体的设置方法为:修改配置文件的maxmemory参数,限制redis最大可用内存大小(单位是字节),当超出了这个限制时redis会依据maxmemory-policy参数指定的策略来删除不需要的键直到redis占用的内存小于指定内存。

maxmemory-policy支持的规则如下

规则说明
volatile-lru使用LRU算法删除一个键(只对设置了过期时间的键)
allkeys-lru使用LRU算法删除一个键
volatile-random随机删除一个键(只对设置了过期时间的键)
allkeys-random随机删除一个键
volatile-ttl删除过期时间最近的一个键
noeviction不删除键,只返回错误

注:LRU(Least Recently Used)算法即“最近最少使用”算法。事实上,redis并不会准确地将整个数据库中最久未被使用的键删除,而是每次随机从数据库中取出3个键,并选择一个最久的未被使用的进行删除,删除过期时间最接近的策略也是这种方式。3这个数字可以通过redis的配置文件中的maxmemory-samples参数设置

  1. 排序

SORT命令 ,该命令可以对集合类型、列表类型、有序集合类型进行排序,例如:
集合类型
reids > sadd tag:ruby:posts 2 6 12 26
(integer) 4
redis > sort tag:ruby:posts
1).“2”
2).“6”
3).“12”
4).“26”

列表类型
redis > LPUSH mylist 4 2 6 1 3 7
(integer) 6
1).“1”
2).“2”
3).“3”
4).“4”
5).“6”
6).“7”

有序集合,对有序集合进行排列时,会忽略元素的分数,只针对元素的值:
redis > ZADD myzset 50 2 40 3 20 1 60 5
(integer) 4
redis > SORT myzset
1).“1”
2).“2”
3).“3”
4).“5”

除了针对数字,redis还可以使用ALPHA参数实现按照字典顺序排列非数字元素,就像这样:
redis > LPUSH mylistalpha a c e d B C A
(integer) 7
redis > SORT mylistalpha ALPHA//如果没有alpha参数,sort命令会尝试转换成双精度浮点数来进行排列,如果无法转换则提示错误
1).“A”
2).“B”
3).“C”
4).“a”
5).“c”
6).“d”
7).“e”

SORT命令的DESC参数可以使元素从大到小排列(默认从小到大),例如:
redis > sort tag:ruby:posts desc
1).“26”
2).“12”
3).“6”
4).“2”

SORT命令还可以用limit参数来指定返回范围的结果,使用语法和sql语句一样,例如:
redis > sort tag:ruby:posts desc limit 1 2
1).“12”
2).“6”

参数BY
SORT键还可以通过外键的值进行排序,利用参数BY。语法为 BY 参考键,其中参考件可以是字符串类型(表示为 键名)或者是散列类型中的某个字段(表示为 键名->字段名)。如果提供BY参数,SORT不会依据元素自身进行排序,,而是对每个元素使用元素的值替换参考件中的第一个“*”并获取其值,然后依据该值对元素排序,例如:
redis > SORT tag:ruby:posts BY post: *->time desc
1).“12”
2).“26”
3).“6”
4).“2”

在上述例子中,SORT命令会读取post:2、post:6、post:12、post:26几个散列键中的time的值并以此决定tag:ruby:posts键中各个文章ID的顺序。

BY 参考键 中的参考键名不包含*时,sort命令将不会执行排序操作。

如果有几个元素的值是相同的,sort命令会再去比较元素本身的值来决定元素的排序。例如:

redis > LPUSH sortbylist 2 1 3 4
(intger) 4
redis > mset itemscore:1 50 itemscore:2 100 itemscore:3 -10 itemscore:4 50
OK
redis > sort scortbylist BY itemscore:* desc
1).“2”
2).“4”
3).“1”
4).“3”

当某个参考键不存在时,会默认参考键的值为0:
redis > LPUSH sortbylist 5
(integer) 5
redis > sort sortbylist BY itemscore:* desc
1).“2”
2).“4”
3).“1”
4).“5”
5).“3”

注意 参考键虽然支持散列类型,但是“*”只能在“->”前面(即键名部分)才有用,在“->”后(即字段名部分)会被当成字段名本身而不会作为占位符被元素的值替换,即常量键名。然而,在实际中,如果字段名中有*,sort还是会对元素进行排序,因为sort by 关键字取决于 by后面的参考键中是否存在*,虽然在字段名部分*不会被转义,会被当做值的一部分来查询,其得到的结果都是相同的,所以redis会按照元素本身的大小进行排列。

GET参数,该参数不影响排序,他的作用是使SORT命令的返回结果不再是元素自身的值,而是GET参数中指定的键值。GET参数的规则和BY参数一样,GET参数也支持字符串类型和散列类型,并使用“*”作为占位符,例如:
实现在排序后直接返回文章标题
redis > sort tag:ruby:posts BY post:*->time desc get post:*->title
1).“title–12”
2).“title–26”
3).“title–6”
4).“title–2”

在一个sort命令中可以有多个get参数(而by只能有一个),例如:
redis > sort tag:ruby:posts BY post:*->time desc get post:*->title get post:*->time
1).“title–12”
2).“1352620100”
3).“title–26”
4).“1352620000”
5).“title–6”
6).“1352619600”
7).“title–2”
8).“1352619200”

如果还要获取*号的值,可以使用GET #,例如:
redis > sort tag:ruby:posts BY post:*->time desc get post:*->title get post:*->time get #
1).“title–12”
2).“1352620100”
3).“12”
4).“title–26”
5).“1352620000”
6).“26”
7).“title–6”
8).“1352619600”
9).“6”
10).“title–2”
11).“1352619200”
12).“2”

STORE参数
默认情况下sort会直接返回排序结果,如果希望保存排序结果,可以使用store参数。如果希望把结果保存到sort.result建中(保存后的键的类型是列表类型):
redis > sort tag:ruby:posts BY post:*->time desc get post:*->title get post:*->time get # store sort.result
(intger) 12
该参数常结合expire命令来缓存排序结果。

性能优化
sort命令的时间复杂度为O(n+mlog(m)),其中n表示要排序的列表(集合或有序集合)中的元素个数,m表示要返回的元素个数。当n较大的时候sort命令的性能较低,并且redis在排序前会建立一个长度为n的临时容器来存储带排序的元素。
所以在开发中使用sort,要注意一下几点。
1).尽可能减少待排序键中元素的数量(使N尽可能少)
2).使用limit参数只获取需要的数据(使M尽可能少)
3).如果排序的数据量比较大,尽可能使用store参数将结果缓存。

  1. 消息通知

1).任务队列
当页面需要进行如发送邮件、复杂数据运算等耗时较长的操作时会阻塞页面的渲染应该使用独立的线程来完成这类操作,有些变成语言或框架不易实现多线程,这时就会想到通过其他进程来完成。

通知的过程可以通过任务队列来完成。与任务队列交互的实体有两类,一类是生产者(producer),另一类是消费者(consumer)。生产者会将需要处理的任务放入任务队列,而消费者则不断从任务队列中读入任务信息并执行。

对于发邮件这个操作来说,页面程序就是生成者,而发送邮件的进程就是消费者。当需要发送邮件时,页面程序会将信息组装成一个任务后存入任务队列中。同时发邮件的进程会不断检查任务队列,一旦发现有新的任务便会将其从队列中取出并执行。

使用任务队列有如下好处:
1、松耦合
生产者和消费者无须知道彼此的实现细节,只需要约定好任务的描述格式。这使得生产者和消费者可以由不同的团队使用不同的编程语言编写。
2、易于扩展
消费者可以由多个,而且可以分布在不同的服务器中,借此可以轻易降低单台服务器的负载。

使用redis实现任务队列

使用 BRPOP 命令,该命令与RPOP差不多,唯一的区别是如果任务队列中 没有新任务,BRPOP命令会一直阻塞防止消费者一直请求,不会执行execute(),伪代码:

loop
    #如果任务队列没有新任务,则一直阻塞下去
    $task = BRPOP quque, 0
    #返回值是一个数组,数组第二个元素时我们需要的任务,第一个元素是key
    execute($task[1])

命令 BRPOP 有两个参数,第一个是键名,第二个是超时时间(秒)。当超过了此事件还没有获得新元素的话就返回nil,设置为0,表示不限制等待时间。如果没有新元素加入列表就会永远阻塞下去。(redis还提供了BLPOP,和BRPOP相对)

优先级队列

当有一种情况就是,当队列里面有1000个元素准备发送邮件,这个时候有人注册了账号,需要发送邮件来确认,这个时候发送邮件的任务就到了1001,到了队尾。但是相较之下发送确认邮件更加紧急,这个时候就需要设置优先级队列了。

BRPOP命令可以同时接受多个键,其完整的命令格式为 BRPOP key [key …] timeout,如:BRPOP queue:1 queue:2 0。同时检测多个键,如果所有键都没有则堵塞,如果其中一个键有则返回,优先级是从左到右。 所以我们,可以把发送确认消息的队列放到第一个参数,优先级最高。

发布订阅模式

除了实现任务队列外,Redis还提供了一组命令可以让开发者实现“发布/订阅”(publish/subscribe)模式。该模式同样可以实现进程间的消息传递,其原理是:
“发布/订阅”模式中包含两种角色,分别是发布者和订阅者。订阅者可以订阅一个或多个频道(channel),而发布者可以向指定频道发送消息,所有订阅此频道的订阅者都会受到此消息。

发布者发消息的命令为 publish,用法是PUBLISH channel message,如向channel1.1说一声“hi”:
redis > publish channel1.1 hi
(integer) 0
返回值是当前订阅者的数量。发出去的消息不会被持久化 ,也就是说,当客户端订阅channel1.1后只能收到后续发布到该频道的消息,之前发送的就收不到了。

订阅者订阅频道的命令是 subscribe ,可以同时订阅多个频道,用法是 SUBSCRIBE [channel …]。现在新开一个redis-cli实例A,用它来订阅channel1.1:
redis > subscribe channel1.1
Reading messages… (press Ctrl-C to quit)

  1. “subscribe”
  2. “channel1.1”
  3. (integer) 1
    使用该命令后客户端会进入订阅状态,处于此状态的客户端不能使用除了SUBSCRIBE、UNSUBSCRIBE、PSUBSCRIBE和PUNSUBSCRIBE这4个属于“发布/订阅”模式命令之外的命令,否则会报错。

进入订阅状态后,客户端可能收到3种类型的回复。每种类型的回复都包含3个值,第一个值是消息的类型,根据消息类型的不同,第二、三个值的含义也不同。消息类型可能的取值有一下3个。
1).subscribe。表示订阅成功的反馈信息,第二个值是订阅频道,第三个至是当前客户端订阅的频道数量。
2).message。这个类型的回复表示它接收到的是消息,第二个值表示产生消息的频道名称,第三个值是消息的内容。
3).unsubscribe。表示成功取消订阅某个频道。第二个值是对应的频道名称,第三个值是当前客户端订阅的频道实例,当此值为0时客户端会退出订阅状态。

使用UNSUBSCRIBE命令可以取消订阅指定的频道,用法为:
unsubscribe [channel …],如果不指定频道,则会取消所有频道。(由于redis-cli限制,我们无法在其中测试unsubscribe命令。)

按照规则订阅
除了使用subscribe命令可以订阅指定名称的频道外,还可以使用psubscribe 命令订阅指定的规则。规则支持glob风格通配符格式,例如:
redis > psubscribe channel.?*

规则channel.?*可以匹配channel1.1和channel1.10,但是不回匹配channel1.。

使用punsubscribe命令只能退订通过psubscribe命令订阅的规则,不会影响直接通过subscribe命令订阅的规则。同样的unsubscribe只能退订subscribe订阅的规则,不会影响psubscribe命令订阅的规则。

  1. 管道

客户端和redis使用tcp协议连接,不论是客户端向redis发送命令还是redis向客户端发送命令的执行结果,都需要经过网络传输,这两部分的总耗时称为往返时延。根据网络性能的不同往返时延也不同,大致来说到本地回环地址(loop back address) 的往返时延在数量级上相当于redis处理一条简单命令 (如lpush list 1 2 3)的时间,如果处理较多命令,每个命令的往返延时还是会产生较多的影响。

在执行多个命令时每命令都需要等待上一条命令执行完(即收到redis的返回结果)才能执行,即使命令不需要上一条命令的执行结果,也是要等待三条命令的往返时延累加的时间。

redis的底层通信协议对管道提供了支持。通过管道可以执行多条命令,并且在执行完成后一次性将结果返回,当一组命令中每条命令都不依赖于之前命令的执行结果时,就可以将这组命令一起通过管道发出。管道通过减少客户端与redis的通信次数来实现降低往返时延累计值的目的。

  1. 节省空间

1). 精简键名和键值
该方法是最直观的减少内存占用的方式,如将键名 very.important.person:20 改为 VIP:20。当然精简键名不能影响可读性。相比于字符串来存储,更好的是用数值、比特位。

2).内部编码优化
如果仅凭精简键名和键值所减少的空间不满足需求,这时就需要根据redis内部编码规则来节省更多的空间。redis为内中数据类型都提供了两种内部编码方式,以散列类型为例,散列类型是通过散列表实现的,这样就可以实现O(1)时间复杂度的查找、赋值操作,然而当键中元素很少的时候,O(1)的操作并不会比O(n)有明显的性能提升,所以这种情况下redis会采用一种更为紧凑但性能稍差(获取元素的时间复杂度为O(n))的内部编码方式。内部编码方式的选择对于开发者来说是透明的,redis会根据实际情况自动调整。当键中元素变多,redis会自动将该键的内部编码方式转换成散列表。如果想查看一个键的内部编码方式可以使用OBJECT ENCODING命令,例如:
redis > set foo bar
OK
redis > object encoding foo
“raw”

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

typedef struct redisObject {
    unsigned type:4;
    unsigned notused:2; /*Not used*/
    unsigned encoding:4;
    unsigned lru:22; /*lru time(relative to server.lruclock)*/
    int refcount;
    void *ptr;
} robj;
//其中type取值如下:
/* Object types */
#define REDIS_STRING 0
#define REDIS_LIST 1
#define REDIS_SET 2
#define REDIS_ZSET 3
#define REDIS_HASH 4
//其中encoding字段表示的就是redis键值内部编码方式,取值为:
/* Objects encoding. Some kind of objects like Strings and Hashes can be
 * internally represented in multiple ways. The 'encoding' field of the object
 * is set to one of this fields for this object. */
#define REDIS_ENCODING_RAW 0     /* Raw representation */
#define REDIS_ENCODING_INT 1     /* Encoded as integer */
#define REDIS_ENCODING_HT 2      /* Encoded as hash table */
#define REDIS_ENCODING_ZIPMAP 3  /* Encoded as zipmap */
#define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */
#define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define REDIS_ENCODING_INTSET 6  /* Encoded as intset */
#define REDIS_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define REDIS_ENCODING_EMBSTR /* Embedded sds string encoding */

各个诗句类型可能采用的内部编码方式以及相应的OBJECT ENCODING命令执行结果

字符串类型:
    REDIS_ENCODING_RAW  		"raw"
    REDIS_ENCODING_INT    		"int"
    REDIS_ENCODING_EMBSTR   "embstr"
散列表:
    REDIS_ENCODING_HT 		    "hashtable"
    REDIS_ENCODING_ZIPLIST    "ziplist"
列表类型:
    REDIS_ENCODING_LINKEDLIST   "linkedlist"
    REDIS_ENCODING_ZIPLIST    "ziplist"
集合类型:
    REDIS_ENCODING_HT            "hashtable"
    REDIS_ENCODING_INTSET    "intset"
有序集合类型
    REDIS_ENCODING_SKIPLIST "skiplist"
    REDIS_ENCODING_ZIPLIST    "ziplist"

2.1).字符串类型
redis使用sdshdr类型的变量来存储字符串,而redisObject的ptr字段指向的是该变量的地址。sdshdr的定义如下:

struct sdshdr {
    int len;
    int free;
    char buf[];
}
len字段表示字符串长度
free字段表示剩余空间
buf字段存储的才是字符串的内容
所以当执行 set key foobar 时,存储键值需要占用空间是sizeof(redisObject) + sizeof(sdshdr) + strlen('foobar') = 30字节。
而当键值内容可以用一个63位有符号整数表示时,redis会将键值换成long类型来存储。如:
set key 123456,实际占用空间是sizeof(Object) = 16字节。
(redis入门指导南 p96)

redisObject中的refcount字段存储的是该键值被引用的数量,即一个键值可以被多个键应用,redis启动后会预先建立10000个分别存储从0到9999这些数字的redisObject类型变量作为共享对象,如果要键一个字符串键值在这10000个数值里面,如:set key 123,则可以直接引用共享对象,而不再建立一个redisObject,也就是说存储键值占用的空间是0字节。

所以redis对于字符串类型键存储对象ID这种小数字是非常节省存储空间的。

注意: 当通过配置文件参数maxmemory设置了redis可用的最大空间大小时,redis不会使用共享对象,因为对于每个键值都需要使用一个redisObject来记录其LRU信息。

此外redis3.0新加入了REDIS_ENCODING_EMBSTR的字符串编码方式,该编码方式与REDIS_ENCODING_RAW类似,都是基于sdshdr实现的,只不过sdshdr的结构体与其对应的分配在同一块连续的内存空间中。

使用REDIS_ENCODING_EMBSTR编码存储字符串后,不论是分配内存还是释放内存,所需要的操作都从两次减少为一次,而且由于内存连续,操作系统缓存可以更好的发挥作用。当键值不超过39字节时,reedis会采用REDIS_ENCODING_EMBSTR编码,同时当对该编码的键进行操作是,redis会将其转换成REDIS_ENCODING_RAW编码。

2.2).散列类型
散列类型的内部编码方式可能是REDIS_ENCODING_HT 或 REDIS_ENCODING_ZIPLIST(2.4及之前版本采用的是REDIS_ENCODING_ZIPMAP)。在配置中可以定义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存储的,所以前面讲到的字符串类型优化方法同样适用于散列类型键的字段和字段值。

注意: redis的键值对存储也是通过散列表实现的,与REDIS_ENCODING_HT编码方式类似,但是键名并非使用redisObject存储,所以键名“123456”并不会比“abcdefg”占用更少空间,之所以不对键名优化,是因为绝大部分情况下键名不会是纯数字。

REDIS_ENCODING_ZIPLIST 编码类型是一种紧凑的编码格式,它牺牲了部分读取性能以换取极高的空间利用率,适合在元素较少时使用。该编码类型同样还在列表类型和有序集合类型中使用REDIS_ENCODING_ZIPLIST的内部存储结构为:

REDIS_ENCODING_ZIPLIST内存结构
zlbytes
zltail
zllen
元素1
元素2
zlend

其中zlbytes是uint32_t类型,表示整个结构占用的空间。
zltail也是uint32_t类型,表示最后一个元素的偏移,记录zltail使用程序可以直接定位到尾部元素而无需遍历整个结构,执行从尾部弹出(对列表类型而言)等操作是速度更快。
zllen是uint16_t类型,存储的是元素的数量。
zlend是一个单字节表示,标记结构的末尾,值永远是255。
其中元素中的数据结构为:

元素内存结构
前一个元素的大小
当前元素的编码类型
当前元素的大小
当前元素的内容

在REDIS_ENCODING_ZIPLIST中每个元素由4个部分组成。
第一个部分用来存储前一个元素的大小,当前一个元素的大小小于253字节时,第一个部分占用1个字节,否则会占用5个字节。

第二、三部分分别是元素的编码类型和元素的大小,当元素的大小小于或等于63个字节时,元素的编码类型是ZIP_STR_06B(即0<<6),同时第三个部分用6个二进制位个来记录元素的长度,所以第二、第三个部分总占用空间是1字节。当元素的大小大于63且小于或等于16383字节时,第二、三部分总占用空间是2字节。当元素的大小大于16383字节时,第二、三部分总占用空间是5字节。

第四部分是元素的实际内容,如果元素可以转换成数字的话redis会使用相应的数据类型来存储以节省空间,并用第二、三个部分来表示数字类型(int16_t、int32_t等)。

使用REDIS_ENCODING_ZIPLIST编码存储散列类型时元素的排列方式是:元素1存储字段1,元素2存储字段值1,以此类推:

REDIS_ENCODING_ZIPLIST存储散列类型的内存结构
zlbytes
zltail
zllen
字段1
字段值1
字段2
字段值2
zlend

例如:
当执行hset hkey foo bar命令后

hkey的内存结构
zitail(20)
zllen(2)
0 (元素1)
ZIP_STR_06B | 3 (元素1)
“foo” (元素1)
5 (元素2)
ZIP_STR_06B |3 (元素2)
“bar” (元素2)
zlend(255) (元素2)

当下次需要执行hset hkey foo anothervalue 时 redis需要从头开始找到值为foo的元素(查找时每次都会跳过一个元素以保证只查找字段名),找到后删除其下的元素,并将anothervalue 插入。删除和插入都需要移动后面的内存数据,而且查找操作也需要遍历才能完成,所以当散列键中数据多时性能很低,所以不宜将hash-max-ziplist-entries和hash-max-ziplist-value两个参数设置得很大。(最好在数据量比较大时,让它自动转为REDIS_ENCODING_HT)

2.3).列表类型

列表类型的内部编码方式可能是REDIS_ENCODING_LINKEDLIST 或 REDIS_ENCODING_ZIPLIST。与散列类型一样ziplist可以再配置中配置其编码的时机。

REDIS_ENCODING_LINKEDLIST编码方式即双向链表,链表中的每个元素是用redis Object 存储的,所以此种编码方式下元素值的优化方法与字符串类型的键值相同。

而使用REDIS_ENCODING_ZIPLIST 编码方式时具体表现和散列类型一样,由于REDIS_ENCODING_ZIPLIST 编码方式同样支持倒序访问,所以采用此种编码方式时获取两端数据依然较快。

redis最新的开发版本新增了 REDIS_ENCODING_QUICKLIST编码方式,该编码方式时REDIS_ENCODING_LINKEDLIST 和REDIS_ENCODING_ZIPLIST的结合,其原理是将一个长列表分成若干个以链表形式组织的ziplist,从而达到减少空间占用的同时提升REDIS_ENCODING_ZIPLIST 的编码性能。

2.4).集合类型

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

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

其中contents存储的就是集合中的元素值,根据encoding的不同,每个元素占用的字节大小不同。encoding默认为INTSET_ENC_INT16(即2个字节)。当新增加的整数元素无法使用2字节表示时,redis会把该集合的encoding升级为INTSET_ENC_INT32(即4字节)并调整之前所有元素的位置和长度,同样还可以升级为INTSET_ENC_INT64(8字节)。

REDIS_ENCODING_INTSET编码以有序的方式存储元素,所以smembers命令获取的结果是有序的。是的可以使用二分算法查找元素。然而无论是添加还是删除元素,redis都要调整后面内存的位置和顺序,所以当元素较多时,性能比较差。

当新增的元素不是整数或超过set-max-intset-entries参数时,redis会自动将存储结构转换为REDIS_ENCODING_HT。

注意 当编码类型变成REDIS_ENCODING_HT之后,就算清空该键的值,该键也不会转变回REDIS_ENCODING_INTSET了,因为如果要redis自动转换的话,每删除一个值,就要循环一遍表,会使得删除变成时间复杂度为O(n)的操作。

2.5).有序集合类型

有序集合类型的内部编码方式可以是REDIS_ENCODING_SKIPLIST 或 REDIS_ENCODING_ZIPLIST。同样在配置文件中可以定义ziplist的编码实际。

当编码方式为REDIS_ENCODING_SKIPLIST时,redis使用散列表和跳跃列表(skip list)来存储有序集合类型键值,其中散列表用来存储元素值与元素分数的映射关系以实现O(1)时间复杂度的ZSCORE等命令。跳跃列表用来存储元素的分数及其到元素值的映射以实现排序的功能。reids对跳跃列表的实现进行了几点修改,其中包括允许跳跃列表中的元素(即分数)相同,还有为每一个跳跃列表增加了一个向前的指针以实现倒序查找。

采用此种编码方式时,元素值是使用redisObject存储的,所以可以使用字符串类型键值的优化方式优化元素值,而元素的分数是使用double类型来存储的。

使用REDIS_ENCODING_ZIPLIST编码时有序集合存储方式按照“元素1的值,元素1的分数,元素2的值,元素2的分数”的顺序排列,并且分数是有序的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值