我所理解的Redis系列·第c篇·如何设计签到系统(基础篇)

2022/03/10 Reids 共 5492 字,约 16 分钟

之前面试的时候问过候选人一个问题——“如何设计一个通用的签到系统”。说实话彼时只是看过一些网上的签到业务相关的博客,觉得这个功能跟在公司做的会员营销业务域关联性挺大的,加上之前也没接触过类似的业务,同时日常生活中用到的一些APP都有类似的功能,所以挺感兴趣的。

虽然签到业务本身比较简单,但是能够将设计方案完整地表述出来,对于从未接触过此类业务的候选人来说还是有一定挑战的。同时,基于签到业务衍化的「签到有礼」营销活动也可以有很多的具体玩法,能够取得提高用户「存留」率,并促进用户的活跃度,也就是「促活」。

于是在那次面试过后,我就萌生了想要把签到系统实现出来的想法,于是也就有了本文。然而在真正实现之前,我遇到片了一些阻碍,于是先有了下面两篇预备篇:

然后才有了本文——实现签到系统。

redis-c-封面

1. 需求整理

在软件工程的生命周期中,一个功能的落地,往往是从需求开始的。要实现一个通用的签到系统,首先需要知道这个业务需要实现哪些功能点,就拿支付宝用户签到页面来举个例子:

redis-c-1

在页面上我们能看到的功能点有:

  • 展示用户累计签到天数(这里先实现比较简单的累计,连续放到后面再实现)
  • 展示用户本周签到记录
  • 展示用户本月签到记录

2. 存储方案

事实上,实现上述这三个功能点的基础就是要解决一个核心问题:使用何种数据结构存储用户签到记录?

一般来说,我们经常会通过关系型数据库如 MySQL 来存储需要持久化的数据,但是由于签到业务是读多写少的,同时随着数据量的增加,查询效率也随之降低。

在翻看了众多网上的签到业务相关的文章后,大部分还是采用 Redis 位图来做签到数据的存储,基于 Redis 本身的 AOF+RDB 持久化能力,就算 Redis 宕机,最多也只有1秒的数据会丢失,并不影响签到这个读多写少的场景。最后,还有基于 Redis 位图占用数据空间少的原因,最终还是决定跟随大流,采用 Redis 位图存储用户的签到记录。

Ps:Redis 位图相关特性可以翻阅之前写的 我所理解的Redis系列·第2篇·位图(Bitmap)详解

3. 位图键值设计

虽然现在已经决定使用 Redis 位图存储用户签到记录,但是具体怎么存还是个问题。为了节省 Redis 存储 key 的数量,考虑到粒度问题,我决定将位图的 key/value 设计定成如下的方案:

  • 位图的 key 为sign:{userId}:{year}

  • 位图的 value 就是用户当年的签到记录,0代表未签到,1代表已签到

每条 Redis 位图记录存储了一个用户一年的签到数据,需求的实现方案如下:

  • 展示用户累计签到天数:通过 BITCOUNT 命令将所有年份的已签到记录相加
  • 展示用户本周签到记录:通过 GETBIT 命令+偏移量取出本周的签到记录,需要解决如何求得本周在一年汇中偏移量
  • 展示用户本月签到记录:与展示用户本周签到记录一样,通过 GETBIT 命令+偏移量取出本月的签到记录,需要解决如何求得本月在一年汇中偏移量

4. 实现签到

基于上两篇中关于 Redis 位图的工具类 BitmapUtil 和 ByteUtil,接下来我们进入到实际编码实现阶段。关于用户登录状态的校验不在本文讨论范畴之内,故此在代码中不体现。

4.1 签到接口

首先定义的是用户签到接口,基于 SpringBoot 中的 @RestController 和 @PostMapping 注解实现的 Restful 接口,这里将 userId 作为签到的入参是为了本案例的调用方便。

redis-c-2

然后是 Service 服务层,这里面包含了计算签到当天与1月1日的偏移量和校验是否重复签到的逻辑,当然还包括与 Redis 的签到交互逻辑,这里就运用到了 BitmapUtil 位图工具类。

首先是计算签到当天与1月1日的偏移量,新增了另一个工具类 DateUtil,这里用到了 JDK1.8 中提供的 LocalDateTime 类,它自身就带有 getDayOfYear 方法,返回了指定日期是今年的第几天,其返回值是一个正整数,范围是从1到366,而在位图中是从第0位开始计算的,所以在获取日期偏移量后进行签到时需要做-1操作才能在正确的日期位置成功签到。

redis-c-3

然后就是核心 Service 方法,其中包含了构建 Redis 位图键的方法,以及调用 DateUtil 获取日期偏移量,然后操作位图进行签到,最后是重复签到的校验逻辑。

redis-c-4

这样,一个简单的签到接口就实现完成了。

最后通过 Postman 来测试该接口,结果如下:

Redis Bitmap key of userId 1 is sign:1:2022.
Today is 2022/3/10,  No.69 of this year.
Bitmap SETBIT operation successfully. Result following: key is {sign:1:2022}, offset is {68}, value is {true}. Former value is {false}

然后再次用重复的入参调用该接口,测试重复签到,结果如下:

Redis Bitmap key of userId 1 is sign:1:2022.
Today is 2022/3/10,  No.69 of this year.
Bitmap SETBIT operation successfully. Result following: key is {sign:1:2022}, offset is {68}, value is {true}. Former value is {true}
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: 签到失败:今天已经签到过了!] with root cause

