列表(list)
-
列表( list)类型是用来存储多个有序的字符串,a、b、c、d、e五个元素从左到右组成了一个有序的列表,列表中的每个字符串称为元素(element),一个列表最多可以存储2的32次方-1个元素。在Redis 中,可以对列表两端插入( push)和弹出(pop),还可以获取指定范围的元素列表、获取指定索引下标的元素等。列表是一种比较灵活的数据结构,它可以充当栈和队列的角色,在实际开发上有很多应用场景。
-
列表类型有两个特点:第一、列表中的元素是有序的,这就意味着可以通过索引下标获取某个元素或者某个范围内的元素列表。第二、列表中的元素可以是重复的。
操作命令
lrange 获取指定范围内的元素列表
-
key start end
-
索引下标特点:从左到右为0到N-1
-
lrange 0 -1命令可以从左到右获取列表的所有元素
rpush 从右向左插入
lpush 从左向右插入
linsert 在某个元素前或后插入新元素
- 这三个返回结果为命令完成后当前列表的长度,也就是列表中包含的元素个数,同时rpush和lpush都支持同时插入多个元素。
lpop 从列表左侧弹出
rpop 从列表右侧弹出
- 上面的操作将列表最左侧的元素b被弹出。rpop将会把列表最左侧的元素a弹出。
lrem 对指定元素进行删除
-
lrem命令会从列表中找到等于value的元素进行删除,根据count的不同分为三种情况:
-
count>0,从左到右,删除最多count个元素。
-
count<0,从右到左,删除最多count绝对值个元素。
-
count=0,删除所有。
-
返回值是实际删除元素的个数。
ltirm 按照索引范围修剪列表
- 例如想保留列表中第0个到第1个元素
lset修改指定索引下标的元素
-
: key index newvalue
-
下面操作会将列表listkey中的第3个元素设置为python
lindex 获取列表指定索引下标的元素
llen 获取列表长度
阻塞式的弹出元素
-
blpop
-
brpop
-
list可以命令停在这里,直到有元素才弹出,所以可以用于实现消息队列
-
blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,除此之外还支持多个列表类型, 也支持设定阻塞时间,单位秒,如果阻塞时间为 0,表示一直阻塞下去。我们以 brpop 为例说明。
-
注意:brpop 后面如果是多个键,那么 brpop 会从左至右遍历键,一旦有一 个键能弹出元素,客户端立即返回。
-
如果多个客户端对同一个键执行 brpop,那么最先执行 brpop 命令的客户端 可以获取到弹出的值,其余的客户端依然处于阻塞。
命令的时间复杂度
- 列表类型的操作命令中,llen,lpop,rpop,blpop 和 brpop 命令时间复杂度都是 O(1),其余的命令的时间复杂度都是 O(n),只不过 n 的值根据命令不同而不同, 比如 lset,lindex 时间复杂度和命令后的索引值大小相关,rpush 和 lpush 和插入元 素的个数相关等等。
使用场景
-
列表类型可以用于比如:
-
消息队列,Redis 的 lpush+brpop 命令组合即可实现阻塞队列,生产者客户 端使用 lrpush 从列表左侧插入元素,多个消费者客户端使用 brpop 命令阻塞式的 “抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
-
文章列表 每个用户有属于自己的文章列表,现需要分页展示文章列表。此时可以考虑 使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。
-
实现其他数据结构
lpush+lpop = Stack(栈)
lpush +rpop = Queue(队列)
lpsh+ ltrim = Capped Collection(有限集合)
lpush+brpop =Message Queue(消息队列)
-
**集合(**set)
- 集合( set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是, 集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元 素。
- 一个集合最多可以存储 2 的 32 次方-1 个元素。Redis 除了支持集合内的增删 改查,同时还支持多个集合取交集、并集、差集,合理地使用好集合类型,能在 实际开发中解决很多实际问题。
集合内操作命令
sadd 添加元素
- 允许添加多个,返回结果为添加成功的元素个数
srem 删除元素
- 允许删除多个,返回结果为成功删除元素个数
scard 计算元素个数
sismember 判断元素是否在集合中
- 如果给定元素 element 在集合内返回 1,反之返回 0
srandmember 随机从集合返回指定个数元素
- 指定个数如果不写默认为 1
spop 从集合随机弹出元素
- 同样可以指定个数,如果不写默认为 1,注意,既然是弹出,spop 命令执行 后,元素会从集合中删除,而 srandmember 不会。
smembers 获取所有元素
- 返回结果是无序的
集合间操作命令
- 现在有两个集合,它们分别是 set:1 和 set:2
sinter 求多个集合的交集
suinon 求多个集合的并集
sdiff 求多个集合的差集
交集和并集满足交换律,差集不满足
将交集、并集、差集的结果保存
-
sinterstore(destination key)[key …]
-
suionstore(destination key) [key …]
-
sdiffstore (destination key) [key …]
-
集合间的运算在元素较多的情况下会比较耗时,所以 Redis 提供了上面三个 命令(原命令+store)将集合间交集、并集、差集的结果保存在 destination key 中,
例如
命令的时间复杂度
- scard,sismember 时间复杂度为 O(1),其余的命令时间复杂度为 O(n),其中 sadd,srem 和命令后所带的元素个数相关,spop,srandmember 和命令后所带 count 值相关,交集运算 O(m*k),k 是多个集合中元素最少的个数,m 是键个数, 并集、差集和所有集合的元素个数和相关。
使用场景
- 用户标签、社交、查询有共同兴趣爱好的人,智能推荐
- 集合类型比较典型的使用场景是标签( tag)
- 例如一个用户可能对娱乐、体 育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。 有了这些数据就可以得到喜欢同一个标签的人,以及用户的共同喜好的标签,这 些数据对于用户体验以及增强用户黏度比较重要。
- 例如一个电子商务的网站会对不同标签的用户做不同类型的推荐,比如对数 码产品比较感兴趣的人,在各个页面或者通过邮件的形式给他们推荐最新的数码 产品,通常会为网站带来更多的利益。 除此之外,集合还可以通过生成随机数进行比如抽奖活动,以及社交图谱等 等。
**有序集合(**ZSET)
- 有序集合相对于哈希、列表、集合来说会有一点点陌生,但既然叫有序集合, 那么它和集合必然有着联系,它保留了集合不能有重复成员的特性,但不同的是, 有序集合中的元素可以排序。但是它和列表使用索引下标作为排序依据不同的是, 它给每个元素设置一个分数( score)作为排序的依据。
- 有序集合中的元素不能重复,但是 score 可以重复,就和一个班里的同学学 号不能重复,但是考试成绩可以相同。
- 有序集合提供了获取指定分数和元素范围查询、计算成员排名等功能,合理 的利用有序集合,能帮助我们在实际开发中解决很多问题。
集合内操作命令
zadd 添加成员
- 向有序集合 u: gaokao 添加用户 james 和他的分数 555:
- 返回结果代表成功添加成员的个数
- zadd u:gaokao 630 king 602 lison 650 jack 699 mark
- 要注意:zadd 命令还有四个选项 nx、xx、ch、incr 四个选项
- nx; member 必须不存在,才可以设置成功,用于添加。
- xx: member 必须存在,才可以设置成功,用于更新。
- ch:返回此次操作后,有序集合元素和分数发生变化的个数
- incr:对 score 做增加,相当于后面介绍的 zincrby
zcard 计算成员个数
zscore 计算某个成员的分数
- 如果成员不存在则返回 nil
zrank 计算成员的排名
- zrank 是从分数从低到高返回排名
- zrevrank 反之
- 很明显,排名从 0 开始计算。
zrem 删除成员
-
允许一次删除多个成员。
-
返回结果为成功删除的个数。
zincrby 增加成员的分数
zrange 和 zrevrange 返回指定排名范围的成员
- 有序集合是按照分值排名的,zrange 是从低到高返回,zrevrange 反之。如果 加上 withscores 选项,同时会返回成员的分数
zrangebyscore 返回指定分数范围的成员
- zrangebyscore key min max [withscores] [limit offset count]
- zrevrangebyscore key max min [withscores][limit offset count]
- 其中 zrangebyscore 按照分数从低到高返回,zrevrangebyscore 反之。例如下 面操作从低到高返回 200 到 221 分的成员,withscores 选项会同时返回每个成员 的分数。
- 同时 min 和 max 还支持开区间(小括号)和闭区间(中括号),-inf 和+inf 分别 代表无限小和无限大:
zcount 返回指定分数范围成员个数
- zcount key min max
zremrangebyrank 按升序删除指定排名内的元素
- zremrangebyrank key start end
zremrangebyscore 删除指定分数范围的成员
- zremrangebyscore key min max
集合间操作命令
zinterstore 交集
-
zinterstore destination numkeys key [key …] [weights weight [weight …]]
-
[aggregate sum / min / max]这个命令参数较多,下面分别进行说明
-
destination:交集计算结果保存到这个键。
-
numkeys:需要做交集计算键的个数。
-
key [key …]:需要做交集计算的键。
-
weights weight [weight …]:每个键的权重,在做交集计算时,每个键中的每个
-
member 会将自己分数乘以这个权重,每个键的权重默认是 1。
-
aggregate sum/ min |max:计算成员交集后,分值可以按照 sum(和)、min(最 小值)、max(最大值)做汇总,默认值是 sum。
-
不太好理解,我们用一个例子来说明。
-
zadd u2:gaokao 630 av 610 lison 578 leo 720 mark
-
zinterstore u_sum 2 u:gaokao u2:gaokao
-
zrange u_sum 0 -1 withscores
-
对 u:gaokao 和 u2:gaokao 进行加权平均计算
-
zinterstore u_avg 2 u:gaokao u2:gaokao weights 0.9 0.1
-
zrange u_avg 0 -1 withscores
zunionstore 并集
-
zunionstore destination numkeys key [key …] [weights weight [weight …]]
-
[ aggregate sunmI minl max]
-
该命令的所有参数和 zinterstore 是一致的,只不过是做并集计算,大家可以 自行实验。
命令的时间复杂度
- zadd key score member [score member …] O(k*log(n)),k 是添加成员的个 数,n 是当前有序集合成员个数
- zcard key O(1)
- zscore key member O(1)
- zrank key member、zrevrank key member O(log(n)),n 是当前有序集合成员 个数
- zrem key member [member …] O(k*1og(n)),k 是删除成员的个数,n 是当前 有序集合成员个数
- zincrby key increment member O(log(n)),n 是当前有序集合成员个数
- zrange key start end[ withscores]和 zrevrange key start end [ withscores] O(log(n)+k),k 是要获取的成员个数,n 是当前有序集合成员个数
- zrangebyscore key min max [ withscores]和 zrevrangebyscore key max min [withscores] O(log(n)+k),k 是要获取的成员个数,n 是当前有序集合成员个数
- zcount O(log(n)),n 是当前有序集合成员个数
- zremrangebyrank key start end 和 zremrangebyscore key min max O(log(n)+k), k 是要删除的成员个数,n 是当前有序集合成员个数
- zinterstore destination numkeys key [key ….] O(nK)+O(mlog(m)),n是成员数 最小的有序集合成员个数,k 是有序集合的个数,m 是结果集中成员个数
- zunionstore destination numkeys key [key……] O(n)+O(m*log(m)),n 是所有有序 集合成员个数和,m 是结果集中成员个数。
使用场景
- 有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上 传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、 按照获得的赞数。
Redis 与 Java 的集成
- 参见 ketang-redis 项目
Maven 和配置
maven
- jedis的maven版本3.6.3
- groupid-redis.clients
- artifactId-jedis
配置
- 1.引入maven包
- 2.property配置redis基础配置
- 3.RedisConfig类
- 4.JedisConnectPool类
使用
- 项目ketang-redis
基本类型
- cn.enjoyedu.redis.redisbase.basetypes.RedisString
实现消息队列
-
基于 list
-
参见 cn.enjoyedu.redis.redismq.ListVer
-
如果业务需求足够简单,想把 Redis 当作队列来使用,肯定最先想到的就 是使用 List 这个数据类型。 因为 List 底层的实现就是一个「链表」,在头部 和尾部操作元素,时间复杂度都是 O(1),这意味着它非常符合消息队列的模型。 如果把 List 当作队列,产者使用 LPUSH 发布消息,消费者这一侧,使用 RPOP 拉取消息。
-
而我们在编写消费者逻辑时,一般是一个「死循环」,这个逻辑需要不断地 从队列中拉取消息进行处理。如果此时队列为空,那消费者依旧会频繁拉取消息, 这会造成「CPU 空转」,不仅浪费 CPU 资源,还会对 Redis 造成压力。 怎么 解决这个问题呢? 也很简单,当队列为空时,我们可以「休眠」一会,再去尝 试拉取消息。
-
这个问题虽然解决了,但又带来另外一个问题:当消费者在休眠等待时,有 新消息来了,那消费者处理新消息就会存在「延迟」。 假设设置的休眠时间是 2s,那新消息最多存在 2s 的延迟。 要想缩短这个延迟,只能减小休眠的时间。 但休眠时间越小,又有可能引发 CPU 空转问题。
-
所以引入阻塞读 blpop 和 brpop(b 代表 blocking),阻塞读在队列没有数据 的时候进入休眠状态,
-
一旦数据到来则立刻醒过来,消息延迟几乎为零。 缺点也很明显,做消费者确认 ACK 麻烦,不能保证消费者消费消息后是否 成功处理的问题(宕机或处理异常等),通常需要维护一个 Pending 列表,保证 消息处理确认;不能做广播模式,如消息发布/订阅模型;不能重复消费,一旦 消费就会被删除;不支持分组消费。
-
基于 zset
-
参见 cn.enjoyedu.redis.redismq.ZsetVer
-
Zset 我们更多的是用来实现延时队列,将消息序列化成一个字符串作为 zset 的 value,这个消息的到期处理时间作为 score,然后用多个线程轮询 zset 获取 到期的任务进行处理。
-
Redis 的 zrem 方法是多线程多进程争抢任务的关键,它的返回值决定了当 前实例有没有抢到任务,因为 获取消息的方法可能会被多个线程、多个进程调 用,同一个任务可能会被多个进程线程抢到,迪过 zrem 来决定唯一的属主。
-
不过缺点很明显,消费者必须使用轮询的方式。
-
除了这两种,还有pub/sub发布订阅模式、redis5中的stream模式
Redis 高级数据结构
Bitmaps
- 现代计算机用二进制(位)作为信息的基础单位,1 个字节等于 8 位,例如“big” 字符串是由 3 个字节组成,但实际在计算机存储时将其用二进制表示,“big”分 别对应的 ASCII 码分别是 98、105、103,对应的二进制分别是 01100010、01101001 和 01100111。
- 许多开发语言都提供了操作位的功能,合理地使用位能够有效地提高内存使 用率和开发效率。Redis 提供了 Bitmaps 这个“数据结构”可以实现对位的操作。 把数据结构加上引号主要因为:
- Bitmaps 本身不是一种数据结构,实际上它就是字符串,但是它可以对字符 串的位进行操作。
- Bitmaps 单独提供了一套命令,所以在 Redis 中使用 Bitmaps 和使用字符串的 方法不太相同。可以把 Bitmaps 想象成一个以位为单位的数组,数组的每个单元 只能存储 0 和 1,数组的下标在 Bitmaps 中叫做偏移量。
操作命令
setbit 设置值
- setbit key offset value
- 设置键的第 offset 个位的值(从 0 算起)。
- 假设现在有 20 个用户,userid=0,2,4,6,18 的用户对网站进行了访问,存储键 名为 u:v日期。
getbit 获取值
- getbit key offset 获取键的第 offset 位的值(从 0 开始算),比如获取 userid=8 的用户是否在 08-15 这天访问过,返回 0 说明没有访问过:
- 当然 offset 是不存在的,也会返回 0。
bitcount 获取 Bitmaps 指定范围值为 1 的个数
-
bitcount [start] [end]
-
下面操作计算 08-15 这天的独立访问用户数量
-
[start]和[end]代表起始和结束字节数
bitop Bitmaps 间的运算
-
bitop op destkey key [key . …]
-
bitop 是一个复合操作,它可以做多个 Bitmaps 的 and(交集)or(并 集)not(非)xor(异或)操作并将结果保存在 destkey 中。
bitpos 计算 Bitmaps 中第一个值为 targetBit 的偏移量
-
bitpos key targetBit [start] [end]
-
计算 0815 当前访问网站的最小用户 id
-
除此之外,bitops 有两个选项[start]和[end],分别代表起始字节和结束字节。
Bitmaps 优势
- 假设网站有 1 亿用户,每天独立访问的用户有 5 千万,如果每天用集合类型 和 Bitmaps 分别存储活跃用户,很明显,假如用户 id 是 Long 型,64 位,则集合 类型占据的空间为 64 位 x50 000 000= 400MB,而 Bitmaps 则需要 1 位×100 000 000=12.5MB,可见 Bitmaps 能节省很多的内存空间。
布隆过滤器
面试题和场景
- 1、目前有 10 亿数量的自然数,乱序排列,需要对其排序。限制条件-在 32 位机器上面完成,内存限制为 2G。如何完成?
- 要求去重可以用位图法
- 2、如何快速在亿级黑名单中快速定位 URL 地址是否在黑名单中?(每条 URL 平均 64 字节)
- hashmap利用率很低,越大越不划算
- 3、需要进行用户登陆行为分析,来确定用户的活跃情况?
- 4、网络爬虫-如何判断 URL 是否被爬过?
- 5、快速定位用户属性(黑名单、白名单等)
- 6、数据存储在磁盘中,如何避免大量的无效 IO?
传统数据结构的不足
- 当然有人会想,我直接将网页 URL 存入数据库进行查找不就好了,或者建立 一个哈希表进行查找不就 OK 了。
- 当数据量小的时候,这么思考是对的,
- 确实可以将值映射到 HashMap 的 Key,然后可以在 O(1) 的时间复杂度内 返回结果,效率奇高。但是 HashMap 的实现也有缺点,例如存储容量占比高, 考虑到负载因子的存在,通常空间是不能被用满的,举个例子如果一个 1000 万 HashMap,Key=String(长度不超过 16 字符,且重复性极小),Value=Integer, 会占据多少空间呢?1.2 个 G。实际上,1000 万个 int 型,只需要 40M 左右空间, 占比 3%,1000 万个 Integer,需要 161M 左右空间,占比 13.3%。
- 可见一旦你的值很多例如上亿的时候,那 HashMap 占据的内存大小就变得 很可观了。
- 但如果整个网页黑名单系统包含 100 亿个网页 URL,在数据库查找是很费时 的,并且如果每个 URL 空间为 64B,那么需要内存为 640GB,一般的服务器很难 达到这个需求。
位图法
- 位图法就是 bitmap 的缩写。所谓 bitmap,就是用每一位来存放某种状态, 适用于大规模数据,但数据状态又不是很多的情况。通常是用来判断某个数据存 不存在的。
布隆过滤器详解
-
本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构 (probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某 样东西一定不存在或者可能存在”。
-
相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但 是缺点是其返回的结果是概率性的,而不是确切的。
-
实际上,布隆过滤器广泛应用于网页黑名单系统、垃圾邮件过滤系统、爬虫 网址判重系统等,Google 著名的分布式数据库 Bigtable 使用了布隆过滤器来查 找不存在的行或列,以减少磁盘查找的 IO 次数,Google Chrome 浏览器使用了布 隆过滤器加速安全浏览服务。
-
在很多 Key-Value 系统中也使用了布隆过滤器来加快查询过程,如 Hbase, Accumulo,Leveldb,一般而言,Value 保存在磁盘中,访问磁盘需要花费大量时 间,然而使用布隆过滤器可以快速判断某个 Key 对应的 Value 是否存在,因此可 以避免很多不必要的磁盘 IO 操作。
-
通过一个 Hash 函数将一个元素映射成一个位阵列(Bit Array)中的一个点。 这样一来,我们只要看看这个点是不是 1 就知道可以集合中有没有它了。这就 是布隆过滤器的基本思想。
-
Hash 面临的问题就是冲突。假设 Hash 函数是良好的,如果我们的位阵列 长度为 m 个点,那么如果我们想将冲突率降低到例如 1%, 这个散列表就只能 容纳 m/100 个元素。显然这就不叫空间有效了(Space-efficient)。解决方法也 简单,就是使用多个Hash,如果它们有一个说元素不在集合中,那肯定就不在。
-
但是布隆过滤器的缺点是什么?
-
因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个 值 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 这个值存在。比如此时来一个不存在的 URL1000,他 经过哈希计算后。发现 bit 位为下:
-
但是这些 bit 位已经被 url1,url2,url3 置为 1,程序就会判断 URL1000 值存在。 这就是布隆过滤器的误判现象。所以:布隆过滤器判断存在的不一定存在,但是判断不存在的一定不存在。False is always false. True is maybe true.
-
布隆过滤器可精确的代表一个集合,可精确判断某一元素是否在此集合中, 精确程度由用户的具体设计决定,达到 100%的正确是不可能的。但是布隆过滤 器的优势在于,利用很少的空间可以达到较高的精确率。
-
很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会 返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率, 布隆过滤器越长其误报率越小。
-
另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的 速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变 高。
-
布隆过滤器的位数组的大小大小如何确定?
-
设 bitarray 大小为 m,样本数量为 n,允许失误率为 p,则
-
如何使得错误率最小,对于给定的 m 和 n,m 指位数组的大小,n 指插入元 素的个数,Hash 函数的个数为 k,错误率的计算公式为:
- m = − n l n p / ( l n 2 ) 2 m=-nlnp/(ln2)^2 m=−nlnp/(ln2)2
-
希望错误率最小,对于给定的 m 和 n,那么合适的 Hash 函数的个数为 k 计 算公式为:
- k = l n 2 ( m / n ) k=ln2(m/n) k=ln2(m/n)