我做了个签到功能,架构师看了后觉得可以优化下

背景

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

  • 每个用户一天只能签到一次;

  • 连续签到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<Long> 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<Long> list = jedis.bitfield(buildSignKey(userId, date), "GET", type, "0");表示获取指定日期之前的bit数,我们看调试,list的值为 114943。

转换成二进制:

11100000011111111

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

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

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

猜你喜欢

转载自blog.csdn.net/wujialv/article/details/108723154