- 如果要统计一篇文章的阅读量,可以直接使用 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基本操作
asyncfunctionbaseBitFun(){
//hello=>二进制【0110100001100101011011000110110001101111】
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映射被限制在512MB之内)。
//!注意,这里的star和end不是指bit的下标,而是字节(byte)的下标。比如start为1,则实际对应的bit下标为8(1byte=8bit)
client.setbit(bitKey,0,'1');
client.bitcount(bitKey,redis.print);
awaitsleep(0.2);
console.log('----获取位置----');
//4.获取第一次出现0或1的位置,获取某个偏移量之后第一次出现0或1的位置
client.bitpos(bitKey,0,redis.print)
client.bitpos(bitKey,1,redis.print)
//1=>82=>16==[8,16]
client.bitpos(bitKey,1,2,redis.print)
awaitsleep(0.2);
console.log('----BITFIELD----');
//设置value=hello
client.setbit(bitKey,0,'0');
client.get(bitKey,redis.print);
//geti40从0开始取4位即0110,有符号/无符号转十进制为6,1*2^2 1*2^1=6,结果一致
constget1Arg=['get','i4',0];
//!geti44从4开始取4位即1000,有符号转十进制为-8,1000=>
constget2Arg=['get','i4',4];
//5.bitfield
client.bitfield(bitKey,get1Arg,redis.print);
client.bitfield(bitKey,get2Arg,redis.print);
//6.incrby
//2位开始连续4位无符号自增
constincrby1Arg=['incrby','u4',2,1];
client.bitfield(bitKey,incrby1Arg,redis.print);
/**
*它用来对指定范围的位进行自增操作。既然提到自增,就有可能出现溢出。如果增加了正数,会出现上溢,如果增加的是负数,就会出现下溢出。
*【Redis默认的处理是折返】。如果出现了溢出,就将溢出的符号位丢掉。如果是8位无符号数255,加1后就会溢出,会全部变零。如果是8位有符号数127,加1后就会溢出变成-128。
*/
awaitsleep(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>setwhello
OK
127.0.0.1:6379>bitfieldwgetu40#从w的第0个位开始取4个位(0110),结果为无符号数(u)
1)(integer)6
127.0.0.1:6379>bitfieldwgetu32#从w的第2个位开始取3个位(101),结果为无符号数(u)
1)(integer)5
127.0.0.1:6379>bitfieldwgeti40#从w的第0个位开始取4个位(0110),结果为有符号数(i)
1)(integer)6
127.0.0.1:6379>bitfieldwgeti32#从w的第2个位开始取3个位(101),结果为有符号数(i)
1)(integer)-3
127.0.0.1:6379>bitfieldwsetu8897#从第9个位开始,将接下来8个位用无符号数97(字母a)替换
1)(integer)101
127.0.0.1:6379>getw
BITOP operation destkey key [key …]
基本操作其实还是用终端比较好,直接贴命令
127.0.0.1:6379>setaa#二进制01100001
OK
127.0.0.1:6379>setcc#二进制01100011
OK
127.0.0.1:6379>bitopanddestkeyac#与操作01100001->a
(integer)1
127.0.0.1:6379>getdestkey"a"
127.0.0.1:6379>setaa#二进制01100001
OK127.0.0.1:6379>setbb#二进制01100010
OK127.0.0.1:6379>bitopordestkeyab#或操作01100011->c
(integer)1
127.0.0.1:6379>getdestkey
"c"
127.0.0.1:6379>setaa#二进制01100001
OK
127.0.0.1:6379>setzZ#二进制01011010(大写的Z)
OK
127.0.0.1:6379>bitopxordestkeyaz#异或00111011->;分号
(integer)1
127.0.0.1:6379>getdestkey
";"
实战前奏
如果对位运算不熟悉的同学,可以先复习一下。
链接放这里了:
理解有符号数和无符号数
位运算世界畅游指南
- 打卡
- 判断某天是否打卡
- 统计某月打卡总次数
- 获取某用户在某月的打卡信息
- 连续打卡的起止时间
- 最长连续天数
- 统计指定区间的打卡次数
(asyncfunction(){
constbitmap=newBitMap();
//初始化数据
awaitbitmap.initData(['2020-10']);
//展示10月份全部签到数据
awaitbitmap.getAllData(['2020-10']);
const[uid1,uid2]=[1,2]
//用户X签到
awaitbitmap.userSign(uid2,'2020-11-18');
//用户X在XX日期是否签到
awaitbitmap.judgeUserSign(uid2,'2020-11-18');
//用户X在XX月总的签到次数
awaitbitmap.getUserSignCount(uid2,'2020-10');
//用户X在XX月第一次签到的日期
awaitbitmap.getFirstSignDate(uid2,'2020-11');
//用户XX在XX月签到的情况
awaitbitmap.getSignInfo(uid2,'2020-10');
//某个区间内,连续签到的人数总和
awaitbitmap.signAllWeek();
awaitcommon.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-nodesign.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
具体实现
实现部分涉及:
- 位操作
- 多种实现方法
/**
*获取某些月份总的签到数据
*@paramtotalMonth如:['2020-10']
*/
publicasyncgetAllData(totalMonth:string[]){
for(letuid=1;uid<=this.allUser;uid ){
consttotal=[];
//获取上月份的起止时间
for(constmonthoftotalMonth){
//month对应的天数
const{days}=UtilDate.daysInMonth(month);
//allUser用户ID作为key中的标示
//【偏移量 1】就是某月对应的几号
letoffset=0;
while(offset<days){
constbit=awaitthis.client.getbit(this.genKey({date:month,uid}),offset);
total.push(bit);
offset ;
}
constresult=`用户${uid}在${month}月签到数据:${total}`;
console.log(result);
}
}
}
/**
*用户在某天签到
*@paramuid用户ID
*@paramdateYYYY-MM—DD
*/
publicasyncuserSign(uid:number,date:string){
constoffset=UtilDate.dayOfNumInMonth(date);
conststatus=SIGN.YES;
awaitthis.client.setbit(this.genKey({date,uid}),offset-1,status);
console.log(`用户${uid}在${date}签到为${status}`);
}
/**
*判断用户在某天是否签到
*@paramuid用户ID
*@paramdateYYYY-MM—DD
*/
publicasyncjudgeUserSign(uid:number,date:string){
constoffset=UtilDate.dayOfNumInMonth(date);
conststatus=awaitthis.client.getbit(this.genKey({date,uid}),offset-1);
awaitthis.getAllData(['2020-11']);
console.log(`用户${uid}在${date}签到状态为${status}`);
}
/**
*用户X在XX月总的签到次数
*@paramuid用户ID
*@paramdateYYYY-MM—DD
*/
publicasyncgetUserSignCount(uid:number,date:string){
constcount=awaitthis.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.
*@paramuid
*@paramdate
*/
publicasyncgetSignInfo(uid:number,date:string){
const{days,dayList}=UtilDate.daysInMonth(date);
constkey=this.genKey({date,uid});
//days该月总天数
constbitValue=awaitthis.genBitIntervalValue({key,start:0,length:days});
if(bitValue===-1){
console.log('相关信息不存在')
return
}
letsignCount=0;
constsignInfo=[];
letsignValue=bitValue;
//从后往前算
for(letindex=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});
}
//统计连续的天数、连续的日期
constresult=[];
for(constitemofsignInfo){
const{signCount,index}=item;
constdays=[];
leti=1;
let_index=index 1;
while(i<=signCount){
days.push(_index );
i ;
}
constarg={
signCount,
days,
}
result.push(arg);
}
//排序函数逆序排列
constcompare=(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/