redis进阶:事务|过期|缓存|排序|空间节省

redis进阶

之前已经讲过redis基础,即redis五种数据类型及相关命令,链接如下:
redis五种数据类型及相关命令
接下来我们讲redis进阶篇,至此我们算是真正踏入了redis的世界

文章目录

  • redis进阶
    • 事务
        • 概述
        • 错误处理
    • 监控(WATCH命令)
    • 过期时间
        • 设置过期
        • 查询过期
        • 取消过期
    • 缓存
    • 排序(SORT命令)
        • BY 参数
        • GET参数
        • STORE参数
        • 性能优化
    • 消息通知
        • 任务队列
        • 优先级队列
        • "发布/订阅" 模式
    • 空间节省(内部编码方式)
        • 字符串类型
        • 散列类型
        • 列表类型
        • 集合类型
        • 有序集合类型

事务

概述

定义: 一组命令的集合
原理: 先将属于一个事务的命令发送给redis,然后再让redis依次执行这些命令
事务和redis命令一样是redis的最小执行单位,一个事务中的命令要么都执行,要么都不执行
具体代码示例:

redis> MULTI
OK
redis> SADD "user:1:follwing" 1
QUEUED
redis> SADD "user:2:follwing" 2
QUEUED
redis> EXEC
1) (integer) 1
2) (integer) 1

MULTI命令告诉redis:下面发给你的是属于一个事务的命令,先别执行,保存起来
redis说:OK,我懂
EXEC命令告诉redis:这个事务的命令都告诉你了,你也都回应已加入事务队列(即返回QUEUED),下面你就按照发送顺序依次执行吧
redis说:好的大佬,您喝杯茶先,我马上把执行结果依次返回

错误处理

如果一个事务中某个命令出现错误,根据错误类型redis做不同处理
错误分两类:

  • 语法错误
    命令不正确或参数不对,redis会说:命令都不会还想蹂躏我?
    结果:只要有一个语法错误,reids直接返回错误,所有命令都不执行(包含是正确命令)
  • 运行错误
    比如用散列类型命令操作集合类型的键,redis会说:可能之前你不知道要操作的键类型,可以原谅,你接着蹂躏我吧
    结果:一条命令运行错误,其他命令依然继续执行(包含错误命令之后的命令)

tips:reids的事务没有关系数据库事务提供的回滚(rollback)功能,为此开发者必须在事务执行出错后自己收拾剩下的摊子(其实两种错误都是可以规避的)

监控(WATCH命令)

可以监控一个或多个键,一旦其中有一个键被修改或删除,之后的一个事务就不会执行,监控一直持续到EXEC命令
可以使用UNWATCH命令取消监控
如果监控了一个拥有过期时间的键,到时间自动删除并不会被认为该键被改变

代码示例:

redis> SET key 1
OK
reids> WATCH key
OK
reids> SET key 2
OK
redis> MULTI
OK
reids> SET key 3
QUEUED
redis> EXEC
(nil)
redis> GET key
"2"

过期时间

设置过期

reids的经典命令,具体语法为:

redis> EXPIRE key 15
(integer) 1

这句话表示key这个键在15秒后过期,将自动删除,返回1表示设置成功,0则表示键不存在或设置失败
有两个不太常用的命令也可以设置过期时间:
EXPIREATEXPIRE 的区别在于前者使用unix时间作为第二个参数表示键的过期时间
PEXPIREATEXPIREAT 区别在于前者的时间单位为毫秒

查询过期

我们也可以查询键还有多久会被删除,返回剩余时间(单位秒)

redis> TTL key
(integer) 10

键不存在返回-2,没设置过期返回-1
tips:tips:毫秒级过期时间设置和查询命令为 PEXPIRE/PTTL

取消过期

我们也可以取消过期时间的设置,成功返回1,否则返回0(键不存在或未设置过过期时间)

redis> PERSIST key
(integer) 1

除了PERSIST命令,还可以使用 SET 和 GETSET 命令为键赋值,同时也会清除键的过期时间

缓存

为提高负载能力,常将一些访问频率高,对CPU或IO资源消耗较大的操作结果设置过期时间,如果redis中存在该结果,直接返回数据,如果不存在,再重新计算并存入redis,以此实现缓存功能
但是实际开发中会发现很难为缓存键设置合理的过期时间,如果大量使用缓存键且时间过长,会导致redis占满内存,如果时间过短,会导致缓存命中率过低且大量内存闲置
为此可以限制redis能够使用的最大内存,并让redis按照一定规则淘汰不需要的缓存键,具体设置方法如下:

  1. 修改配置文件的 maxmemory 参数,限制redis最大可用内存大小(单位字节)
  2. 修改配置文件 maxmemory-policy 参数,指定删除策略