java.lang.RuntimeException: 签到失败:今天已经签到过了!
 at io.walkers.planes.pandora.redis.sign.service.SignService.sign(SignService.java:26) ~[classes/:na]

由此可见,签到接口已经可以使用,同时重复签到也能够通过业务性异常让用户知道。

接下来我们再来实现其他功能。

4.2 获取累计签到天数

这个比较简单,直接调用 BitmapUtil#bitCountTrue 方法即可得到结果。

redis-c-5

然后通过 Postman 来测试该接口,结果如下:

Redis Bitmap key of userId 1 is sign:1:2022.
Bitmap BITCOUNT operation successfully. Result following: key is {sign:1:2022}, result is {1}.
User which id is 1 has 1 days signed.

返回结果是1,没有问题。

4.3 获取本周签到记录

本周的签到记录我们有两种获取方案:

  1. 先求出本周一的日期偏移量,然后进行七次 GETBIT 操作获取本周七天的签到记录,最终组装成0-1字符串返回
  2. 先求出本周一的日期偏移量,然后将用户今年的所有签到记录获取出来,并转化为0-1字符串,最后进行字符串截取,获得本周七天的0-1字符串签到记录

虽然 Redis 操作很快,但是频繁且不必要的 Redis 会带来一定影响,在 我所理解的Redis系列·第b篇·Redis位图如何以二进制字符串的形式展示 这篇文章中我们也进行过对比,第二种方案的效率明显要高。所以在这里我们直接选用第二种方案进行实现。

redis-c-6

通过 Postman 来测试该接口,结果如下:

Redis Bitmap key of userId 1 is sign:1:2022.
Bitmap GET operation successfully. Result following: key is {sign:1:2022}, value is {000000000000000000000000000000000000000000000000000000000000000000001000}.
Today is 2022/3/10,  No.69 of this year.
WeekFirstDay is 2022/3/6, No.65 of this year.
User which id is 1, week sign record is 0001000.

最终返回的本周签到记录结果是:0010000,符合预期。

4.4 获取本月签到记录

这与获取本周签到记录是同样的实现方案,只不过偏移量改成了本月1号,实现代码如下:

redis-c-7

获取本月签到记录逻辑中需要注意的是,获取的签到记录二进制长度可能不足本月的31天,不足的天数需要补全0,否则就会出现在调用 String#substring 时超出 String 长度的报错。

通过 Postman 来测试该接口,结果如下:

Redis Bitmap key of userId 1 is sign:1:2022.
Bitmap GET operation successfully. Result following: key is {sign:1:2022}, value is {000000000000000000000000000000000000000000000000000000000000000000001000}.
Today is 2022/3/10,  No.69 of this year.
MonthFirstDay is 2022/3/20, is No.59 of this year.
User which id is 1, month sign record is 0000000001000000000000000000000.

最终返回的本周签到记录结果是:0000000001000000000000000000000,符合预期。

4.5 获取连续签到天数

获取连续签到天数我们可以首先通过获取所有签到记录,然后从前一天开始向前遍历,直到遇到第一个为0的值,遍历的长度就是连续签到天数。这里需要额外注意的是,当天如果已签到,连续签到天数需要再加1,因为我们是从前一天开始计算连续签到天数的。

下面是代码实现:

redis-c-8

连续签到天数难以直接通过单个接口模拟,所以这里通过单测来测试该接口,测试用例如下:

redis-c-9

首先是在未签到情况下获取连续签到天数,期望结果应该是0;然后是在昨天签到,期望获取的连续签到天数为1

;六天前签到,期望获取的连续签到天数为1;今天签到,期望获取的连续签到天数为2。

运行结果如下:

Redis Bitmap key of userId 1 is sign:1:2022.
Bitmap DELETE operation successfully. Result following: key is {sign:1:2022}.
第一次获取连续签到天数
Redis Bitmap key of userId 1 is sign:1:2022.
Today is 2022/3/10,  No.69 of this year.
User which id is 1, continuous Sign days is 0.
Today is 2022/3/10,  No.69 of this year.
昨天签到
Bitmap SETBIT operation successfully. Result following: key is {sign:1:2022}, offset is {67}, value is {true}. Former value is {false}
第二次获取连续签到天数
Redis Bitmap key of userId 1 is sign:1:2022.
Today is 2022/3/10,  No.69 of this year.
User which id is 1, continuous Sign days is 1.
六天前签到
Bitmap SETBIT operation successfully. Result following: key is {sign:1:2022}, offset is {62}, value is {true}. Former value is {false}
第三次获取连续签到天数
Redis Bitmap key of userId 1 is sign:1:2022.
Today is 2022/3/10,  No.69 of this year.
User which id is 1, continuous Sign days is 1.
今天签到
Bitmap SETBIT operation successfully. Result following: key is {sign:1:2022}, offset is {68}, value is {true}. Former value is {false}
第四次获取连续签到天数
Redis Bitmap key of userId 1 is sign:1:2022.
Today is 2022/3/10,  No.69 of this year.
User which id is 1, continuous Sign days is 2.

符合期望。

至此,一个基于 Redis 位图实现的简单签到系统已经初具规模,包含了存储签到记录,实现签到、获取累计签到天数、获取本周签到记录、获取本月签到记录、获取连续签到天数功能。

后面还有一篇拓展篇,我们会基于当前的系统进行优化,包括数据存储、获取数据效率、数据同步、其他玩法等,敬请期待。

5. 参考资料

最后,本文收录于个人语雀知识库: 我所理解的后端技术,欢迎来访。

文档信息

Search

    Table of Contents