七、Redis数据类型剖析与实用场景介绍

   Redis 不是一个普通的键值存储,它实际上是一个数据结构服务器,支持不同类型的值。这意味着,在传统的键值存储中,您将字符串键与字符串值相关联,而在 Redis 中,值不仅限于简单的字符串,还可以包含更复杂的数据结构。以下是Redis支持的所有数据结构的列表,本篇将一一介绍:

  • 二进制安全字符串(String)
  • 列表(List):根据插入顺序排序的字符串元素的集合。它们基本上是链表。
  • 集合(Sets):唯一的、未排序的字符串元素的集合。
  • 排序集合( Sorted sets),类似于集合,但其中每个字符串元素都与一个浮点值相关联,称为score。元素总是按它们的分数排序,因此与 Sets 不同,它可以检索一系列元素(例如,您可能会问:给我前 10 名,或后 10 名)。
  • 哈希(Hash),它是由与值关联的字段组成的映射。字段和值都是字符串。这与 Ruby 或 Python 哈希非常相似。
  • 位数组(Bitmaps)(或简单的位图):使用特殊命令可以像处理位数组一样处理字符串值:您可以设置和清除单个位,计算所有设置为 1 的位,找到第一个设置或未设置的位,等等。
  • HyperLogLogs:这是一种概率数据结构,用于估计集合的基数。不要害怕,它比看起来更简单…请参阅本教程后面的 HyperLogLog 部分。
  • Streams:提供抽象日志数据类型的类似地图条目的仅附加集合。它们在Redis Streams 简介中有深入介绍。

一 、Redis String

1.1 String 基本set/get了解

Redis 字符串类型是您可以与 Redis 键关联的最简单的值类型。它是 Memcached 中唯一的数据类型,所以新手在 Redis 中使用它也是很自然的。

让我们稍微使用字符串类型,使用redis-cli(redis-cli本篇中的所有示例都将通过)。

set mykey somevalue
OK
get mykey
“somevalue”

正如您所见,使用SET和GET命令是我们设置和检索字符串值的方式。请注意,SET将替换已存储在键中的任何现有值,如果键已经存在,即使键与非字符串值相关联。所以SET执行一个赋值。

值可以是各种字符串(包括二进制数据),例如您可以在值中存储 jpeg 图像。值不能大于 512 MB。
该SET命令有有趣的选项,这是作为附加参数。例如,如果键已经存在,我可能会要求SET失败,或者相反,只有当键已经存在时它才会成功:

set mykey newval nx
(nil)
set mykey newval xx
OK

即使字符串是 Redis 的基本值,您也可以使用它们执行一些有趣的操作。例如,一个是原子增量:

set counter 100
OK
incr counter
(integer) 101
incr counter
(integer) 102
incrby counter 50
(integer) 152

INCR 命令由一个解析字符串值作为一个整数,它的增量,并最终将获得的值作为新的值。还有其他类似的命令,如INCRBY、 DECR和DECRBY。在内部,它总是相同的命令,以稍微不同的方式行事。

INCR 是原子的意味着什么?即使多个客户端针对同一个密钥发出 INCR 也永远不会进入竞争条件(因为Redis是单进程的)。例如,永远不会发生客户端 1 读取“10”,客户端 2 同时读取“10”,两者都增加到 11,并将新值设置为 11。最终值将始终为 12,读取的值 在所有其他客户端未同时执行命令时执行增量设置操作。

有许多用于操作字符串的命令。例如,GETSET命令将键设置为新值,返回旧值作为结果。您可以使用此命令,例如,如果您有一个系统, 每次您的网站收到新访问者时都会使用INCR递增 Redis 密钥。您可能希望每小时收集一次此信息,而不要丢失一个增量。您可以GETSET键,为其分配新值“0”并读回旧值。

在单个命令中设置或检索多个键的值的能力对于减少延迟也很有用。出于这个原因,有MSET和MGET命令:

mset a 10 b 20 c 30
OK
mget a b c

  1. “10”
  2. “20”
  3. “30”

当使用MGET 时,Redis 返回一个值数组。

1.2更改和查询密钥空间

有些命令没有在特定类型上定义,但对于与键空间交互很有用,因此可以与任何类型的键一起使用。

例如,EXISTS命令返回 1 或 0 以表示数据库中是否存在给定的键,而DEL命令删除键和关联的值,无论值是什么。

