Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:
(1) Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。
(2) Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

首先我们要理解几个命令,特别是最后一个,这些命令可以参考Redis文档。
SETBIT key offset value
设置或者清空key的value(字符串)在offset处的bit值。
SETBIT bit:sign 2 1
GETBIT key offset
返回key对应的string在offset处的bit值,当offset超出了字符串长度的时候,这个字符串就被假定为由0比特填充的连续空间。
GETBIT bit:sign 2
BITCOUNT key [start end]
统计字符串被设置为1的bit数。对一个不存在的 key 进行 BITCOUNT 操作,结果为 0 。
SETBIT bit:sign 2 1 SETBIT bit:sign 5 1 BITCOUNT bit:sign
BITPOS key bit [start] [end]
返回字符串里面第一个被设置为1或者0的bit位。
BITPOS bit:sign 1
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
BITFIELD命令能操作多字节位域,它会执行一系列操作,并返回一个响应数组,在参数列表中每个响应数组匹配相应的操作。
BITFIELD bit:sign get u1 2
从offset-2开始,取一位,结果为无符号数(u),也可以进行多个操作
BITFIELD bit:sign get u1 2 get u1 5
其次是如何在我们的代码中使用,由于目前代码都集成Spring使用RedisTemplate来操作,这里使用RedisTemplate来演示。
代码中的位运算操作,可以百度“Java语言位运算符详解”参考别人文章理解。
/**
* 用户签到功能
*/
private static final String USER_SIGN = "USER_SIGN:%d:%s";
private static String buildSignKey(Long uid, LocalDate date) {
return String.format(USER_SIGN, uid, formatDate(date));
}
@Test
public void testBit() {
String bitKey = buildSignKey(100000L, LocalDate.now());
// 当前签到情况
LocalDate date = LocalDate.now();
redisTemplate.delete(bitKey);
// offset是从0开始的,因此2号签到,offset要标记为1
int todayOffset = date.getDayOfMonth() - 1;
redisTemplate.opsForValue().setBit(bitKey, 3, true); // 4号签到
redisTemplate.opsForValue().setBit(bitKey, 4, true); // 5号签到
redisTemplate.opsForValue().setBit(bitKey, 5, true); // 6号签到
redisTemplate.opsForValue().setBit(bitKey, 6, true); // 7号签到
redisTemplate.opsForValue().setBit(bitKey, todayOffset, true); // 当天签到
// 当月-累计签到
Long count = (Long) redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(bitKey.getBytes()));
System.out.println("当月-累计签到:" + count);
// 当月-首次签到日期
long bitPosition = (Long) redisTemplate.execute((RedisCallback) cbk -> cbk.bitPos(bitKey.getBytes(), true)) + 1;
System.out.println("当月-首签日期:" + bitPosition);
// LinkedHashMap 有序Map
Map<String, Boolean> signMap = new LinkedHashMap<>(date.getDayOfMonth());
// Long 64位
List<Long> list = redisTemplate.opsForValue().bitField(bitKey, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType
.unsigned(date.lengthOfMonth())).valueAt(0));
if (null != list && list.size() > 0) {
// 由低位到高位,为0表示未签,为1表示已签
long v = list.get(0) == null ? 0 : list.get(0);
// 例如这个月一共31天,那么取到的二进制就是31个的0或1的组合
System.out.println("签到二进制位输出:" + Long.toBinaryString(v));
for (int i = date.lengthOfMonth(); i > 0; i--) {
LocalDate d = date.withDayOfMonth(i);
// 按位操作向右位移1,再移动回来,再移回来时是用0补位,如果补位后相等说明这个位本来就是0,所以这里判断是不相等,不相等即为1,说明签到了
signMap.put(formatDate(d, "yyyy-MM-dd"), v >> 1 << 1 != v);
// 是右移后赋值,意思是向右移动一位出去,此时变量v的值将发生变化
v >>= 1;
}
}
System.out.println("当月签到情况:" + JSON.toJSONString(signMap, true));
int signCount = 0;
boolean isBreak = false;
//定义一个死循环,有断签就结束循环
while (true) {
if (isBreak) break; //判断是否可以跳出循环
list = redisTemplate.opsForValue().bitField(bitKey, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType
.unsigned(date.getDayOfMonth())).valueAt(0));
if (list != null && list.size() > 0) {
// 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况
long v = list.get(0) == null ? 0 : list.get(0);
for (int i = 0; i < date.getDayOfMonth(); i++) {
if (v >> 1 << 1 == v) {
if (i > 0) { // 低位为0且非当天说明连续签到中断了
isBreak = true; //断签,准备跳出循环
break;
}
} else {
signCount += 1;
}
v >>= 1;
}
//当月签到情况获取完,日期调整为上一个月继续获取
date = date.minusMonths(1).with(TemporalAdjusters.lastDayOfMonth());
}
}
System.out.println("累计签到:" + signCount); // 这个累计就是从当天开始往前推
// 不同用法,下面更直观。获取 1bit 下标手动指定
BitFieldSubCommands command = BitFieldSubCommands.create()
.get(BitFieldSubCommands.BitFieldType.unsigned(1)).valueAt(3)
.get(BitFieldSubCommands.BitFieldType.unsigned(1)).valueAt(4)
.get(BitFieldSubCommands.BitFieldType.unsigned(1)).valueAt(5)
.get(BitFieldSubCommands.BitFieldType.unsigned(1)).valueAt(6)
.get(BitFieldSubCommands.BitFieldType.unsigned(1)).valueAt(todayOffset);
list = redisTemplate.opsForValue().bitField(bitKey, command);
System.out.println("固定区间取值:" + JSON.toJSONString(list, true));
System.out.println("今天是否签到:" + redisTemplate.opsForValue().getBit(bitKey, todayOffset));
}
private static String formatDate(LocalDate date, String pattern) {
return date.format(DateTimeFormatter.ofPattern(pattern));
}
private static String formatDate(LocalDate date) {
return formatDate(date, "yyyyMM");
}打印输出内容参考
当月-累计签到:5
当月-首签日期:4
签到二进制位输出:1111000000000000001000000
当月签到情况:{
"2023-02-28":false,
"2023-02-27":false,
"2023-02-26":false,
"2023-02-25":false,
"2023-02-24":false,
"2023-02-23":false,
"2023-02-22":true,
"2023-02-21":false,
"2023-02-20":false,
"2023-02-19":false,
"2023-02-18":false,
"2023-02-17":false,
"2023-02-16":false,
"2023-02-15":false,
"2023-02-14":false,
"2023-02-13":false,
"2023-02-12":false,
"2023-02-11":false,
"2023-02-10":false,
"2023-02-09":false,
"2023-02-08":false,
"2023-02-07":true,
"2023-02-06":true,
"2023-02-05":true,
"2023-02-04":true,
"2023-02-03":false,
"2023-02-02":false,
"2023-02-01":false
}
累计签到:1
固定区间取值:[
1,
1,
1,
1,
1
]
今天是否签到:trueEND
Java小强
未曾清贫难成人,不经打击老天真。
自古英雄出炼狱,从来富贵入凡尘。
发表评论: