[C/C++后端开发学习]19 Redis对象与命令

文章目录

  • 关于Redis
  • key-value数据对象
    • 应用场景
  • 对象的基础数据结构及其选择
  • 数据对象及其操作命令
    • 1 string
      • 1.1 基础数据结构
      • 1.2 基础命令
      • 1.3 应用
    • 2 list
      • 2.1 基础数据结构
      • 2.2 基础命令
      • 2.3 应用
    • 3 hash
      • 3.1 基础数据结构
      • 3.2 基础命令
      • 3.3 应用
    • 4 set
      • 4.1 基础数据结构
      • 4.2 基础命令
      • 4.3 应用
    • 5 zset
      • 5.1 基础数据结构
      • 5.2 基础命令
      • 5.3 应用

关于Redis

RedisRemote Dictionary Service,远程字典服务)是一种内存数据库,也被称为KV数据库、数据结构数据库。

Redis常用作数据的缓存,缓存热点数据,减少数据库的压力。

完整的Redis命令查看:Redis命令中心。

key-value数据对象

Redis对外的接口总是通过键key来获取数据值value。
Redis对外提供了5种数据对象来存储key-value:string、list、hash、set以及zset。
[C/C++后端开发学习]19 Redis对象与命令_第1张图片

应用场景

  • 缓存热点数据,减少数据库压力(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:
[C/C++后端开发学习]19 Redis对象与命令_第2张图片

我个人理解这样设计的好处就是将对外提供的数据操作接口统一成key-value的形式,内部再根据不同的数据形式和数据结构进行不同方式的数据编码。

数据结构的选择以及选择的条件如下表所示:
[C/C++后端开发学习]19 Redis对象与命令_第3张图片

注意:Redis中的多数字符串是简单动态字符串(Simple Dynamic String,SDS),它是一种二进制安全字符串,可以存储图片等二进制数据。一般来说key和value都是以SDS形式存储。

数据对象及其操作命令

1 string

动态字符串,实质是字符数组的封装,最大长度512M。可扩容,字符串长度小于1M时,每次加倍扩容;超过1M时,每次只多扩1M。

1.1 基础数据结构

  • 字符串长度小于等于 20 且能转成整数,则使用int存储;
  • 字符串长度小于等于 44,则使用embstr存储;
  • 字符串长度大于 44,则使用raw存储;

embstr是专门用于保存短字符串的一种优化编码方式,与raw字符串几乎相同,都使用对象描述符结构体redisObject+sds描述符结构体sdshdr来表示一个字符串对象(字符串内容的空间紧随sdshdr其后),只不过raw字符串会分两次来为二者分配内存,而embstr仅调用一次内存分配来为整体分配一块连续的空间,好处是减少内存分配和释放的次数,也更容易利用缓存。

redis的内存分配器在数据长度小于等于64字节时,认为是小字符串,而在大于64字节时,认为是大字符串。以此为分界Redis将对数据做不同的处理。然而上面对string对象不同存储方式的分界却为44字节。这是与Redis内部描述对象的结构体redisObject以及描述string对象的结构体sdshdr大小有关,这些结构体会使用19字节空间,同时字符串要给\0预留一个字节,所以就要占用掉20个字节,对应地区分大小字符串的分界线就变成44字节了。

1.2 基础命令

其中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删除。

1.3 应用

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

2 list

双向链表,首尾数据操作(删除和增加)的时间复杂度O(1) ;查找中间元素的时间复杂度为O(n)。

何时不会对表内节点的数据进行压缩:

  • 元素长度小于 48字节,不压缩;
  • 元素压缩前后长度差不超过 8字节,不压缩;

2.1 基础数据结构

  • 节点数量小于等于512且字符串长度小于等于64字节时使用压缩列表ziplist
  • 节点数量大于512或字符串长度大于64字节时采用双向链表quicklist

2.2 基础命令

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 之间的元素

2.3 应用

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 命令。

3 hash

3.1 基础数据结构

  • 节点数量小于等于512且字符串长度小于等于64字节时采用压缩列表ziplist
  • 节点数量大于512或字符串长度大于64字节时采用字典dict

当使用ziplist实现hash时,每个元素都使用两个挨在一起的节点来保存,前一个节点保存键值对的键,后一个节点保存键值对的值。

为什么数据量大时反而不使用压缩列表呢?因为压缩列表需要需要顺序检索,数据量大了之后性能就变差了。

3.2 基础命令

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 中所有的字段和值。返回值中,每个字段名的下一个是它的值,所以返回值的长度是哈希集大小的两倍

3.3 应用

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"

4 set

集合用来存储具有唯一性的字段,不要求有序。

4.1 基础数据结构

  • 元素都为整数且节点数量小于等于 512,则使用整数数组intset存储;
  • 元素当中有一个不是整数或者节点数量大于 512,则使用字典dict存储;

当使用dict存来实现set时,set的成员字符串保存为键值对的键,而值都置为NULL。因为set成员并没有这个“值”。

4.2 基础命令

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 ...]	# 返回给定的多个集合的并集中的所有成员

4.3 应用

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"

5 zset

有序集合(sorted set),存储有序的唯一性字段,其中的每个元素还会带有一个分值score用于排序。

5.1 基础数据结构

  • 节点数量大于 128或者有一个字符串长度大于64,则使用跳表skiplist
  • 节点数量小于等于128且所有字符串长度小于等于64,则使用ziplist存储;

当使用ziplist实现zset时,每个元素都使用两个挨在一起的节点来保存,前一个节点保存成员字符串,后一个节点保存其分值。

跳表skiplist使得zset可以快速地完成元素的范围查找。实际上,当使用跳表skiplist时,同时还会增加一个字典dict来辅助实现zset时,通过字典,zset可以在O(1)时间复杂度下检索成员的分值。跳表与字典的结合使得zset的元素检索和范围查找都能够尽可能快地执行。

5.2 基础命令

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命令。

5.3 应用

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次。

你可能感兴趣的:(C/C++后端开发学习笔记,网络,linux,运维)