聊聊Redis二进制数组Bitmap

好久没有更新了,之前公司在做 关注/粉丝 这块儿缓存的时候,我选择的就是 Bitmap ,那时是我第一次见识到这种数据数组形式,用到的有 SETBIT , GETBIT , BITCOUNT ,命令如何使用就不说了,今天来仔细看看这三个命令的实现和原理。

选用 bitmap 的考量:

位数组的实现

关注关系需求中 关注对象 和 被关注人 都是 0-几千万 的数据对象,存储这种对应关系时,采用bitmap 这种位数组,明显要比 uid 的 set 方式要节省存储空间,redis 的 内存 是很宝贵的,这值得作为考量的地方。

位数组大致可表示为:0101010000100000....0100 这样的二进制串, 在 Redis 的 SDS字符串 一文中可以看到 Redis 中的字符串对象实现,SDS数据结构是二进制安全的,所以 Redis 可以使用字符串来表示 位数组 。 所以根据上面说的,位数组是以字符串的形式:buf[0]|buf[1]...这样一个一个字节存放的。

SETBIT 和 GETBIT

GETBIT 的实现:

  # 返回 位数组 bitarray 在 offset 偏移量上的二进制位(byte*8+bit)的值
  getbit <bitarray> <offset>
  # 字节
  byte = offset / 8
  # 位
  bit = (offset mod 8) + 1
  # 可以看到 O(1)

SETBIT 的实现:

  # 将 位数组 bitarray 在offset 偏移量上的二进制位的值设置为 value
  setbit <bitarray> <offset> <value>
  # 计算保存二进制位需要多少 字节
  len = [offset / 8] + 1
  # 鱿鱼 ? 二进制位数组使用的数据结构是 sds ,而 sds 记录长度的是len ,正常进行扩展,同空间预分配 ,扩展位为`00000`
  # 字节
  byte = offset / 8
  # 位
  bit = (offset mod 8) + 1
  # 记录 (byte*8+bit) 上 oldvalue ,再赋予新值,返回 oldvalue

Bitcount 的实现

BITCOUNT 统计给定位数组中,值为1 的数量,也就是统计汉明重量(见 Leetcode 191、338),其实是一个老问题,看看几种算法,和 redis 的做法。

#1. 粗暴遍历 O(n)

class Solution(object):
    def hammingWeight(self, n):
        rst = 0
        mask = 1
        for i in range(32):
            if n & mask :
                rst += 1
            mask = mask << 1
        return  rst

#2. 查表法
# 查表法原理在于建立N个位数组,如果是8位,即减少查询次数/8 ,建表越大,则查询次数越少
# 空间换时间的典型操作,不禁让我想起了 计数排序O(n+k)
# 内存考虑:这里可以算一下 8位的查表耗费的内存百字节,16位查表为百Kb,32位为十多个G,实际中,服务器只能接受16位
# CPU考虑:表长越大,miss 率越大。而 max 为16 也不能解决大数组的查表效率问题
# 这种算法 O(n/m) S(m) m<=16 

#3.variable-precision SWAR 算法 O(1)

#4.Redis
# redis 未处理的二进制数 >= 128,使用variable-precision SWAR
# redis 未处理的二进制数 < 128,使用 查表法

Redis-高性能bitmap

实时指标

Redis bitmap可用于快速、简单的实现实时指标。

传统情况下,由批量job生成指标数据。但是redis的bitmap支持实时指标计算,而且具有极高的空间利用率。例如1.28亿用户,实时统计日UV,仅仅占用16MB内存空间,在mbp上耗时50ms。

bitset

可视为由0和1组成的数组。在bitset中的每个bit可为0或1,使用offset表示bit在数组的位置。支持多个bitset间进行位操作,如与或非等。

群体统计

bitset的群体统计表示bitset中数据为1的bit数量。使用bitset做群体统计是非常高效的。如具有十亿bit的bitset,其90%空间已设置数据,在mbp上进行群体统计仅耗时几十或几百ms。

redis bitmap

bitmap是二进制数据,可通过set key offset val指令设置具体offset位置bit的数据,且时间复杂度为O(1)。

日活用户

