当前位置:首页 > 问答 > 正文

用Redis做签到那些事儿,踩过的坑和没想到的问题分享

主要是根据我在实际项目中用Redis做签到功能时的一些经历来写的,参考了CSDN博客上一些开发者的经验分享,也结合了我们团队自己踩的坑。

一开始我们觉得签到功能很简单,不就是用户点一下按钮,然后记录他今天签过了嘛,用Redis的位图(BitMap)结构听起来是完美方案,因为一个用户一年的签到记录,用位图存储,365天也就365个bit,非常节省空间,key可以设计成 uid:sign:userId:yyyyMM, value就是那个位图,签到了就把对应日期的位设为1,查询用户本月签到情况,或者计算连续签到天数,用Redis的位操作命令都能搞定。

听起来很美好对吧?但我们真正做的时候,发现想的太简单了。

第一个没想到的坑:时间问题,尤其是跨时区问题。

我们的用户分布在全球,如果简单地用服务器本地时间(比如北京时间)来判断是否算“签到,那对于欧美用户就乱套了,他们的“可能还是我们的“昨天”或者“明天”,这就导致一个用户可能在我们的系统里显示他明天已经签过到了,或者昨天漏签了,连续签到直接中断。

解决方案后来我们折腾了好久,最后是要求客户端在请求签到时,必须带上用户当地的日期(格式化为yyyy-MM-dd),并且要加密防止篡改,服务端收到后,会验证这个日期是不是合理的(不能是未来的日期,也不能是过于过去的日期),然后Redis的key和位偏移量,都基于这个客户端传来的日期进行计算,这就引出了第二个问题。

第二个坑:如何防止重复签到和作弊。

如果key和偏移量完全信任客户端传过来的日期,那恶意用户完全可以修改日期,天天传2025年、2026年的日期,岂不是能提前把未来几年的到都签了?或者反复签同一天来刷积分。

所以我们做了限制:

  1. 防未来签到:服务端会严格校验客户端传来的日期,绝对不能超过服务端的当前日期(这里要用UTC时间或者一个统一的基准时间来判断,比如不超过UTC时间的当天)。
  2. 防重复签到:在执行签到SETBIT操作前,先用GETBIT检查一下这位是不是已经是1了,如果是1,就直接返回“已签到”,不再给奖励。
  3. 防补签漏洞:我们当时没有做补签卡功能,所以对于过去的日期,我们只允许签一天,也就是紧挨着最后一次签到日期的后一天,比如用户最后一次签到是5号,他6号漏签了,7号他来签到,他只能签7号这一天,如果他试图直接签6号,系统会拒绝,因为这不属于正常的连续签到逻辑,除非有补签卡道具,这个逻辑判断需要在服务端用代码严格实现,Redis本身不管你这个。

第三个没想到的问题:连续签到计算的性能和数据恢复。

用位图计算连续签到天数,经典的算法是从今天开始往前数位图里第一个0的位置,Redis没有直接的命令,通常是用BITFIELD命令一次性取出最近一段时间的位值,然后在应用层写循环判断,如果用户连续签了500天,你总不能取500位的值吧?一般会取一个合理的长度,比如最近60天,如果60天内都没断过,就认为连续签到至少60天。

但这个计算毕竟是在服务端跑的,用户量大了,每天凌晨签到高峰期,这个计算会不会成为瓶颈?我们当时做了压力测试,发现对于单用户这个计算很快,但确实要注意不要一次性取太长的位图。

还有一个更头疼的问题是数据恢复和持久化,Redis毕竟是内存数据库,虽然有AOF和RDF持久化,但我们总担心万一整个Redis实例出问题,数据丢了怎么办?签到数据看起来不关键,但对用户来说是实实在在的资产(积分、成长值等),我们不得不定期(比如每天一次)把用户的签到关键数据(如最后一次签到日期、连续签到天数)异步同步到MySQL里做冷备份,这样即使Redis数据真的没了,我们还能从MySQL恢复个大概,虽然可能损失一点精度,但总比全丢了好,这个备份逻辑又增加了一些系统复杂性。

第四个问题:运营活动带来的灵活性挑战。

后来产品经理说要搞签到活动,连续签到7天额外奖励”、“每月签到满20天奖励”,用位图存储原始数据是没问题的,但怎么高效地统计这些复杂的条件呢?

每月签到满20天”,我们最初的想法是每个月结束时,用BITCOUNT命令统计该用户这个月key的位图中1的个数,但问题是,活动可能月中才开始,或者需要实时显示进度给用户,我们不可能每次用户查看进度都BITCOUNT整个月(虽然也能接受),但更精细化的活动,第二周和第四周的周一签到有双倍积分”,这种基于周的复杂规则,位图本身就不够用了,需要结合额外的集合(Set)或者哈希(Hash)来记录特殊日期的签到,或者在应用层做更复杂的逻辑判断。

用Redis做签到,位图在存储空间和基础查询上优势巨大,但它不是一个“一键搞定”的方案,你必须仔细考虑清楚时区、防作弊、数据持久化备份以及未来业务规则扩展带来的挑战,很多坑都是在具体业务逻辑里,而不是Redis本身。

用Redis做签到那些事儿,踩过的坑和没想到的问题分享