set mykey hello
OK
exists mykey
(integer) 1
del mykey
(integer) 1
exists mykey
(integer) 0
从示例中,您还可以看到DEL本身如何返回 1 或 0,具体取决于键是否被删除(它存在)或不(没有具有该名称的键)。

与键空间相关的命令有很多,但以上两个是必不可少的,还有TYPE命令,它返回存储在指定键上的值的种类:

set mykey x
OK
type mykey
string
del mykey
(integer) 1
type mykey
none

1.3 Redis 过期:生存时间有限的键

在继续讨论更复杂的数据结构之前,我们需要讨论另一个无论值类型如何都有效的特性,称为Redis expires。基本上,您可以为密钥设置超时,这是有限的生存时间。当生存时间结束时,密钥会自动销毁,就像用户使用密钥调用DEL命令一样。

关于 Redis 过期的一些快速信息:

它们可以使用秒或毫秒精度进行设置。
但是,过期时间分辨率始终为 1 毫秒。 有关过期的信息被复制并保存在磁盘上,当您的 Redis
服务器保持停止状态时,时间实际上已经过去了(这意味着 Redis 会保存密钥的过期日期)。

设置过期很简单:

set key some-value
OK
expire key 5
(integer) 1
get key (immediately)
“some-value”
get key (after some time)
(nil)

密钥在两次GET调用之间消失了,因为第二次调用延迟了 5 秒以上。在上面的例子中,我们使用EXPIRE来设置过期时间(它也可以用来为已经拥有的键设置不同的过期时间,比如可以使用PERSIST来删除过期时间并使键永久持久化)。但是,我们也可以使用其他 Redis 命令创建带有过期时间的键。例如使用SET选项:

set key 100 ex 10
OK
ttl key
(integer) 9

上面的示例设置了一个值为字符串的键100,其过期时间为 10 秒。稍后调用TTL命令以检查密钥的剩余生存时间

二、Redis List 列表

为了解释 List 数据类型,最好从一些理论开始,因为信息技术人员经常以不正确的方式使用术语List。例如,“Python 列表”并不是名称所暗示的(链接列表),而是数组(实际上在 Ruby 中将相同的数据类型称为数组)。

从一个非常普遍的角度来看,List 只是一个有序元素的序列:10,20,1,2,3 是一个列表。但是使用 Array 实现的 List 的属性与使用Linked List实现的 List 的属性非常不同

Redis 列表是通过链表实现的。这意味着即使列表中有数百万个元素,在列表的头部或尾部添加新元素的操作也是在常数时间内执行的。使用LPUSH命令在10个元素的列表头添加一个新元素的速度与在1000万个元素的列表头添加一个元素的速度是一样的。

有什么缺点?在使用 Array 实现的列表(恒定时间索引访问)中通过索引访问元素非常快,而在由链表实现的列表中则没有那么快(其中操作需要的工作量与访问元素的索引成正比)。

当快速访问大量元素的中间很重要时,可以使用不同的数据结构,称为排序集。本教程稍后将介绍排序集。

2.1 List 存放、获取元素

  所述LPUSH命令将一个新元素到一个列表,在左侧(在头部),而RPUSH命令将一个新元素到一个列表,在右侧(在尾部)。最后 LRANGE命令从列表中提取元素范围:

rpush mylist A
(integer) 1
rpush mylist B
(integer) 2
lpush mylist first
(integer) 3
lrange mylist 0 -1

  1. “first”
  2. “A”
  3. “B”

请注意,LRANGE需要两个索引,即要返回的范围的第一个和最后一个元素。两个索引都可以是负数,告诉 Redis 从末尾开始计数:所以 -1 是最后一个元素,-2 是列表的倒数第二个元素,依此类推。

如您所见,RPUSH将元素附加在列表右侧,而最终的LPUSH将元素附加在列表左侧。

这两个命令都是可变参数命令,这意味着您可以在一次调用中自由地将多个元素推送到列表中:

rpush mylist 1 2 3 4 5 “foo bar”
(integer) 9
lrange mylist 0 -11)
“first”
2) “A”
3) “B”
4) “1”
5) “2”
6) “3”
7) “4”
8) “5”
9) “foo bar”

2.2 弹出元素

