背景

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

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

界面是这样的:

bit map原理理解(我做了个签到功能)(1)

签到

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

我的设计

首先需要一个签到表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代码:

bit map原理理解(我做了个签到功能)(2)

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

bit map原理理解(我做了个签到功能)(3)

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

bit map原理理解(我做了个签到功能)(4)

addSignCountsById()方法代码:

bit map原理理解(我做了个签到功能)(5)

累加签到次数。

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

bit map原理理解(我做了个签到功能)(6)

sign表数据如下:

bit map原理理解(我做了个签到功能)(7)

continue_sign表数据如下:

bit map原理理解(我做了个签到功能)(8)

这样我们用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位的情况

bit map原理理解(我做了个签到功能)(9)

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。

bit map原理理解(我做了个签到功能)(10)

转换成二进制:

bit map原理理解(我做了个签到功能)(11)

11100000011111111

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

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

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

你们觉得哪种比较好呢?

,