bitmap优化 (bitmap算法优化)

背景

签到功能相信大家都很熟悉了,功能就是用户每天可以签到一次,连续签到固定天数可以获得奖励。这里我把功能简单化:

“每个用户一天只能签到一次;连续签到7天送优惠券;”

界面是这样的:

bitmap签到功能,bitmap签到设计

签到

签到对于引流是一个不错的小工具,下面来看看我是如何做的(这里为了讲解,逻辑变简单了。实际可能要更复杂)。

我的设计

首先需要一个签到表sign,用于记录签到时间。

CREATETABLE`sign`(
`id`varchar(255)CHARACTERSETutf8mb4NOTNULLCOMMENT'主键id',
`user_id`varchar(255)CHARACTERSETutf8mb4DEFAULT''COMMENT'用户id',
`sign_date`datetimeDEFAULTNULLCOMMENT'签到时间',
KEY`idx_id`(`id`)USINGBTREE
)ENGINE=InnoDBDEFAULTCHARSET=utf8COMMENT='签到';

还需要一个统计连续签到的表continue_sign。用于统计连续签到次数。

CREATETABLE`continue_sign`(
`id`varchar(255)CHARACTERSETutf8mb4NOTNULLCOMMENT'主键id',
`user_id`varchar(255)CHARACTERSETutf8mb4DEFAULT''COMMENT'用户id',
`continue_counts`int(11)DEFAULTNULLCOMMENT'连续签到次数',
KEY`idx_id`(`id`)USINGBTREE
)ENGINE=InnoDBDEFAULTCHARSET=utf8COMMENT='连续签到统计';

我们来看看Java代码:

bitmap签到功能,bitmap签到设计

首先用户签到,插入数据到签到表,同一天一个用户只能签到一次。如果已经签到,用户当天再签到,会有如下提示。

bitmap签到功能,bitmap签到设计

签到表插入数据后,连续签到表进行统计。

bitmap签到功能,bitmap签到设计

addSignCountsById()方法代码:

bitmap签到功能,bitmap签到设计

累加签到次数。

当连续签到次数大于或等于7时,发放优惠券。

bitmap签到功能,bitmap签到设计

sign表数据如下:

bitmap签到功能,bitmap签到设计

continue_sign表数据如下:

bitmap签到功能,bitmap签到设计

这样我们用MySQL完成了签到功能。

架构师的优化

架构师看了,说随着时间的发展签到表数据会越来越大,如果用户上万,甚至上千万。查询就比较慢了,这个会影响用户体验的。这个时候就要考虑缓存,还要考虑分库分表。

但是一个小小的签到功能就要做这么多,有没有更简单的方法呢?

Redis里面有一种数据结构Bitmap可以解决这个问题。

Bitmap是一个二进制的数组,长度不限(当长度为20亿时,占用内存200多MB)。数组内的值为0或1。

例如:sign:1:202009 表示id为1的用户2020年9月的签到记录

SETBITsign:1:20200901#偏移量0开始,表示9月1号签到一天
SETBITsign:1:20200911#表示9月2号签到一天
SETBITsign:1:20200921#表示9月3号签到一天

BITCOUNTsign:1:202009#统计2月份的签到次数

BITFIELDsign:1:202009getu300#获取前30位的情况

bitmap签到功能,bitmap签到设计

Java代码实现

签到方法:

/**
*签到
*@paramuserId
*@paramdate
*@return
*/
publicbooleandoSign(intuserId,LocalDatedate){
intoffset=date.getDayOfMonth()-1;
returnjedis.setbit(buildSignKey(userId,date),offset,true);
}

连续签到次数:

publiclonggetContinueSignCount(intuserId,LocalDatedate){
intsignCount=0;
Stringtype=String.format("u%d",date.getDayOfMonth());
List<Long>list=jedis.bitfield(buildSignKey(userId,date),"GET",type,"0");
if(CollectionUtils.isNotEmpty(list)){
//取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况
longvalue=list.get(0)==null?0:list.get(0);
intbound=date.getDayOfMonth();
//连续签到判定算法
for(inti=0;i<bound;i++){
if(value>>1<<1==value){
//低位为0且非当天说明连续签到中断了
if(i>0){
break;
}
}else{
signCount+=1;
}
value>>=1;
}
}
returnsignCount;
}

签到测试

publicvoidtestDoSign(){
SignRedisServiceservice=newSignRedisService();
LocalDatetoday=LocalDate.now();
booleansigned=service.doSign(1,today);
if(signed){
System.out.println("您已签到:"+formatDate(today,"yyyy-MM-dd"));
}else{
System.out.println("签到完成:"+formatDate(today,"yyyy-MM-dd"));
}
}

经调试,我们获得的签到记录如下:

签到完成:2020-09-17
签到完成:2020-09-16
签到完成:2020-09-15
签到完成:2020-09-14
签到完成:2020-09-13
签到完成:2020-09-12
签到完成:2020-09-11
签到完成:2020-09-10

连续签到测试

@Test
publicvoidgetContinueSign(){
SignRedisServiceservice=newSignRedisService();
LocalDatetoday=LocalDate.now();
longcontinueSignCount=service.getContinueSignCount(1,today);
System.out.println("连续签到次数:"+continueSignCount);
}

测试结果

连续签到次数:8

代码List<Long> list = jedis.bitfield(buildSignKey(userId, date), "GET", type, "0");表示获取指定日期之前的bit数,我们看调试,list的值为 114943。

bitmap签到功能,bitmap签到设计

转换成二进制:

bitmap签到功能,bitmap签到设计

11100000011111111

表示第17为之前的bit值,第10位到第17位都为1,表示9月10号到17号都签到了。

架构师说,使用Redis的Bitmap,速度很快,在高并发情况下有更优良的性能。而且占用空间很小,Bitmap大约可以存储个bit位(bit数组大约五六亿的长度)。

虽然使用Bitmap看着很高大上,但是我还是觉得使用MySQL展示的信息更全面,也便于查询。如果并发高可以使用MySQL + 缓存。

你们觉得哪种比较好呢?