Redis 列表上定义的一个重要操作是弹出元素的能力。弹出元素是同时从列表中检索元素和从列表中删除元素的操作。您可以从左侧和右侧弹出元素,类似于如何在列表的两侧推送元素:

rpush mylist a b c
(integer) 3
rpop mylist
“c”
rpop mylist
“b”
rpop mylist
“a”

我们添加了三个元素并弹出了三个元素,所以在这个命令序列的末尾,列表是空的,没有更多的元素要弹出。如果我们尝试弹出另一个元素,这就是我们得到的结果:

rpop mylist
(nil)

Redis 返回一个 NULL 值以表示列表中没有元素。


2.3 列表常见实用实例

列表对许多任务很有用,以下是两个非常有代表性的用例:

  • 记住用户发布到社交网络的最新更新。
  • 进程之间的通信,使用消费者-生产者模式,其中生产者将项目推送到列表中,而消费者(通常是工作人员)消费这些项目并执行操作。Redis 有特殊的列表命令,使这个用例更加可靠和高效。

例如,流行的 Ruby 库resque和 sidekiq都在后台使用 Redis 列表来实现后台作业。

为了逐步描述一个常见的用例,假设您的主页显示了在照片共享社交网络中发布的最新照片,并且您希望加快访问速度。

每次用户发布新照片时,我们都会将其 ID 添加到带有LPUSH的列表中。当用户访问首页时,我们使用LRANGE 0 9以获取最新发布的10个项目。

2.4 截取列表元素

在许多用例中,我们只想使用列表来存储最新的项目,无论它们是什么:社交网络更新、日志或其他任何东西。

Redis 允许我们使用列表作为上限集合,只记住最新的 N 项并使用LTRIM命令丢弃所有最旧的项。

LTRIM命令类似于LRANGE,但不是显示元件的规定的范围内将其设置在该范围作为新的列表值。而是删除给定范围之外的所有元素。

一个例子会更清楚:

rpush mylist 1 2 3 4 5
(integer) 5
ltrim mylist 0 2
OK
lrange mylist 0 -1
1)“1”
2) “2”
3) “3”

其他须知:

1、 当我们向聚合数据类型添加元素时,如果目标键不存在,则会在添加元素之前创建一个空的聚合数据类型。
2、当我们从聚合数据类型中删除元素时,如果值保持为空,则键会自动销毁。Stream 数据类型是此规则的唯一例外。
3、调用只读命令,例如LLEN(返回列表的长度),或使用空键删除元素的写命令,总是产生相同的结果,就好像该键持有类型为命令期望找到。

三、 Redis Hash

Redis 散列看起来与人们可能期望的“散列”看起来完全一样,具有字段值对:

hmset user:1000 username antirez birthyear 1977 verified 1
OK
hget user:1000 username
“antirez”
hget user:1000 birthyear
“1977”
hgetall user:1000
1) “username”
2) “antirez”
3) “birthyear”
4) “1977”
5) “verified”
6) “1”

虽然散列可以方便地表示对象,但实际上可以放入散列中的字段数量没有实际限制(可用内存除外),因此您可以在应用程序中以多种不同方式使用散列。
     命令HMSET设置散列的多个字段,而HGET检索单个字段。HMGET类似于HGET,但返回一组值:

hmget user:1000 username birthyear no-such-field

  1. “antirez”
  2. “1977”
  3. (nil)

有些命令也可以对单个字段执行操作,例如HINCRBY:

hincrby user:1000 birthyear 10
(integer) 1987
hincrby user:1000 birthyear 10
(integer) 1997

您可以在文档中找到哈希命令的完整列表。
值得注意的是,小的散列(即一些具有小值的元素)在内存中以特殊方式编码,这使得它们非常有效。

四、Redis 集 Sets

Redis 集合是无序的字符串集合。该 SADD命令添加新的元素,一组。还可以对集合执行许多其他操作,例如测试给定元素是否已经存在,执行多个集合之间的交集、并集或差集,等等。

sadd myset 1 2 3
(integer) 3
smembers myset
1). 3
2). 1
3.) 2

在这里,我向我的集合中添加了三个元素,并告诉 Redis 返回所有元素。正如您所看到的,它们没有排序——Redis 可以在每次调用时以任何顺序自由返回元素,因为与用户没有关于元素排序的合同。

Redis 有测试成员资格的命令。例如,检查元素是否存在:

