前言
- 如果要统计一篇文章的阅读量,可以直接使用 Redis 的 incr 指令来完成。
- 如果要求阅读量必须按用户去重,那就可以使用 set 来记录阅读了这篇文章的所有用户 id,获取 set 集合的长度就是去重阅读量。但是如果爆款文章阅读量太大,set 会浪费太多存储空间。
- 这时候我们就要使用 Redis 提供的 HyperLogLog 数据结构 来代替 set,它只会占用最多 12k 的存储空间就可以完成海量的去重统计。但是它牺牲了准确度,它是模糊计数,误差率约为 0.81%。
按照官网的说法,Redis位图Bitmaps不是实际的数据类型,而是在字符串类型上定义的一组 面向位的操作。在Redis中**字符串限制最大为512MB**, 所以位图中最大可以设置**2^32个不同的位(42.9亿个)**。图位的最小单位是比特(bit),每个bit的值只能是0或1。位图适合存bool数据,当某个业务只有两种结果的时候,位图是不二之选 位图的存储大小计算: (maxOffset / 8 / 1024 / 1024)MB。其中maxOffset为位图的最大位数
基本操作
// bit 基本操作
async function baseBitFun() {
// hello => 二进制 【01101000 01100101 01101100 01101100 01101111】
const [bitKey, bitValue] = ['bitKey', 'hello']
client.set(bitKey, bitValue, redis.print);
client.get(bitKey, redis.print);
// 1. 根据偏移量获取bit上的值 0=》0;1-》1
client.getbit(bitKey, 1, redis.print);
// 2. bitcount 获取全部的 1 的总数
client.bitcount(bitKey, redis.print);
// 3. setbit 设置指定偏移量的值,0 || 1
// !offset 参数必须0到 2^32 (bit 映射被限制在 512 MB 之内)。
// !注意,这里的star和end不是指bit的下标,而是字节(byte)的下标。比如start为1,则实际对应的bit下标为8(1byte = 8 bit)
client.setbit(bitKey, 0, '1');
client.bitcount(bitKey, redis.print);
await sleep(0.2);
console.log('----获取位置----');
// 4. 获取第一次出现0或1的位置,获取某个偏移量之后第一次出现0或1的位置
client.bitpos(bitKey, 0, redis.print)
client.bitpos(bitKey, 1, redis.print)
// 1=>8 2=>16 == [8, 16]
client.bitpos(bitKey, 1, 2, redis.print)
await sleep(0.2);
console.log('----BITFIELD----');
// 设置value=hello
client.setbit(bitKey, 0, '0');
client.get(bitKey, redis.print);
// get i4 0 从0开始取4位即0110,有符号/无符号转十进制为6, 1*2^2+1*2^1 = 6, 结果一致
const get1Arg = ['get', 'i4', 0];
// !get i4 4 从4开始取4位即 1000 , 有符号转十进制为 -8, 1000 =>
const get2Arg = ['get', 'i4', 4];
// 5. bitfield
client.bitfield(bitKey, get1Arg, redis.print);
client.bitfield(bitKey, get2Arg, redis.print);
// 6. incrby
// 2位开始连续4位无符号自增
const incrby1Arg = ['incrby', 'u4', 2, 1];
client.bitfield(bitKey, incrby1Arg, redis.print);
/**
* 它用来对指定范围的位进行自增操作。既然提到自增,就有可能出现溢出。如果增加了正数,会出现上溢,如果增加的是负数,就会出现下溢出。
* 【Redis 默认的处理是折返】。如果出现了溢出,就将溢出的符号位丢掉。如果是 8 位无符号数 255,加 1 后就会溢出,会全部变零。如果是 8 位有符号数 127,加 1 后就会溢出变成 -128。
*/
await sleep(0.2);
console.log('----BITOP----');
};
20 58 14 19 13 62 98 50 99 54 18 55 48 91 11 21 42 97 3 30 70 49 1 100 7 64 82 75 68 15 26 69 73 41 51 27 84 56 33 16 6 10 94 32 34 90 40 5 78 37 45 57 88 96 8 67 76 4 2 60 89 83 23 39 46 95 77 47 29 63 52 43 25 79 87 22 36 81 44 65 72 59 31 38 61 80 53 24 0 85 35 92 66 74 93 28 17 9 86 12
返回值
/**
Reply: OK
Reply: hello
Reply: 1
Reply: 21
Reply: 22
----获取位置----
Reply: 3
Reply: 0
Reply: 17
----BITFIELD----
Reply: hello
Reply: 6
Reply: -8
Reply: 11
----BITOP----
*/
BITFIELD
字母数值二进制(高位<-低位)h1040110 1000e1010110 0101l1080110 1100l1080110 1100o1110110 1111
127.0.0.1:6379> set w hello
OK
127.0.0.1:6379> bitfield w get u4 0 #从w的第0个位开始取4个位(0110),结果为无符号数(u)
1) (integer) 6
127.0.0.1:6379> bitfield w get u3 2 #从w的第2个位开始取3个位(101),结果为无符号数(u)
1) (integer) 5
127.0.0.1:6379> bitfield w get i4 0 #从w的第0个位开始取4个位(0110),结果为有符号数(i)
1) (integer) 6
127.0.0.1:6379> bitfield w get i3 2 #从w的第2个位开始取3个位(101),结果为有符号数(i)
1) (integer) -3
127.0.0.1:6379> bitfield w set u8 8 97 #从第9个位开始,将接下来8个位用无符号数97 ( 字母a) 替换
1) (integer) 101
127.0.0.1:6379> get w
BITOP operation destkey key [key …]
基本操作其实还是用终端比较好,直接贴命令
127.0.0.1:6379> set a a # 二进制 01100001
OK
127.0.0.1:6379> set c c # 二进制 01100011
OK
127.0.0.1:6379> bitop and destkey a c # 与操作 01100001 -> a
(integer) 1
127.0.0.1:6379> get destkey "a"
127.0.0.1:6379> set a a # 二进制 01100001
OK 127.0.0.1:6379> set b b # 二进制 01100010
OK 127.0.0.1:6379> bitop or destkey a b # 或操作 01100011 -> c
(integer) 1
127.0.0.1:6379> get destkey
"c"
127.0.0.1:6379> set a a # 二进制 01100001
OK
127.0.0.1:6379> set z Z # 二进制 01011010 (大写的Z)
OK
127.0.0.1:6379> bitop xor destkey a z # 异或 00111011 -> ; 分号
(integer) 1
127.0.0.1:6379> get destkey
";"
实战前奏
如果对位运算不熟悉的同学,可以先复习一下。
链接放这里了:
理解有符号数和无符号数
位运算世界畅游指南
位图实战
目标
- 打卡
- 判断某天是否打卡
- 统计某月打卡总次数
- 获取某用户在某月的打卡信息
- 连续打卡的起止时间
- 最长连续天数
- 统计指定区间的打卡次数
总的调用
(async function () {
const bitmap = new BitMap();
// 初始化数据
await bitmap.initData(['2020-10']);
// 展示10月份全部签到数据
await bitmap.getAllData(['2020-10']);
const [uid1, uid2] = [1, 2]
// 用户X 签到
await bitmap.userSign(uid2, '2020-11-18');
// 用户X在XX日期 是否签到
await bitmap.judgeUserSign(uid2, '2020-11-18');
// 用户X在XX月 总的签到次数
await bitmap.getUserSignCount(uid2, '2020-10');
// 用户X在XX月 第一次签到的日期
await bitmap.getFirstSignDate(uid2, '2020-11');
// 用户XX在XX月 签到的情况
await bitmap.getSignInfo(uid2, '2020-10');
// 某个区间内,连续签到的人数总和
await bitmap.signAllWeek();
await common.sleep(1);
process.exit(1);
})()
66 94 10 31 57 26 76 53 91 27 37 32 49 17 6 44 25 33 13 16 77 90 46 80 85 64 51 42 9 54 47 38 36 14 96 97 71 3 12 63 41 35 55 39 58 99 89 81 45 69 4 84 22 60 65 5 72 29 78 21 23 95 7 59 11 40 68 79 62 34 98 82 18 83 56 67 20 93 24 70 52 28 92 87 1 8 61 75 19 15 2 100 74 88 43 0 50 48 73 30
返回值
➜ 2位图 git:(main) ✗ ts-node sign.ts
用户1在 2020-10月 签到数据: 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
用户2在 2020-10月 签到数据: 0,0,1,1,1,1,0,0,0,0,1,0,1,0,0,0,1,0,1,0,0,0,1,1,0,1,1,0,1,1,1
用户3在 2020-10月 签到数据: 1,0,1,0,0,1,0,0,1,0,0,1,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,1
用户4在 2020-10月 签到数据: 1,0,0,1,0,1,1,0,1,0,0,0,0,1,0,1,0,0,1,1,1,1,1,1,0,1,1,0,1,1,0
用户5在 2020-10月 签到数据: 1,0,1,1,0,1,1,1,0,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,1,1,0,1
用户6在 2020-10月 签到数据: 0,0,0,1,0,0,1,0,0,0,1,1,1,1,0,1,1,0,1,1,0,0,0,1,1,0,0,1,1,1,0
用户2在 2020-11-18签到为1
用户1在 2020-11月 签到数据: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
用户2在 2020-11月 签到数据: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0
用户3在 2020-11月 签到数据: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
用户4在 2020-11月 签到数据: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
用户5在 2020-11月 签到数据: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
用户6在 2020-11月 签到数据: 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
用户2在 2020-11-18签到状态为1
用户2在 2020-10 月份 签到总次数为15
用户2在 2020-11 月份 首次签到 日期为2020-11-18
------用户2在2020-10签到情况-------
当月签到连续情况为:
[ { signCount: 4, days: [ 3, 4, 5, 6 ] },
{ signCount: 3, days: [ 29, 30, 31 ] },
{ signCount: 2, days: [ 26, 27 ] },
{ signCount: 2, days: [ 23, 24 ] },
{ signCount: 1, days: [ 19 ] },
{ signCount: 1, days: [ 17 ] },
{ signCount: 1, days: [ 13 ] },
{ signCount: 1, days: [ 11 ] } ]
最长的连续签到次数:4
最长的连续签到次数日期为:3,4,5,6
----------本月某七天-----------
本月第4到10天 中所有的签到次数: 1
具体实现
实现部分涉及:
- 位操作
- 多种实现方法
/**
* 获取某些月份总的签到数据
* @param totalMonth 如: ['2020-10']
*/
public async getAllData(totalMonth: string[]) {
for (let uid = 1; uid <= this.allUser; uid++) {
const total = [];
// 获取上月份的起止时间
for (const month of totalMonth) {
// month对应的天数
const { days } = UtilDate.daysInMonth(month);
// allUser 用户ID作为key中的标示
// 【偏移量+1】就是某月对应的几号
let offset = 0;
while (offset < days) {
const bit = await this.client.getbit(this.genKey({ date: month, uid }), offset);
total.push(bit);
offset++;
}
const result = `用户${uid}在 ${month}月 签到数据: ${total}`;
console.log(result);
}
}
}
/**
* 用户在某天 签到
* @param uid 用户ID
* @param date YYYY-MM—DD
*/
public async userSign(uid: number, date: string) {
const offset = UtilDate.dayOfNumInMonth(date);
const status = SIGN.YES;
await this.client.setbit(this.genKey({ date, uid }), offset - 1, status);
console.log(`用户${uid}在 ${date}签到为${status}`);
}
/**
* 判断用户在某天是否签到
* @param uid 用户ID
* @param date YYYY-MM—DD
*/
public async judgeUserSign(uid: number, date: string) {
const offset = UtilDate.dayOfNumInMonth(date);
const status = await this.client.getbit(this.genKey({ date, uid }), offset - 1);
await this.getAllData(['2020-11']);
console.log(`用户${uid}在 ${date}签到状态为${status}`);
}
/**
* 用户X在XX月总的签到次数
* @param uid 用户ID
* @param date YYYY-MM—DD
*/
public async getUserSignCount(uid: number, date: string) {
const count = await this.client.bitcount(this.genKey({ date, uid }));
console.log(`用户${uid}在 ${date} 月份 签到总次数为${count}`);
}
35 81 93 47 87 30 64 40 78 97 36 21 18 14 96 90 43 68 89 58 45 84 23 5 22 92 57 32 38 10 69 4 31 3 86 72 41 62 55 8 79 60 75 66 80 15 13 99 73 33 63 37 94 51 49 34 50 85 71 1 59 82 12 27 53 48 65 39 16 100 24 17 42 61 20 83 70 7 74 29 54 44 56 88 98 0 2 9 26 76 91 11 77 28 25 52 6 95 19 67
/**
* 获取当月签到情况
* 1. 当月最长的签到天数
* 2.
* @param uid
* @param date
*/
public async getSignInfo(uid: number, date: string) {
const { days, dayList } = UtilDate.daysInMonth(date);
const key = this.genKey({ date, uid });
// days 该月总天数
const bitValue = await this.genBitIntervalValue({ key, start: 0, length: days });
if (bitValue === -1) {
console.log('相关信息不存在')
return
}
let signCount = 0;
const signInfo = [];
let signValue = bitValue;
// 从后往前算
for (let index = dayList.length; index > 0; index--) {
// 位运算
// 先左后右,如果和原数据相等,则标示最低位是0,即,没有签到
// 从该月最后一天往前算。
if (signValue >> 1 << 1 === signValue) {
if (signCount > 0) {
// 记录连续的长度&位置
signInfo.push({ signCount, index });
// 重置连续次数
signCount = 0;
}
} else {
signCount++;
}
signValue >>= 1;
}
// 记录最后的一次连续【高位】
if (signCount > 0) {
signInfo.push({ signCount, index: 0 });
}
// 统计连续的天数、连续的日期
const result = [];
for (const item of signInfo) {
const { signCount, index } = item;
const days = [];
let i = 1;
let _index = index + 1;
while (i <= signCount) {
days.push(_index++);
i++;
}
const arg = {
signCount,
days,
}
result.push(arg);
}
// 排序函数 逆序排列
const compare = (p: any) => (m: any, n: any) => -(m[p] - n[p]);
result.sort(compare('signCount'));
console.log(`------用户${uid}在${date}签到情况-------`)
console.log("当月签到连续情况为:", '\n', result);
console.log(`最长的连续签到次数:${result[0].signCount}`);
console.log(`最长的连续签到次数日期为:${result[0].days}`);
}
15 52 25 34 1 16 36 48 38 35 12 20 86 13 94 91 74 23 59 17 57 83 90 11 10 28 69 56 93 55 50 26 44 33 54 97 51 80 95 14 30 2 70 63 79 6 60 67 39 78 19 66 73 99 3 84 41 65 92 18 24 61 46 75 47 88 40 64 37 32 49 62 22 68 42 89 21 85 100 87 8 98 7 31 71 29 58 53 9 4 96 81 43 27 5 77 0 45 72 82
具体的实例代码:https://github.com/simuty/Integration/blob/main/Redis/
注意:本文归作者所有,未经作者允许,不得转载