counter

计数抽象服务

点赞、关注、评论等社区服务的基础抽象服务

在老东家已开始主要负责的是社区中台的统一解决方案
顾名思义中台社区服务体系包括:

  • 评论服务
  • 点赞服务
  • 关注服务
  • 打赏服务
  • Feed流的一些收件箱、发件箱等服务

这些服务有很多共同的地方就是需要一个记录 ==某个实体A与某个实体B的某个关系(C)==

背景 Background

为业务方支持诸如点赞、点踩、投票等业务赋能的服务

抽象 Abstraction

抽象为状态计数服务,对外关联业务方的实体信息,对内提供:

  • 创建
  • 查询
  • 更新
  • 删除
  • 列举

对外提供:业务实体在用户维度的对应状态/计数的原子功能。

注:

  1. bizType(业务类型)的理解:

    • 可以认为点赞是一种bizType,点赞业务可以存在3种状态,nothing,like,dislike
    • 评论也是一种bizType,评论业务可以存在2种状态:nothing,commented
    • 投票也是一种bizType,问卷业务可以存在多个状态(如果是4个选项的投票,则有5个状态,nothing,A,B,C,D,其中bizId代表某项投票,subjectId代表某个投票用户。
  2. state(状态)的理解:

    • 业务实体(bizId)与状态主体(subjectId)构成一种关系,这种关系可以用state来表示。
    • state值及其含义由业务方定义,但是,为方便扩展,对非互斥类型的状态,建议业务方使用bit位表示状态,如0b01表示点赞,0b10表示踩(那么0x11表示又赞又踩哈哈)。
      目前本服务的state为16bit存储,即支持16种非互斥状态(可以换算为对多选投票最多支持16个选项)。

详细设计 Details

架构图 Architecture Diagram

待补充

数据库设计 Database Design

容量预估

单个业务内部的点赞、评论的行为总量在6亿以内(500万 * 128)。

前期采用MySQL做持久化数据存储。

分库分表策略

按照app_id进行分库,按照biz_id模128分表(即每个业务方的数据散落在128张表中)。

每个库采用主从结构,做读写分离。

建表语句(索引设计)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CREATE TABLE `status_xxx`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`app_id` smallint NOT NULL DEFAULT 0 COMMENT '应用ID',
`biz_type` smallint NOT NULL DEFAULT 0 COMMENT '业务实体类型枚举',
`biz_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '业务实体ID',
`subject_type` smallint NOT NULL DEFAULT 0 COMMENT '计数主体类型枚举,默认0表示用户',
`subject_id` bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT '计数主体ID',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT '软删除',
`state` smallint NOT NULL DEFAULT 0 COMMENT '业务方定义的状态枚举',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_idx_app_id_biz_type_biz_id_subject_type_subject_id` (`app_id`, `biz_type`, `biz_id`, `subject_type`, `subject_id`) USING BTREE,
KEY `idx_app_id_subject_type_subject_id_biz_type_biz_id_state` (`app_id`, `subject_type`, `subject_id`, `biz_type`, `biz_id`, `is_deleted`,`state`) USING BTREE,
KEY `idx_update_time` (`update_time` desc) USING BTREE
) ENGINE = InnoDB
DEFAULT CHARSET = UTF8MB4
ROW_FORMAT = COMPACT;

SQL语句

  • 查询状态计数

    1
    select `state`,count(*) from status where `app_id`=? and `biz_type`=? and `biz_id`=? and `is_deleted`=0 and `state` in (?,?) group by `state`
  • 批量查询状态计数

    1
    select `biz_id`,`state`,count(*) from `status` where `app_id`=? and `biz_type`=? and `biz_id` in (?,?) and `is_deleted`=0 and `state` in (?,?) group by `biz_id`,`state`
  • 批量查询状态

    1
    select `biz_id`,`state` from `status` where `app_id`=? and `biz_type`=? and `biz_id` in (?,?) and `is_deleted`=0 and `subject_type`=? and `subject_id`=?
  • 查询主体

    1
    select `subject_id`,`state`,`update_time` from `status` where `app_id`=? and `biz_type`=? and `biz_id`=? and `is_deleted`=0 and `subject_type`=? and `state` in (?,?)
  • 查询业务实体

    1
    select `biz_id`,`state`,`update_time` from `status` where `app_id`=? and `subject_type`=? and `subject_id`=? and `is_deleted`=0 and `biz_type`=? and `state` in (?,?)