sismember myset 3
(integer) 1
sismember myset 30
(integer) 0
“3”是集合的成员,而“30”不是。

集合有利于表达对象之间的关系。

五、有序集合 Sorts Sets

5.1 基本添加操作

    有序集合是一种类似于集合和哈希混合的数据类型。与集合一样,有序集合由唯一的、不重复的字符串元素组成,因此在某种意义上,有序集合也是一个集合。
     此外,有序集合中的元素是按顺序获取的(因此它们不是按请求排序的,顺序是用于表示有序集合的数据结构的特性)。它们根据以下规则排序:

   如果 A 和 B 是具有不同分数的两个元素,如果 A.score > B.score则 A > B 。
    如果 A 和 B 的分数完全相同, 如果 A 字符串按字典顺序大于 B 字符串,则 A > B。A 和 B 字符串不能相等,因为排序集只有唯一元素。
     让我们从一个简单的例子开始,添加一些选定的黑客名称作为排序集合元素,他们的出生年份作为“分数”。

zadd hackers 1940 “Alan Kay”
(integer) 1
zadd hackers 1957 “Sophie Wilson”
(integer) 1
zadd hackers 1953 “Richard Stallman”
(integer) 1
zadd hackers 1949 “Anita Borg”
(integer) 1
zadd hackers 1965 “Yukihiro Matsumoto”
(integer) 1
zadd hackers 1914 “Hedy Lamarr”
(integer) 1
zadd hackers 1916 “Claude Shannon”
(integer) 1
zadd hackers 1969 “Linus Torvalds”
(integer) 1
zadd hackers 1912 “Alan Turing”
(integer) 1

正如您所看到的,ZADD类似于SADD,但需要一个额外的参数(放置在要添加的元素之前),即分数。 ZADD也是可变参数,因此您可以自由指定多个分值对,即使在上面的示例中没有使用。

使用排序集返回一个按出生年份排序的黑客列表是微不足道的,因为实际上他们已经排序了。

实现说明:Sorted set 是通过双端口数据结构实现的,包含一个skip list 和一个hash table,所以每次我们添加一个元素Redis 都会执行一个O(log(N)) 的操作。这很好,但是当我们要求排序元素时,Redis 根本不需要做任何工作,它已经全部排序了:

zrange hackers 0 -1

  1. “Alan Turing”
  2. “Hedy Lamarr”
  3. “Claude Shannon”
  4. “Alan Kay”
  5. “Anita Borg”

注意:0 和 -1 表示从元素索引 0 到最后一个元素(-1 在这里的作用就像在LRANGE命令的情况下一样)。

如果我想以相反的方式订购它们,从小到大怎么办?使用ZREVRANGE而不是ZRANGE

5.2在范围内操作

排序集比这更强大。它们可以在范围内操作。让我们把所有出生到 1950 年的人都包括在内。我们使用ZRANGEBYSCORE命令来做到这一点:

zrangebyscore hackers -inf 1950

  1. “Alan Turing”
  2. “Hedy Lamarr”
  3. “Claude Shannon”
  4. “Alan Kay”
  5. “Anita Borg”

我们要求 Redis 返回分数在负无穷大和 1950 之间的所有元素(包括两个极端)。

也可以删除元素范围。让我们从排序集中删除所有在 1940 年到 1960 年之间出生的黑客:

zremrangebyscore hackers 1940 1960
(integer) 4

ZREMRANGEBYSCORE可能不是最好的命令名称,但它可能非常有用,并返回已删除元素的数量。

为有序集合元素定义的另一个非常有用的操作是 get-rank 操作。可以问一个元素在有序元素集中的位置是什么。

zrank hackers “Anita Borg”
(integer) 4

该ZREVRANK命令也可以为了获得军衔,考虑的要素排序的下降方式。

5.3 有序集合使用实例

在切换到下一个主题之前,只是关于排序集的最后说明。排序集的分数可以随时更新。仅针对已包含在排序集中的元素调用ZADD将更新其分数(和位置),时间复杂度为O(log(N))。因此,当有大量更新时,排序集是合适的。

由于这个特性,一个常见的用例是排行榜。典型的应用程序是 Facebook 游戏,您可以将按高分排序的用户与获取排名操作结合起来,以显示前 N 个用户,以及用户在排行榜中的排名(例如,“你是这里的#4932 最高分”)。

命令: zrange 键名 start stop (从低到高)
命令:zrevrange 键名 start stop (从高到低)

在这里插入图片描述

六 、位图 bitmap

6.1 基本命令

SETBIT
对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。

SETBIT key offset value

offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内)。

