Redis(Remote Dictionary Service,远程字典服务)是一种内存数据库,也被称为KV数据库、数据结构数据库。
Redis常用作数据的缓存,缓存热点数据,减少数据库的压力。
完整的Redis命令查看:Redis命令中心。
Redis对外的接口总是通过键key来获取数据值value。
Redis对外提供了5种数据对象来存储key-value:string、list、hash、set以及zset。
- 缓存热点数据,减少数据库压力(hash)
- 记录朋友圈点赞数、评论数和点击数(hash)
- 记录朋友圈说说列表(排序),便于快速显示朋友圈(zset或list)
- 记录文章的标题、摘要、作者和封面,用于列表页展示(hash)
- 记录朋友圈的点赞用户ID列表,评论ID列表,用于显示和去重计数(zset)
- 如果朋友圈说说ID是整数id,可使用redis来分配朋友圈说说id(计数器)(string)
- 通过集合(set)的交并差集运算来实现记录好友关系(set)
- 游戏业务中,每局战绩存储(list)
Redis将key通过siphash函数转化为哈希值,然后以该值为索引在hashtable数组中找到对应的key-value对象,此时并没有直接得到value值,因为对于不同的对象,其内部value的存储形式是不一样的(或者说value是按不同的形式进行编码的),还需要到具体的基础数据结构中去检索value值。此处的数据结构包含字典、双向链表、压缩列表、跳表、整数数组及动态字符串等等。这个检索的过程大致如下图所示,包括两个层次,第一个层次就是前面讲的通过hashtable去找到key-value对象,第二个层次就是在具体的数据结构中找到value:
我个人理解这样设计的好处就是将对外提供的数据操作接口统一成key-value的形式,内部再根据不同的数据形式和数据结构进行不同方式的数据编码。
注意:Redis中的多数字符串是简单动态字符串(Simple Dynamic String,SDS),它是一种二进制安全字符串,可以存储图片等二进制数据。一般来说key和value都是以SDS形式存储。
动态字符串,实质是字符数组的封装,最大长度512M。可扩容,字符串长度小于1M时,每次加倍扩容;超过1M时,每次只多扩1M。
int
存储;embstr
存储;raw
存储;embstr
是专门用于保存短字符串的一种优化编码方式,与raw
字符串几乎相同,都使用对象描述符结构体redisObject
+sds描述符结构体sdshdr
来表示一个字符串对象(字符串内容的空间紧随sdshdr
其后),只不过raw
字符串会分两次来为二者分配内存,而embstr
仅调用一次内存分配来为整体分配一块连续的空间,好处是减少内存分配和释放的次数,也更容易利用缓存。
redis的内存分配器在数据长度小于等于64字节时,认为是小字符串,而在大于64字节时,认为是大字符串。以此为分界Redis将对数据做不同的处理。然而上面对string对象不同存储方式的分界却为44字节。这是与Redis内部描述对象的结构体redisObject
以及描述string对象的结构体sdshdr
大小有关,这些结构体会使用19字节空间,同时字符串要给\0
预留一个字节,所以就要占用掉20个字节,对应地区分大小字符串的分界线就变成44字节了。
其中key
为作为键的字符串,val
为value值的字符串
SET key val # 设置 key 的 value 值
GET key # 获取 key 的 value
DEL key # 删除 key-val 键值对
SETNX key value # 如果key不存在,这种情况下等同SET命令。 当key存在时,什么也不做
INCR key # 执行原子加一的操作
INCRBY key increment # 执行原子加一个整数的操作
DECR key # 执行原子减一的操作
DECRBY key decrement # 执行原子减一个整数的操作
SETBIT key offset value # 设置或者清空key的value(字符串)在offset处的bit值。
GETBIT key offset # 返回key对应的string在offset处的bit值
BITCOUNT key # 统计字符串被设置为1的bit数.
因为设值的value都是字符串,所以输入一个数字并通过GETBIT
得到的bit值是遵循其ASIC码的,而不是数字的二进制形式。
不论哪种数据类型,所有的key都可以用DEL
删除。
a)对象存储:
> set student:1001 '{["name"]:"Mike",["sex"]:"male",["age"]:26}'
OK
> get student:1001
"{[\"name\"]:\"Mike\",[\"sex\"]:\"male\",[\"age\"]:26}"
> del student:1001
(integer) 1 # 删除成功
student:1001
是键的名称,完全是自定义的。
b)累加器(如统计阅读量、访问量等):
> set reads 0
OK
> incr reads # 累加1
(integer) 1
> incrby reads 10 # 累加10
(integer) 11
c)分布式锁:
> setnx lock 1 # 拿到锁
(integer) 1
> setnx lock 1 # 没拿到锁
(integer) 0
> del lock # 释放锁
(integer) 1
d)位运算(如月签到次数统计等):
> setbit sign:202111 1 1 # 2021.11.1签到
(integer) 0
> setbit sign:202111 2 1 # 2021.11.2签到
(integer) 0
> bitcount sign:202111 # 2021.11签到次数
(integer) 2
双向链表,首尾数据操作(删除和增加)的时间复杂度O(1) ;查找中间元素的时间复杂度为O(n)。
何时不会对表内节点的数据进行压缩:
ziplist
quicklist
LPUSH key value [value ...] # 从队列的左侧入队一个或多个元素
LPOP key # 从队列的左侧弹出一个元素
BLPOP key timeout # 从队列的左侧弹出一个元素,没有元素则阻塞;超时单位s
RPUSH key value [value ...] # 从队列的右侧入队一个或多个元素
RPOP key # 从队列的右侧弹出一个元素,没有元素也立即返回
BRPOP key timeout # 从队列的右侧弹出一个元素,没有元素则阻塞;超时单位s
LRANGE key start end # 返回从队列的 start 和 end 之间的元素,左在前,右在后
LREM key count value # 从列表里移除前 count 次出现的值为 value 的元素
LTRIM key start end # 截断队列,仅保留 start 和 end 之间的元素
a)实现栈(先进后出):
> lpush stack 'one'
(integer) 1
> lpush stack 'two'
(integer) 2
> lpop stack
"two"
> lpop stack
"one"
b)实现队列(先进先出):
> lpush queue "one"
(integer) 1
> lpush queue "two"
(integer) 2
> rpop queue
"one"
> rpop queue
"two"
c)实现阻塞队列:
> blpop queue 3 # 阻塞3秒,如果超时时间设为0将永久阻塞直至有其他客户端插入了元素
(nil)
(3.08s)
d)异步消息队列:
一个客户端插入元素,另外一个客户端取出元素。
e)获取固定窗口记录(如最近若干条记录):
> lpush news '{"topic":"holiday","view":20}'
> lpush news '{"topic":"nothing","view":10}'
> lpush news '{"topic":"a dog","view":100}'
> lpush news '{"topic":"shit","view":200}'
> lpush news '{"topic":"joker","view":122}'
> lpush news '{"topic":"batman","view":255}'
> ltrim news 0 4 # 截断到最新的前5条记录
OK
> lrange news 0 -1 # -1是倒数第一条,-2是倒数第2条,以此类推
1) "{\"topic\":\"batman\",\"view\":255}"
2) "{\"topic\":\"joker\",\"view\":122}"
3) "{\"topic\":\"shit\",\"view\":200}"
4) "{\"topic\":\"a dog\",\"view\":100}"
5) "{\"topic\":\"nothing\",\"view\":10}"
实际项目中需要保证命令的原子性,所以一般用 lua 脚本或者使用 pipeline 命令。
ziplist
dict
当使用ziplist
实现hash时,每个元素都使用两个挨在一起的节点来保存,前一个节点保存键值对的键,后一个节点保存键值对的值。
为什么数据量大时反而不使用压缩列表呢?因为压缩列表需要需要顺序检索,数据量大了之后性能就变差了。
HSET key field value # 设置 key 对应 hash 中的 field 对应的值
HGET key field # 获取 key 对应 hash 中的 field 对应的值
HDEL key field # 删除 key 对应的 hash 的键值对,该键为field
HMSET key field1 value1 field2 value2 ... fieldn valuen # 设置多个hash键值对
HMGET key field1 field2 ... fieldn # 获取多个field的值
HINCRBY key field increment # 给 key 对应 hash 中的 field 对应的值加一个整数值
HLEN key # 获取 key 对应的 hash 有多少个键值对
HGETALL key # 返回 key 对应 hash 中所有的字段和值。返回值中,每个字段名的下一个是它的值,所以返回值的长度是哈希集大小的两倍
a)存储对象:
> hset user:1001 name Dave age 26 sex male # hset 也可以代替hmset
(integer) 3
> hset user:1001 age 28 # 修改user:1001的age字段
(integer) 0
> hmget user:1001 name age sex
1) "Dave"
2) "28"
3) "male"
b)实现一个购物车:
需要用hash来保存每一个商品对象,但hash是无序的,还需要一个list来按加购顺序排列物品。
# key为User:1001:Cart的hash保存商品对象,field为加购商品id,value为商品加购数量;
# key为User:1001:Items的list按加购顺序保存商品id
# key field value
> hset User:1001:Cart itemId:20001 3
> lpush User:1001:Items itemId:20001
> hset User:1001:Cart itemId:20002 2
> lpush User:1001:Items itemId:20002
> hset User:1001:Cart itemId:20003 1
> lpush User:1001:Items itemId:20003
> hlen User:1001:Cart # 商品种类数量
(integer) 3
# 修改某个商品的数量
> hincrby User:1001:Cart itemId:20001 1 # 数量加1
(integer) 4
> hincrby User:1001:Cart itemId:20001 -1 # 数量减1
(integer) 3
# 删除某个商品,hash和list都要操作
> hdel User:1001:Cart itemId:20001
(integer) 1
> lrem User:1001:Items 1 itemId:20001
(integer) 1
# 获取所有物品
> lrange User:1001:Items 0 -1
1) "itemId:20003"
2) "itemId:20002"
> hgetall User:1001:Cart
1) "itemId:20002"
2) "2"
3) "itemId:20003"
4) "1"
集合用来存储具有唯一性的字段,不要求有序。
intset
存储;dict
存储;当使用dict
存来实现set时,set的成员字符串保存为键值对的键,而值都置为NULL。因为set成员并没有这个“值”。
SADD key member [member ...] # 添加一个或多个指定的member元素到集合 key中
SREM key member [member ...] # 在 key集合中移除指定的元素
SISMEMBER key member # 返回成员 member 是否是存储的集合 key的成员
SCARD key # 计算集合元素个数
SMEMBERS key # 返回key集合所有的元素
SRANDMEMBER key [count] # 随机返回key集合中的一个或者多个元素,不删除这些元素
SPOP key [count] # 从存储在key的集合中移除并返回一个或多个随机元素
SDIFF key [key ...] # 返回一个集合与给定集合的差集的元素
SINTER key [key ...] # 返回指定所有的集合的成员的交集
SUNION key [key ...] # 返回给定的多个集合的并集中的所有成员
a)实现抽奖:
> sadd luckyman 1001 1002 1003 1004 # 添加参与抽奖的用户id
(integer) 4
> smembers luckyman # 列出所有参与抽奖的用户id
1) "1001"
2) "1002"
3) "1003"
4) "1004"
> srandmember luckyman 1 # 到底是谁这么幸运呢?
1) "1003"
b)查找共同好友和推荐可能认识的好友:
> sadd User:1001:Friends 1002 1003 1004 1005
(integer) 4
> sadd User:1002:Friends 1001 1004 1005 1006
(integer) 4
> sinter User:1001:Friends User:1002:Friends # 求交集得到共同好友
1) "1004"
2) "1005"
# 推荐你可能认识的人
> sdiff User:1001:Friends User:1002:Friends # 求1001对1002的差集,1003可推荐给1002
1) "1002"
2) "1003"
有序集合(sorted set),存储有序的唯一性字段,其中的每个元素还会带有一个分值score
用于排序。
skiplist
;ziplist
存储;当使用ziplist
实现zset时,每个元素都使用两个挨在一起的节点来保存,前一个节点保存成员字符串,后一个节点保存其分值。
跳表skiplist
使得zset可以快速地完成元素的范围查找。实际上,当使用跳表skiplist
时,同时还会增加一个字典dict
来辅助实现zset时,通过字典,zset可以在O(1)时间复杂度下检索成员的分值。跳表与字典的结合使得zset的元素检索和范围查找都能够尽可能快地执行。
ZADD key [NX|XX] [CH] [INCR] score member [score member ...] # 添加到键为key的有序集合里面
ZREM key member [member ...] # 从键为key有序集合中删除 member 的键值对
ZINCRBY key increment member # 为有序集key的成员member的score值加上增量increment
ZCARD key # 返回key的有序集元素个数
ZSCORE key member # 返回有序集key中,成员member的score值
ZRANK key member # 返回有序集key中成员member的排名
# 下面的命令可以传递WITHSCORES选项,以便将元素的分数与元素一起返回
ZRANGE key start stop [WITHSCORES] # 返回有序集合key中的指定范围的元素(按得分从最低到最高排列)
ZREVRANGE key start stop [WITHSCORES] # 返回有序集和key种的指定区间内的成员(按score值递减来排列)
ZREMRANGEBYSCORE key min max # 移除有序集key中,所有score值介于min和max之间(包括等于min或max)的成员
ZADD
的选项参数的详细说明可参考zadd命令。
a)热搜排行:
# 先添加6个新闻,初始分数都是50
> zadd hotnews:20211128 50 2021112801 50 2021112802 50 2021112803 50 2021112804 50 2021112805 50 2021112806
(integer) 6
# 开始改变新闻的热度(分数)
> zincrby hotnews:20211128 1 2021112803
"51"
> zincrby hotnews:20211128 1 2021112803
"52"
> zincrby hotnews:20211128 1 2021112805
"51"
> zincrby hotnews:20211128 5 2021112805
"56"
> zincrby hotnews:20211128 10 2021112802
"60"
# 列出排行前2的新闻
> zrevrange hotnews:20211128 0 2 WITHSCORES
1) "2021112802"
2) "60"
3) "2021112805"
4) "56"
5) "2021112803"
6) "52"
b)延时队列:
将消息序列化成一个字符串作为 zset 的member,而将这个消息的到期处理时间作为score,然后通过lua等脚本语言用多个线程轮询 zset 获取最新到期的消息进行处理。
c)时间窗口限流:
系统限定用户的某个行为在指定的时间里只能发生N次。将“用户id+行为”作为key,时间作为field和value。通过ZREMRANGEBYSCORE
移除时间窗口之外的数据,然后用ZCARD
统计时间窗口内的行为次数,便可以判断时间窗口内行为执行是否超出N次。