【SpringBoot DB 系列】Redis 高级特性之 Bitmap 使用姿势及应用场景介绍

【SpringBoot DB 系列】Redis 高级特性之 Bitmap 使用姿势及应用场景介绍

前面介绍过 redis 的五种基本数据结构,如 String,List, Set, ZSet, Hash,这些属于相对常见了;在这些基本结果之上,redis 还提供了一些更高级的功能,如 geo, bitmap, hyperloglog,pub/sub,本文将主要介绍 Bitmap 的使用姿势以及其适用场景,主要知识点包括

  • bitmap 基本使用
  • 日活统计应用场景中 bitmap 使用姿势
  • 点赞去重应用场景中 bitmap 使用姿势
  • 布隆过滤器 bloomfilter 基本原理及体验 case

<!-- more -->

I. 基本使用

1. 配置

我们使用 SpringBoot 2.2.1.RELEASE来搭建项目环境,直接在pom.xml中添加 redis 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

如果我们的 redis 是默认配置,则可以不额外添加任何配置;也可以直接在application.yml配置中,如下

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:

2. 使用姿势

bitmap 主要就三个操作命令,setbitgetbit以及 bitcount

a. 设置标记

setbit,主要是指将某个索引,设置为 1(设置 0 表示抹去标记),基本语法如下

# 请注意这个index必须是数字,后面的value必须是0/1
setbit key index 0/1

对应的 SpringBoot 中,借助 RestTemplate 可以比较容易的实现,通常有两种写法,都可以

@Autowired
private StringRedisTemplate redisTemplate;

/**
 * 设置标记位
 *
 * @param key
 * @param offset
 * @param tag
 * @return
 */
public Boolean mark(String key, long offset, boolean tag) {
    return redisTemplate.opsForValue().setBit(key, offset, tag);
}

public Boolean mark2(String key, long offset, boolean tag) {
    return redisTemplate.execute(new RedisCallback<Boolean>() {
        @Override
        public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
            return connection.setBit(key.getBytes(), offset, tag);
        }
    });
}

上面两种写法的核心区别,就是 key 的序列化问题,第一种写法使用默认的 jdk 字符串序列化,和后面的getBytes()会有一些区别,关于这个,有兴趣的小伙伴可以看一下我之前的博文: RedisTemplate 配置与使用#序列化问题

b. 判断存在与否

getbit key index,如果返回 1,表示存在否则不存在

/**
 * 判断是否标记过
 *
 * @param key
 * @param offest
 * @return
 */
public Boolean container(String key, long offest) {
    return redisTemplate.opsForValue().getBit(key, offest);
}

c. 计数

bitcount key,统计和

/**
 * 统计计数
 *
 * @param key
 * @return
 */
public long bitCount(String key) {
    return redisTemplate.execute(new RedisCallback<Long>() {
        @Override
        public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
            return redisConnection.bitCount(key.getBytes());
        }
    });
}

3. 应用场景

前面的基本使用比较简单,在介绍 String 数据结构的时候也提过,我们重点需要关注的是 bitmap 的使用场景,它可以干嘛用,什么场景下使用它会有显著的优势

  • 日活统计
  • 点赞
  • bloomfilter

上面三个场景虽有相似之处,但实际的应用场景还是些许区别,接下来我们逐一进行说明

a. 日活统计

统计应用或网站的日活,这个属于比较常见的 case 了,如果是用 redis 来做这个事情,首先我们最容易想到的是 Hash 结构,一般逻辑如下

  • 根据日期,设置 key,如今天为 2020/10/13, 那么 key 可以为 app_20_10_13
  • 其次当用户访问时,设置 field 为 userId, value 设置为 true
  • 判断日活则是统计 map 的个数hlen app_20_10_13

上面这个逻辑有毛病么?当然没有问题,但是想一想,当我们的应用做的很 nb 的时候,每天的日活都是百万,千万级时,这个内存开销就有点吓人了

接下来我们看一下 bitmap 可以怎么做

  • 同样根据日期设置 key
  • 当用户访问时,index 设置为 userId,setbit app_20_10_13 uesrId 1
  • 日活统计 bitcount app_20_10_13

