Redis位图以及用户签到功能的实现

阅读本文约“12分钟”

适读人群:初级Java

这周出差福建龙岩了解到一种叫牛兜汤的小吃

类似牛杂碎组成的,汤偏稠,味咸

7fb8bd9d40bd623fe829d034cedb043b.jpeg

Pexels 上的 Vova Krasilnikov 拍摄的图片

前言

最近开发的项目中需要实现一个用户累计签到的功能,看到这个需求的时候第一反应就是利用Redis位图来实现。之前在学习Redis数据结构的时候就有接触到位图,不过位图的应用场景不多,所以一直没有机会使用到。先简单介绍一下Redis的位图吧。

位图的原理

位图不是特殊的数据结构,它的内容其实就是普通的字符串,也就是 byte 数组。我们可以使用普通的 get/set 直接获取和设置整个位图的内容,也可以使用位图操作 getbit/setbit 等将 byte 数组看成「位数组」来处理。

——《Redis深度历险 核心原理与应用实践》

实际上,位图的本质还是操作Redis里的"String"数据结构。我们都知道字符串是由多个字节组成的,每个字节都有对应的ASCII码,每个ASCII码的二进制都是8位数。操作位图实际上就是操作String字符串里的字节的二进制数字。听起来有点拗口?咱们撸个指令看看位图的基本使用。

假如我们现在要向Redis插入一个key为"name",value为"lee"的字符串。我们有两种办法:

1.通过set指令

1本地redis:0>set "name" lee
2"OK"
3本地redis:0>get "name"
4"lee"

2.通过位图的setbit指令
首先在开撸之前,我们要先拆解一下value值,“lee"实际上是由"l”,“e”,"e"这三个字节组成的。
‘l’ 的ASCII码的二进制是0110 1100

6d7732231ce82f38b80571c382ddda20.png

'e’ 的ASCII码的二进制是0110 0101

5ed32bf7faf07988b860c739187738e0.png

如图表所示,将"lee"的字符的ASCII码的二进制连起来是

b353479a0e0835d58cbe7c6194a1abff.png

这里需要注意一下:ASCII的低位到高位是从右到左的,而我们在用setbit定位是从左到右的。

SETBIT key offset value

可用版本:>= 2.2.0

时间复杂度: O(1)

对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。

位的设置或清除取决于 value 参数,可以是 0 也可以是 1 。

当 key 不存在时,自动生成一个新的字符串值。

字符串会进行伸展(grown)以确保它可以将 value 保存在指定的偏移量上。当字符串值进行伸展时,空白位置以 0 填充。

offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内)。所以在Redis中字符串类型的Value最多可以容纳的数据长度是 512 MB 。

Redis 的位数组是自动扩展,如果设置了某个偏移位置超出了现有的内容范围,就会自动将位数组进行零扩充。所以我们在操作过程中只需要去处理设置值为1的位,可以看到,第一个字符"l",需要将下标1,2,4,5设置为1。第二个字符"e",需要下标9,10,13,15设置为1。第三个字符"e",需要下标17,18,21,23设置为1。

1本地redis:0>del "name"
 2"1"
 3本地redis:0>setbit "name" 1 1
 4"0"
 5本地redis:0>setbit "name" 2 1
 6"0"
 7本地redis:0>setbit "name" 4 1
 8"0"
 9本地redis:0>setbit "name" 5 1
10"0"
11本地redis:0>get "name"
12"l" #第一个字符录入成功
13本地redis:0>setbit "name" 9 1
14"0"
15本地redis:0>setbit "name" 10 1
16"0"
17本地redis:0>setbit "name" 13 1
18"0"
19本地redis:0>setbit "name" 15 1
20"0"
21本地redis:0>get "name"
22"le" #第二个字符录入成功
23本地redis:0>setbit "name" 17 1
24"0"
25本地redis:0>setbit "name" 18 1
26"0"
27本地redis:0>setbit "name" 21 1
28"0"
29本地redis:0>setbit "name" 23 1
30"0"
31本地redis:0>get "name"
32"lee" #第三个字符录入成功

同理,有setbit指令,就有getbit指令

GETBIT key offset
可用版本:>= 2.2.0
时间复杂度:O(1)

另外还有bitcount指令

BITCOUNT key [start] [end]

可用版本:>= 2.6.0

时间复杂度:O(N)

计算给定字符串中,被设置为 1 的比特位的数量。

一般情况下,给定的整个字符串都会被进行计数,通过指定额外的 start 或 end 参数,可以让计数只在特定的位上进行。

start 和 end 参数的设置和 GETRANGE key start end 命令类似,都可以使用负数值:比如 -1 表示最后一个字节, -2 表示倒数第二个字节,以此类推。

不存在的 key 被当成是空字符串来处理,因此对一个不存在的 key 进行 BITCOUNT 操作,结果为 0 。

利用位图来实现签到功能

我们可以利用用户的唯一标识来做为key值,以项目正式上线日期到当前时间的日期差做为偏移值。利用setbit和bitcount来做为签到以及统计签到的功能。上一个签到实现的Java代码吧。(为什么用日期差来做偏移值,而不直接用日期的Long类型来做,原因在于日期的数字比较长,会浪费很多空间存储0)

1    private final String onlineDate = "2020-05-03";
 2    @Override
 3    public void signed(String userId) {
 4        LocalDate beginday = LocalDate.parse(onlineDate, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
 5        LocalDate today = LocalDate.now();
 6        //获取上线之日到今天过了多久,做为redis的offset
 7        long offset = beginday.until(today, ChronoUnit.DAYS);
 8        String key = "signed" + userId;
 9        //使用redis的位图机制来做登录记录,签到为1(true),未签到自动为0(false)
10        Boolean flag = redisTemplate.execute((RedisCallback<Boolean>) con -> con.setBit(key.getBytes(), offset, true));
11        //这个flag返回值是存储位原来的值,所以返回false说明签到成功,返回true说明重复签到
12        if (!flag) {
13            //获取用户累计签到次数
14            Long signedNum = redisTemplate.execute((RedisCallback<Long>)con -> con.bitCount(key.getBytes()));
15            //根据累计签到次数做业务处理
16        }
17
18    }

位图的好处

速度快、节省空间

位图的setbit和getbit的时间复杂度都是O(1),而bitcount的时间复杂度虽然是O(N),但是在刚才描述的用户签到业务场景下,即使运行 10 年,占用的空间也只是每个用户 10*365 比特位,也即是每个用户 456 字节。对于这种大小的数据来说, BITCOUNT key [start] [end] 的处理速度就像 GET key 和 INCR key 这种 O(1) 复杂度的操作一样快。

面试相关

了解完redis的位图,最后上一道大厂的经典算法面试题,大家一起思考一下吧

给20亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中并且所耗内存尽可能的少?

9cbfcd00d7f5b059c4a4263212fe74fd.jpeg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值