REDIS
2019-08-01 14:08:18 0 举报
AI智能生成
redis学习框架
作者其他创作
大纲/内容
基础
基本数据类型
string
hash类型
hset 对象名称 key value
QQ截图20170530103710.png
list
相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)
当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收
Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。
lpush 从左边加入元素 rpush 从右边加入元素
lpop 从左边取元素 rpop 从右边取元素
linsert 插入元素
QQ截图20170530111735.png
QQ截图20170530115138.png
set
set是通过hashtable实现的,对集合可以去交集、并集、差集
sadd
向名称为key的set中添加元素
smemebers
查看set集合的元素
srem
删除set集合元素
spop
随机返回删除的key
sdiff
返回两个集合中的不同元素
那个集合在前面就以哪个集合为标准
sdiffstore
将返回不同元素存储到另一个集合里
sdiffstore 存储到的集合名称 参照的集合名称 比较的集合名称
QQ截图20170530132802.png
zset
类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。
它的内部实现用的是一种叫做「跳跃列表」的数据结构。
最下面一层所有的元素都会串起来。然后每隔几个元素挑选出一个代表来,再将这几个代表使用另外一级指针串起来。然后在这些代表里再挑出二级代表,再串起来。
跳跃列表结构示意图.png
QQ截图20170530132942.png
命令
zadd 有序集合名称 序号 value
zrange books 0 -1
# 按 score 排序列出,参数区间为排名范围(第0个-第N个) -1表示所有
zrevrange books 0 -1
# 按 score 逆序列出,参数区间为排名范围
zcard books
# 相当于 count()
zscore books "java concurrency"
# 获取指定 value 的 score
zrank books "java concurrency"
# 排名
zrangebyscore books 0 8.91
# 根据分值区间遍历 zset
zrangebyscore books -inf 8.91 withscores
# 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。
zrem books "java concurrency"
# 删除 value
基本操作
set
setnx(not exist)
如果不存在则进行设置,如果存在就不进行设置,返回0
setex(EXPIRED)
setex color 10 red
设置color的有效期为10秒,10秒后返回nil(redis里面nil表示为空)
setrange(替换字符串)
setrange email 10 ww 10标识从第几位开始替换,后面跟上替换的字符串,ww是两个字母则只替换两个字母,索引从0开始
mset/mget
一次性设置多个或者获取多个值
mset key1 value1 key2 value2 key3 value3
getset
返回旧值并设置新值
incr和decr
对某一个值进行递减或者递增
incrby和decyby
对某个值进行指定长度的递减和递增
语法:incrby key 步长
append name
字符串追加方法
strlen name
获取字符串的长度
数据结构
字符串
Redis 的字符串叫着「SDS」,也就是Simple Dynamic String。它的结构是一个带长度信息的字节数组。
struct SDS<T> {
T capacity; // 数组容量
T len; // 数组长度
byte flags; // 特殊标识位,不理睬它
byte[] content; // 数组内容
}
如果 capacity 不够容纳追加的内容,就会重新分配字节数组并复制原字符串的内容到新数组中,再 append 新内容
如果字符串的长度非常长,这样的内存分配和复制开销就会非常大。
字符串在长度小于 1M 之前,扩容空间采用加倍策略,也就是保留 100% 的冗余空间。当长度超过 1M 之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配 1M 大小的冗余空间。
存储方式
长度特别短时,使用 emb 形式存储 (embeded)
当长度超过 44 时,使用 raw 形式存储
字典
dict 结构内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的。但是在 dict 扩容缩容时,需要分配新的 hashtable,然后进行渐进式搬迁,这时候两个 hashtable 存储的分别是旧的 hashtable 和新的 hashtable。待搬迁结束后,旧的 hashtable 被删除,新的 hashtable 取而代之。
压缩列表
为了节约内存空间使用,zset 和 hash 容器对象在元素个数较少的时候,采用压缩列表 (ziplist) 进行存储。
压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙。
快速列表
quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。
单个 ziplist 长度为 8k 字节,超出了这个字节数,就会新起一个 ziplist。ziplist 的长度由配置参数list-max-ziplist-size决定。
跳跃列表
紧凑列表
listpack,它是对 ziplist 结构的改进,在存储空间上会更加节省,而且结构上也比 ziplist 要精简。
基数树
Rax 是 Redis 内部比较特殊的一个数据结构,它是一个有序字典树 (基数树 Radix Tree)
按照 key 的字典序排列,支持快速地定位、插入和删除操作。
rax 跟 zset 的不同在于它是按照 key 进行排序的。
技术点
redis为单线程
如何处理多并发客户端连接
非阻塞 IO
当我们调用套接字的读写方法,默认它们是阻塞的,比如read方法要传递进去一个参数n,表示最多读取这么多字节后再返回,如果一个字节都没有,那么线程就会卡在那里,直到新的数据到来或者连接关闭了,read方法才可以返回,线程才能继续处理。而write方法一般来说不会阻塞,除非内核为套接字分配的写缓冲区已经满了,write方法就会阻塞,直到缓存区中有空闲空间挪出来了。
非阻塞 IO 在套接字对象上提供了一个选项Non_Blocking,当这个选项打开时,读写方法不会阻塞,而是能读多少读多少,能写多少写多少。能读多少取决于内核为套接字分配的读缓冲区内部的数据字节数,能写多少取决于内核为套接字分配的写缓冲区的空闲空间字节数。读方法和写方法都会通过返回值来告知程序实际读写了多少字节。
事件轮询 API(多路复用)
解决数据不完整的情况
select函数、epoll(linux)、kqueue(freebsd & macosx)
指令队列
客户端的指令通过队列来排队进行顺序处理,先到先服务。
响应队列
定时任务
事务
使用multi方法打开事务,然后进行设置,这是设置的数据都会进入队列进行保存,最后执行exec,吧数据一次存储到redis中,使用discard方法取消事务
redis不能保证同时成功或者失败进行提交或者回滚
持久化
rdb
snapshotting 默认方式,将内存中以快照的方式写入二进制文件中,默认为dump.rdb。可以通过配置设置自动做快照持久化方式,可以配置redis在n秒内如果超过m个key修改就自动做快照
如果redis意外down就会丢失最后一次快照的所有修改的数据
冷备份
aof
生产环境使用方式
数据-->os cache -->磁盘
appendonly yes 启用aof持久化的方式
appendfsync always 收到写命令就立即写入到磁盘 可以保证持久化 --常用
appendfsync everysec 每秒写入磁盘一次
appendfsync no 完全依赖os 性能最好 但持久化没保证
rewrite
AOF会自动在后台每隔一定时间做rewrite操作,
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
比如说上一次AOF rewrite之后,是128mb
然后就会接着128mb继续写AOF的日志,如果发现增长的比例,超过了之前的100%,256mb,就可能会去触发一次rewrite
但是此时还要去跟min-size,64mb去比较,256mb > 64mb,才会去触发rewrite
AOF备份原理:AOF 日志存储的是 Redis 服务器的顺序指令序列,AOF 日志只记录对内存进行修改的指令记录。Redis 会在收到客户端修改指令后,先进行参数校验,如果没问题,就立即将该指令文本存储到 AOF 日志中,也就是先存到磁盘,然后再执行指令。这样即使遇到突发宕机,已经存储到 AOF 日志的指令进行重放一下就可以恢复到宕机前的状态。
AOF瘦身方案:Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。
fsync
AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核会异步将脏数据刷回到磁盘的。
这就意味着如果机器突然宕机,AOF 日志内容可能还没有来得及完全刷到磁盘中,这个时候就会出现日志丢失。那该怎么办?
Linux 的glibc提供了fsync(int fd)函数可以将指定文件的内容强制从内核缓存刷到磁盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。
快照是通过开启子进程的方式进行的,它是一个比较耗资源的操作
遍历整个内存,大块写磁盘会加重系统负载
AOF 的 fsync 是一个耗时的 IO 操作,它会降低 Redis 性能,同时也会增加系统 IO 负担
所以通常 Redis 的主节点是不会进行持久化操作,持久化操作主要在从节点进行。从节点是备份节点,没有来自客户端请求的压力,它的操作系统资源往往比较充沛。
AOF和RDB同时工作
(1)如果RDB在执行snapshotting操作,那么redis不会执行AOF rewrite; 如果redis再执行AOF rewrite,那么就不会执行RDB snapshotting
(2)如果RDB在执行snapshotting,此时用户执行BGREWRITEAOF命令,那么等RDB快照生成之后,才会去执行AOF rewrite
(3)同时有RDB snapshot文件和AOF日志文件,那么redis重启的时候,会优先使用AOF进行数据恢复,因为其中的日志更完整
数据备份方案
(1)写crontab定时调度脚本去做数据备份
(2)每小时都copy一份rdb的备份,到一个目录中去,仅仅保留最近48小时的备份
(3)每天都保留一份当日的rdb的备份,到一个目录中去,仅仅保留最近1个月的备份
(4)每次copy备份的时候,都把太旧的备份给删了
(5)每天晚上将当前服务器上所有的数据备份,发送一份到远程的云服务上去
数据恢复
如果是redis进程挂掉,那么重启redis进程即可,直接基于AOF日志文件恢复数据,其他选择 相应备份重启即可
AOF没有破损,也是可以直接基于AOF恢复的
AOF append-only,顺序写入,如果AOF文件破损,那么用redis-check-aof fix
appendonly.aof + dump.rdb,优先用appendonly.aof去恢复数据
redis启动的时候,自动重新基于内存的数据,生成了一份最新的rdb快照,
混合持久化
重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。
通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。
Redis 4.0将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
持久化的同时,内存数据结构还在改变怎么处理?
使用操作系统的多进程 COW(Copy On Write) 机制来实现快照持久化
Redis 在持久化时会调用 glibc 的函数fork产生一个子进程,快照持久化完全交给子进程来处理,父进程继续处理客户端请求。
子进程刚刚产生时,它和父进程共享内存里面的代码段和数据段。
子进程做数据持久化,它不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。
但是父进程不一样,它必须持续服务客户端请求,然后对内存数据结构进行不间断的修改。
这个时候就会使用操作系统的 COW 机制来进行数据段页面的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复制一份分离出来,然后对这个复制的页面进行修改。这时子进程相应的页面是没有变化的,还是进程产生时那一瞬间的数据。
随着父进程修改操作的持续进行,越来越多的共享页面被分离出来,内存就会持续增长。但是也不会超过原有数据内存的 2 倍大小。另外一个 Redis 实例里冷数据占的比例往往是比较高的,所以很少会出现所有的页面都会被分离,被分离的往往只有其中一部分页面。每个页面的大小只有 4K,一个 Redis 实例里面一般都会有成千上万的页面。
子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化叫「快照」的原因。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。
什么时候对复制出来的数据进行合并?
怎么解决合并中出现的问题
主从
replication
(1)redis采用异步方式复制数据到slave节点,不过redis 2.8开始,slave node会周期性地确认自己每次复制的数据量
(2)一个master node是可以配置多个slave node的
(3)slave node也可以连接其他的slave node
(4)slave node做复制的时候,是不会block master node的正常工作的
(5)slave node在做复制的时候,也不会block对自己的查询操作,它会用旧的数据集来提供服务; 但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了
(6)slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量
如果采用了主从架构,那么建议必须开启master node的持久化!
主从架构的核心原理
当启动一个slave node的时候,它会发送一个PSYNC命令给master node
如果这是slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据; 否则如果是slave node第一次连接master node,那么会触发一次full resynchronization
开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。
slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。
主从复制的断点续传
从redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份
master node会在内存中常见一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制
但是如果没有找到对应的offset,那么就会执行一次resynchronization
无磁盘化复制
master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了
repl-diskless-sync
repl-diskless-sync-delay,等待一定时长再开始复制,因为要等更多slave重新连接过来
过期key处理
slave不会过期key,只会等待master过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave。
哨兵
作用
(1)集群监控,负责监控redis master和slave进程是否正常工作
(2)消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员
(3)故障转移,如果master node挂掉了,会自动转移到slave node上
(4)配置中心,如果故障转移发生了,通知client客户端新的master地址
sdown和odown转换机制
哨兵集群的自动发现机制
slave配置的自动纠正
slave->master选举算法
quorum和majority
configuration epoch
configuraiton传播
cluster
海量数据+高并发+高可用
缓存清理策略
缓存清理的流程
(1)客户端执行数据写入操作
(2)redis server接收到写入操作之后,检查maxmemory的限制,如果超过了限制,那么就根据对应的policy清理掉部分数据
(3)写入操作完成执行
maxmemory-policy,可以设置内存达到最大闲置后,采取什么策略来处理
(1)noeviction: 如果内存使用达到了maxmemory,client还要继续写入数据,那么就直接报错给客户端
(2)allkeys-lru: 就是我们常说的LRU算法,移除掉最近最少使用的那些keys对应的数据
(3)volatile-lru: 也是采取LRU算法,但是仅仅针对那些设置了指定存活时间(TTL)的key才会清理掉
(4)allkeys-random: 随机选择一些key来删除掉
(5)volatile-random: 随机选择一些设置了TTL的key来删除掉
(6)volatile-ttl: 移除掉部分keys,选择那些TTL时间比较短的keys
(7)volatile-lfu Redis 4.0以上支持
(8)allkeys-lfu Redis 4.0以上支持
LRU
Least Recently Used,最近最少使用算法
maxmemory,设置redis用来存放数据的最大的内存大小,一旦超出这个内存大小之后,就会立即使用LRU算法清理掉部分数据
redis的LRU近似算法
redis采取的是LRU近似算法,也就是对keys进行采样,然后在采样结果中进行数据清理
redis 3.0开始,在LRU近似算法中引入了pool机制,表现可以跟真正的LRU算法相当,但是还是有所差距的,不过这样可以减少内存的消耗
redis LRU算法,是采样之后再做LRU清理的,跟真正的、传统、全量的LRU算法是不太一样的
maxmemory-samples,比如5,可以设置采样的大小,如果设置为10,那么效果会更好,不过也会耗费更多的CPU资源
LFU
Least Frequently Used,表示按最近的访问频率进行淘汰,它比 LRU 更加精准地表示了一个 key 被访问的热度。
如果一个 key 长时间不被访问,只是刚刚偶然被用户访问了一下,那么在使用 LRU 算法下它是不容易被淘汰的,因为 LRU 算法认为当前这个 key 是很热的。而 LFU 是需要追踪最近一段时间的访问频率,如果某个 key 只是偶然被访问一次是不足以变得很热的,它需要在近期一段时间内被访问很多次才有机会被认为很热。
过期策略
redis 会将每个设置了过期时间的 key 放入到一个独立的过期字典
惰性策略
客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期了就立即删除
定时扫描策略
从过期字典中随机 20 个 key;
删除这 20 个 key 中已经过期的 key;
如果过期的 key 比率超过 1/4,那就重复步骤 1;
为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。
从库的过期策略
从库不会进行过期扫描,从库对过期的处理是被动的
主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
发布订阅消息
使用subscribe 进行订阅监听 subscribe 频道
使用 publish 进行发布消息广播 publish 频道 内容
管道 (Pipeline)
服务器根本没有任何区别对待,还是收到一条消息,执行一条消息,回复一条消息的正常的流程。客户端通过对管道中的指令列表改变读写顺序就可以大幅节省 IO 时间。管道中指令越多,效果越好。
old.png
new.png
请求交互流程图.png
请求交互流程
客户端进程调用write将消息写到操作系统内核为套接字分配的发送缓冲send buffer。
客户端操作系统内核将发送缓冲的内容发送到网卡,网卡硬件将数据通过「网际路由」送到服务器的网卡。
服务器操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲recv buffer。
服务器进程调用read从接收缓冲中取出消息进行处理。
服务器进程调用write将响应消息写到内核为套接字分配的发送缓冲send buffer。
服务器操作系统内核将发送缓冲的内容发送到网卡,网卡硬件将数据通过「网际路由」送到客户端的网卡。
客户端操作系统内核将网卡的数据放到内核为套接字分配的接收缓冲recv buffer。
客户端进程调用read从接收缓冲中取出消息返回给上层业务逻辑进行处理。
结束。
我们开始以为 write 操作是要等到对方收到消息才会返回,但实际上不是这样的。write 操作只负责将数据写到本地操作系统内核的发送缓冲然后就返回了。剩下的事交给操作系统内核异步将数据送到目标机器。但是如果发送缓冲满了,那么就需要等待缓冲空出空闲空间来,这个就是写操作 IO 操作的真正耗时。
我们开始以为 read 操作是从目标机器拉取数据,但实际上不是这样的。read 操作只负责将数据从本地操作系统内核的接收缓冲中取出来就了事了。但是如果缓冲是空的,那么就需要等待数据到来,这个就是读操作 IO 操作的真正耗时。
所以对于value = redis.get(key)这样一个简单的请求来说,write操作几乎没有耗时,直接写到发送缓冲就返回,而read就会比较耗时了,因为它要等待消息经过网络路由到目标机器处理后的响应消息,再回送到当前的内核读缓冲才可以返回。这才是一个网络来回的真正开销。
而对于管道来说,连续的write操作根本就没有耗时,之后第一个read操作会等待一个网络的来回开销,然后所有的响应消息就都已经回送到内核的读缓冲了,后续的 read 操作直接就可以从缓冲拿到结果,瞬间就返回了。
如何保证写入数据的成功与否?
压力测试工具redis-benchmark
-P参数,它表示单个管道内并行的请求数量
内存
小对象压缩存储 (ziplist)
内存回收机制
如果当前 Redis 内存有 10G,当你删除了 1GB 的 key 后,再去观察内存,你会发现内存变化不会太大。原因是操作系统回收内存是以页为单位,如果这个页上只要有一个 key 还在使用,那么它就不能被回收。Redis 虽然删除了 1GB 的 key,但是这些 key 分散到了很多页面中,每个页面都还有其它 key 存在,这就导致了内存不会立即被回收。
Redis 虽然无法保证立即回收已经删除的 key 的内存,但是它会重用那些尚未回收的空闲内存。
内存分配算法
Redis 可以使用 jemalloc(facebook) 库来管理内存
安全
修改一些危险的指令
rename-command keys abckeysabc
rename-command flushall ""
应用
分布式锁
当一个线程进入时就加锁,并且制定expire的时间,到时间自动释放,加锁和expire是原子性的
Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻辑执行完之间拿到了锁。
加锁失败时处理
直接抛出异常,通知用户稍后重试;
sleep 一会再重试;
将请求转移至延时队列,过一会再试;
这种方式比较适合异步消息处理,将当前冲突的请求扔到另一个队列延后处理以避开冲突。
集群模式下的问题
主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。
消息队列
Redis 的 list(列表) 数据结构常用来作为异步消息队列使用,使用rpush/lpush操作入队列,使用lpop 和 rpop来出队列。
Redis 的消息队列不是专业的消息队列,它没有非常多的高级特性,没有 ack 保证,如果对消息的可靠性有着极致的追求,那么它就不适合使用。
问题:如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop,又没有数据。这就是浪费生命的空轮询。空轮询不但拉高了客户端的 CPU,redis 的 QPS 也会被拉高,如果这样空轮询的客户端有几十来个,Redis 的慢查询可能会显著增多。
解决方案:使用blpop/brpop 阻塞读在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。消息的延迟几乎为零。
方案问题:如果线程一直阻塞,Redis 的客户端连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源占用。这个时候blpop/brpop会抛出异常来。
所以编写客户端消费者的时候要小心,注意捕获异常,还要重试。
延时队列可以通过 Redis 的 zset(有序列表) 来实现
消息序列化成一个字符串作为 zset 的value,这个消息的到期处理时间作为score,然后用多个线程轮询 zset 获取到期的任务进行处理
Redis 的 zrem 方法是多线程多进程争抢任务的关键,它的返回值决定了当前实例有没有抢到任务,因为 loop 方法可能会被多个线程、多个进程调用,同一个任务可能会被多个进程线程抢到,通过 zrem 来决定唯一的属主。
队列.java
Redis5.0新增数据结构Stream
支持多播的可持久化的消息队列
Stream数据结构.png
位图
场景:在我们平时开发过程中,会有一些 bool 型数据需要存取,比如用户一年的签到记录,签了是 1,没签是 0,要记录 365 天。如果使用普通的 key/value,每个用户要记录 365 个,当用户上亿的时候,需要的存储空间是惊人的。
为了解决这个问题,Redis 提供了位图数据结构,这样每天的签到记录只占据一个位,365 天就是 365 个位,46 个字节 (一个稍长一点的字符串) 就可以完全容纳下,这就大大节约了存储空间。
[object Object]
位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。
保存用户是否登录信息事例
Key结构:前缀_年Y-月m_用户类型_用户ID
标准Key:KEYS loginLog_2017-10_client_1001
检索全部:KEYS loginLog_*
检索某年某月全部:KEYS loginLog_2017-10_*
检索单个用户全部:KEYS loginLog_*_client_1001
检索单个类型全部:KEYS loginLog_*_office_*
设置用户1001,217-10-25登录:SETBIT loginLog_2017-10_client_1001 25 1
获取用户1001,217-10-25是否登录:GETBIT loginLog_2017-10_client_1001 25
获取用户1001,217-10月是否登录:BITCOUNT loginLog_2017-10_client_1001
获取用户1001,217-10/9/7月是否登录:BITOP OR stat loginLog_2017-10_client_1001 loginLog_2017-09_client_1001 loginLog_2017-07_client_1001
每周同步一次数据到数据库,表中一条数据对应一个BitMap,记录一个月数据。每次更新已存在的、插入没有的
HyperLogLog
可以接受多个元素作为输入,并给出输入元素的基数估算值:
优点是,有去重功能,且即使输入元素的数量或者体积非常非常大,计算基数所需的空间总是固定的、并且是很小的。每个 HyperLogLog 键只需要花费 12 KB 内存
弊端:不精确,统计有一定的误差,无法查询一个元素是否存在
指令
pfadd
pfadd 用法和 set 集合的 sadd 是一样的,
pfcount
获取计数值
pfmerge
用于将多个 pf 计数值累加在一起形成一个新的 pf 值
代码事例
public class JedisTest {
public static void main(String[] args) {
Jedis jedis = new Jedis();
for (int i = 0; i < 100000; i++) {
jedis.pfadd("codehole", "user" + i);
}
long total = jedis.pfcount("codehole");
System.out.printf("%d %d\n", 100000, total);
jedis.close();
}
}
布隆过滤器 (Bloom Filter)
和HyperLogLog类似,但多个查询元素是否存在功能
缺点:无法删除,有一定的误差
指令
bf.add 添加一个
bf.exists 查询一个
bf.madd 添加多个
bf.mexists 查询多个
代码事例
public class BloomTest {
public static void main(String[] args) {
Client client = new Client();
client.delete("codehole");
for (int i = 0; i < 100000; i++) {
client.add("codehole", "user" + i);
boolean ret = client.exists("codehole", "user" + (i + 1));
if (ret) {
System.out.println(i);
break;
}
}
client.close();
}
}
简单限流
根据zset 的特殊结构来实现
限流.txt
地理位置 GEO 模块
geoadd company 116.48105 39.996794 juejin
geodist company juejin jd km
geopos company juejin
Scan搜索key值
复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程;
提供 limit 参数,可以控制每次返回结果的最大条数,limit 只是一个 hint,返回的结果可多可少;
同 keys 一样,它也提供模式匹配功能;
服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;
返回的结果可能会有重复,需要客户端去重复,这点非常重要;
遍历的过程中如果有数据修改,改动后的数据能不能遍历到是不确定的;
单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零;
0 条评论
下一页