什么是BitMap
所谓 BitMap 就是用一个 bit 位来标记某个元素对应的 value,而 key 即是这个元素。由于采用bit为单位来存储数据,因此在可以大大的节省存储空间。
优点:
- 效率高,不许进行比较和移位
- 占用内存少,比如N=10000000;只需占用内存为N/8 = 1250000Bytes = 1.2M,如果采用int数组存储,则需要38M多
缺点:
为什么选择BitMap
假如我们用下面这样一张表来存储用户的签到信息
1 2 3 4 5 6 7 8 9
| CREATE TABLE `tb_sign` ( `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` bigint(20) UNSIGNED NOT NULL COMMENT '用户id', `year` year NOT NULL COMMENT '签到的年', `month` tinyint(2) NOT NULL COMMENT '签到的月', `date` date NOT NULL COMMENT '签到的日期', `is_backup` tinyint(1) UNSIGNED NULL DEFAULT NULL COMMENT '是否补签', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact
|
每签到一次需要使用(8 + 8 + 1 + 1 + 3 + 1)共22字节的内存,一个月则最多需要600多字节
那么,如果用BitMap呢?
我们按越来统计用户签到信息,签到记为1
,未签到则记为0
把每一个bit位对应当月的每一天,形成映射关系。
每个用户每个月仅消耗4字节不到!
如何实现BitMap
Redis是利用string
类型数据结构实现BitMap,因此最大上限是512M,转换为bit则是2 ^ 32 个bit位。
常用的指令有如下:
SETBIT
:向指定位置(offset)存入一个0或1
GETBIT
:获取指定位置(offset)的bit值
BITCOUNT
:统计BitMap中值为1的bit位的数量
BITFIELD
:操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值
BITFIELD_RO
:获取BitMap中bit数组,并以十进制形式返回
BITOP
:将多个BitMap的结果做位运算(与 、或、异或)
BITPOS
:查找bit数组中指定范围内第一个0或1出现的位置
怎么用BitMap实现签到
实现签到功能时,业务流程如下:
- 获取当前登录用户
- 获取日期
- 拼接key
- 获取今天是本月第几天
- 写入redis SETBIT key offset 1|0
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Override public void sign() { Long userId = UserHolder.getUser().getId(); LocalDateTime curTime = LocalDateTime.now(); String key = RedisConstants.USER_SIGN_KEY + userId + curTime.format(DateTimeFormatter.ofPattern("yyyy:MM")); int offsetDay = curTime.getDayOfMonth() - 1; stringRedisTemplate.opsForValue().setBit(key, offsetDay, true); }
|
怎么统计连续签到天数
先获取这个月的签到数据,然后从前往后统计
实现很简单,直接上代码了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Override public Integer signCount() { Long userId = UserHolder.getUser().getId(); LocalDateTime curTime = LocalDateTime.now(); String key = RedisConstants.USER_SIGN_KEY + userId + curTime.format(DateTimeFormatter.ofPattern("yyyy:MM")); int offsetDay = curTime.getDayOfMonth(); List<Long> result = stringRedisTemplate.opsForValue().bitField( key, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(offsetDay)).valueAt(0) ); if (result == null || result.isEmpty()) return 0; Long num = result.get(0); if (num == null || num == 0) return 0; int cnt = 0; while ((num & 1) == 1) { num >>>= 1; cnt++; } return cnt; }
|
这个功能有个bug,无法统计这个月之前的签到天数
于是,签到功能就写完啦