第二节:Redis数据类型、使用场景、常用指令剖析
一. String
1. 数据类型介绍
典型的Key-Value集合,不少人喜欢用来存实体,需要序列化成字符串,获取的时候需要反序列化一下。(还是建议合理选择redis的数据结构,而不是万物都用string)
2. 应用场景
(1). 缓存
string类型最简单的一个应用就是Key-value缓存,value可以是简单string、int,也可以是序列化后的实体,可以替代Session进行存储,和其它缓存一样,也可以设置缓存的过期时间
(常用的键设计:表名_id ,如: UserInfor_001 )
经典例子:秒杀将库存放到redis的string中存储,从而应对高并发。
(2). 计数器(利用自增自减)
A. 单个网站或单条消息的点击量(点赞量、访问量等等)
B. 上面秒杀库存的扣减(后面会介绍使用Lua脚本扣减库存)
(3). 进程外共享Session
解决了IIS重启Session丢失的问题,解决了Session空间有限容易被挤爆的问题,但不能解决浏览器重启找不到Session的问题!
详见:https://www.cnblogs.com/yaopengfei/p/9583168.html https://www.cnblogs.com/yaopengfei/p/11270816.html
3. 相关指令
通用指令
1. 查看所有的键 #非常危险,容易造成客户端卡顿,线上禁止适用 keys * 2. 查看键的总数 #dbsize命令在计算键总数的时候不是遍历所有键,而是直接获取redis内置键的数量 dbsize
3.检查键是否存在 (存在返回1,不存在返回0)
exists key
4. 删除键(无论什么类型,都可以用这个命令删除)
del key
5. 查看key的数据类型
type key
6. 设置key的过期时间(超过过期时间会自动删除)
expire key seconds
7. 查看key剩余过期时间( 大于0的时候,显示的剩余过期时间;-1代表key存在,但是没有设置过期时间;-2代表key不存在)
ttl key
8. 删除所有key
flushdb
string相关
1. 存储: set key value 2. 获取: get key 3. 删除: del key
4. 自增1: incr key
自增n: incrby key n
5. 自减1:decr key
自减n:decr key n
PS:关于key和string类型的还有很多指令,详细请查看指令手册:
补充两个指令:info 和 scan
(1). info: 查看redis服务的运行信息,一堆内容,分为9部分
(2). scan指令:前面说到,查询redis中的key不要用 keys *指令,那么可以用 scan指令替代。
参考文章:https://www.cnblogs.com/yaopengfei/p/11912930.html
二. Hash
1. 数据类型介绍
一个key,对应一个Key-Value集合,即:hashid → {key:value;key:value;key:value;},相当于value又是一个“键值对集合” 或者值是另外一个 Dictionary。
2. 应用场景
(1). 购物车
以用户id作为hashid,商品id作为key,商品数量作为value,利用自增和自减功能来实现增加商品数量和减少商品数量功能。也可以删除商品,获取商品总数,获取购物车中所有商品。
(2). 群聊
临时存储消息(根据实际场景考虑是否合适),比如:群名为hashid, 用户id当做key,内容作为value。 这样存储可以,但是取数据的时候必须一下全部取出来,不能根据时间取前n条。
(3). 对象缓存
可以把一个对象中的每个字段当做一个key,存放到hash中。
3. 相关指令
1. 存储: hset key field value 127.0.0.1:6379> hset myhash username lisi (integer) 1 127.0.0.1:6379> hset myhash password 123 (integer) 1 2. 获取: hget key field: 获取指定的field对应的值 127.0.0.1:6379> hget myhash username "lisi" hgetall key:获取所有的field和value 127.0.0.1:6379> hgetall myhash 1) "username" 2) "lisi" 3) "password" 4) "123" 3. 删除: hdel key field 127.0.0.1:6379> hdel myhash username (integer) 1
其它指令详见指令文档:
4. 优缺点
优点:
(1). 同类数据归类整合存储,方便数据管理
(2). 相比string操作消耗内存与cpu更小
(3). 相比string存储更节省空间
缺点:
(1).过期功能不能使用在内层的field上,只能用在最外层的key上
(2).Redis集群架构下不适合大规模使用
参考文章:https://www.cnblogs.com/yaopengfei/p/11912930.html
三. List
1. 数据类型介绍
它是一个双向链表,支持左进、左出、右进、右出,所以它即可以充当队列使用,也可以充当栈使用。
(1). 队列:先进先出, 可以利用List左进右出,或者右进左出
(2). 栈:先进后出,可以利用List左进左出,或者右进右出
(3). 阻塞队列:数据的生产者可以通过LPUSH命令从左边插入数据,多个数据消费者,可以使用BRPOP命令阻塞的“抢”列表尾部的数据
2. 应用场景
(1). 消息队列
A. 流量削峰:秒杀服务,用户下单请求加到消息队列中,然后开启另外一个线程从队列中读取进行业务处理(拉模式) 。
B. 应用解耦(异步):比如登录成功后,要增加积分或者发送邮件,可以引入消息队列进行解耦,异步处理增加积分 或者 发送邮件的请求。
(2). 高性能分页(栈的特性)
解决查询缓慢的问题
比如发帖网站,会有非常多的帖子,而且数量每日俱增,首页显示的是最新发布的10条帖子,显示的是:发帖人名称 和 发帖标题,如果从数据库中查询可能会非常慢,这时候可以把发帖人名称和发帖标题(包括帖子id),存到Redis队列中,这个时候利用 栈 的特性,LRANGE:获取key指定索引的值(从左往右算), 获取前10条数据,还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率。
查看详情的时候,再根据帖子的id到数据库中查。
(3). 模拟发布订阅模式
要求:以微博为例(或者微信的订阅号),博主A,博主B都关注了博主C、博主D,在博主A(或B)的版面,应该显示的是博主C和博主D发布的最新博文(最新发布的在最上面),换句话说博主C或者博主D每发一篇博文,都要推送给关注他们的博主A和博主B。
技术分析
①. 数据结构的设计:一个博主对应一个List链表,用来存储该博主应该显示的博文消息。 以博主的用户id作为key,博文消息的id作为value。
②. 博主C每发一条博文,就需要向关注他的粉丝(A和B)对应的链表中分别存储一下 该博文消息的id。
③. 博主D每发一条博文,就需要向关注他的粉丝(A和B)对应的链表中分别存储一下 该博文消息的id。
④. 博主A就可以到自己对应的链表中利用栈的特性,获取最新的n条博文消息id。
PS: 拿到博文消息id了,剩下的就容易了,根据id去关系型数据中查内容就很快了,或者也可以将标题或者内容的前100字也存储到Redis中,便于页面显示(这样value的格式就是:博文id-博文标题-博文内容前100字)。
3. 相关指令
1. 添加: (1). lpush key value: 将元素加入列表左表 (2). rpush key value:将元素加入列表右边 127.0.0.1:6379> lpush myList a (integer) 1 127.0.0.1:6379> lpush myList b (integer) 2 127.0.0.1:6379> rpush myList c (integer) 3 2. 获取: lrange key start end :范围获取 127.0.0.1:6379> lrange myList 0 -1 (-1代表列表最后一个元素) 1) "b" 2) "a" 3) "c" 3. 删除: lpop key: 删除列表最左边的元素,并将元素返回 rpop key: 删除列表最右边的元素,并将元素返回
4. 阻塞获取
blpop key timeout:从key列表左侧弹出一个元素,并删除。如果没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待
brpop key timeout:从key列表右侧弹出一个元素,并删除。如果没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待
其它指令详见指令文档:
参考文章:https://www.cnblogs.com/yaopengfei/p/11940420.html
四. Set
1. 数据类型介绍
1个key→多个value,value的值不重复! (无序集合,自动去重)
Set一种无序且元素内容不重复的集合,不用做重复性判断了,和我们数学中的集合概念相同,可以对多个集合求交集、并集、差集,key可以理解成集合的名字。
注:set 用哈希表来保持字符串的唯一性,没有先后顺序,是按照自己的一个存储方式来遍历,因为没有保存插入顺序。
2. 应用场景
(一).抽奖
(1) 背景:用户参与抽奖,抽奖大致分两类:
A:只抽1次,1次抽n个人。
B:抽多次,比如三等奖抽3名,二等奖抽2名,一等奖抽1名。
(2) 技术分析:
主要利用Set结构元素的不重复性和获取随机数的方法来实现,以“specialPrize”为key,参与用户的id当做value
A:用户点击参与抽奖则执行SADD方法。
B:可以获取所有参与用户SMEMBERS 和 判断某个用户是否参与抽奖了sismember
C:随机抽一次奖用srandmember, 不删除
如果需要删除抽奖用spop
(3) 代码分析(基于stackexchange.redis)
1 //1.模拟用户参与抽奖 2 for (int i = 100; i <= 115; i++) 3 { 4 db.SetAdd("specialPrize", i.ToString()); 5 } 6 //2. 获取所有的参与抽奖的用户 7 var data = db.SetMembers("specialPrize").Select(u => (string)u).ToList(); 8 string idList = ""; 9 foreach (var item in data) 10 { 11 idList = idList + "," + item; 12 } 13 Console.WriteLine($"参与抽奖的用户有:{idList}"); 14 //3. 判断用户103是否参与抽奖了 15 var data2 = db.SetContains("specialPrize", "103"); 16 if (data2 == true) 17 { 18 Console.WriteLine($"用户103参与了抽奖"); 19 } 20 else 21 { 22 Console.WriteLine($"用户103没有参与抽奖"); 23 } 24 //4. 抽奖 25 //4.1 只抽一次奖,抽奖人数为两名 26 { 27 var d1 = db.SetRandomMembers("specialPrize", 2).Select(u => (string)u).ToList(); 28 foreach (var item in d1) 29 { 30 Console.WriteLine($"获奖用户为:{item}"); 31 } 32 } 33 //4.2 抽三次奖 34 { 35 var d1 = db.SetPop("specialPrize", 3).Select(u => (string)u).ToList(); 36 foreach (var item in d1) 37 { 38 Console.WriteLine($"三等奖用户为:{item}"); 39 } 40 var d2 = db.SetPop("specialPrize", 2).Select(u => (string)u).ToList(); 41 foreach (var item in d2) 42 { 43 Console.WriteLine($"二等奖用户为:{item}"); 44 } 45 var d3 = db.SetPop("specialPrize", 1).Select(u => (string)u).ToList(); 46 foreach (var item in d3) 47 { 48 Console.WriteLine($"一等奖用户为:{item}"); 49 } 50 }
(二). 微信或微博中消息的点赞(或者某篇文章的收藏)
(1). 背景
微信朋友圈用户A的某条消息的点赞功能,要实现点赞、取消点赞、获取点赞列表、获取点赞用户数量、判断某用户是否点赞过。
(2). 技术分析
利用Set结构, 以用户Id-消息id作为key,点赞过该消息的用户id作为value。
A:点赞 SetAdd方法
B:取消点赞 SetRemove方法
C:获取点赞列表 SetMembers方法
D:获取点赞用户数量 SetLength方法
E:判断某用户是否点赞过 SetContains方法
对应redis指令:
(三). 关注模型
(1).背景
比如微博关注或者共同好友的问题,以微博关注为例,要实现:共同关注、关注的和、关注A的用户中也关注B的、当A进入B页面,求可能认识的人。
(2). 技术分析
利用Set结构,一个博主对应一个Set结构,博主的id作为key,关注该博主的用户id作为value。
A:关注和取消关注: SetAdd方法 和 SetRemove方法
B:共同关注:求交集
C:关注的和:求并集
D:关注A的用户中也关注B的:遍历A中的用户,利用SetContains判断是否B中也存在
E:当A进入B页面,求可能认识的人:这里指的是关注B中的用户 扣去 里面也关注A的用户,就是A可能认识的人。求差集:B-A
对应redis命令:
(3). 代码分享
1 //关注lmr的用户有: 2 db.SetAdd("lmr", "小1"); 3 db.SetAdd("lmr", "小2"); 4 db.SetAdd("lmr", "小3"); 5 db.SetAdd("lmr", "小4"); 6 db.SetAdd("lmr", "小5"); 7 db.SetAdd("lmr", "rbp"); 8 9 //关注ypf的用户有: 10 db.SetAdd("ypf", "小4"); 11 db.SetAdd("ypf", "小5"); 12 db.SetAdd("ypf", "小6"); 13 db.SetAdd("ypf", "小7"); 14 db.SetAdd("ypf", "小8"); 15 db.SetAdd("ypf", "rbp"); 16 17 //同时关注lmr和ypf的用户有: 18 string[] arry1 = { "lmr", "ypf" }; 19 RedisKey[] keyList1 = arry1.Select(u => (RedisKey)u).ToArray(); 20 var d1 = db.SetCombine(SetOperation.Intersect, keyList1).Select(u => (string)u).ToList(); //交集 21 foreach (var item in d1) 22 { 23 Console.WriteLine("同时关注lmr和ypf的用户有:" + item); 24 } 25 26 //关注lmr和ypf的用户有(需要去重): 27 string[] arry2 = { "lmr", "ypf" }; 28 RedisKey[] keyList2 = arry2.Select(u => (RedisKey)u).ToArray(); 29 var d2 = db.SetCombine(SetOperation.Union, keyList2).Select(u => (string)u).ToList(); //并集 30 foreach (var item in d2) 31 { 32 Console.WriteLine("关注lmr和ypf的用户有:" + item); 33 } 34 35 //关注lmr的人中也关注ypf的有: 36 var d3 = db.SetMembers("lmr").Select(u => (string)u).ToList(); 37 foreach (var item in d3) 38 { 39 var isExist = db.SetContains("ypf", item); 40 if (isExist) 41 { 42 Console.WriteLine("关注lmr的人中也关注ypf的有:" + item); 43 } 44 } 45 46 //当ypf进入lmr的页面,显示可能认识的人(应该显示:小1,小2,小3) 47 string[] arry4 = { "lmr", "ypf" }; // lmr-ypf 48 RedisKey[] keyList4 = arry4.Select(u => (RedisKey)u).ToArray(); 49 var d4 = db.SetCombine(SetOperation.Difference, keyList4).Select(u => (string)u).ToList(); //差集 lmr-ypf 50 foreach (var item in d4) 51 { 52 Console.WriteLine("当ypf进入lmr的页面,显示可能认识的人:" + item); 53 }
(四). 利用唯一性,可以统计访问网站的所有IP
同上
(五). 集合操作实现电商网站的筛选
3. 相关指令
1. 存储:sadd key value 127.0.0.1:6379> sadd myset a (integer) 1 127.0.0.1:6379> sadd myset a (integer) 0 2. 获取:smembers key:获取set集合中所有元素 127.0.0.1:6379> smembers myset 1) "a" 3. 删除:srem key value:删除set集合中的某个元素 127.0.0.1:6379> srem myset a (integer) 1
其它指令详见指令文档:
参考文章:https://www.cnblogs.com/yaopengfei/p/11936144.html
五. SortedSet
1. 数据类型介绍
将Set中的元素增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列(有序集合,自动去重), 三个字段:key-member-score, key代表键,member代表值,score代表权重或者打分值
2. 应用场景
(1). 热搜
A. 需求:统计某个网站热词搜索,并实时显示前5名的词及其搜索次数。
PS:实时显示无非就是每隔几秒查询一次,大部分情况我们不建议直接去Redis里查排名,可以把前五名的相关数据存储到Redis的String结构中,设置5分钟过期。
B. 技术分析: 利用Redis中SortedSet(key-member-score)这一数据结构, 利用SortedSetIncrement方法的原子性,每搜索一个词, Score值加1(member不存在则执行的是增加操作), 然后再利用SortedSetRangeByRankWithScores方法获取Score值前五的memeber和Score数据。
(2). 用户投票
分析:和上面的热词原理一样,利用SortedSet的1个key对应多个不重复的 member-score,key用来存储一个标记,比如叫做“AllChildren”,代表该标记下存储宝宝的投票情况,member可以存储宝宝的标记id,score存储该宝宝的投票数量.
同样原理:利用SortedSetIncrement进行自增存储,利用SortedSetRangeByRankWithScores获取排名情况。
(3). 排行榜
类似案例:主播-粉丝刷礼物的排行榜(同时看主播的排行和每个主播下面粉丝的排行), 原理和上面都一样。
(4). 特殊要求:最近3天的热点新闻排行榜前十?
思路分析:
A. key以天来划分,比如每天产生一个key,格式为:hotNews:20201228,member为新闻id,每点击一次,增加1.
对应redis指令: zincrby hotNews20201228 1 newsId (newId为新闻编号)
B. 展示当天排行前十的新闻
对应redis指令:zrevrange hotNews:20201228 0 10 withscores , 拿到前十的newsId后,再去MySQL中查询其他信息,然后显示。
C. 把最近3天的新闻合并汇总到一个新的key中( hotNews:20201228-20201226 ),相同的member的score默认是相加的。
对应redis指令: zunionstore hotNews:20201228-20201226 3 hotNews:20201228 hotNews:20201227 hotNews:20201226
D. 展示三日排行前十
对应redis指令:zrevrange hotNews:20201228-20201226 0 10 withscores
总结:
SortedSet和String利用其自增自减(并返回当前值)原子性均可以实现计数器的作用,String是针对单个,Sorted是针对某个类别下的多个或每一个,并且实现排序功能。 Hash类型也能实现某个类别下多个物品的计数,但它不具有排序功能。
3. 对应指令
1. 存储:zadd key score value 127.0.0.1:6379> zadd mysort 60 zhangsan (integer) 1 127.0.0.1:6379> zadd mysort 50 lisi (integer) 1 127.0.0.1:6379> zadd mysort 80 wangwu (integer) 1 2. 获取:zrange key start end [withscores] 127.0.0.1:6379> zrange mysort 0 -1 1) "lisi" 2) "zhangsan" 3) "wangwu" 127.0.0.1:6379> zrange mysort 0 -1 withscores 1) "zhangsan" 2) "60" 3) "wangwu" 4) "80" 5) "lisi" 6) "500" 3. 删除:zrem key value 127.0.0.1:6379> zrem mysort lisi (integer) 1
其它指令详见文档:
参考文章:https://www.cnblogs.com/yaopengfei/p/11936144.html
六. Geo、HyperLogLog、Stream、Bitmap
1.Geo
(1). 含义:可以用来保存地理位置,并作位置距离计算或者根据半径计算位置等,该功能在 Redis 3.2 版本新增。
(2). 案例:有没有想过用Redis来实现附近的人,或者计算最优地图路径。
(3). 常用指令如下
geoadd:添加地理位置的坐标。
geopos:获取地理位置的坐标。
geodist:计算两个位置之间的距离。
georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
geohash:返回一个或多个位置对象的 geohash 值。
参考:https://www.runoob.com/redis/redis-geo.html
2.HyperLogLog
(1). 什么是基数?
比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数即不重复元素个数为5。
基数估计就是在误差可接受的范围内,快速计算基数。
(2). 含义:供不精确的去重计数功能,比较适合用来做大规模数据的去重统计,例如统计 UV; Redis 在 2.8.9 版本添加了 HyperLogLog 结构。
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
(3). 指令
1.添加指定元素到 HyperLogLog 中。 pfadd key element [element ...] 2.返回给定 HyperLogLog 的基数估算值。 pfcount key [key ...] 3.将多个 HyperLogLog 合并为一个 HyperLogLog pfmerge destkey sourcekey [sourcekey ...]
如图:
参考:https://www.runoob.com/redis/redis-hyperloglog.html
3.Stream
Redis Stream 是 Redis 5.0 版本新增加的数据结构。
Redis Stream 主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。
简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息。而 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
参考:https://www.runoob.com/redis/redis-stream.html
4. Bitmap
位图是支持按 bit 位来存储信息,可以用来实现 布隆过滤器(BloomFilter)
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。