0%

高可用签到业务实现

签到业务场景:

一般像微博,各种社交软件,游戏等APP,都会有一个签到功能,连续签到多少天,送什么东西,比如:

  • 签到1天送10积分,连续签到2天送20积分,3天送30积分,4天以上均送50积分等
  • 如果连续签到中断,则重置计数,每月初重置计数
  • 显示用户连签天数
  • 用户断签可补签

签到业务存储策略

mysql

设计一个签到记录表记录用户签到信息:

uid连续签到天数签到日期签到类型签到时间
100072021-09-03补签 or 正常签到2021-09-03 00:00:00

上述的签到表结构信息已经可以满足我们的签到业务实现,但是仔细分析,会有一些隐患:

  • 解决不了流量突发问题,比如0点签到上万qps,mysql扛不住

  • 补签和签到并行操作会出现连签计算问题
    -img.png

  • 补签需要维护签到天数的更新,如果用户中间某一天断签了,补签后后续每一天都需要连续更新,接口响应耗时增大

解决方案:

  • 问题一:可以通过对mysql数据表按uid分表解决
  • 问题二:可以通过一个多原子性sql解决
    1
    2
    UPDATE $table SET num = IF (last_checkin_time = 昨天, num + 1, 1)
    WHERE id = ? AND last_checkin_time < 今天
  • 问题三:涉及到批量更新,耗时长只能将补签操作放到异步队列实现

上述列出的三种解决方案看似解决了签到问题,但是还存在很多问题

  • 比如问题一,分表虽然分担了并发读写单表的压力,但是补签产生批量更新的问题还是没解决,虽然问题三提出了使用消息队列来解决长任务执行问题,但是异步方案一是对用户体验不好,用户没办法实时看到补签效果,而且qps高的场景下,堆积问题明显,延时问题更严重,造成用户流失

  • 问题二:多原子性的sql操作实现复杂,而且存在性能问题

  • 问题三:同问题一的延时问题

此时,很明显,要满足上百、上千qps的签到业务,mysql业务显然已经满足不了了,要引入新的解决方案

那么是否可以直接套用业界秒杀问题解决方案呢?

先来看下秒杀场景:

  • 首先秒杀场景可以做库存数量限制,超过秒杀数量服务可以屏蔽调秒杀逻辑,但是对于签到场景显然不可以限制用户去签到
  • 一些秒杀场景可以容忍超卖,比如手机多卖出几百台,直接叫生产方生产就好了,对公司没经济影响。但是签到业务显然不可以,用户签到多了会引起其他用户不满,用户签到少了用户自身会不满

所以我们必须选择一种能支撑高qps,并且并发读写性能都好的数据结构,而Redis恰好可以满足这个条件的

Redis

redis可以使用bitmap来解决高并发下的断签、补签问题

Redis-bitmap
比特位图是基于redis基本数据结构string的一种高阶数据类型。Bitmap支持最大位数2^32位。计算了一下,使用512M的内存就可以存储42.9亿的字节信息(2 ^32 -> 4294967296)

img_1.png
签到数据占用的内存也很小,查询统计的性能也不错,很好的解决了常规思路存在的问题。

如何基于bitmap来进行签到业务实现?

已周期为月的签到日历为例:

签到

比如uid为1000的用户在2019-02-15月签到了一次,可以执行

1
SETBIT u:sign:1000:201902 15 1

获取当天是否签到

1
GETBIT u:sign:1000:201902 15

返回1即为当日已签到,返回0未签到

统计当月签到天数

1
BITCOUNT u:sign:1000:201902

连签天数计算

1
2
BITFIELD u:sign:1000:201902 get u28 0
201333251

201333251为当前签到位图的10进制转换,可以转换为二进制观察签到情况

201333251转换成二进制为1100000000000001101000000011

通过肉眼可观察出连续签到天数为最后两天(如果按后面日期推导,根据产品策略定)

我们可以通过程序的位运算来进行统计,以php为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

$signCount = 0;
$v = 201333251;
for ($i = 28; $i > 0; $i--) {
// i 表示位移操作次数
// 右移再左移,如果等于自己说明最低位是 0,表示未签到
if ($v >> 1 << 1 == $v) {
// 低位 0 且非当天说明连续签到中断了
if ($i != 27) {
break;
}
} else {
$signCount++;
}
// 右移一位并重新赋值,相当于把最低位丢弃一位
$v >>= 1;
}

实现原理很简单,就是对redis返回给我们的签到十进制数v进行左移右移,对比是否和原值相同,相同说明未签到,不同则已签到,然后对v进行>>=,每次丢弃最低位达到逐位比较的目的

上面说的都是正常签到的情况,对与补签,我们只有计算出补签日期的偏移量,然后重新执行签到操作就可以了,由于连续签到天数是我们逐位对比计算的,所以不存在连签天数计算出错问题

总结

虽然redis bitmap操作性能好,能满足高并发场景下的签到业务的签到、补签需求,但是由于没有签到业务数据明细,不利于数据分析和数据归档,这时,我们可以把mysql当作log使用,异步记录签到logredis来支撑前端业务展示,
最后,如果我们的异步消息队列存在消息丢失,或者消息重发

  • 对于消息丢失,我们可以做定期同步,比如,签到时,可以使用 set 数据结构来记录当日用户签到的uid集合,如果uid太多,可以将rediskey分布式存储,然后拿到set集合uidbitmap查询签到信息,然后同步到mysql,每日同步一次;

    为了保证set集合操作和签到操作是原子性的,可以通过使用redislua脚本执行到达原子性

  • 对于消息丢失,我们可以通过签到业务主健设计或者添加唯一索引来保证数据记录是幂等