redis数据结构、订阅及案例
# Redis 支持的数据类型: SHLSS
数据类型:支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
- 全局key操作: 全局key操作命令:忽略与key关联的value的类型
- String(字符串): string是redis最基本的类型,一个key对应一个value。
- hash(哈希):Redis hash 是一个键值对(key-value)集合。Redis hash 是一个 String 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
- list(列表):list 是字符串列表,按照插入顺序排序。元素可以在列表的头部(左边)或者尾部(右边)进行添加。redis中提供了list接口,这个list提供了lpush和rpop,这两个方法具有原子性,可以插入队列元素和弹出队列元素。
- set(集合):Redis 的 set 是 String 类型的无序集合。集合是通过哈希表实现的,所以添加,删除,查找的
复杂度都是O(1)
。 - zset(sorted set 有序集合):Redis
zset 和 set 一样也是 String 类型元素的集合,且不允许重复的成员
。不同的 zset 是每个元素都会关联一个 double 类型的分数
。zset 通过这个分数来为集合中所有元素进行从小到大的排序。zset 的成员是唯一的,但分数(score)却可以重复。
# Redis 常用的命令有哪些
- 图示分类:
- 常用指令:
- 各个数据类型应用场景:SHLSS:
# 各个数据类型最大存储量
- Strings类型:一个 String 类型的 value 最大可以存储 512M
- Sets类型:元素个数最多为 2^32-1 个,也就是 4294967295 个。
- Lists类型:list 的元素个数最多为 2^32-1 个,也就是 4294967295 个。
- Hashes类型:键值对个数最多为 2^32-1 个,也就是 4294967295 个。
- SortedSets类型:跟 Sets 类型相似。
# 设置键的生存时间和过期时间命令
- EXPIRE 以秒为单位,设置键的生存时间
- PEXPIRE 以毫秒为单位,设置键的生存时间
- EXPIREAT 以秒为单位,设置键的过期 UNIX 时间戳
- PEXPIREAT 以毫秒为单位,设置键的过期 UNIX 时间戳
# 场景解析
# String 类型使用场景
场景一:商品库存数
- 从业务上,商品库存数据是热点数据,交易行为会直接影响库存。而 Redis 自身 String 类型提供了:
incr key && decr key && incrby key increment && decrby key decrement
- set goods_id 10; 设置 id 为 good_id 的商品的库存初始值为 10;
- 从业务上,商品库存数据是热点数据,交易行为会直接影响库存。而 Redis 自身 String 类型提供了:
decr goods_id; 当商品被购买时候,库存数据减 1。
依次类推的场景:商品的浏览次数,问题或者回复的点赞次数等。这种计数的场景都可以考虑利用 Redis 来实现。
场景二:时效信息存储
- Redis 的数据存储具有自动失效能力。也就是存储的 key-value 可以设置过期时间:set(key, value, expireTime)。
- 比如,用户登录某个 App 需要获取登录验证码, 验证码在 30 秒内有效。那么我们就可以使用 String 类型存储验证码,同时设置 30 秒的失效时间。
keys = redisCli.get(key); if(keys != null){ return false; } else { sendMsg() redisCli.set(keys,value,expireTime) }
1
2
3
4
5
6
7
# Hash 类型使用场景
- Redis 在存储对象(例如:用户信息)的时候需要对对象进行序列化转换然后存储。
- 还有一种形式,就是将对象数据转换为 JSON 结构数据,然后存储 JSON 的字符串到 Redis。
- 对于一些对象类型,还有一种比较方便的类型,那就是按照 Redis 的 Hash 类型进行存储。
hset key field value
例如,我们存储一些网站用户的基本信息, 我们可以使用:
hset user101 name “小明”
hset user101 phone “123456”
hset user101 sex “男”
2
3
这样就存储了一个用户基本信息,存储信息有:{name : 小明, phone : “123456”,sex : “男”} 当然这种类似场景还非常多, 比如存储订单的数据,产品的数据,商家基本信息等。大家可以参考来进行存储选型。
# List 类型使用场景
list 是按照插入顺序排序的字符串链表。可以在头部和尾部插入新的元素(双向链表实现,两端添加元素的时间复杂度为 O(1))
比方说用他的List来做FIFO双向链表,实现一个
轻量级的高性能消息队列服务
, 用他的Set可以做高性能的 tag 系统等等。
场景一:消息队列实现
- 目前有很多专业的消息队列组件 Kafka、RabbitMQ 等。 我们在这里仅仅是使用 list 的特征来实现消息队列的要求。在实际技术选型的过程中,大家可以慎重思考。
- list 存储就是一个队列的存储形式:
- lpush key value; 在 key 对应 list 的头部添加字符串元素;
- rpop key;移除列表的最后一个元素,返回值为移除的元素。
- 不要使用redis去做消息队列,这不是redis的设计目标。但实在太多人使用redis去做去消息队列,redis的作者看不下去。kafka才好用。
场景二:最新上架商品
- 在交易网站首页经常会有***新上架产品推荐的模块***, 这个模块是存储了最新上架前 100 名。这时候使用 Redis 的 list 数据结构,来进行 TOP 100 新上架产品的存储。
- Redis ltrim 指令对一个列表进行修剪(trim),这样 list 就会只包含指定范围的指定元素。
- ltrim key start stop; start 和 stop 都是由 0 开始计数的,这里的 0 是列表里的第一个元素(表头),1 是第二个元素。
如下伪代码演示:
//把新上架商品添加到链表里 ret = r.lpush(“new:goods", goodsId) //保持链表 100 位 ret = r.ltrim("new:goods", 0, 99) //获得前 100 个最新上架的商品 id 列表 newest_goods_list = r.lrange("new:goods", 0, 99)
1
2
3
4
5
6用户最近访问记录
说明:比如想知道最近访问的20个用户,如果用mysql数据库实现很麻烦,可以使用redis实现,这时候要用到redis队列属性这么个事,先进先出原则,和redis提供的lpush lpop
# Set 类型使用场景
set 也是存储了一个集合列表功能。和 list 不同,set 具备去重功能。当需要存储一个列表信息,同时要求列表内的元素不能有重复,这时候使用 set 比较合适。与此同时,set 还提供的交集、并集、差集。
例如,在交易网站,我们会存储用户感兴趣的商品信息,在进行相似用户分析的时候, 可以通过计算两个不同用户之间感兴趣商品的数量来提供一些依据。
//userid 为用户 ID , goodID 为感兴趣的商品信息。 sadd “user:userId” goodID; sadd “user:101”, 1 sadd “user:101”, 2 sadd “user:102”, 1 Sadd “user:102”, 3 sinter “user:101” “user:101”
1
2
3
4
5
6
7获取到两个用户相似的产品, 然后确定相似产品的类目就可以进行用户分析。类似的应用场景还有, 社交场景下共同关注好友, 相似兴趣 tag 等场景的支持。
说明: redis数据类型中有一个set类型,set结构在存储数据的时候是无序的,而且每个值是不一样的,不能重复,这样就可以快速的查找元素中某个值是否存在,精确的进行增加删除操作。redis锁防刷机制实现; 例如设置一个值不重复并且设置失效时间一天就可以达到一天一个用户只能投票一次的效果;
# Sorted Set 类型使用场景
- Redis sorted set 的使用场景与 set 类似,区别是 set 不是自动有序的,而 sorted set 可以通过提供一个 score 参数来为存储数据排序,并且***是自动排序,插入既有序。***sorted set 适合有排序需求的集合存储场景。 Redis在内存中对数字进行递增或递减的操作实现的非常好。 正好提供了这两种数据结构。
- 业务中如果需要一个有序且不重复的集合列表,就可以选择 sorted set 这种数据结构。
- 比如,商品的购买热度可以将购买总量 num 当做商品列表的 score,这样获取最热门的商品时就是可以自动按售卖总量排好序。
- 我们要从排序集合中获取到排名最靠前的 10 个用户–我们称之为“user_scores”, 我们只需要像下面一样执行即可:当然, 这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数, 你需要这样执行:
ZRANGE user_scores 0 10 WITHSCORES
# 类型深度分析
# 链表数据结构的特征
双端、无环、带长度记录
- 双端引用:链表的最前和最后节点都有引用,获取前后节点的复杂度为 o(1)
- 无环链表:对于链表的访问都是以 null 结束
- 长度计数器:通过 len 属性来记录链表长度
多态:使用 void*
指针来保存节点值, 可以通过 dup
、 free
、 match
为节点值设置类型特定函数, 可以保存不同类型的值。
# 字典的实现
其实字典这种数据结构也内置在很多高级语言中,但是c语言没有,所以redis自己实现了
。
应用也比较广泛,比如redis的数据库就是字典实现的。不仅如此,当一个哈希键包含的键值对比较多,或者都是很长的字符串,redis就会用字典作为哈希键的底层实现
。
# String类型底层实现
Redis底层实现了简单动态字符串的类型(SSD),来表示 String 类型。 没有直接使用 C 语言定义的字符串类型。
struct sdshdr{
//记录 buf 数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
2
3
4
5
6
7
8
9
# String类型使用SSD方式实现的好处
- 避免缓冲区溢出:进行字符修改时候,可以根据 len 属性来检查空间是否满足要求
- 减少内存分配次数:len 和 free 两个属性,可以协助进行空间预分配以及惰性空间释放
- 二进制安全:SSD 不是以空字符串来判断是否结束,而是以 len 属性来判断字符串是否结束
- 常数级别获取字符串长度:获取字符串的长度只需要读取 len 属性就可以获取
- 兼容C字符串函数:可以重用 C 语言库的 的一部分函数
# SortedSet(zset)以及底层实现机制
底层实现机制:SortedSet 的实现方式可能有两种:ziplist 或者 skiplist。
- 当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:
- 保存的元素数量小于 128;
- 保存的所有元素长度都小于 64 字节。
- 不能满足上面两个条件的使用 skiplist 编码。
- ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。
- skiplist 编码的有序集合对象使用 zset 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表。字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值。
# redis采用跳表(skiplist)而不是红黑树(平衡树)
- 在做范围查找的时候,平衡树比skiplist操作要复杂。
- 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
- 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
- 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
- 从算法实现难度上来比较,skiplist比平衡树要简单得多。
# sds相对c的改进
获取长度:c字符串并不记录自身长度,所以获取长度只能遍历一遍字符串,redis直接读取len即可。
缓冲区安全:c字符串容易造成缓冲区溢出,比如:程序员没有分配足够的空间就执行拼接操作。而redis会先检查sds的空间是否满足所需要求,如果不满足会自动扩充。
内存分配:由于c不记录字符串长度,对于包含了n个字符的字符串,底层总是一个长度n+1的数组,每一次长度变化,总是要对这个数组进行一次内存重新分配的操作。因为内存分配涉及复杂算法并且可能需要执行系统调用,所以它通常是比较耗时的操作。
# Redis的发布订阅功能
Redis 提供了基于“发布/订阅”模式的消息机制,消息发布者和订阅者不能直接通信,客户端发布消息的时候指定发送的频道,然后订阅了该频道的用户可以接收到该消息。 具体指令如下:
发布消息:publish channel message 订阅消息:subscribe channel [……] 退订消息:punsubscribe
1
2
3
# 消息队列
Redis提供了两种方式来做消息队列,一种是生产消费模式,另一种是发布订阅模式。
Redis实现轻量级的消息队列与消息中间件相比,没有高级特性也没有ACK保证,无法做到数据不重不漏,如果业务简单而且对消息的可靠性不是那么严格可以尝试使用。
Redis中对列表List
的操作命令中,L
表示从左侧头部开始插入和弹出,R
表示从右侧尾部开始插入和弹出。
# 生产消费模式
生产消费模式会让一个或多个客户端监听消息队列,一旦消息到达,消费者马上消费,谁先抢到算谁的。如果队列中没有消息,消费者会继续监听。
模型:及bull的状态图: PUSH/POP //List(列表)
Redis数据结构的列表List
提供了push
和pup
命令,遵循着先入先出FIFO
的原则。使用push/pop
方式的优点在于消息可以持久化,缺点是一条消息只能被一个消费者接收,消费者完全靠手速来获取,是一种比较简陋的消息队列。
Redis的队列list
是有序的且可以重复的,作为消息队列使用时可使用rpush/lpush
操作入队,使用lpop/rpop
操作出队。当发布消息是执行lpush
命令,将消息从列表左侧加入队列。消息接收方执行rpop
命令从列表右侧弹出消息。
LPUSH/BRPOP (生产者/消费者);使用brpop
和blpop
实现阻塞读取
由于需要一直调用rpop/lpop
才可以实现不停的监听且消费消息,为解决这个问题,Redis提供了阻塞命令brpop/blpop
。使用brpop
会阻塞队列,而且每次只会弹出一个消息,如果没有消息则会阻塞。
Redis列表List
支持带阻塞的命令,生产者从列表左侧lpush
加入消息到队列,消费者使用brpop
命令从列表右侧弹出消息并设置超时时间,如果列表中没有消息则一直阻塞直到超时。这样做的目的在于减小Redis的压力。
对于Redis来说提供了blpop/brpop
阻塞读,阻塞读在队列没有数据时会立即进入休眠状态,一旦数据到来则立即被唤醒,消息的延迟几乎为零。需要注意的是如果线程一直阻塞在那里,连接就会被服务器主动断开来减少资源占用,这时blpop/brpop
会抛出异常,所以编写消费段时需要注意异常的处理。
Redis的PUSH/POP
机制,利用Redis的列表list
数据结构,生产者lpush
消息,消费者brpop
消息并设定超时时间以减少Redis压力。这种方案相对于发布订阅模式的好处是数据可靠性提高了,只有在Redis宕机且数据没有持久化的情况下会丢失数据。**可以根据业务通过AOF和缩短持久化间隔来保证较高的可靠性,也可以通过多个客户端来提高消息速度。**但相对于专业的消息队列中间件,发布订阅模式的状态过于简单(没有状态),而且没有ACK
机制,消息取出后消费失败依赖于客户端记录日志或重新push
到队列中。
# 发布订阅模式
Redis 发布订阅(pub/sub)是一种消息通信模式:
发送者(pub)发送消息,订阅者(sub)接收消息。redis内置了发布/订阅功能,可以作为消息机制使用。Redis 客户端可以订阅任意数量的频道。
发布订阅机制模型: 首先发布者将消息发布到频道,客户端订阅频道后就能获得频道的消息。
Redis自带pub/sub
机制即发布订阅模式,此模式中生产者producer
和消费者consumer
之间的关系是一对多的,也就是一条消息会被多个消费者所消费,当只有一个消费者时可视为一对一的消息队列。
Redis同样支持消息的发布/订阅(Pub/Sub)模式,这和中间件activemq有些类似。订阅者(Subscriber)可以订阅自己感兴趣的频道(Channel),发布者(Publisher)可以将消息发往指定的频道(Channel),正式通过这种方式,可以将消息的发送者和接收者解耦。另外,由于可以动态的Subscribe和Unsubscribe,也可以提高系统的灵活性和可扩展性。
# 模式匹配
通配符的Pub/Sub; 客户端可以订阅满足一个或多个规则的channel消息,相应的命令是PSUBSCRIBE和PUNSUBSCRIBE。和subscribe/unsubscribe的输出类似,可以看到第一部分是消息类型“psubscribe”,第二部分是订阅的规则“chi*”,第三部分则是该客户端目前订阅的所有规则个数。PSUBSCRIBE sam*
# 常用命令
SUBSCRIBE redisChat samy1 samy2 //可以多个;
//先重新开启个 redis 客户端,然后在同一个频道 redisChat 发布两次消息,订阅者就能接收到消息。
PUBLISH redisChat "Redis is a great caching technique"
PUBLISH redisChat "Learn redis by samy.com"
# 订阅者的客户端会显示如下消息
1) "message"
2) "redisChat"
3) "Redis is a great caching technique"
1) "message"
2) "redisChat"
3) "Learn redis by samy.com"
2
3
4
5
6
7
8
9
10
11
消息队列的实现:(php语言的实现)
使用PHP+Redis实现消息队列 操作流程:
PHP接收请求和数据
PHP将数据写入Redis队列rpush(入队)
Shell定时调用PHP读取队列数据并写入数据库lpop(出队)
Bull及其他队列消息库的比较:
参照使用:
https://github.com/bee-queue/bee-queue https://optimalbits.github.io/bull/ https://docs.bullmq.io/
# 实际案例
# 设计一下在交易网站首页展示当天最热门售卖商品的前五十名商品列表?
这个题目很明显可以看出来,考察 Redis 的实际技术选型。 从题目中我们可以看到几个信息:
- 首页:代表访问量非常大
- 当天: 热门选择的时间跨度为当天的 24 小时
- 热门售卖商品:售卖的越多越热门,售卖个数
那我们可以就可以使用 Redis 来存储这个热门商品的榜单列表, 使用 Redis 的 zset 来进行存储
。
- Key:为商品 ID; value:商品当天售卖次数
然后就可以获取有序的产品售卖排行榜。
Redis: Failed opening .rdb for saving: Permission denied
redis服务器会生成dump.rdb
文件存储缓存,如果文件权限不够则无法读写该文件
在/usr/local/bin/
(默认文件目录)下执行命令; cd /usr/loal/bin && chmod 777 dump.rdb
# 一个简单的论坛系统分析
该论坛系统功能如下:
- 可以发布文章;
- 可以对文章进行点赞;
- 在首页可以按文章的发布时间或者文章的点赞数进行排序显示;
文章信息 HASH 来存储
文章包括标题、作者、赞数等信息,在 Redis 中使用 HASH 来存储每种信息以及其对应的值的映射
Redis 使用命名空间的方式来实现类似表的功能、命名空间可以扩展树的深度 set test1:test2:test3 123 类似json
键名的前面部分存储空间名,后面部分存储空间 ID,整个组成Hash的健名
使用【冒号 : 】分隔。例如下面的 HASH 的键名为 article:92617,其中 article 为命名空间,ID 为 92617。
点赞功能 set
建立文章的已投票用户集合,set交集操作检查是否已点过赞
点赞 votes 字段进行加 1 操作
设置一周的过期时间,过后就不能再点赞
对文章进行排序 zset
为建立一个文章发布时间的有序集合和一个文章点赞数的有序集合