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 中bitMap使用及实现访问量的更多相关文章

  1. Redis中bitmap的妙用

    BitMap是什么 就是通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身.我们知道8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间. Redis中的 ...

  2. Redis 中 BitMap 的使用场景

    BitMap BitMap 原本的含义是用一个比特位来映射某个元素的状态.由于一个比特位只能表示 0 和 1 两种状态,所以 BitMap 能映射的状态有限,但是使用比特位的优势是能大量的节省内存空间 ...

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

    前言 Reids 在 Web 应用的开发中使用非常广泛,几乎所有的后端技术都会有涉及到 Redis 的使用.Redis 种除了常见的字符串 String.字典 Hash.列表 List.集合 Set. ...

  4. 超大批量删除redis中无用key+配置

    目前线上一个单实例redis中无用的key太多,决定删除一部分. 1.删除指定用户的key,使用redis的pipeline 根据一定条件把需要删除的用户统计出来,放到一个表里面,表为 del_use ...

  5. Redis 中 HyperLogLog 的使用场景

    什么是基数估算 HyperLogLog 是一种基数估算算法.所谓基数估算,就是估算在一批数据中,不重复元素的个数有多少. 从数学上来说,基数估计这个问题的详细描述是:对于一个数据流 {x1,x2,.. ...

  6. 5、分布式缓存Redis之bitmap、setbit

    基本语法: 1)SETBIT redis 127.0.0.1:6379> setbit KEY_NAME OFFSET VALUE //该命令用于对 key 所储存的字符串值,设置或清除指定偏移 ...

  7. Redis中的LRU淘汰策略分析

    Redis作为缓存使用时,一些场景下要考虑内存的空间消耗问题.Redis会删除过期键以释放空间,过期键的删除策略有两种: 惰性删除:每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除 ...

  8. redis的bitmap

    BitMap是什么 就是通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身.我们知道8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间. Redis中的 ...

  9. 动手实现 LRU 算法,以及 Caffeine 和 Redis 中的缓存淘汰策略

    我是风筝,公众号「古时的风筝」. 文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在里面. 那天我在 LeetCode 上刷到一道 LRU 缓存机制的问题, ...

  10. Redis解读(4):Redis中HyperLongLog、布隆过滤器、限流、Geo、及Scan等进阶应用

    Redis中的HyperLogLog 一般我们评估一个网站的访问量,有几个主要的参数: pv,Page View,网页的浏览量 uv,User View,访问的用户 一般来说,pv 或者 uv 的统计 ...

随机推荐

  1. python在使用过程中安装库的方法

    背景: 在学习python的过程中难免会出现python解释器中没有所需要的库,这时我们就要自行的去安装这些库了:当然如果使用的anaconda集成环境的话在安装python一些依赖环境中会简单不少( ...

  2. JDK8提供的常用计量单位

    时间计量单位:Duration @DurationUnit(ChronoUnit.HOURS) private Duration serverTimeout; 空间计量单位:DataSize @Dat ...

  3. 浅谈 Socket.D 与响应式编程

    一.Socket.D 的主要特性 首先,Scoket.D 是高效一个二进制的网络通讯协议(官方我讲法是:基于事件和语义消息流的网络应用协议),能够满足很多场景下使用.其次,Scoket.D 是温和的响 ...

  4. 前端异步编程——async/await

    async 从字面上看就是"异步",它放在函数定义之前,是使该函数在调用时开一个子线程,以不影响主线程的运行. 而 await 经常和 async 组合使用,在 async 定义的 ...

  5. intel 虚拟化 VT-d VT-x VT-c 的区别

    intel 虚拟化 VT-d VT-x VT-c 有什么区别,各是什么意思,有什么作用 简单描述理解 VT-d VT-x VT-c VT-d 英文全程为 Virtualization Technolo ...

  6. 秒懂ajax轮询、long poll 、websocket

    ajax轮询 场景再现: 客户端:啦啦啦,有没有新信息(Request) 服务端:没有(Response) 客户端:啦啦啦,有没有新信息(Request) 服务端:没有..(Response) 客户端 ...

  7. shutdown详解

    linux下shutdown命令详解 shutdown命令安全地将系统关机. 有些用户会使用直接断掉电源的方式来关闭linux,这是十分危险的.因为linux与windows不同,其后台运行着许多进程 ...

  8. 在线录屏-通过Web API接口轻松实现录屏

    在线录屏是指在互联网上进行屏幕录制的过程.它允许用户通过网络连接,将自己的屏幕活动记录下来,并可以在需要时进行播放.共享或存档.在线录屏常用于教育.培训.演示.游戏等场景,可以帮助用户展示操作步骤.解 ...

  9. 规模化敏捷框架何从入手?这篇文章把SAFe讲透了!

    摘要:敏捷软件开发理念已渐渐被业界普遍接受,越来越多的公司和团队不得不面对一个新的问题,就是规模化敏捷的引入和实现.目前市场上规模化框架主要有SAFe,Less,Scrum of Scrums, Sp ...

  10. 跟我学Python图像处理丨5种图像阈值化处理及算法对比

    摘要:本篇文章主要讲解Python调用OpenCV实现图像阈值化处理操作,包括二进制阈值化.反二进制阈值化.截断阈值化.反阈值化为0.阈值化为0. 本文分享自华为云社区<[Python图像处理] ...