Redis知识整合(一):常用数据结构&指令集
前言:Redis各版本重大更新
截至2021年5月,Redis的稳定版本已经更新到了6.2,所以这里我想整理一下redis自发布以来每一个大版本的重要更新,当然我不会全部都展示出来,只挑那些具有重要革新的功能说。
redis2.6
发行于2012年,相比旧版,它新支持了以下功能:
- redis-server支持Lua脚本
- 键的过期时间可以精确到毫秒
- slave节点提供只读功能
- 新指令:
bitcount
、bittop
(操作位图用的) - 基于浮点数的自增功能:
incrbyfloat
、hincrbyfloat
- redis-cli支持用
--eval
参数实现Lua脚本
redis2.8
发行于2013年11月,新功能如下:
- 支持部分主从复制,降低了由于网络问题导致频繁全量复制RDB时对系统造成的压力
- 支持IPv6
- 支持
config rewrite
指令可以将config set
持久化到redis的配置文件中 - 新指令:pubsub(给订阅发布功能用的)
- redis哨兵v2
redis3.0
发行于2015年4月,新功能如下:
- 支持redis集群(
Redis Cluster
),这是redis官方的集群方案,在这之前,有两种方案也可以实现redis的集群:Codis
和Twemproxy
- LRU算法大幅提升
migrate
连接缓存,大幅提升键的迁移速度incr
、bitcount
命令性能提升
redis3.2
- 新增GEO功能
- 对redis底层字符串(
SDS
)在速度和节省空间上都做了相关优化 - 支持用
upstart
或systemd
管理redis进程 - 新的list编码类型:quicklist
- 新增
hstrlen
指令、增强debug
、spop
指令(支持的参数更多了)、增强cluster nodes
指令(提速) - 新RDB格式(兼容旧版),提升了RDB文件的加载速度
- Jemalloc更新至4.0.3版本
redis4.0
- 提供模块功能,方便第三方拓展功能
- PSYNC2.0:优化了之前版本中,主从节点切换必然引起全量复制的问题
- 支持LFU,并对已有缓存过期算法进行了优化
- 提供非阻塞式的
del
和flushall
/flushdb
功能,解决了删除大key时可能会造成redis阻塞的问题 - 提供RDB-AOF混合持久化格式,充分结合了RDB和AOF的优点
- 新增
memory
指令,使内存监控更加全面 - Redis Cluster兼容NAT和docker
一、五种数据结构
redis有五种数据结构,在操作它们时会使用到不同的api,在实际开发中,这些api也是离我们开发最近的一块内容,所以熟练掌握常用的api对我们非常有用。
字符串(string)
最基本的k-v格式,value是一个字符串,它的内部编码如下:
指令集:
指令 | 解释 | 时间复杂度 |
---|---|---|
set [k] [v] |
最常用的质量,写入一对k-v | O(1) |
mset [k1] [v1] [k2] [v2] … |
批量写入k-v | O(k) k=键数 |
set [k] [v] nx |
只有当key不存在时才会写入成功(新增) | O(1) |
setnx [k] [v] |
作用等同于上面一条 | O(1) |
set [k] [v] xx |
只有当key存在时才会写入成功(修改) | O(1) |
setxx [k] [v] |
作用等同于上面一条 | O(1) |
get [k] |
取value值 | O(1) |
del [k1] [k2] … |
删除掉k-v数据,支持传多个key | O(k) k=键数 |
incr [k] |
自增 | O(1) |
decr [k] |
自减 | O(1) |
incrby [k] [num] |
自增指定的数字 | O(1) |
decrby [k] [num] |
自减指定的数字 | O(1) |
incrbyfloat [k] [num] |
自增指定的数字(针对浮点数自增) | O(1) |
append [k] [v] |
往原有字符串尾部追加内容 | O(1) |
strlen [k] |
获取value的长度(单位:字节) | O(1) |
setrange [k] [offset] [v] |
字符替换,offset为被替换字符的位置 | O(1) |
getrange [k] [start] [end] |
获取部分字符串,截取区间:[start, end] | O(n) n=字符串长度 |
适用范围:做基本的分布式缓存、分布式锁、计数/限流、保存集群下的登录状态等。
列表(list)
顾名思义,一个key对应了一个列表,列表内保存着多个value,其内部结构和编码如下:
其中list-max-ziplist-entries
和list-max-ziplist-value
都是redis的配置项。
指令集:
指令 | 解释 | 时间复杂度 |
---|---|---|
rpush [k] [v1] [v2] … |
从列表的右边插入元素 | O(k) k=插入元素数 |
lpush [k] [v1] [v2] … |
从列表的左边插入元素 | 同上 |
linsert [k] before/after [vx] [vn] |
向vx的前或后面插入vn | O(n) n=vx与表头/尾的距离 |
lrange [k] [start] [end] |
输出指定范围的列表,范围:[start, end],start/end为下标值 | O(s+n) s=start偏移量;n=start~end的范围 |
lindex [k] [index] |
获取指定下标的value | O(n) n=index偏移量 |
llen [k] |
获取列表长度 | O(1) |
lpop [k] |
弹出左边第一个元素 | O(1) |
rpop [k] |
弹出右边第一个元素 | O(1) |
blpop [k] [timeout] |
阻塞式弹出左边第一个元素,timout非必须,当列表中没有元素时阻塞 | O(1) |
brpop [k] [timeout] |
阻塞式弹出右边第一个元素,timout非必须,当列表中没有元素时阻塞 | O(1) |
lrem [num] [v] |
删除v包括其前后的元素,这取决于num:num>0,从左至右删除最多num个元素;num<0,从右至左删除最多|count|个元素;num=0,全部删除 | O(n) n=列表长度 |
ltrim [k] [start] [end] |
只保留[start, end]区间的数据 | O(n) n=start~end的长度 |
lset [k] [index] [v] |
修改指定下标中的元素 | O(n) n=index偏移量 |
如你所见,我们可以利用lpush和brpop来实现一个消息队列:
除此之外,我们可以利用它来保存一个信息id列表,然后用lrange
指令实现列表的高效分页,拿到单页id集合后再去批量查询这些id对应的详细信息,最终聚合成完整信息输出给客户端,这样既节约资源,又便于我们设计和复用缓存结构。(ps:根据lrange的复杂度不难推断出在数据量庞大的情况下会影响其性能,这时可以尝试大key拆分成小key,比如可以利用CRC32(value) % key_num来做数据拆分,当然也可以利用redis3.2推出的quicklist
内部编码实现,它对大key的性能一样很高)
集合(set)
跟list一样,一个key对应一堆value,但是set不允许有重复的value,同时它也是无序的,其内部结构和编码如下:
其中set-max-intset-entrie
可配。
指令集:
指令 | 解释 | 时间复杂度 |
---|---|---|
sadd [k] [v1] [v2] … |
添加元素 | O(k) k=插入元素数 |
srem [k] [v1] [v2] … |
删除元素 | O(k) k=删除元素数 |
scard [k] |
获取元素总数 | O(1) |
sismember [k] [v] |
v是否在集合k中 | O(1) |
srandmember [k] [num] |
从集合k中随机返回num个元素 | O(k) k=num |
spop [k] [num] |
从集合k中随机弹出num个元素 | O(1) |
smembers [k] |
获取所有元素(结果无序) | O(n) n=元素总数 |
sscan [k] |
迭代出所有元素,类似下面scan 的用法 |
同上 |
sinter [k1] [k2] … |
求多个集合的交集 | O(m*k) k=多个集合中最少元素数,m=集合数 |
sinterstore [dk] [k1] [k2] … |
将算出的交集存入dk中 | 同上 |
suinon [k1] [k2] … |
求多个集合的并集 | O(k) k=多个集合的总元素数 |
suinonstore [dk] [k1] [k2] … |
将算出的并集存入dk中 | 同上 |
sdiff [k1] [k2] … |
求多个集合的差集 | 同上 |
sdiffstore [dk] [k1] [k2] … |
将算出的差集存入dk中 | 同上 |
相比list,set并不能用来分页以及展示一个列表,因为它是无序的,但我们仍然可以为它找到合适的应用场景,比如可以用它来存储用户的喜好标签,便于利用取交集
、差集
等功推荐同好;也可以存储某些权益信息,比如某大V粉丝集合,利用sismember
指令可以很快得出一个用户是否是另一个用户的粉丝(得出按钮展示“已关注”还是“未关注”),这同样可以计算一个大V与另一个大V的粉丝重合度。
有序集合(zset)
相比set,zset可以进行灵活排序、分页等操作,同样它也不允许有重复元素,其内部结构和编码如下:
其中zset-max-ziplist-entries
和zset-max-ziplist-value
都是redis的配置项。
指令集:
指令 | 解释 | 时间复杂度 |
---|---|---|
zadd [k] [s1] [v1] [s2] [v2] … |
添加成员 | O(k*log(n)) k=插入个数,n=总个数 |
zscore [k] [v] |
获取某成员分数 | O(1) |
zcard [k] |
获取元素总数 | O(1) |
zcount [k] [min] [max] |
获取score值在[min, max]区间的元素总数 | O(log(n)) n=总个数 |
zincrby [k] [num] [v] |
给v加num分 | 同上 |
zrank [k] [v] |
返回v的正序排名 | 同上 |
zrevrank [k] [v] |
返回v的倒序排名 | 同上 |
zrem [k] [v1] [v2] … |
删除元素 | O(k*log(n)) k=删除个数,n=总个数 |
zscan [k] |
迭代出[k]里所有的元素,类似下面scan 的用法 |
O(N) N=总个数 |
zrange [k] [start] [end] withsocres |
按分数从小到大返回[start, end]区间内的数据(score+v都会返回) | O(log(n)+k) k=获取数;n=总个数 |
zrevrange [k] [start] [end] withscores |
按分数从大到小返回[start, end]区间内的数据(score+v都会返回) | 同上 |
zrangebyscore [k] [min] [max] withscores |
返回score值在[min, max]区间的数据(按从小到大排序) | 同上 |
zrevrangebyscore [k] [max] [min] withscores |
返回score值在[max, min]区间的数据(按从大到小排序) | 同上 |
zremrangebyrank [k] [start] [end] |
删除[start, end]名次区间内的数据 | O(log(n)+k) k=删除数;n=总个数 |
zremrangebyscore [k] [min] [max] |
删除分数在[min, max]区间内的数据 | 同上 |
zinterstore [dk] [num] [k1] [k2] … |
将k1、k2等集合的交集存入dk中 | O(nk)+O(mlog(m)) n=成员数最小集合的成员数,k=参与取交集的集合数,m=结果集中的成员数 |
zunionstore [dk] [num] [k1] [k2] … |
将k1、k2等集合的并集存入dk中 | O(n)+O(m*log(m)) n=所有集合成员数之和,m=结果集中的成员数 |
作为一个支持正序、倒序、分页的集合,它特别适合做一些排行榜类的需求,这样可以很方便的获取名次、分数等关键信息;
除此之外也可以做一些含排序操作的列表页:比如b站的个人空间中up主的稿件列表,可以按照时间、播放量、收藏量来进行正序和倒序展示,同时还支持分页,那么我们就可以建设3个统计维度的有序集合(分别以投稿时间、播放量、收藏量做score),value保存稿件id,然后按照排序类型、页码查询出单页数据,再利用单页的id集合批量查询稿件详细信息,最终将这些信息聚合成接口返回给客户端即可。
哈希(hash)
hash类型的数据本身就是一个k-v结构,也就是一个redis key对应多个k-v,结构和内部编码如下:
其中hash-max-ziplist-entries
和hash-max-ziplist-value
都是redis的配置项。
指令集:
指令 | 解释 | 时间复杂度 |
---|---|---|
hset [k] [KEY] [VALUE] |
为[k]添加一对[KEY]-[VALUE] | O(1) |
hget [k] [KEY] |
获取[k]里[KEY]对应的value | O(1) |
hdel [k] [KEY1] [KEY2] … |
删除[k]里的某些[KEY] | O(k) k=被删KEY的个数 |
hlen [k] |
获取[k]里[KEY]的数量 | O(1) |
hgetall [k] |
获取[k]里所有的[KEY]-[VALUE] | O(n) n=KEY数 |
hscan [k] |
迭代的方式获取[k]里所有的[KEY]-[VALUE],用法跟后面的scan 类似 |
O(n) n=单次迭代结果数 |
hmget [k] [KEY1] [KEY2] … |
批量获取[k]里一些[KEY]的[VALUE] | O(k) k=获取的KEY数 |
hmset [k] [KEY1] [VALUE1] [KEY2] [VALUE2] … |
为[k]添加多个[KEY]-[VALUE] | O(k) k=插入的KEY数 |
hexists [k] [KEY] |
判断[k]内是否存在[KEY]键 | O(1) |
hkeys [k] |
获取[k]内所有的[KEY] | O(n) n=KEY数 |
hvals [k] |
获取[k]内所有的[VALUE] | 同上 |
hsetnx [k] [KEY] [VALUE] |
当[VALUE]不存在时才设置成功 | O(1) |
hincrby [k] [KEY] [num] |
对[k]内[KEY]的值自增num | O(1) |
hincrbyfloat [k] [KEY] [num] |
对[k]内[KEY]的浮点值自增num | O(1) |
hstrlen [k] [KEY] |
获取[k]内[KEY]对应[VALUE]的length | O(1) |
用处主要是用来结构化的存储一些数据,比如可以把一个对象各个属性和属性值当成hash结构缓存入redis。
二、通用指令
下面是一些通用的指令,不同于上面的指令,这些指令不会针对某些数据结构生效,而是全局有效。
指令 | 解释 | 时间复杂度 |
---|---|---|
keys [xx]* |
获取所有前缀为“xx”的key,若直接为keys * 就是获取所有key,重操作 |
O(n) n=key总数 |
scan [cursor] [xx]* [count] |
非阻塞式获取所有key,相比keys ,scan 是利用[cursor]做游标进行迭代的,[count]则是返回的条数,默认10条,每次迭代结果中会返回下次迭代所需的[cursor],这样就可以一步步迭代出来所有的key了,它不是一个重操作,也就不会长阻塞redis线程 |
O(n) n=单次迭代结果数 |
dbsize |
获取当前db内的key总数 | O(1) |
exists [k1] [k2] … |
是否存在[k1]、[k2]等键 | O(n) n=查看key的个数 |
del [k1] [k2] … |
删除[k1]、[k2]等键 | O(n) n=删除key的个数 |
expire [k] [time] |
让[k]time秒后过期 | O(1) |
persist [k] |
取消[k]的过期时间 | O(1) |
ttl [k] |
返回[k]的剩余过期时间,若返回-1,说明没设过期时间,-2表示[k]不存在,>0表示剩余的时间,单位秒 | O(1) |
type [k] |
查看[k]对应值的类型(string、set、zset、list、hash) | O(1) |
object encoding [k] |
查看[k]的内部编码(int、embstr、raw、ziplist、hashtable、linkedlist、skiplist、intset) | O(1) |
rename [k1] [k2] |
将[k1]重命名为[k2] | O(1) |
renamenx [k1] [k2] |
当k2不存在时才能重命名成功 | O(1) |
randomkey |
随机返回一个key | O(1) |
dump [k] |
将[k]的值dump成RDB格式的数据 | O(1)+O(M*N) M=k对应的对象数;N=对象平均大小,对于值很小的情况,复杂度趋近O(1) |
restore [k] [ttl] [v] |
跟dump结合使用,[v]=dump出的数据,适用于数据转移(比如将redis1中的[k]转移到redis2中);[ttl]为过期时间,若为0,则表示没有过期时间(这种迁移方式非原子性) | 同上,但有序集合为O(N * M * log(N)) |
migrate [ip] [port] `key |
“”[db] [timeout] copyreplace keys` [k1] [k2]… |
数据迁移(这个命令实际上是在源实例中执行一次dump+del,在目标实例中执行一次restore),[ip]+[port]为迁移的目标机器,`key |
select [db] |
选择redis数据库,db为数据库下标(一般常为0)redis3中已弱化db功能,redis cluster更是不支持多db,默认0号db | O(1) |