当redis占用内存超出 maxmemory 限制的时候,reids会依据 maxmemory-policy 指定的策略来删除不需要的键直到reids占用内存小于指定内存
maxmemory-policy支持的规则:

  1. volatile-lru //使用LRU算法(最近最少使用)删除一个键(只对设置了过期时间的键)
  2. allkeys-lru //使用LRU算法删除一个键
  3. volatile-random //随机删除一个键(只对设置了过期时间的键)
  4. allkeys-random //随机删除一个键
  5. volatile-ttl //删除过期时间最近的一个键
  6. noeviction //不删除键,只返回错误

tips:LUR算法并不会准确的将整个数据库中最久未被使用的键删除,而是每次从数据库中随机取3个键,并删除三个键中最久未被使用的键,删除过期时间最近的键的实现方法也是这样,3这个数字可以通过maxmemory-samples参数设置

排序(SORT命令)

sort是redis最强大最复杂的命令之一,对于有序集合类型排序会忽略元素的分数,只针对元素自身的值进行排序
命令如下:

SORT key [ALPHA] [DESC] [LIMIT offset count]

[ALPHA]: SORT命令尝试将所有元素转换成双精度浮点数比较,如果有非浮点数元素,需使用 ALPHA 参数,按照字典顺序排序非数字元素
[DESC]: 默认从小到大排序,DESC参数实现从大到小排序
[LIMIT offset count]: LIMIT指定返回部分结果,跳过前offset个元素,并获取之后的count个元素
除了上面三个参数外,sort命令还有如下两个 很重要的参数

BY 参数

语法为 BY 参考键 ,其中参考键可以是字符串类型键或者是散列类型键的某个字段(表示为键名->字段名)
如果提供了 BY 参数,SORT命令将不再依据元素自身的值进行排序,而是对每个元素使用元素值的值替换参考键中的第一个"*"并获得其值,然后依据该值对元素排序
听起来似乎有点晕,实际代码示例:

//现在redis中有如下数据:
//集合类型的键 tag:ruby:posts ,键值为: "2"  "6"  "12"  "26"
//散列类型的键 post:2 ,键值中有字段 time ,值为1352619200
//散列类型的键 post:6 ,键值中有字段 time ,值为1352619600
//散列类型的键 post:12 ,键值中有字段 time ,值为1352620100
//散列类型的键 post:26 ,键值中有字段 time ,值为1352620000
//现在按照 四个散列类型的键的 time字段的值 给 tag:ruby:posts 排序
//排序结果应为 "12"  "26"  "6"  "2"
redis> SORT tag:ruby:posts BY post:*->time DESC
1) "12"
2) "26"
3) "6"
4) "2"

上例中sort命令会读取post:2、post6、post12、post26几个散列键中的time字段的值并以此决定 tag:ruby:posts 键中各个元素的顺序
除了散列类型外,参考键还可以是字符串类型,比如:

redis> LPUSH sortbylist 1 2 3
(integer) 3
redis> SET itemscore:1 50
OK
redis> SET itemscore:2 100
OK
redis> SET itemscore:3 -10
OK
redis> SORT sortbylist BY itemscore:* DESC
1) "2"
2) "1"
3) "3"

当参考键中不包含 “*” 时(特殊情况:散列类型时只能在"->"前面,在后面会当作字段名的一部分,即常量名,但会按照tag:ruby:posts元素值排序)(即常量名,与元素值无关),SORT命令将不会执行排序操作,因为redis认为这种情况是没有意义的
在不需要排序但需要借助SORT命令获得与元素相关联的数据时(见下面GET参数),常量键名是很有用的
如果几个元素的参考键值相同,会再比较元素本身的值来排序
如果参考键不存在,默认为0

GET参数

GET参数不影响排序,它的作用是使 SORT 命令的返回结果不再是元素自身的值,而是GET参数中指定的键值
参数规则和 BY 一样,但是可以有多个 GET 参数,只能有一个 BY 参数, GET # 会返回元素本身的值
具体示例:

//在上面介绍BY参数的散列类型参考键时,有5个键
//现在 post:* 的四个散列类型的键还有对应的 title 字段
//如果需要按  post:* 的time字段排序并获取相应的 title 的值和 time值
//且获取的这些键名的 "*" 部分的值(一般为数据id),都在列表类型键 tag:ruby:posts 中
//即可使用下面的命令
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"

STORE参数

将 SORT 结果保存 在key中,键key为列表类型,如果已存在,将会覆盖

