Redis 中bitMap使用及实现访问量

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis 中bitMap使用及实现访问量

1. Bitmap 是什么

  Bitmap(也称为位数组或者位向量等)是一种实现对位的操作的'数据结构',在数据结构加引号主要因为:

    Bitmap 本身不是一种数据结构,底层实际上是字符串,可以借助字符串进行位操作。

    Bitmap 单独提供了一套命令,所以与使用字符串的方法不太相同。可以把 Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储 0 和 1,数组的下标在 Bitmap 中叫做偏移量 offset。

2.占用存储空间

   如上我们知道 Bitmap 本身不是一种数据结构,底层实际上使用字符串来存储。由于 Redis 中字符串的最大长度是 512 MB字节,所以 BitMap 的偏移量 offset 值也是有上限的,其最大值是:8 * 1024 * 1024 * 512 = 2^32。由于 C 语言中字符串的末尾都要存储一位分隔符,所以实际上 BitMap 的偏移量 offset 值上限是:2^32-1。Bitmap 实际占用存储空间取决于 BitMap 偏移量 offset 的最大值,占用字节数可以用 (max_offset / 8) + 1 公式来计算或者直接借助底层字符串函数 strlen 来计算:

                                 

 

  需要注意的是,在第一次初始化 Bitmap 时,假如偏移量 offset 非常大,由于需要分配所需要的内存,整个初始化过程执行会比较慢,可能会造成 Redis 的阻塞。在 2010 款 MacBook Pro 上,设置第 2^32-1 位,由于需要分配 512MB 内存,所以大约需要 300 毫秒;设置第 2^30-1 位(128 MB)大约需要 80 毫秒;设置第 2^28 -1 位(32MB)需要约 30 毫秒;设置第 2^26 -1(8MB)需要约 8 毫秒。一旦完成第一次分配,随后对同一 key 再设置将不会产生分配开销。

3. bit 常用的命令:

127.0.0.1:6379> setbit login:20221204 0 1
(integer) 0
127.0.0.1:6379> strlen login:20221204
(integer) 1
127.0.0.1:6379> setbit login:20221204 8 1
(integer) 0
127.0.0.1:6379> strlen login:20221204
(integer) 2
127.0.0.1:6379> bitcount login:20221204
(integer) 2
127.0.0.1:6379> getbit login:20221204 8
(integer) 1
127.0.0.1:6379> type login:20221204
string

过以上命令可以看到,bit 在redis 中使用的是string 的存储结构

4.  SETBIT

  语法格式:

SETBIT key offset value

  SETBIT 用来设置 key 对应第 offset 位的值(offset 从 0 开始算),可以设置为 0 或者 1。当指定的 KEY 不存在时,会自动生成一个新的字符串值。字符串会进行扩展以确保可以将 value 保存在指定的偏移量 offset 上。当字符串值进行扩展时,空白位置用 0 来填充。需要注意的是 offset 需要大于或等于 0,小于 2 的 32 次方。

  假设现在有 10 个用户,用户id为 0、1、5、9 的 4 个用户在 20220514 进行了登录,那么当前 Bitmap 初始化结果如下图所示:

                       

 

 

    假设用户 uid 为 15 的用户也登录了 App,那么 Bitmap 的结构变成了如下图所示,第 10 位到第 14 位都用 0 填充,第 15 位被置为 1:

                             

  很多应用的用户id以一个指定数字(例如 150000000000)开头,直接将用户id和 Bitmap 的偏移量对应势必会造成一定的浪费,通常的做法是每次做 setbit 操作时将用户id减去这个指定数字。在第一次初始化 Bitmap 时,假如偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成 Redis 的阻塞。

5. 使用 bitMap 统计访问量  

package com.hys.redis;
 
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
 
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
 
import redis.clients.jedis.BitOP;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Pipeline;
 
/**
 * 统计累计和日均活跃用户人数
 * @author Robert Hou
 * @date 2019年5月31日
*/
public class Counter {
 
    /**
     * ip地址
     */
    private static final String IP_ADDRESS = "127.0.0.1";
    /**
     * 端口号
     */
    private static final int    PORT       = 6379;
    /**
     * jedis客户端
     */
    private Jedis               jedis;
    /**
     * 累计用户人数key
     */
    private static final String TOTAL_KEY  = "totalKey";
    /**
     * 日均活跃用户人数key
     */
    private static final String ACTIVE_KEY = "activeKey:";
 
    public Counter() {
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setMaxTotal(50);
        poolConfig.setMaxIdle(50);
        poolConfig.setMaxWaitMillis(1000);
        JedisPool jedisPool = new JedisPool(poolConfig, IP_ADDRESS, PORT);
        jedis = jedisPool.getResource();
    }
 