针对关注点(某个页面或某个操作),统计活跃用户数量。

key规则:关注点名称+日时间戳

val:创建bitmap,宽度为当前用户数量,每个用户的id作为offset,这个用户ID是记录ID,不可能是由特定规则生成的userID。

当用户访问关注点时,针对具体bitset,将用户IDoffset位置数据设置为1。之后对该bitset进行群体统计,即为关注点的日活用户量。

采取关注点名称+时间戳的方式,可以存储不同时间维度的活跃用户。

如每小时播放音乐的用户量,key定义为play_yyyyMMddhh;每天播放音乐的用户量,key定义为play_yyyyMMdd。

当需要统计较大时间范围的用户量时,可以先对多个bitset求并集,然后再群体统计,如统计一周、一个月的用户量。

性能对比

1.28亿用户,使用bitset记录日活,使用日活并集统计7日 15日日活。
PERIOD TIME (MS)
Daily 50.2
Weekly 392.0
Monthly 1624.8

特性分析

周维度访问关注点且绑定手机号的唯一用户,采用绑定手机号用户bitset 交集 周维度访问关注点的用户bitset

最近n天唯一用户量的滚动统计,对最近n天每天的日活用户bitset求并集

涉及的指令

群体统计使用bitcount key

交集并集使用bitop and/or dest key1 keyn

对用户IDoffset设置数据使用setbit key offset 1