GETBIT
对 key 所储存的字符串值,获取指定偏移量上的位(bit)。

GETBIT key offset

BITCOUNT
计算给定字符串中,被设置为 1 的比特位的数量。

BITCOUNT key

BITPOS
返回位图中第一个值为 bit 的二进制位的位置。

BITPOS key bit [start] [end]

6.2 操作演示

这里举例设计一个统计用户当月签到的数据情况的实际场景
0为未签到,1为签到 ,用户名称为xu ,这里为了简单从简,就编写一星期为例1~7:

注意位数虽然是从1开始的,但offset是从0开始的,所以offset0 代表第一位,因此为了方便我们从0代表星期一

在这里插入图片描述

6.3 应用实例

每日签到业务场景

在基本所有游戏类型中,签到功能的需求是必不可少的。比如签到领取奖励等需求

大致可根据策划分为:

每日签到可领取奖励

累计签到N天可领取额外奖励,中断则重置计数,每月初重置计数

统计每个月的签到次数、首次签到时间等

      对于应付这些涉及签到相关的需求,无论怎么变动,无疑记录玩家当月完整的签到信息是可以通通搞定的。
      例如:如果实际需求是这样的,玩家每天签到可以领取一次奖励,而无论玩家是从当月哪一天开始第一次签到,领取的奖励都是第一天奖励,而后依次签到顺应与第二天奖励、第n天奖励隔月的情况下,刷新重计~

      这种需求背景下,对于玩家签到已记录了玩家上一次签到的时间并且当月已签到次数,如果新增需求,玩家当月累计签到n天可领取额外奖励,中断重新计数,则再需求记录一个已累计签到天数等等

当然这不是今天的重点~下面进入正题

6.3.1 状态统计

接下来提供另外一种思路实现签到功能:

分析一下签到的实际场景其实是属于01状态统计,即集合元素的取值只有0和1两种,签到(1)或者未签到(0),在Redis当中,BitMap是很适宜这种场景需求的数据结构。

官方解释:Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计01状态的数据类型。String
类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态。即可以把 Bitmap
看作是一个 bit 数组。Bitmap 提供了 GETBIT/SETBIT 操作,使用一个偏移值 offset 对 bit 数组的某一个
bit 位进行读和写。不过,需要注意的是,Bitmap 的偏移量是从 0 开始算的,也就是说 offset 的最小值是 0。当使用SETBIT 对一个 bit 位进行写操作时,这个 bit 位会被设置为 1。Bitmap 还提供了 BITCOUNT 操作,用来统计这个bit 数组中所有“1”的个数。

因为每月初会重置签到数据,所以最简单的方式是每月存一条签到数据。

例如玩家100301在202106月的签到可以将key定为userId:sign:100301:202106

      而记录100301玩家在20210603当天签到则可如下:

虽然位图是从第一位开始的,但offset是从0开始的,为简化逻辑,我们将 0 作为每月的1号,所以0603就代表6月3号

setbit userId:sign:100301:202106 2 1        

    而检测100301该用户在20210603当天是否签到则可如下:

getbit userId:sign:100301:202106 2

      而统计该玩家在202106月的签到天数则可如下:

bitcount userId:sign:100301:202106

下面实际演示:
在这里插入图片描述

扩展一下:在这种情况下,每个玩家每个月签到的数据都有对应一个key,一个玩家一年则12条数据,一亿的玩家一年下来则为12亿条数据,跨月的时候可根据需求删除历史key问题也不大,实际存储1亿用户一个月的记录就做到一亿条数据,而每条数据的value采用长度4个字节(32位)的位图,最大月份是31天,完整足够表示了。

举例:
在这里插入图片描述
4字节可以存一个用户一个月的签到情况
48字节可以存一个用户一年的签到情况
则一百万用户一年的签到情况只需占用:460M左右的内存
48*1000000=48000000 bit
可见其性能之高
在这里插入图片描述



6.3.2 思考?如何记录用户的最长连续签到天数



谢谢观看!



上一篇: 六、Redis集群讲解与搭建(保姆式解析!!!)



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿小许

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值