需求分析

用户活跃度榜单是一个能够鼓励用户参与社区活动的有效手段。

用户活跃度计算方式:

  1. 用户每访问一个新的页面+1分
  2. 对于一篇文章,点赞、收藏+2分;取消点赞、取消收藏,将之前的活跃分收回
  3. 文章评论+3分,删除评论,将之前的活跃分收回
  4. 关注用户 +2分,取消关注,将之前的活跃分收回
  5. 发布—篇审核通过的文章+10分,删除文章,将之前的活跃分收回

榜单(分为月榜和日榜):

  • 展示活跃度最高的前三十名用户

方案设计

首先是存储单元,针对一个排行榜,思考排行榜上面每一位需要存储哪些信息:

1
2
3
4
5
6
//用来表明具体的用户
long userId;
//用户在排行榜上的排名
long rank;
//用户的积分
long score;

我们可以使用Redis中的 zset 数据结构来实现,zset维护了一个带权重的有序集合:

  • set:集合确保里面的每个元素只出现一次
  • 权重:就是我们的score
  • zset:根据score进行排序的集合

从zset的特性来看,我们每个用户的积分,丢到zset中,就是一个带权重的元素,而且是已经排好序的了,只需要获取元素对应的index,就是我们预期的排名,非常方便。

如何实现

业务流程

先来梳理一下业务流程,注意我们要考虑幂等性,防止重复加活跃度:

  1. 根据业务实体,计算需要增加/减少的活跃度
  2. 对于增加活跃度时:
    1. 做一个幂等,防止重复添加,因此需要判断下之前有没有重复添加过相关活跃度
    2. 若幂等了,说明之前增加过活跃度了,则直接返回;否则,执行更新,并做好幂等保存
  3. 对于减少活跃度时:
    1. 判断之前有没有加过活跃度,防止扣减为负数
      1. 之前没有加过,则不可以减少,原样不动
      2. 之前加过,则执行扣减,并移除幂等判定

幂等策略

详细的幂等策略可以查看这篇文章如何实现API接口的幂等性 | Raining的小站

我们在这里实现幂等的方式是这样的,直接将用户的每个加分项直接记录在Redis中,在每次加分的时候,查看是否存在记录,从而实现幂等判断。

数据结构

用户的加分项(即记录)如下所示:

1
2
3
//HASH
key由用户和每一天标定:activity_rank_820240408
{field : value} field是用户加分项记录,value是这次加分项记录对应的得分

image-20240409170652578

image-20240409170617464

排行榜数据结构如下:

1
2
3
4
// zset
key: activity_rank_{user_id}_{年月日}
field: 活跃度更新key
value: 添加的活跃度

这里的 key 就对应着每一天或者每一个月的排行榜,field 是用户 id,value 是活跃度:

1
2
activity_rank_202404 {8:50} 表示2024年4月,userId=8的用户活跃度是50
activity_rank_20240408 {8:5} 表示2024年4月8日,userId=8的用户活跃度是5

实现逻辑

  1. 计算活跃度score(正为加活跃度,负为减活跃度)和加分项记录field
  2. 进行幂等判断,判断之前是否更新过相关的活跃度(时间范围为当天内)
    1. 之前没有记录过
      1. 如果是加分操作
        1. 增加加分项记录
        2. 更新当天和当月的活跃度排行榜,如果是首次初始化榜单,则设置过期时间(日活跃榜单,保存31天;月活跃榜单,保存1年)、
      2. 如果是减分操作,说明是之前点过赞,今天取消点赞,这样我们不做处理
    2. 之前记录过
      1. 如果是加分操作
        1. 不做处理,因为之前记录过说明已经加过分了,不重复加分。
      2. 如果是减分操作
        1. 删除加分项记录
        2. 更新当天和当月的活跃度排行榜

这里要注意一个细节:

昨天收藏,今天取消收藏,那么按照实际的业务场景,我们应该是要扣减昨天的活跃度(如果扣减今天的活跃度,那就可能成负数了)

但是问题在于,在实际的业务场景中,跨天之后,历史的活跃度─般是固化的,如果你现在去扣减之前的活跃度,给用户的感觉就是我昨天的活跃度100,今天操作—下,咋的昨天活跃度还降低了呢?

所以这种情况我们就不处理了。