//SORT key
redis> SORT tag:ruby:posts BY post:*->time DESC GET post:*->title GET post:*->time SORT sort.res
(integer) 8

性能优化

SORT是redis中最强大,最复杂的命令之一,如果使用不好很容易成为性能瓶颈
SORT命令的时间复杂度为 O(n+mlog(m)),其中n表示要排序的列表(集合或有序集合)中元素的个数,m表示要返回的元素个数
当n较大时SORT命令的性能相对较低
开发中使用SORT命令时注意以下几点:

  1. 尽可能减少待排序键中元素的数量(使n尽可能小)
  2. 使用LIMIT参数只获取需要的数据(使m尽可能小)
  3. 如果要排序的数据数量较大,尽可能使用STORE参数将结果缓存

消息通知

任务队列

列表类型使用LPUSH和RPOP命令实现队列
生产者使用LPUSH加入任务,消费者使用RPOP取出任务
BRPOP命令与RPOP类似,唯一的区别是没有元素时会阻塞连接,直到有新元素加入
BRPOP key,time(超时时间,秒,0表示不限制等待时间)
返回两个值,第一个是键名,第二个是键值

优先级队列

BRPOP命令可以同时接收多个键,如果都没元素则阻塞,如果都有元素则按从左到右取第一个键中的一个元素
例如有两个任务列表 key1和key2,下面命令优先执行key1中的任务

redis> BRPOP key1,key2 0

“发布/订阅” 模式

发布者:

redis> PUBLISH channel1.1 hi!
(integer) 0

向频道channel1.1发送消息 hi! , 返回接收到消息的订阅者数量

订阅者:

redis> SUBSCRIBE channel1.1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1.1"
3) (integer) 1

订阅状态的客户端只能使用四个属于发布/订阅的命令: SUBSCRIBE UNSUBSCRIBE PSUBSCRIBE PUNSUBSCRIBE 否则报错
订阅者可能收到3种类型的回复,每种类型的回复都包含3个值,第一个是消息类型:

  1. subscribe : 表示订阅成功的返回消息; 订阅成功的频道名称; 当前客户端订阅的频道数量
  2. message : 接收到的消息; 消息产生频道; 消息内容
  3. unsubscribe : 成功取消订阅某个频道; 对应频道名称; 当前客户端订阅的频道数量(为0是退出订阅模式)

UNSUBSCRIBE取消指定频道,无参数取消所有订阅
按规则订阅:

redis> PSUBSCRIBE channel*

后面的参数支持glob风格通配符,收到的消息包含4个值:

  1. “pmesssage”, 表示通过PSUBSCRIBE命令订阅收到的
  2. 订阅时使用的通配符
  3. 频道
  4. 消息内容

PUNSUBSCRIBE [pattern] 取消某种规则的订阅
退订指定规则的频道,不会影响SUBSCRIBE订阅的频道

空间节省(内部编码方式)

最简单的方式为精简键名和键值,但如果单纯为了节约空间而使用不易理解的键名是不理智的,下面主要讲内部编码优化的问题
redis为每种数据类型都提供了两种内部编码方式,比如散列类型是通过散列表实现的,这样可以实现O(1)时间复杂度的查找,赋值操作,然而当键中元素很少时,O(1)的操作并不会比O(n)有明显性能提升,所以这种情况下redis会采用一种更紧凑但性能稍差的内部编码方式,redis会根据实际情况自动调整
查看一个键的内部编码方式:

redis> SET foo bar
OK
redis> OBJECT ENCODING foo
"raw"

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

redis进阶:事务|过期|缓存|排序|空间节省_第1张图片
其中type字段表示键值的数据类型,取值可以是:

  1. #define REDIS_STRING 0
  2. #define REDIS_LIST 1
  3. #define REDIS_SET 2
  4. #define REDIS_ZSET 3
  5. #define REDIS_HASH 4

encoding字段表示键值的内部编码方式,各个数据类型可能采用的内部编码方式及相应的OBJECT ENCODING命令执行结果如图所示:
redis进阶:事务|过期|缓存|排序|空间节省_第2张图片
下面针对每种数据类型分别介绍其内部编码方式及优化方式:

字符串类型

redis使用一个sdshdr类型的变量来存储字符串,而redisObject的ptr字段指向的是该变量的地址,sdshdr的定义如下:
redis进阶:事务|过期|缓存|排序|空间节省_第3张图片
其中len表示字符串长度,free表示buf中剩余空间,而buf存储的才是字符串的内容
SET key foobar 需占用30字节
SET key 123456 需占用16字节
具体结构如下:

redis进阶:事务|过期|缓存|排序|空间节省_第4张图片
redisObject中refcount字段存储的是该键值被引用的数量,即一个键值可以被多个键引用
tips: redis启动后会预先建立10000个分别存储从0到9999这些数字的redisObject类型变量作为共享对象,如果要设置的字符串键值在这10000个数字内(如 SET key 123),则可以直接引用共享对象而不用再建立一个redisObject了,也就是说键值占用0字节,但是 如果配置了 maxmemory 可用最大空间大小时,redis不会使用共享对象,因为每个键值都需要记录其LRU信息
由此可见,使用字符串类型键存储id这种小数字非常节省存储空间的,只需存储键名和一个共享对象的引用即可
redis3.0新加入了 REDIS_ENCODING_EMBSTR 的字符串编码方式,当键值内容不超过39字节时,采用该编码,优点: 分配/释放 内存操作从两次减少为一次! 而且内存连续,操作系统缓存可以更好的发挥作用
结构如图所示:
redis进阶:事务|过期|缓存|排序|空间节省_第5张图片

散列类型

内部编码方式可能是: REDIS_ENCODING_HTREDIS_ENCODING_ZIPLIST
可通过配置文件定义使用REDIS_ENCODING_ZIPLIST的时机:

  1. hash-max-ziplist-entries 512
  2. hash-max-ziplist-value 64

当散列类型键的字段个数少于 hash-max-ziplist-entries 且每个字段名和字段值的长度都小于 hash-max-ziplist-value (单位字节) 时,就会使用 REDIS_ENCODING_ZIPLIST 来存储该键,否则就是 REDIS_ENCODING_HT

REDIS_ENCODING_HT 编码即散列表,其字段名和字段值都是使用redisObject存储的,所以前面的字符串键值优化方法同样适用于散列类型键的字段和字段值
REDIS_ENCODING_ZIPLIST 是一种紧凑的编码格式,牺牲了部分的读取性能以换取极高的空间利用率,适合在元素较少时使用
redis进阶:事务|过期|缓存|排序|空间节省_第6张图片
当散列键中数据多时性能将很低,所以不宜将 hash-max-ziplist-entries 或 hash-max-ziplist-value 两个参数设置的很大

列表类型

内部编码方式可能为: REDIS_ENCODING_LINKEDLISTREDIS_ENCODING_ZIPLIST ,与散列类型一样一样可以配置使用 REDIS_ENCODING_ZIPLIST 方式编码的时机,转换方式相同:

  1. list-max-ziplist-entries 512
  2. list-max-ziplist-value 64

REDIS_ENCODING_LINKEDLIST即双向链表,链表中每个元素都是redisObject存储的,此时元素值优化方法同字符串类型
后来新增了 REDIS_ENCODING_QUICKLIST 编码方式,该编码方式是上面两种的结合,原理是将一个长列表分成若干个以链表形式组成的 ziplist ,从而达到减少空间占用的同时提升 REDIS_ENCODING_ZIPLIST 编码性能的效果

集合类型

内部编码方式可能是: REDIS_ENCODING_HTREDIS_ENCODING_INTSET ,

当集合中所有元素都是整数且元素的个数小于配置文件中 set-max-intset-entries 参数指定值(默认512)时,redis会使用 REDIS_ENCODING_INTSET 编码存储该集合,否则会使用REDIS_ENCODING_HT

REDIS_ENCODING_INTSET 以有序的方式存储元素,添加/删除 元素,都需要调整后面元素的内存位置,所以当集合中元素太多时性能较差
tips:当转换为REDIS_ENCODING_HT之后,即使删除元素以满足条件,也无法转换回REDIS_ENCODING_INTSET

有序集合类型

内部编码方式可能为: REDIS_ENCODING_SKIPLISTREDIS_ENCODING_ZIPLIST

同样可以配置使用REDIS_ENCODING_ZIPLIST的时机:

  1. zset-max-ziplist-entries 512
  2. zset-max-ziplist-value 64

REDIS_ENCODING_SKIPLIST,使用散列表和跳跃列表存储键值, 散列表存储元素值与元素分数的映射关系以实现O(1)时间复杂度的 ZSCORE 等命令, 跳跃列表存储元素的分数及其到元素值的映射以实现排序功能
此时元素值都是使用redisObject存储的,所以可以使用字符串类型优化方式优化值,而分数是以double类型存储的

REDIS_ENCODING_ZIPLIST 存储方式按照"元素1的值,元素1的分数,元素2的值,元素2的分数"的顺序排列,并且分数是有序的

你可能感兴趣的:(redis进阶:事务|过期|缓存|排序|空间节省)