    /**
     * 更新累计和日均活跃用户人数
    * @param userId 用户id
    * @param time 当前日期
     */
    private void updateUser(long userId, String time) {
        if (StringUtils.isBlank(time)) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
            time = sdf.format(new Date());
        }
        Pipeline pipeline = jedis.pipelined();
        pipeline.setbit(TOTAL_KEY, userId, true);
        pipeline.setbit(ACTIVE_KEY + time, userId, true);
        pipeline.syncAndReturnAll();
    }
 
    /**
     * 获取累计用户人数
    * @return 累计用户人数
     */
    private Long getTotalUserCount() {
        Pipeline pipeline = jedis.pipelined();
        pipeline.bitcount(TOTAL_KEY);
        List<Object> totalKeyCountList = pipeline.syncAndReturnAll();
        return (Long) totalKeyCountList.get(0);
    }
 
    /**
     * 获取指定天数内的日均活跃人数
    * @param dayNum 指定天数
    * @return 日均活跃人数
     */
    private Long getActiveUserCount(int dayNum) {
        if (dayNum < 1) {
            return (long) 0;
        }
        List<String> pastDaysKey = new ArrayList<>();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < dayNum; i++) {
            //保存距今dayNum天数的key的集合
            sb.append(ACTIVE_KEY).append(sdf.format(DateUtils.addDays(new Date(), -i)));
            pastDaysKey.add(sb.toString());
            sb.delete(0, sb.length());
        }
        if (pastDaysKey.isEmpty()) {
            return (long) 0;
        }
        String lastDaysKey = "last" + dayNum + "DaysActive";
        Pipeline pipeline = jedis.pipelined();
        pipeline.bitop(BitOP.AND, lastDaysKey, pastDaysKey.toArray(new String[pastDaysKey.size()]));
        pipeline.bitcount(lastDaysKey);
        //设置过期时间为5分钟
        pipeline.expire(lastDaysKey, 300);
        List<Object> activeKeyCountList = pipeline.syncAndReturnAll();
        return (Long) activeKeyCountList.get(1);
    }
 
    public static void main(String[] args) {
        Counter c = new Counter();
        //这里假设当前日期为2019年5月31日,测试的时候需要更改为当前日期的前几天
        for (int i = 0; i < 15; i++) {
            c.updateUser(i, "20190531");
        }        
        for (int i = 6; i < 15; i++) {
            c.updateUser(i, "20190530");
        }        
        System.out.println("累计用户数:" + c.getTotalUserCount());
        System.out.println("两天内的活跃人数:" + c.getActiveUserCount(2));
    }
}

 

6. BitMap使用注意事项

  setbit key offset 1 设置某个offset的位为0或者1时,offset之前的所有byte[]的内存都要被占用,也就是说比如offset=100000,那么对于redis来说他至少需要申请100000/8=12500长度的byte[]数组才行,相当于只有byte[12500]这个字节真正使用到了,前面的byte[0-12499]都没有真正用到,这些内存就白白浪费掉了,所以使用redis的bitmap一定要注意尽量从小整数的序号开始往上加,否则bitmap结构带来的不是redis内存的节省,而是redis内存的爆炸溢出.


  所以 bitmap 这个数据结构使用要非常慎重才行!!!

  

 

标签: redis

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
6月前
|
存储 NoSQL Serverless
位运算的魅力:使用Redis Bitmap高效处理百万级布尔值
位运算的魅力:使用Redis Bitmap高效处理百万级布尔值
330 0
|
6月前
|
存储 NoSQL BI
Redis 实战篇:巧用 Bitmap 实现亿级海量数据统计
Redis 实战篇:巧用 Bitmap 实现亿级海量数据统计
177 0
|
17天前
|
存储 NoSQL PHP
如何用Redis高效实现点赞功能?用Set?还是Bitmap?
在众多软件应用中,点赞功能几乎成为标配。本文从实际需求出发,探讨如何利用 Redis 的 `Set` 和 `Bitmap` 数据结构设计高效点赞系统,分析其优缺点,并提供 PHP 实现示例。通过对比两种方案,帮助开发者选择最适合的存储方式。
28 3
|
1月前
|
消息中间件 分布式计算 NoSQL
大数据-41 Redis 类型集合(2) bitmap位操作 geohash空间计算 stream持久化消息队列 Z阶曲线 Base32编码
大数据-41 Redis 类型集合(2) bitmap位操作 geohash空间计算 stream持久化消息队列 Z阶曲线 Base32编码
27 2
|
4月前
|
存储 NoSQL Java
Java中使用redis的bitMap实现签到功能
这个实现示例提供了一种灵活、高效的方式,展示了如何使用Redis来解决现实中的问题。
300 2
|
3月前
|
NoSQL Java Redis
Redis字符串数据类型之INCR命令,通常用于统计网站访问量,文章访问量,实现分布式锁
这篇文章详细解释了Redis的INCR命令,它用于将键的值增加1,通常用于统计网站访问量、文章访问量,以及实现分布式锁,同时提供了Java代码示例和分布式锁的实现思路。
121 0
|
4月前
|
存储 NoSQL 数据管理
如何借助Redis巧妙的管理用户签到?——Bitmap篇
Redis位操作用于高效存储分析,如用户签到。通过位操作,每个用户签到只需1位,节省空间。使用`setbit`设置签到状态,`getbit`查询,`bitcount`统计签到天数。适用于用户特征标记、系统功能开关和在线状态追踪。高效率、低空间占用,适合大数据场景。
78 0
|
5月前
|
存储 NoSQL Redis
蓝易云 - Redis之bitmap类型解读
需要注意的是,虽然bitmap可以高效地存储和计算大量的位,但是它也有一些局限性,例如,它不能直接获取或设置某一范围内的所有位,也不能直接获取或设置多个不连续的位。
25 2
|
6月前
|
NoSQL 算法 Java
Redis入门到通关之BitMap实现签到
Redis入门到通关之BitMap实现签到
97 2
|
6月前
|
存储 监控 NoSQL
使用Redis的Bitmap统计一周连续登录的用户
使用Redis的Bitmap统计一周连续登录的用户
211 1
下一篇
无影云桌面