简单对比一下上面两种方案

当数据量小时,且 userid 分布不均匀,小的为个位数,大的几千万,上亿这种,使用 bitmap 就有点亏了,因为 userId 作为 index,那么 bitmap 的长度就需要能容纳最大的 userId,但是实际日活又很小,说明 bitmap 中间有大量的空白数据

反之当数据量很大时,比如百万/千万,userId 是连续递增的场景下,bitmap 的优势有两点:1.存储开销小, 2.统计总数快

c. 点赞

点赞的业务,最主要的一点是一个用户点赞过之后,就不能继续点赞了(当然某些业务场景除外),所以我们需要知道是否可以继续点赞

上面这个 hash 当然也可以实现,我们这里则主要讨论一下 bitmap 的实现逻辑

  • 比如我们希望对一个文章进行点赞统计,那么我们根据文章 articleId 来生成 redisKey=like_1121,将 userId 作为 index
  • 首先是通过getbit like_1121 userId 来判断是否点赞过,从而限制用户是否可以操作

Hash 以及 bitmap 的选择和上面的考量范围差不多

d. 布隆过滤器 bloomfilter

布隆过滤器可谓是大名鼎鼎了,我们这里简单的介绍一下这东西是啥玩意

  • 底层存储为一个 bitmap
  • 当来一个数据时,经过 n 个 hash 函数,得到 n 个数值
  • 将 hash 得到的 n 个数值,映射到 bitmap,标记对应的位置为 1

如果来一个数据,通过 hash 计算之后,若这个 n 个值,对应的 bitmap 都是 1,那么表示这个数据可能存在;如果有一个不为 1,则表示这个数据一定不存在

请注意:不存在时,是一定不存在;存在时,则不一定

从上面的描述也知道,bloomfilter 的底层数据结构就是 bitmap,当然它的关键点在 hash 算法;根据它未命中时一定不存在的特性,非常适用于缓存击穿的问题解决

体验说明

Redis 的布隆过滤器主要针对>=4.0,通过插件的形式提供,项目源码地址为: https://github.com/RedisBloom/RedisBloom,下面根据 readme 的说明,简单的体验一下 redis 中 bloomfilter 的使用姿势

# docker 方式安装
docker run -p 6379:6379 --name redis-redisbloom redislabs/rebloom:latest

# 通过redis-cli方式访问
docker exec -it redis-redisbloom bash

# 开始使用
# redis-cli
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> bf.add newFilter hello
(integer) 1
127.0.0.1:6379> bf.exists newFilter hello
(integer) 1
127.0.0.1:6379> bf.exists newFilter hell
(integer) 0

bloomfilter 的使用比较简单,主要是两个命令bf.add添加元素,bf.exists判断是否存在,请注意它没有删除哦

4. 小结

bitmap 位图属于一个比较精巧的数据结构,通常在数据量大的场景下,会有出现的表现效果;redis 本身基于 String 数据结构来实现 bitmap 的功能支持,使用方式比较简单,基本上就下面三个命令

  • setbit key index 1/0: 设置
  • getbit key index: 判断是否存在
  • bitcount key: 计数统计

本文也给出了 bitmap 的三个常见的应用场景

  • 日活统计:主要借助bitcount来获取总数(后面会介绍,在日活十万百万以上时,使用 hyperLogLog 更优雅)
  • 点赞: 主要借助setbit/getbit来判断用户是否赞过,从而实现去重
  • bloomfilter: 基于 bitmap 实现的布隆过滤器,广泛用于去重的业务场景中(如缓存穿透,爬虫 url 去重等)

总的来讲,bitmap 属于易用,巧用的数据结构,用得好即能节省内存也可以提高效率,用得不好貌似也不会带来太大的问题

II. 其他

0. 项目

系列博文

工程源码

1. 一灰灰 Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog

阅读 200

推荐阅读
hhui
用户专栏

一灰灰Blog的个人编程记录

15 人关注
101 篇文章
专栏主页