阅读本文约“12分钟”适读人群:初级Java 这周出差福建龙岩了解到一种叫牛兜汤的小吃 类似牛杂碎组成的,汤偏稠,味咸

redis放图片有问题吗 redis可以存图片吗_位图


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" lee2"OK"3本地redis:0>get "name"4"lee"

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


redis放图片有问题吗 redis可以存图片吗_redis_02

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


redis放图片有问题吗 redis可以存图片吗_redis放图片有问题吗_03

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


redis放图片有问题吗 redis可以存图片吗_简单的签到代码_04

这里需要注意一下: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 110"0"11本地redis:0>get "name"12"l" #第一个字符录入成功13本地redis:0>setbit "name" 9 114"0"15本地redis:0>setbit "name" 10 116"0"17本地redis:0>setbit "name" 13 118"0"19本地redis:0>setbit "name" 15 120"0"21本地redis:0>get "name"22"le" #第二个字符录入成功23本地redis:0>setbit "name" 17 124"0"25本地redis:0>setbit "name" 18 126"0"27本地redis:0>setbit "name" 21 128"0"29本地redis:0>setbit "name" 23 130"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        }1718    }

位图的好处

速度快、节省空间

位图的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亿个数当中并且所耗内存尽可能的少?