java bitset.cardinality()/and(bitset)/or(bitset)

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Redis中3种特殊的数据类型(BitMap、Geo和HyperLogLog)

    前言 Reids 在 Web 应用的开发中使用非常广泛,几乎所有的后端技术都会有涉及到 Redis 的使用.Redis 种除了常见的字符串 String.字典 Hash.列表 List.集合 Set.有序集合 SortedSet 等等之外,还有一些不常用的数据类型,这里着重介绍三个.下面话不多说了,来一起看看详细的介绍吧. BitMap BitMap 就是通过一个 bit 位来表示某个元素对应的值或者状态, 其中的 key 就是对应元素本身,实际上底层也是通过对字符串的操作来实现.Redis 从

  • Redis基于Bitmap实现用户签到功能

    目录 功能分析 更多应用场景 总结 参考资料 很多应用上都有用户签到的功能,尤其是配合积分系统一起使用.现在有以下需求: 签到1天得1积分,连续签到2天得2积分,3天得3积分,3天以上均得3积分等. 如果连续签到中断,则重置计数,每月重置计数. 显示用户某月的签到次数和首次签到时间. 在日历控件上展示用户每月签到,可以切换年月显示. ... 功能分析 对于用户签到数据,如果直接采用数据库存储,当出现高并发访问时,对数据库压力会很大,例如双十一签到活动.这时候应该采用缓存,以减轻数据库的压力,Re

  • Redis高级数据类型Hyperloglog、Bitmap的使用

    前言 很多小伙伴在面试中都会被问道 Redis的常用数据结构有哪些? 可能很大一部分回答都是 string.hash.list.set.zset.当然啦,这个答案肯定是没有错的,但是相信这个答案,面试官已经听的耳朵都起茧了. 本身我们选择的这个行业竞争就极强,学历拼不过难道还要知识都拼不过吗??? 希望进来的小伙伴能好好看完这篇文章,也希望你以后的回答能是 常用的数据结构有string.hash.list.set.zset,但我平时可能还会用到 Hyperloglog和Bitmap.相信面试官听

  • 聊聊Redis二进制数组Bitmap

    好久没有更新了,之前公司在做 关注/粉丝 这块儿缓存的时候,我选择的就是 Bitmap ,那时是我第一次见识到这种数据数组形式,用到的有 SETBIT , GETBIT , BITCOUNT ,命令如何使用就不说了,今天来仔细看看这三个命令的实现和原理. 选用 bitmap 的考量: 位数组的实现 关注关系需求中 关注对象 和 被关注人 都是 0-几千万 的数据对象,存储这种对应关系时,采用bitmap 这种位数组,明显要比 uid 的 set 方式要节省存储空间,redis 的 内存 是很宝贵

  • 浅谈Redis位图(Bitmap)及Redis二进制中的问题

    Redis位图(Bitmap)及二进制的问题 SETBIT key offset value 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit).位的设置或清除取决于 value 参数,可以是 0 也可以是 1 .当 key 不存在时,自动生成一个新的字符串值.字符串会进行伸展(grown)以确保它可以将 value 保存在指定的偏移量上.当字符串值进行伸展时,空白位置以 0 填充.offset 参数必须大于或等于 0 ,小于 2^32 (bit 映射被限制在 512 MB 之内

  • Redis中的bitmap详解

    1.什么是bitmap? bitmap也叫位图,也就是用一个bit位来表示一个东西的状态,我们都知道bit位是二进制,所以只有两种状态,0和1. 2.为什么要有bitmap? bitmap的出现就是为了大数据量而来的,但是前提是统计的这个大数据量每个的状态只能有两种,因为每一个bit位只能表示两种状态. 下面我们直接以一个统计亿级用户活动的状态来说明吧. 3.案例说明 3.1.案例描述 如果有一个上亿用户的系统,需要我们去统计每一天的用户登录情况,我们应该如何去解决? 前提条件:设置在9月19号

  • Redis特殊数据类型bitmap位图

    目录 Redis数据类型bitmap位图 一.setbit 二.getbit 三.bitcount Redis数据类型bitmap位图 bitmap数据结构,是基于二进制位来进行操作记录的,只有0 和 1两个状态.可以想象成一个数组,里面只有0或者1. 能干嘛呢? 现实中会有这些场景,比如统计用户信息,活跃用户和非活跃用户.登录的.未登录的用户,打卡的.未打卡的,像这种只有2个状态,并且数据量非常大的,就适合使用bitmap. 网上找了一个对比,可以帮助记忆下bitmap的优点. 一.setbi

  • Vue利用Blob下载原生二进制数组文件

    本文实例为大家分享了Vue利用Blob下载原生二进制数组文件的具体代码,供大家参考,具体内容如下 在服务端推送过来的二进制数组(JSON格式),在前端要处理成JS原生数组以后才能做成Blob,有两个地方要注意(详细注释),代码如下: Vue.prototype.$downloadFile = (filename, data) => { if (!data) return; let arr8 = Uint8Array.from(data); //!!!注意1:应根据数据的类型选择适当的JS原生数组

  • Python如何读写二进制数组数据

    问题 你想读写一个二进制数组的结构化数据到Python元组中. 解决方案 可以使用 struct 模块处理二进制数据. 下面是一段示例代码将一个Python元组列表写入一个二进制文件,并使用 struct 将每个元组编码为一个结构体. from struct import Struct def write_records(records, format, f): ''' Write a sequence of tuples to a binary file of structures. '''

  • redis中的bitmap你了解吗

    目录 1.BitMap是什么 2.setbit命令介绍 总结 1.BitMap是什么 通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身.我们知道8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间.2^32次方40亿数据只需要500M内存,需要内存少了8倍 2.setbit命令介绍 setbit key offset value #设置bitmapkey为20220328 uid为100的用户已签到1 setbit 20220320 100 1 s

  • 详细聊聊Redis的过期策略

    保存过期时间 Redis可以为每个key设置过期时间,会将每个设置了过期时间的key放入一个独立的字典中. typedef struct redisDb { int id; //id是数据库序号,为0-15(默认Redis有16个数据库) long avg_ttl; //存储的数据库对象的平均ttl(time to live),用于统计 dict *dict; //存储数据库所有的key-value dict *expires; //存储key的过期时间 dict *blocking_keys;

  • redis的string类型及bitmap介绍

    目录 redis运行原理 redis使用 redis二进制安全 getset命令 位图(bitmap) 场景题 总结 redis运行原理 redis有很多的客户端连接进来,站在redis所在机器的角度来说,就是有很多socket的连接,并且是打在内核上面的,redis是一个进程,进程可以调用内核上的epoll,来遍历寻找哪一个客户端发送数据过来了(这里是单进程单线程来处理用户数据的). redis使用 redis默认有16个库 输入:进入基本分组 keys * 查询所有的keyFLUSHDB 清

随机推荐