签到功能相信大家都很熟悉了,功能就是用户每天可以签到一次,连续签到固定天数可以获得奖励。这里我把功能简单化:
“”
每个用户一天只能签到一次;
连续签到7天送优惠券;
界面是这样的:
签到
签到对于引流是一个不错的小工具,下面来看看我是如何做的(这里为了讲解,逻辑变简单了。实际可能要更复杂)。
首先需要一个签到表sign
,用于记录签到时间。
CREATE TABLE `sign` (
`id` varchar(255) CHARACTER SET utf8mb4 NOT NULL COMMENT '主键id',
`user_id` varchar(255) CHARACTER SET utf8mb4 DEFAULT '' COMMENT '用户id',
`sign_date` datetime DEFAULT NULL COMMENT '签到时间',
KEY `idx_id` (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='签到';
还需要一个统计连续签到的表continue_sign
。用于统计连续签到次数。
CREATE TABLE `continue_sign` (
`id` varchar(255) CHARACTER SET utf8mb4 NOT NULL COMMENT '主键id',
`user_id` varchar(255) CHARACTER SET utf8mb4 DEFAULT '' COMMENT '用户id',
`continue_counts` int(11) DEFAULT NULL COMMENT '连续签到次数',
KEY `idx_id` (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='连续签到统计';
我们来看看Java代码:
首先用户签到,插入数据到签到表,同一天一个用户只能签到一次。如果已经签到,用户当天再签到,会有如下提示。
签到表插入数据后,连续签到表进行统计。
addSignCountsById()
方法代码:
累加签到次数。
当连续签到次数大于或等于7时,发放优惠券。
sign
表数据如下:
continue_sign
表数据如下:
这样我们用MySQL完成了签到功能。
架构师看了,说随着时间的发展签到表数据会越来越大,如果用户上万,甚至上千万。查询就比较慢了,这个会影响用户体验的。这个时候就要考虑缓存,还要考虑分库分表。
但是一个小小的签到功能就要做这么多,有没有更简单的方法呢?
“Redis里面有一种数据结构Bitmap可以解决这个问题。
”
Bitmap是一个二进制的数组,长度不限(当长度为20亿时,占用内存200多MB)。数组内的值为0或1。
例如:sign:1:202009
表示id为1的用户2020年9月的签到记录
SETBIT sign:1:202009 0 1 #偏移量0开始,表示9月1号签到一天
SETBIT sign:1:202009 1 1 #表示9月2号签到一天
SETBIT sign:1:202009 2 1 #表示9月3号签到一天
BITCOUNT sign:1:202009 # 统计2月份的签到次数
BITFIELD sign:1:202009 get u30 0 #获取前30位的情况
Java代码实现
签到方法:
/**
* 签到
* @param userId
* @param date
* @return
*/
public boolean doSign(int userId, LocalDate date) {
int offset = date.getDayOfMonth() - 1;
return jedis.setbit(buildSignKey(userId, date),offset,true);
}
连续签到次数:
public long getContinueSignCount(int userId, LocalDate date) {
int signCount = 0;
String type = String.format("u%d", date.getDayOfMonth());
List list = jedis.bitfield(buildSignKey(userId, date), "GET", type, "0");
if (CollectionUtils.isNotEmpty(list)) {
// 取低位连续不为0的个数即为连续签到次数,需考虑当天尚未签到的情况
long value = list.get(0) == null ? 0 : list.get(0);
int bound = date.getDayOfMonth();
//连续签到判定算法
for (int i = 0; i < bound; i++) {
if (value >> 1 << 1 == value) {
// 低位为0且非当天说明连续签到中断了
if (i > 0) {
break;
}
} else {
signCount += 1;
}
value >>= 1;
}
}
return signCount;
}
签到测试
public void testDoSign() {
SignRedisService service = new SignRedisService();
LocalDate today = LocalDate.now();
boolean signed = 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
public void getContinueSign() {
SignRedisService service = new SignRedisService();
LocalDate today = LocalDate.now();
long continueSignCount = service.getContinueSignCount(1, today);
System.out.println("连续签到次数:"+continueSignCount);
}
测试结果
连续签到次数:8
代码List
表示获取指定日期之前的bit数,我们看调试,list
的值为 114943。
转换成二进制:
11100000011111111
表示第17为之前的bit值,第10位到第17位都为1,表示9月10号到17号都签到了。
架构师说,使用Redis的Bitmap,速度很快,在高并发情况下有更优良的性能。而且占用空间很小,Bitmap大约可以存储个bit位(bit数组大约五六亿的长度)。
虽然使用Bitmap看着很高大上,但是我还是觉得使用MySQL展示的信息更全面,也便于查询。如果并发高可以使用MySQL + 缓存。