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