实现消息队列
基于 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 来决定唯一的属主。
不过缺点很明显,消费者必须使用轮询的方式
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✌️日期。
getbit 获取值 getbit key offset
获取键的第 offset 位的值(从 0 开始算),比如获取 userid=8 的用户是否在 08-15 这天访问过,返回 0 说明没有访问过:
当然 offset 是不存在的,也会返回 0。
bitcount 获取 Bitmaps 指定范围值为 1 的个数
bitcount [start] [end]
下面操作计算 08-15 这天的独立访问用户数量
[start]和[end]代表起始和结束字节数
bitpos 计算 Bitmaps 中第一个值为 targetBit 的偏移量
bitpos key targetBit [start] [end]
计算 0815 当前访问网站的最小用户 id