1. 预备知识
redis按照键值对的方式存储数据
1.1 基本全局命令
KEYS
返回所有满⾜样式(pattern)的key,⽀持如下统配样式:
- h?llo 匹配hello,hallo,hxllo
- h*llo 匹配hllo,heeeello
- h[ae]llo 只匹配hallo hello
- h[^e]llo 匹配除hello,heee..llo以外的
- h[a-b]llo 配置hallo,hbllo
KEYS pattern O(N)
注意:
keys命令的时间复杂度是O(N),
所以,在生产环境上,一般禁止使用此命令,尤其是keys *
EXISTS
判断某个key是否存在
EXISTS key [key ...] O(1)
注:
分开的写法,会产生更多轮次的网络通信,效率比较低,成本比较高
DEL
删除指定的key
DEL key [key ...] O(1)
EXPIRE
为指定的key添加秒级的过期时间
EXPIRE key seconds O(1)
TTL
获取指定key的过期时间,秒级
TTL key O(1)
[面试题]redis的key的过期策略是怎么实现的?
一个redis中可能同时存在很多key,这些key中可能有很大一部分都有过期时间,那redis服务器怎么知道哪些key已经过期要被删除,哪些key还没过期?
如果之间遍历所有的key,显然不行,效率太低
整体的策略为:定期删除+惰性删除
定期删除:
每次抽取一部分,进行验证过期时间,保证这个抽取检查的过程,足够快
这里对定期删除的时间,为什么有明确的要求呢?
因为redis是单线程程序,如果扫描过期key消耗的时间太多,就可能导致正常处理请求命令被阻塞
惰性删除:
假设这个key已经到过期时间了,但是暂时还没删除,key还存在
紧接着,后面又一次访问,正好用到了这个key,于是这次访问就会让redis服务器触发删除这个key的操作,返回一个nil
虽然有上述的两种策略结合,但是整体效果一般,仍然会有很多过期的key残留,redis为了进行补充,提供了一系列的内存淘汰策略
定时器(Redis并未使用)
①基于优先级队列/堆
正常的队列是先进先出
优先级队列是按照指定的优先级先出
在redis过期key的场景中,就可以通过"过期时间越早,优先级越高"
现在假定有很多key设置了过期时间,就可以把这些key加入到一个优先级队列中,指定优先级规则是过期时间早的,先出队列
队首元素,就是最早过期的key
此时定时器只要分配一个线程,让这个线程去检查队首元素,看是否过期即可,如果队首还没过期,后续元素一定没过期
此时,扫描线程只需要盯住这一个队首元素即可
另外在扫描线程检查队首元素过期时间时,也不能太频繁
可以根据当前时刻和队首元素的过期时间,设置一个等待,当时间差不多了,系统再唤醒这个线程
此时扫描线程不需要高频扫描队首元素,把cpu的开销也节省下来了
万一在线程休眠的时候,来了一个新任务,是11:00执行
可以在新任务添加的时候,唤醒一下刚才的线程,重新检查一下队首元素,再根据时间差距重新调整阻塞时间即可
②基于时间轮实现的定时器
把时间划分成很多小段
每个小段上都挂着一个链表,每个链表都代表一个要执行的任务
(相当于一个函数指针,以及对应的参数什么的)
此时这个指针,就会每隔固定的间隔,每次走到一个格子,就把这个格子上链表的任务尝试执行一下
对于时间轮来说,每个格子是多少时间,一共多少个格子,都是需要根据实际场景设置
TYPE
返回key对应的数据类型
TYPE key O(1)
1.2 数据结构和内部编码
Redis的5种数据类型:
string(字符串)、list(列 表)、hash(哈希)、set(集合)、zset(有序集合)
有序集合,是除了存储member之外,还需要存储一个score
实际上Redis针对每种数据结构都有⾃⼰的底层内部编码实现,⽽且是多种实现,这样Redis会在合适的场景选择合适的内部编码
内部编码:
数据结构 | 内部编码 |
string | raw int embstr |
hash | hashtable ziplist |
list | linkedlist ziplist |
set | hashtable intset |
zset | skiplist ziplist |
- ziplist:压缩列表,能够节省空间
- quicklist:兼顾linkedlist和ziplist的优点,把空间和效率都兼顾
- skiplist:跳表,也是链表,不同于普通链表,每个节点上有多个指针域,巧妙地搭配这些指针域地指向,就可以做到,时间复杂度O(logN)
通过object encoding命令查询内部编码
Redis 这样设计有两个好处:
1可以改进内部编码,⽽对外的数据结构和命令没有任何影响,这样⼀旦开发出更优秀的内部编码, ⽆需改动外部数据结构和命令
2) 多种内部编码实现可以在不同场景下发挥各⾃的优势,例如ziplist⽐较节省内存,但是在列表元素⽐较多的情况下,性能会下降,这时候Redis会根据配置选项将列表类型的内部实现转换为 linkedlist,整个过程⽤⼾同样⽆感知
1.3 单线程架构
引入
redis只使用一个线程,处理所有的命令请求,不是说一个redis服务器进程内部真的就只有一个线程,其实也有多个线程,多个线程在处理网络IO
假设,有多个客户端,同时操作一个服务器:
当前这两个客户端,也相当于"并发"发起请求了
那服务器是否会存在类似的线程安全问题呢?
并不会,redis服务器实际上是单线程模型,保证了当前收到的这多个请求是串行执行的
多个请求同时到达redis服务器,也是要现在在队列中排队,在等待redis服务器一个一个的取出里面的命令再执行
微观上讲,redis服务器是串行/顺序执行这多个命令的
redis虽然是单线程模型,为什么效率这么高,速度这么快?
- redis访问内存,数据库则访问硬盘
- redis的核心功能,比数据库简单.数据库对于数据的插入删除查询,都有更复杂的功能支持,会花费更多的开销
- 单线程模型,避免了一些不必要的线程竞争开销.redis每个基本操作,都是短平快的,不会特别消耗cpu
- 处理网络IO的时候,使用了epoll这样的IO多路复用机制 (一个线程,管理多个socket)
注:
虽然单线程给Redis带来很多好处,但还是有⼀个致命的问题:
对于单个命令的执⾏时间都是有要求的。如果某个命令执⾏过⻓,会导致其他命令全部处于等待队列中,迟迟等不到响应,造成客⼾端的阻塞,对于Redis这种⾼性能的服务来说是⾮常严重的,所以Redis是⾯向快速执⾏场景的数据库
2. String字符串
2.1 常见命令
SET
如果key之前存在,则覆盖,⽆论原来的数据类型是什么
之前关于此key的TTL也全部失效
SET key value [ EX seconds|PX milliseconds] [NX|XX]
- EX seconds⸺使⽤秒作为单位设置key的过期时间。
- PX milliseconds⸺使⽤毫秒作为单位设置key的过期时间。
- NX ⸺只在key不存在时才进⾏设置,即如果key之前已经存在,设置不执⾏。
- XX ⸺只在key存在时才进⾏设置,即如果key之前不存在,设置不执⾏
GET
获取key对应的value
如果key不存在,返回nil
如果value的数据类型不是string,会报错
GET key
MGET
⼀次性获取多个key的值。
如果对应的key不存在或者对应的数据类型不是string,返回nil
MGET key [key ...]
MSET
⼀次性设置多个key的值
MSET key value [key value ...]
SETNX
设置key-value但只允许在key之前不存在的情况下
SETNX key value
2.2 计数命令
INCR
针对value+1
此时key对应的value必须是整数
如果key不存在,则视为key对应的value是0
INCR key
INCRBY
针对value+n
此时key对应的value必须是整数
如果key不存在,则视为key对应的value是0
INCRBY key decrement
DECR
针对value-1
此时key对应的value必须是整数
如果key不存在,则视为key对应的value是0
DECR key
DECYBY
针对value-n
如果key不存在,则视为key对应的value是0
此时key对应的value必须是整数
DECRBY key decrement
INCRBYFLOAT
将key对应的string表⽰的浮点数加减上对应的值
只能用加上负数的形式实现减法
运算的操作数可以是浮点数
INCRBYFLOAT key increment
2.3 其它命令
APPEND
如果key已经存在并且是⼀个string,命令会将value追加到原有string的后边
如果key不存在, 则效果等同于SET命令
APPEND KEY VALUE
注意:
qppend返回值,长度的单位是字节
redis的字符串,不会对字符编码做出任何处理
当前xshell终端,默认的字符编码是utf8
一个汉字在utf8字符集中,通常是三个字节
在启动redis客户端时,加上--raw,就可以使客户端自动把二进制数据尝试翻译
GETRANGE
返回key对应的string的⼦串,左闭右闭。
可以使⽤负数表⽰倒数: -1代表倒数第⼀个字符
超过范围的偏移量会根据string的⻓度调整成正确的值
GETRANGE key start end
SETRANGE
覆盖字符串的⼀部分,从指定的偏移开始
offset 偏移量,从第几个字节,开始进行替换
SETRANGE key offset value
如果当前的value是一个中文字符串,进行setrange的时候,可能会出问题:
setrange对不存在的key也可以进行操作,不过会把offset之前的内容填充为0x00:
STRLEN
获取key对应的string的⻓度,单位是字节
STRLEN key
2.4 命令总结
命令 | 执行结果 | 时间复杂度 |
set | 设置key的值是value | O(k),k是键个数 |
get | 获取key的值 | O(1) |
del | 删除指定的key | O(k),k是键个数 |
mset | 批量设置指定的key和value | O(k),k是键个数 |
mget | 批量获取key的值 | O(k),k是键个数 |
incr | 指定的key的值+1 | O(1) |
decr | 指定的key的值-1 | O(1) |
incrby | 指定的key的值+n | O(1) |
decrby | 指定的key的值-n | O(1) |
incrbyfloat | 指定的key的值+n | O(1) |
append | 指定的key的值追加value | O(1) |
strlen | 获取指定key的值的⻓度 | O(1) |
setrange | 覆盖指定key的从offset开始的部分值 | O(n),n是字符 串⻓度,通常视 为O(1) |
getrange | 获取指定key的从start到end的部分值 | O(n),n是字符 串⻓度,通常视 为O(1) |
2.5 内部编码
- int:64位/8个字节的⻓整型
- embstr:压缩字符串,⼩于等于39个字节的字符串
- raw:普通字符串,⼤于39个字节的字符串
2.6 典型使用场景
缓存功能
整体思路:
应用服务器访问数据的时候,先查询redis
如果redis上数据存在,就直接从redis取数据交给服务器,不再访问数据库
如果redis上数据不存在,再读取mysql,把读到的数据给应用服务器,通过把数据写入redis
redis这样的缓存,经常用来存储"热点数据"
上述策略,存在一个问题:
随着时间的推移,会有更多的key在redis上访问不到,从而会读取mysql写入redis,那么redis的数据不是会更多么?
1)把数据写入redis的同时,设置一个过期时间
2)redis在内存不足时,提供"淘汰策略"
计数功能
使⽤Redis作为计数的基础⼯具,它可以实现快速计数、查询缓存的功能,同时数据可以异步处理或者落地到其他数据源
例如:
⽤⼾每播放⼀次视频,相应的视频播放数就会⾃增1
redis不擅长数据统计:
比如,统计播放量前100的视频有哪些,基于redis就会很麻烦,而mysql一个sql就搞定了
共享会话
①Session分散存储:
如果每隔应用服务器,维护自己的会话数据,彼此之间不共享,用户请求访问到不同的服务器上,就会出现一些不能正确处理的情况
②Redis集中管理Session:
手机验证码
为了短信接⼝不会频繁访问,会限制⽤⼾每分钟获取验证码的频率
例如:⼀分钟不能超过5次
3. Hash哈希
3.1 常见命令
HSET
设置hash中指定的字段(field)的值(value)
HSET key field value [field value ...]
HGET
获取hash中指定字段的值
HGET key field
HEXISTS
判断hash中是否有指定的字段
HEXISTS key field
HDEL
删除hash中指定的字段
HDEL key field [field ...]
HKEYS
获取hash中的所有字段
HKEYS key
HVALS
获取hash中的所有的值
HVALS key
HGETALL
获取hash中的所有字段以及对应的值
HGETALL key
HMGET
⼀次获取hash中多个字段的值
HMGET key field [field ...]
HLEN
获取hash中的所有字段的个数
HLEN key
HSETNX
在字段不存在的情况下,设置hash中的字段和值
HSETNX key field value
HINCRBY
将hash中字段对应的数值添加指定的值
HINCRBY key field increment
HINCRBYFLOAT
HINCRBY的浮点数版本
HINCRBYFLOAT key field increment
3.2 命令总结
命令 | 执⾏效果 | 时间复杂度 |
hset | 设置值 | O(1) |
hget | 获取值 | O(1) |
hdel | 删除field | O(k), k 是field 个数 |
hlen | 计算field个数 | O(1) |
hgetall | 获取所有的field-value | O(k), k 是field 个数 |
hmget | 批量获取field-value | O(k), k 是field 个数 |
hmset | 批量获取field-value | O(k), k 是field 个数 |
hexists | 判断field是否存在 | O(1) |
hkeys | 获取所有的field | O(k), k 是field 个数 |
hvals | 获取所有的value | O(k), k 是field 个数 |
hsetnx | 设置值,但必须在field不存在时才能设置成功 | O(1) |
hincrby | 对应field-value+n | O(1) |
hincrbyfloat | 对应field-value+n | O(1) |
hstrlen | 计算value的字符串⻓度 | O(1) |
3.2 内部编码
- ziplist(压缩列表):当哈希类型元素个数⼩于hash-max-ziplist-entries配置(默认512个)、 同时所有值都⼩于hash-max-ziplist-value配置(默认64字节)时,Redis会使⽤ziplist作为哈 希的内部实现,节省内存空间。进行读写元素时,速度比较慢
- hashtable(哈希表):当哈希类型⽆法满⾜ziplist的条件时,Redis会使⽤hashtable作为哈希 的内部实现,因为此时ziplist的读写效率会下降,⽽hashtable的读写时间复杂度为O(1)。
1)当field个数⽐较少且没有⼤的value时,内部编码为ziplist:
2)当有value⼤于64字节时,内部编码会转换为hashtable:
3)当field个数超过512时,内部编码也会转换为hashtable
3.4 典型使用场景
用户信息
关系型数据表保存⽤⼾信息:
映射关系表⽰⽤⼾信息:
使用hash的方式表示UserInfo,,就可以使用field表示对象的每个属性(数据表的每个列),此时就可以很方便的修改/获取任何一个属性的值,但是付出了更多空间的代价
使用string的方式表示UserInfo,万一只想获取或者修改其中的某个field, 就需要把整个json都读出来,解析成对象,操作field,再重写转成json字符串写回去
相⽐于使⽤JSON格式的字符串缓存⽤⼾信息,哈希类型变得更加直观,并且在更新操作上变得更灵活
哈希类型是稀疏的,⽽关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的field,⽽ 关系型数据库⼀旦添加新的列,所有⾏都要为其设置值,即使为null
关系数据库可以做复杂的关系查询,⽽Redis去模拟关系型复杂查询,例如联表查询、聚合查询等 基本不可能,维护成本⾼
4. List列表
列表中允许有重复元素
元素是有序的(位置颠倒后的list不等价)
4.1 常见命令
LPUSH
头插
LPUSH key element [element ...]
LPUSHX
在key存在时,头插
不存在,直接返回
LPUSHX key element [element ...]
RPUSH
尾插
RPUSH key element [element ...]
RPUSHX
在key存在时,尾插
RPUSHX key element [element ...]
LRANGE
获取从start到end区间的所有元素,左闭右闭
LRANGE key start stop
LPOP
头删
LPOP key
RPOP
尾删
RPOP key
LINDEX
获取从左数第index位置的元素
LINDEX key index
LINSERT
在特定位置插⼊元素
LINSERT key <BEFORE|AFTER> pivot element
LLEN
获取list⻓度
LLEN key
4.2 阻塞版本命令
在列表中有元素的情况下,阻塞和⾮阻塞表现是⼀致的
如果列表中没有元素,⾮阻塞版本会返回nil
但阻塞版本会根据timeout,阻塞⼀段时间,期间Redis可以执⾏其他命令,但要求执⾏该命令的客⼾端会表现为阻塞状态,如果没有阻塞时间,就阻塞到不为空
此处的blpop和brpop看起来阻塞很久,但实际上不会对redis服务器产生负面影响
可以同时尝试获取多个key的列表的元素,多个key对应多个list,哪个list有元素,就返回哪个元素
多个客户端同时执行pop,则最先执行命令的客户端会得到弹出的元素
BLPOP
LPOP的阻塞版本
BLPOP key [key ...] timeout
BRPOP
RPOP的阻塞版本
BRPOP key [key ...] timeout
1)针对一个非空的列表进行操作:
返回的结果相当于一个pair,一方面告诉我们当前的数据来自哪个key,一方面告诉我们取得的数据是什么
2)针对一个空的列表进行操作:
3)针对多个key进行操作:
此时,只有key3有元素
4.3 命令总结
操作类型 | 命令 | 时间复杂度 |
添加 | rpush lpush linsert lrange | O(k),k是元素个数 O(k),k是元素个数 O(n),n是pivot距离头尾的距离 |
查找 | lrange lindex llen | O(s+n),s是start偏移量,n是start到end的范围 O(n),n是索引的偏移量 O(1) |
删除 | lpop rpop lremkey ltrim | O(1) O(1) O(k),k是元素个数 O(k),k是元素个数 |
修改 | lset | O(n),n是索引的偏移量 |
阻塞操作 | blpop/brpop | O(1) |
4.4 内部编码
- ziplist(压缩列表):当列表的元素个数⼩于list-max-ziplist-entries配置(默认512个),同时 列表中每个元素的⻓度都⼩于list-max-ziplist-value配置(默认64字节)时,Redis会选⽤ ziplist来作为列表的内部编码实现来减少内存消耗
- linkedlist(链表):当列表类型⽆法满⾜ziplist的条件时,Redis会使⽤linkedlist作为列表的内 部实现
1)当元素个数较少且没有⼤元素时,内部编码为ziplist:
quicklist相当于链表和压缩列表的结合,整体还是一个链表,链表的每个节点,是一个压缩列表
2)当元素个数超过512时,内部编码为linkedlist
3)当某个元素的⻓度超过64字节时,内部编码为linkedlist
4.5 典型使用场景
消息队列
生产者消费者模型
①Redis阻塞消息队列模型:
谁先执行的这个brpop指令,谁就能拿到这个新来的元素
这样的设定,构成一个"轮询"式的效果
消费者1拿到元素之后,就从brpop中返回了,如果消费1还想继续消费,就需要重写执行brpop~
②分频道的消息队列:
不同的消费 者可以通过brpop不同的键值,实现订阅不同频道的理念
多个频道的场景十分常见:
比如,一个通道用来发送视频,一个用来点赞,一个用来评论~
搞成多个频道,就可以在某种数据发生问题的时候,不会对其它数据造成影响(解耦合)
选择列表类型时:
同侧存取(lpush+lpop或者rpush+rpop)为栈
异侧存取(lpush+rpop或者rpush+lpop)为队列
数组
①mysql表示学生和班级信息:
②redis:
就可以很方便的实现"查询指定班级中有哪些学生"
5. Set集合
元素无序
不能重复(唯一)
5.1 常见命令
SADD
重复的元素⽆法添加到set中
SADD key member [member ...]
SMEMBERS
获取⼀个set中的所有元素
元素间的顺序是⽆序的
SMEMBERS key
SISMEMBER
判断⼀个元素在不在set中
SISMEMBER key member
SCARD
获取⼀个set中的元素个数
SCARD key
SPOP
从set中删除并返回⼀个或者多个元素
由于set内的元素是⽆序的,所以取出哪个元素实际是随机的
SPOP key [count]
SMOVE
将⼀个元素从源set取出并放⼊⽬标set中
要移动的元素在source中不存在,返回0,移动失败
SMOVE source destination member
SREM
将指定的元素从set中删除
SREM key member [member ...]
5.2 集合间操作
SINTER(交集)
获取给定set的交集中的元素
SINTER key [key ...]
SINTERSTORE
获取给定set的交集中的元素并保存到⽬标set中
SINTERSTORE destination key [key ...]
SUNION(并集)
获取给定set的并集中的元素
SUNION key [key ...]
SUNIONSTORE
获取给定set的并集中的元素并保存到⽬标set中
SUNIONSTORE destination key [key ...]
SDIFF(差集)
获取给定set的差集中的元素
SDIFF key [key ...]
SDIFFSTORE
获取给定set的差集中的元素并保存到⽬标set中
SDIFFSTORE destination key [key ...]
5.3 命令总结
命令 | 时间复杂度 |
sadd | O(k),k是元素个数 |
srem | O(k),k是元素个数 |
scard | O(1) |
sismember | O(1) |
srandmember | O(n),n是count |
spop | O(n),n是count |
smembers | O(k),k是元素个数 |
sinter | O(m*k),k是⼏个集合中元素最⼩的个数,m是键个数 |
sunion | O(k),k是多个集合的元素个数总和 |
sdiff | O(k),k是多个集合的元素个数总和 |
5.4 内部编码
- intset(整数集合):当集合中的元素都是整数并且元素的个数⼩于set-max-intset-entries配置 (默认512个)时,Redis会选⽤intset来作为集合的内部实现,从⽽减少内存的使⽤。
- hashtable(哈希表):当集合类型⽆法满⾜intset的条件时,Redis会使⽤hashtable作为集合 的内部实现。
1)当元素个数较少并且都为整数时,内部编码为intset:
2)当元素个数超过512个,内部编码为hashtable
3)当存在元素不是整数时,内部编码为hashtable:
5.5 典型使用场景
保存用户的"标签
用户画像:根据信息分析用户的特征,转化为"标签"(简短的字符串), 保存在set里
计算用户之间的共同好友
基于集合求交集
统计UV
去重~
一个互联网产品,根据PV,UV衡量用户量,每个用户访问服务器,就会产生一个UV,但是同一个用户多次访问, 不会使UV增加,通过set来实现去重
6. Zset有序集合
给zset的中member引入了一个属性score(浮点类型)
进行排序的时候,就是依照此处的分数大小进行升序/降序,默认升序
score可以重复,member唯一
不能把member和score理解为键值对,键值对有明显的角色区分,一定是键=>值,而有序集合可以互相匹配
6.1 常见命令
ZADD
ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]
- XX:仅仅⽤于更新已经存在的元素,不会添加新元素。
- NX:仅⽤于添加新元素,不会更新已经存在的元素。
- CH:默认情况下,ZADD返回的是本次添加的元素个数,但指定这个选项之后,就会还包含本次更新的元素的个数。
- INCR:此时命令类似ZINCRBY的效果,将元素的分数加上指定的分数。此时只能指定⼀个元素和 分数。
ZCARD
获取zset中的元素个数
ZCARD key
ZCOUNT
返回分数在min和max之间的元素个数
ZCOUNT key min max
ZRANGE
返回指定区间⾥的元素,分数按照升序
带上WITHSCORES可以把分数也返回
ZRANGE key start stop [WITHSCORES]
ZREVRANGE
返回指定区间⾥的元素,分数按照降序
带上WITHSCORES可以把分数也返回,,并且功能合并到ZRANGE中
ZREVRANGE key start stop [WITHSCORES]
ZRANGEBYSCORE
返回分数在min和max之间的元素
ZRANGEBYSCORE key min max [WITHSCORES]
ZPOPMAX
删除并返回分数最⾼的count个元素
ZPOPMAX key [count]
此处疑问:
此处删除的是最大值,而有序集合,最大值就相当于最后一个元素(尾删),那为什么不把最后一个元素的位置记录下来,后续删除就可以o(1)了??
O(logN)=>此处如果N不是特别的大,基本可以近似看作O(1)的
优化空间??
BZPOPMAX
ZPOPMAX的阻塞版本
BZPOPMIN key [key ...] timeout
key标识了有序集合
ZPOPMIN
删除并返回分数最低的count个元素
ZPOPMIN key [count]
BZPOPMIN
ZPOPMIN的阻塞版本
BZPOPMIN key [key ...] timeout
ZRANK
返回指定元素的排名,升序
ZRANK key member
ZREVRANK
返回指定元素的排名,降序
ZREVRANK key member
ZSCORE
返回指定元素的分数
ZSCORE key member
ZREM
删除指定的元素
ZREM key member [member ...]
ZREMRANGEBYRANK
按照排序,升序删除指定范围的元素,左闭右闭
ZREMRANGEBYRANK key start stop
ZREMRANGEBYSCORE
按照分数删除指定范围的元素,左闭右闭
ZREMRANGEBYSCORE key min max
ZINCRBY
为指定的元素的关联分数添加指定的分数值
ZINCRBY key increment member
6.2 集合间操作
ZINTERSTORE
求出给定有序集合中元素的交集并保存进⽬标有序集合中,在合并过程中以元素为单位进⾏合并,元素对应的分数按照不同的聚合⽅式和权重得到新的分数
ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE ]
ZUNIONSTORE
求出给定有序集合中元素的并集并保存进⽬标有序集合中,在合并过程中以元素为单位进⾏合并,元 素对应的分数按照不同的聚合⽅式和权重得到新的分数
ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE ]
6.3 命令总结
命令 | 时间复杂度 |
zadd | O(k *log(n)),k 是添加成员的个数,n是当前有序集合的元 素个数 |
zcard | O(1) |
zscore | O(1) |
zrank | O(log(n)),n是当前有序集合的元素个数 |
zrem | O(k*log(n)),k是删除成员的个数,n是当前有序集合的元 素个数 |
zincrby | O(log(n)),n是当前有序集合的元素个数 |
zrange | O(k+log(n)),k是获取成员的个数,n是当前有序集合的元 素个数 |
zrevrange | O(k+log(n)),k是获取成员的个数,n是当前有序集合的元 素个数 |
zrangebyscore | O(k+log(n)),k是获取成员的个数,n是当前有序集合的元 素个数 |
zrevrangebyscore | O(k+log(n)),k是获取成员的个数,n是当前有序集合的元 素个数 |
zcount | O(log(n)),n是当前有序集合的元素个数 |
zremrangebyrank | O(k+log(n)),k是获取成员的个数,n是当前有序集合的元 素个数 |
zremrangebyscore | O(k+log(n)),k是获取成员的个数,n是当前有序集合的元 素个数 |
zinterstor | O(n*k)+O(m*log(m)),n是输⼊的集合最⼩的元素个数, k是集合个数,m是⽬标集合元素个数 |
zunionstore | O(n)+O(m*log(m)),n是输⼊集合总元素个数,m是⽬标 集合元素个数 |
6.4 内部编码
- ziplist(压缩列表):当有序集合的元素个数⼩于zset-max-ziplist-entries配置(默认128个), 同时每个元素的值都⼩于zset-max-ziplist-value配置(默认64字节)时,Redis会⽤ziplist来作 为有序集合的内部实现,ziplist可以有效减少内存的使⽤。
- skiplist(跳表=>复杂链表o(logN)):当ziplist条件不满⾜时,有序集合会使⽤skiplist作为内部实现,因为此时 ziplist的操作效率会下降
1)当元素个数较少且每个元素较⼩时,内部编码为ziplist:
2)当元素个数超过128个,内部编码skiplist
3)当某个元素⼤于64字节时,内部编码skiplist:
6.5 典型使用场景
排行榜系统
①比如游戏玩家排行:
只需要把玩家信息和对应的分数放到有序集合即可,自动就形成了排行榜
随时可以按照下标,分数进行范围查询
修改分数也是比较方便的,排行顺序也会自动调整
但也有的排行榜要复杂一些
②如微博热度:
要考虑浏览量,点赞量等多个因素的综合数值,计算权重weight
此时可以借助zinterstore/zunionstore按照加权方式处理
就可以把上述每个维度的数值都放到一个有序集合中,member就是微博id,score就是各自维度的数值,通过集合间运算得到结果的集合的分数就是热度,从而排行榜就出来了
7. 补充类型
Streams
模拟实现事件传播的机制
那么事件是干什么的?我们也不知道它什么时候出现,只能等这个事情出现了之后,采取动作
比如:一旦着火,立即灭火,没着火,就等着
此处的着火就是"事情",灭火就是"事件触发的回调函数"
Geospatial
用来存储坐标(经纬度)
存储一些点之后,就可以让用户给定一个坐标,去从刚才存储的点进行查找
这个功能在地图中很重要
HyperLogLog
估算集合中的元素个数
之前在Set应用场景中提到了统计UV,但是有一个问题,UV数据量很大,就会消耗更多内存空间
而HyperLogLog不存储元素的内容,但是能记录"元素的特征",从而在新增元素的时候,能够知道当前新增的元素是一个已经存在的,还是新的
存储元素的时候,提取特征的过程是不可逆的,不能通过特征得到内容(信息量丢失了)
Bitmaps
使用bit位表示整数
位图本质上,还是一个集合,是set类型针对整数的特化版,会存储元素
比较高效,更节省空间
Bitfields
和C中的位域,十分相似,更精确地进行位操作
可以理解为一串二进制序列(字节数组),同时把这个字节数组的某几个位,赋予特定的含义,并且可以进行读取/修改/算术运算等操作
相比于String/hash,节省空间
8. 渐进式遍历
keys *一次性把整个redis中所有的key都获取到,这个操作比较危险,可能会阻塞redis服务器
Redis 使⽤scan命令进⾏渐进式遍历,进⽽解决直接使⽤keys获取键时可能出现的阻塞问题
每执行一个命令,只获取其中的一小部分
每次scan命令的时间复杂度是O(1),但是要完整地完成所有键的遍历,需要执⾏多次scan
SCAN
注意:
- cursor不能理解成"下标",不是一个连续递增的整数,仅仅就是一个字符串
- 光标这个概念,客户端是不能热认识的,redis服务器则知道对应的元素位置
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
count限制这一次遍历能够获取到多少个元素,默认是10
count只是一个建议,但是写入的count和实际返回的key的个数不一定相同,也不会差太多
时间复杂度:O(1)
返回值:下⼀次scan的游标(cursor)以及本次得到的键
注意:
- 这里的渐进式遍历,在遍历过程中,不会在服务器这边存储任何状态信息,此处的遍历是可以随时终止的,不会对服务器产生任何的副作用
- 渐进性遍历scan虽然解决了阻塞的问题,但如果在遍历期间键有所变化(增加、修改、删除),可能导致遍历时键的重复遍历或者遗漏