文章目录
- 前言
- 字符串
- 常见命令
- 内部编码
- 使用场景
- 1、缓存(Cache)功能
- 2、计数
- 3、共享Session
- 4、限速
- 哈希
- 命令
- 内部编码
- 使用场景
- 存储结构化数据
- 列表
- 命令
- 内部编码
- 使用场景
- 1.阻塞消息队列模型
- 2.文章列表
- 3.微博 Timeline
- 集合
- 命令
- 内部编码
- 使用场景
- 1.给用户增加标签
- 2.通过Set来计算用户之间的共同好友
- 3.使用Set统计UV
- 有序集合
- 命令
- 内部编码
- 使用场景
- 排行榜
前言
Redis提供了5种数据结构,理解每种数据结构的特点对于Redis开发运维非常重要,同时掌握Redis的单线程命令处理机制,会使数据结构和命令的选择事半功倍。
字符串
字符串类型是 Redis 最基础的数据类型,关于字符串需要特别注意:1)⾸先 Redis 中所有的键的类型都是字符串类型,⽽且其他⼏种数据结构也都是在字符串类似基础上构建的,例如列表和集合的元素类型是字符串类型,所以字符串类型能为其他 4 种数据结构的学习奠定基础。
常见命令
常用命令: set,get,decr,incr,mget 等。
大概得命令一览
内部编码
字符串类型的内部编码有 3 种:
• int:8 个字节的⻓整型。
• embstr:⼩于等于 39 个字节的字符串。
• raw:⼤于 39 个字节的字符串。
Redis 会根据当前值的类型和⻓度动态决定使⽤哪种内部编码实现。
当然这里的具体字节,大家不用强行记忆,因为都是根据具体的场景去规定使用哪种编码格式实现。
使用场景
1、缓存(Cache)功能
是比较典型的缓存使用场景,其中Redis作为缓存层,MySQL作为存储层,绝大部分请求的数据都是从Redis中获取。
具体的伪代码实现如下
2、计数
许多应用都会使用Redis作为计数的基础工具,它可以实现快速计数、查询缓存的功能,同时数据可以异步落地到其他数据源。例如笔者所在团队的视频播放数系统就是使用Redis作为视频播放数计数的基础组件,用户每播放一次视频,相应的视频播放数就会自增1,如图所示:
具体的伪代码实现
3、共享Session
一个分布式Web服务将用户的Session信息(例如用户登录信息)保存在各自服务器中,这样会造成一个问题,出于负载均衡的考虑,分布式服务会将用户的访问均衡到不同服务器上,用户刷新一次访问可能会发现需要重新登录,这个问题是用户无法容忍的。
为了解决这个问题,可以使用Redis将用户的Session进行集中管理,如图所示,在这种模式下只要保证Redis是高可用和扩展性的,每次用户更新或者查询登录信息都直接从Redis中集中获取。
4、限速
很多应用出于安全的考虑,会在每次进行登录时,让用户输入手机验证码,从而确定是否是用户本人。但是为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。
上述就是利用Redis实现了限速功能,例如一些网站限制一个IP地址不
能在一秒钟之内访问超过n次也可以采用类似的思路。
哈希
几乎所有的编程语言都提供了哈希(hash)类型,它们的叫法可能是哈希、字典、关联数组。在Redis中,哈希类型是指键值本身又是一个键值对结构,形如value={{field1,value1},…{fieldN,valueN}},Redis键值对和哈希类型二者的关系可以用来表示。
命令
常用命令: hget,hset,hgetall 等。
具体的hash命令的时间复杂度:
Redis 的哈希(Hash)数据结构允许用户存储键值对的集合。哈希中的每个字段(field)都对应一个值(value),并且这些字段都是唯一的。下面是一些 Redis 哈希数据结构的常用命令及其解释:
HSET
命令:HSET key field value
解释:将哈希表 key 中的字段 field 的值设为 value。如果 key 不存在,一个空哈希表会被创建并执行 HSET 操作。如果字段已经存在于哈希表中,该操作会覆盖旧值。
HGET
命令:HGET key field
解释:获取存储在哈希表 key 中给定字段 field 的值。
HGETALL
命令:HGETALL key
解释:返回哈希表 key 中,所有的字段和值。
HMSET
命令:HMSET key field1 value1 field2 value2 … fieldN valueN
解释:同时设置哈希表 key 中的多个字段值。
HMGET
命令:HMGET key field1 field2 … fieldN
解释:获取哈希表 key 中多个字段的值。
HINCRBY
命令:HINCRBY key field increment
解释:将哈希表 key 中指定字段 field 的值增加 increment。
HINCRBYFLOAT
命令:HINCRBYFLOAT key field increment
解释:将哈希表 key 中指定字段 field 的值增加 increment,increment 可以是浮点数。
HDEL
命令:HDEL key field1 field2 … fieldN
解释:删除哈希表 key 中的一个或多个字段,不存在的字段将被忽略。
HEXISTS
命令:HEXISTS key field
解释:检查哈希表 key 中,指定的字段 field 是否存在。
HLEN
命令:HLEN key
解释:返回哈希表 key 中字段的数量。
HKEYS
命令:HKEYS key
解释:获取哈希表 key 中的所有字段。
HVALS
命令:HVALS key
解释:获取哈希表 key 中的所有值。
这些命令提供了对 Redis 哈希数据结构的基本操作,使得用户能够轻松地存储、检索和修改哈希表中的字段和值。
内部编码
哈希类型的内部编码有两种:
- ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
- hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。
使用场景
存储结构化数据
关系型数据表记录的两条用户信息,用户的属性作为表的列,每条用户信息作为行。
相比于使用字符串序列化缓存用户信息,哈希类型变得更加直观,并且在更新操作上会更加便捷。可以将每个用户定义的id定义为后缀,对file-value对应的每个用户的属性。
另外还有一个原因就是,如果使用String(json)的格式来存储UserInfo。万一我们想修改field,或者修改某个field,就需要吧整个json都读取出来,解析成对象,操作filed,再重写json字符串,再写回去。
如果使用hash的方式来表示UserInfo,就可以使用field表示对象的每个属性,此时就可以非常方便的修改、获取任何一个属性的值了。具体的伪代码如下。
但是需要注意的是哈希类型和关系型数据库有两点不同之处:
• 哈希类型是稀疏的,⽽关系型数据库是完全结构化的,例如哈希类型每个键可以有不同的 field,而关系型数据库⼀旦添加新的列,所有⾏都要为其设置值,即使为 null,如图 所示 。
• 关系数据库可以做复杂的关系查询,而Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等基本不可能,维护成本高。
开发人员需要将两者的特点搞清楚,才能在适合的场景使用适合的技术。到目前为止,我们已经能够用三种方法缓存用户信息,下面给出三种方案的实现方法和优缺点分析。
- 1)原生字符串类型:每个属性一个键。
set user:1:name tom
set user:1:age 23
set user:1:city beijing
优点:简单直观,每个属性都支持更新操作。
缺点:占用过多的键,内存占用量较大,同时用户信息内聚性比较差,
所以此种方案一般不会在生产环境使用。
- 2)序列化字符串类型:将用户信息序列化后用一个键保存。
set user:1 serialize(userInfo)
优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
缺点:序列化和反序列化有一定的开销,同时每次更新属性都需要把全部数据取出进行反序列化,更新后再序列化到Redis中。
- 3)哈希类型:每个用户属性使用一对field-value,但是只用一个键保存.
hmset user:1 name tomage 23 city beijing
优点:简单直观,如果使用合理可以减少内存空间的使用。
缺点:要控制哈希在ziplist和hashtable两种内部编码的转换,hashtable会消耗更多内存
列表
列表(list)类型是用来存储多个有序的字符串,如图所示,a、b、c、d、e五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element),一个列表最多可以存储2^32-1 个元素。在Redis中,可以对列表两端插入(push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景.
列表类型的特点:
第⼀、列表中的元素是有序的,这意味着可以通过索引下标获取某个元素或者某个范围的元素列表,
例如要获取图中 的第 5 个元素,可以执行 lindex user:1:messages 4 或者倒数第 1 个元素,lindex
user:1:messages -1 就可以得到元素 e。
第⼆、区分获取和删除的区别,例如图 2-20 中的 lrem 1 b 是从列表中把从左数遇到的前 1 个 b 元素删
除,这个操作会导致列表的⻓度从 5 变成 4;但是执⾏ lindex 4 只会获取元素,但列表⻓度是不会变化
的。
第三、列表中的元素是允许重复的,例如图 2-21 中的列表中是包含了两个 a 元素的。
命令
常用命令: lpush,rpush,lpop,rpop,lrange等
Redis 的列表(List)数据结构是一个简单的字符串列表,按照插入顺序排序。你可以从列表的两端推入或者弹出元素。下面是一些 Redis 列表数据结构的常用命令及其解释:
LPUSH
命令:LPUSH key value1 value2 … valueN
解释:将一个或多个值插入到列表头部。如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作。当 key 存在但不是列表类型时,会返回一个错误。
RPUSH
命令:RPUSH key value1 value2 … valueN
解释:将一个或多个值插入到列表尾部。如果 key 不存在,一个空列表会被创建并执行 RPUSH 操作。当 key 存在但不是列表类型时,会返回一个错误。
LPOP
命令:LPOP key
解释:移除并获取列表的第一个元素。如果列表没有元素会返回 nil。
RPOP
命令:RPOP key
解释:移除并获取列表的最后一个元素。如果列表没有元素会返回 nil。
LINDEX
命令:LINDEX key index
解释:获取列表在指定索引上的元素。索引是从 0 开始的。如果索引超出范围,返回 nil。
LRANGE
命令:LRANGE key start stop
解释:获取列表指定范围内的元素。start 和 stop 是索引,其中 0 表示列表的第一个元素,-1 表示最后一个元素,-2 表示倒数第二个元素,以此类推。
LLEN
命令:LLEN key
解释:获取列表的长度。
LINSERT
命令:LINSERT key BEFORE|AFTER pivot value
解释:在列表中找到指定元素 pivot,并在其前(BEFORE)或后(AFTER)插入 value。如果列表不存在,或者 pivot 不在列表中,该命令不做任何操作。
LREM
命令:LREM key count value
解释:移除列表中等于 value 的元素。count 的值决定了移除操作的执行方式:
count > 0:从头向尾移除最多 count 个等于 value 的元素。
count < 0:从尾向头移除最多 abs(count) 个等于 value 的元素。
count = 0:移除所有等于 value 的元素。
LSET
命令:LSET key index value
解释:将列表 key 下标为 index 的元素的值设置为 value。
LTRIM
命令:LTRIM key start stop
解释:对一个已存在的列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间内的元素都将被删除。
BLPOP 和 BRPOP
命令:BLPOP key1 key2 … keyN timeout 和 BRPOP key1 key2 … keyN timeout
解释:它们是阻塞版本的 LPOP 和 RPOP,当给定列表内没有任何元素可供弹出的时候,将阻塞连接直到等待超时或发现可弹出元素为止。
这些命令为 Redis 列表数据结构提供了丰富的操作选项,使得用户能够轻松地管理列表中的元素。
内部编码
列表类型的内部编码有两种。
- ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时(默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使用。
- linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用linkedlist作为列表的内部实现。
使用场景
1.阻塞消息队列模型
Redis 可以使⽤ lpush + brpop 命令组合实现经典的阻塞式⽣产者-消费者模型队列,
⽣产者客⼾端使⽤ lpush 从列表左侧插⼊元素,多个消费者客⼾端使⽤ brpop 命令阻塞式地从队列中"争抢" 队⾸元素。通过多个客⼾端来保证消费的负载均衡和⾼可⽤性。
2.文章列表
每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
1)每篇文章使用哈希结构存储,例如每篇文章有3个属性title、
hmset acticle:1 title xx timestamp 1476536196 content xxxx
2)向用户文章列表添加文章,user:{id}:articles作为用户文章列表的键
lpush user:1:acticles article:1 article3
...
lpush user:k:acticles article:5
3)分页获取用户文章列表,例如下面伪代码获取用户id=1的前10篇文章:
articles = lrange user:1:articles 0 9
for article in {articles}
hgetall {article}
使用列表类型保存和获取文章列表会存在两个问题。
第一,如果每次分页获取的文章个数较多,需要执行多次hgetall操作。
第二,分页获取文章列表时,lrange命令在列表两端性能较好,但是如果列表较大,获取列表中间范围的元素性能会变差。
3.微博 Timeline
每个⽤⼾都有属于⾃⼰的 Timeline(微博列表),现需要分⻚展⽰⽂章列表。此时可以考虑使⽤列表,因为列表不但是有序的,同时⽀持按照索引范围获取元素。
1)每篇微博使⽤哈希结构存储,例如微博中 3 个属性:title、timestamp、content
hmset mblog:1 title xx timestamp 1476536196 content xxxxx
...
hmset mblog:n title xx timestamp 1476536196 content xxxxx
2)向用户 Timeline 添加微博,user::mblogs 作为微博的键:
lpush user:1:mblogs mblog:1 mblog:3
...
lpush user:k:mblogs mblog:9
3)分页获取用户的 Timeline,例如获取⽤⼾ 1 的前 10 篇微博
keylist = lrange user:1:mblogs 0 9
for key in keylist {
hgetall key
}
实际上列表的使用场景很多,在选择时可以参考以下口诀:
集合
集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。如图所示,集合user:1:follow包含着"it"、“music”、“his”、"sports"四个元素,一个集合最多可以存储2^32-1个元素。Redis除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在实际开发中解决很多实际问题。
命令
常用命令: sadd,spop,smembers,sunion 等
内部编码
集合类型的内部编码有两种:
- intset(整数集合):当集合中的元素都是整数且元素个数小于set-maxintset-entries配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用。
- hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使
用hashtable作为集合的内部实现。
使用场景
集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。例如一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码产品,通常会为网站带来更多的利益。
1.给用户增加标签
下面使用集合类型实现标签功能的若干功能。
(1)给用户添加标签
sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4
(2)给标签添加用户
sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3
sadd tagk:users user:1 user:2
(3)删除用户下的标签
srem user:1:tags tag1 tag5
(4)删除标签下的用户
srem tag1:users user:1
srem tag5:users user:1
(5)计算用户共同感兴趣的标签
可以使用sinter命令,来计算用户共同感兴趣的标签,如下代码所示:
sinter user:1:tags user:2:tags
2.通过Set来计算用户之间的共同好友
基于集合求交集
3.使用Set统计UV
使用Redis的Set集合来统计UV(Unique Visitors,独立访客)是一个常见且高效的做法。UV通常用于衡量网站的独立访问用户数量,即不同用户访问的次数。下面是一个使用Redis Set集合来统计UV的基本思路:
设计Key:
首先,你需要为UV统计设计一个Redis的key。这个key通常与统计的时间范围相关,比如按天统计的UV,你可以使用uv:2023-10-23这样的key。
添加用户ID:
每当有用户访问网站时,你需要将用户的唯一标识(比如用户ID)添加到Redis的Set集合中。这个操作可以通过SADD命令来实现。例如:
SADD uv:2023-10-23 user_id_123
如果user_id_123已经存在于集合中,SADD命令不会有任何效果,也不会报错。这确保了每个用户ID在集合中只会出现一次。
获取UV数量:
要获取某个时间范围内的UV数量,你可以使用SCARD命令来获取Set集合的元素数量。例如:
SCARD uv:2023-10-23
这个命令会返回uv:2023-10-23这个集合中的元素数量,也就是那一天的独立访客数量。
定期清理:
由于UV统计通常是按时间范围进行的(如按天、按小时等),你需要定期清理旧的UV数据,以释放Redis的内存空间。这可以通过删除旧的key来实现,比如使用DEL命令:
DEL uv:2023-10-22
你也可以使用Redis的过期键功能,为key设置一个TTL(Time To Live),让Redis自动在一段时间后删除这些key。
并发控制:
在高并发的场景下,多个请求可能同时尝试向同一个Set集合添加同一个用户ID。由于Redis的SADD命令是原子性的,你不需要额外的并发控制机制来确保数据的准确性。
通过以上步骤,你可以利用Redis的Set集合高效地统计网站的UV数据。需要注意的是,这种方法只适用于统计UV数量,如果你需要更详细的用户行为数据或更复杂的分析功能,可能需要结合其他数据库或分析工具来实现。
有序集合
有序集合相对于哈希、列表、集合来说会有一点点陌生,但既然叫有序集合,那么它和集合必然有着联系,它保留了集合不能有重复成员的特性,但不同的是,有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是,它给每个元素设置一个分数(score)作为排序的依据。如图所示,该有序集合包含kris、mike、frank、tim、martin、tom,它们的分数分别是1、91、200、220、250、251,有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能,合理的利用有序集合,能帮助我们在实际开发中解决很多问题。
命令
常用命令: zadd,zrange,zrem,zcard等
内部编码
有序集合类型的内部编码有两种:
- ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist-entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。
- skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。
使用场景
排行榜
有序集合比较典型的使⽤场景就是排行榜系统。例如常见的网站上的热榜信息,榜单的维度可能是多方面的:按照时间、按照阅读量、按照点赞量。本例中我们使用点赞数这个维度,维护每天的热榜:
例如用户mike上传了一个视频,并获得了3个赞,可以使用有序集合的
zadd和zincrby功能:
(1)添加用户赞数
zadd user:ranking:2016_03_15 mike 3
如果之后再获得一个赞,可以使用zincrby:
zincrby user:ranking:2016_03_15 mike 1
(2)取消用户赞数
由于各种原因(例如用户注销、用户作弊)需要将用户删除,此时需要
将用户从榜单中删除掉,可以使用zrem。例如删除成员tom:
zrem user:ranking:2016_03_15 mike
(3)展示获取赞数最多的十个用户
此功能使用zrevrange命令实现:
zrevrangebyrank user:ranking:2016_03_15 0 9
(4)展示用户信息以及用户分数
此功能将用户名作为键后缀,将用户信息保存在哈希类型中,至于用户
的分数和排名可以使用zscore和zrank两个功能:
hgetall user:info:tom
zscore user:ranking:2016_03_15 mike
zrank user:ranking:2016_03_15 mike