注:走的索引与众不同。

缓存设计 Cache Design

redis策略

硬过期 + 软过期,软过期小于硬过期时间。

软过期之后的查询,先命中缓存,判断软过期,如果是计数缓存,判断计数大小,如果计数较大,则返回缓存数据,并异步开启一次DB回源。
如果计数较小,则直接删除软过期数据,回源DB并返回DB数据。

注:软过期的实现暂时简单处理——直接删除缓存,回源DB。

redis key设计

1. 主体视角(右视角)的状态值缓存

可作为业务实体列表查询的缓存。

类型 KEY 过期时间
ZSET cnt:sv:r:{appId}:{subjectType}:{subjectId} 60±1分钟,随机上下浮动

ZSET设计:

  • member

    {bizType}:{bizId} (bizType按36进制转成字符串)

  • score

    1
    2
    3
    4
    5
    +--------------------------------- ---+
    | 16bit | 16bit | 32bit | . | 32bit |
    +---------------------------------- --+
    | btype | state | utime | . | ctime |
    +-------------------------------------+

使用姿势:

  • 批量获取是否点赞
    redis pipeline:zscore KEY biz_1,zscore KEY biz_2

  • 获取用户点赞过的评论列表(假设点赞的bizType为0x01,点赞过的状态为0x01,按照点赞时间降序排列)
    zrangebyscore KEY 0x01010000 0x0101FFFF

2. 业务实体视角(左视角)的状态计数缓存

类型 KEY 过期时间
ZSET cnt:sc:{appId}:{bizType}:{bizId%8} 10±1分钟,随机上下浮动

ZSET设计:

  • member

    {bizId}:{state} (state按36进制转成字符串)

  • score

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    +----------------+
    | 32bit | 32bit |
    +----------------+
    | count | expire |
    +----------------+
    ```
    (count作为uint32存入,充分利用空间)
    >单个zset最多可容纳约40亿个元素,即40亿个计数器。
    使用姿势:
    - 批量获取点赞数
    redis pipeline:`zscore KEY biz_1`, `zscore KEY biz_2`
    - 点赞(计数+1)
    `zincrby KEY 0x00010000 biz_1`
    - 取消赞(计数-1)
    `zincrby KEY -0x00010000 biz_1`
    - 获取某个业务方下点赞数最高的topN评论
    该业务方的点赞计数器有多个zset,分别取topN,聚合再排序取topN。
    #### 3. 业务实体视角(左视角)的状态值缓存
    >>与【主体视角(右视角)的状态值缓存】的副本,可作为主体列表查询的缓存。
    |类型|KEY|过期时间|
    |---|---|---|
    |ZSET|`cnt:sv:l:{appId}:{bizType}:{bizId}`|60±1分钟,随机上下浮动|
    ZSET设计:
    - member
    `{subjectType}:{subjectId}` (subjectType和subjectId按36进制转成字符串)
    - score

    +——————————— —+
    | 16bit | 16bit | 32bit | . | 32bit |
    +———————————- –+
    | stype | state | utime | . | ctime |
    +————————————-+
    ```

使用姿势:

  • 批量获取是否点赞
    redis pipeline:zscore KEY1 subject_member,zscore KEY2 subject_member

  • 获取点赞过评论的用户列表(假设点赞的bizType为0x01,点赞过的状态为0x01,按照点赞时间降序排列)
    zrangebyscore KEY 0x01010000 0x0101FFFF

安全 Security

  • 增加限频策略

杂项 Misc

应对大规模场景的优化建议

  1. 针对是否存在某一状态的判断,可以追加bloom filter作加速