计数抽象服务
在老东家已开始主要负责的是社区中台的统一解决方案
顾名思义中台社区服务体系包括:
- 评论服务
- 点赞服务
- 关注服务
- 打赏服务
- Feed流的一些收件箱、发件箱等服务
这些服务有很多共同的地方就是需要一个记录 ==某个实体A与某个实体B的某个关系(C)==
背景 Background
为业务方支持诸如点赞、点踩、投票等业务赋能的服务
抽象 Abstraction
抽象为状态计数服务,对外关联业务方的实体信息,对内提供:
- 创建
- 查询
- 更新
- 删除
- 列举
对外提供:业务实体在用户维度的对应状态/计数的原子功能。
注:
对
bizType(业务类型)的理解:- 可以认为点赞是一种bizType,点赞业务可以存在3种状态,
nothing,like,dislike - 评论也是一种bizType,评论业务可以存在2种状态:
nothing,commented - 投票也是一种bizType,问卷业务可以存在多个状态(如果是4个选项的投票,则有5个状态,
nothing,A,B,C,D,其中bizId代表某项投票,subjectId代表某个投票用户。
- 可以认为点赞是一种bizType,点赞业务可以存在3种状态,
对
state(状态)的理解:- 业务实体(bizId)与状态主体(subjectId)构成一种关系,这种关系可以用
state来表示。 state值及其含义由业务方定义,但是,为方便扩展,对非互斥类型的状态,建议业务方使用bit位表示状态,如0b01表示点赞,0b10表示踩(那么0x11表示又赞又踩哈哈)。
目前本服务的state为16bit存储,即支持16种非互斥状态(可以换算为对多选投票最多支持16个选项)。
- 业务实体(bizId)与状态主体(subjectId)构成一种关系,这种关系可以用
详细设计 Details
架构图 Architecture Diagram
待补充
数据库设计 Database Design
容量预估
单个业务内部的点赞、评论的行为总量在6亿以内(500万 * 128)。
前期采用MySQL做持久化数据存储。
分库分表策略
按照app_id进行分库,按照biz_id模128分表(即每个业务方的数据散落在128张表中)。
每个库采用主从结构,做读写分离。
建表语句(索引设计)
|
|
SQL语句
查询状态计数
1select `state`,count(*) from status where `app_id`=? and `biz_type`=? and `biz_id`=? and `is_deleted`=0 and `state` in (?,?) group by `state`批量查询状态计数
1select `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`批量查询状态
1select `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`=?查询主体
1select `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 (?,?)查询业务实体
1select `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
12345+--------------------------------- ---+| 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
12345678910111213141516171819202122232425262728293031323334353637383940414243+----------------+| 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
应对大规模场景的优化建议
- 针对是否存在某一状态的判断,可以追加bloom filter作加速