如何设计用户群组系统?-批量触达场景

1,045 阅读9分钟

中台和平台的说法都存在,没有必要咬文嚼字两者的区别和各自定义。本文中统一使用平台。

上一回讲到,美丽温柔爱思考的小美设计了用户群组的系统模型 ###程序媛小美在营销中台的成长 ,leader 王哥针对系统扩展性和数据规模提了几个问题。

爱思考的小美很快就有了解决办法,这一次的设计方案在评审会上王哥非常满意。

考虑到用户群组获取用户Id 的方式多种多样,且未来可能会增加多种方式。小美进行了如下设计

classDiagram
class UserGroup {
 +long id,
 +string name,
 int biz,产品线类型
 int groupType, 群组类型 分为指定用户、文件系统获取、课程群组购买用户、课程 id 购买用户等
 int groupSubType, //群组的细分类型。
 int identityType,//用户标识类型: 手机号,用户 Id 
 text content,
 text ext, 扩展字段
 int readyTime, 
 int realType, 
 int version,
 int expireTime, //截止时间,截止时间过后,数据可归档。需要对此字段建议索引。
 long parentUserGroupId,  父级用户群组 id。
 +long ctime,
 +long utime,
 +int status,状态字段 已就绪、更新中、
}

  1. biz 产品线,区分公司内各个产品形态,未来新的产品线使用用户群组将为其分配一个 biz
  2. groupType 群组类型定义为 群组获取用户的方式。包括指定用户、从文件系统获取、课程群组或课程 id 购买用户。这样未来如果新增其他用户获取方式,直接新增类型即可。此外 如果文件系统类型有变化,例如公司自建文件系统、云厂商 S3等通过 groupSubType 来进行区分。未来其他 groupType 有需要再细分的需求,都可以使用 groupSubType 进行额外扩展。
  3. 获取方式的详细内容,例如文件系统路径、课程群组 id、课程 id、指定用户等方式。 统一使用mysql text 类型进行存储,如果运营指定的用户 id列表长度过大就在前端限制,建议运营上传到文件系统再创建用户群组。不同的 groupType 解析 content 的方式也不同。但是新增用户获取方式无需增加数据库字段,也许连扩展字段也无需增加。
  4. ext 扩展字段,json 格式,应对未来的需求变化。
  5. readyTime 主要考虑到某个课程的购买用户是动态变化的,所以新增readyTime, 记录用户群组一次性的构建完成时间。目前用户群组定义为一次性使用,并不打算实时更新购买的用户 id,readyTime 是预留字段。
  6. realType, 实时类型,是指用户群组是否实时更新,例如某个课程一直在卖课,每次用户购买完成都会被更新到用户群组中。目前默认是不需要实时更新。未来可能需要更新,那么使用 realType 区分。
  7. version 是考虑到未来用户群组可能会实时更新或被运营手动更新,新增了版本标记。
  8. status用来标记用户群组的构建状态。目前只有更新中、已就绪两种状态。
  9. 小美和 leader 王哥沟通后了解到,运营可能有诉求 同时选择多种获取方式 。那么增加一个 parentUserGroupId关联上父级用户群组 id 就可以了。
  10. expireTime 用于归档用户群组,考虑到用户群组的关联关系数量巨大,存在归档数据的需求。这需要在 UserGroup 上新增过期时间,定时扫描可归档的用户群组,删除掉用户群组、用户群组关联关系。

针对 leader 王哥提出的问题,用户群组的规模非常大,可能一个用户群组包含上千万、上亿的数据,userId 和用户群组直接关联会导致数据库记录过多。小美想到之前的经验:应对大数据量存储和查询的时候,思路只有两个,切分和聚合。既然数据量过大,可以考虑将 userId聚合为一组记录是不是更好呢?小美进行了如下设计。

classDiagram
class UserGroupRelation  用户和群组关联关系{
+int id,
int biz, 业务线
long userGroupId,
text userIds,
long ctime,
long utime
}
  1. 用户群组和用户id的关联数据规模非常大,小美打算使用userGroupId进行分表。先暂定分100个表。
  2. 每 10000 个userId 对应一条关系记录, 这样 1 亿条用户也仅仅对应 1w 条记录。其中 userId 加逗号,大致共10个字符,1w 个 userId 对应 100K个字符。考虑到Utf-8 编码,数字和逗号都是 1 个字节。大致对应存储空间是 100K。针对批量查询的场景,不会对数据库有较大的压力。并且小美打算1W的阈值要支持动态可调整,这样万一线上压力过大,她可以随时调整。
  3. userId 列表有去重的诉求,如果需要去重,写入 数据库之前,需要根据userId+ userGroupId查询,确定不具备关联关系。 同时小美记得王哥说过,以后用户群组需要具备查询某个用户所有的用户群组的能力,也需要提供接口判断userId 和用户群组是否具备关联的查询能力。那么最好的方案当然是 redis 了。为此 小美打算使用redis hash 存储结构。 大key是userId(及必要的前缀), 小 key 是 userMatchId。当用户群组归档后,对应的小 key也要被清理。避免 hash 结构过大。(实际上还可以加上 userId, userGroupId 反向关联。即 userId 所属的 userGroupId 被存储到一条关系记录里。)

各位兄弟姐妹们,如果觉得本文没有注水,关注、点赞、收藏转发,怎么方便怎么来,小弟在此拜谢了。你的一次点赞就足够让我开心一上午。比心

问题:既然用户和用户群组的关联关系规模如此大,使用更大的分库分表方案是否可以,例如切分为10库, 100表/库,总共1K个表 有的场景可以,有的场景不可以。

  1. 查询用户群组包含的 userId 列表,使用即使用更大规模的分表方案不合适。即便分表后,一个用户群组的用户 id 也只被分配到 1 个表里,一次性扫描表中 1000w条记录, mysql 压力和查询时长都不可接受。更致命的是这样做一个表行数会非常庞大,mysql 性能会急剧下降。
  2. 数据库查询用户所属的用户群组。可以考虑使用更大规模的分表方案,但不推荐。一个用户所属的用户群组总归数量较少,因为存在归档,一般是1000 以下。所以分表方案也是可以的。但是新建一个大数据量群组,更新的记录数较多,性能也非常堪忧。所以这个方案,需要配套优化写入逻辑。

改良后的用户群组方案相比之前思考深度增加了很多。

  1. 考虑到未来获取用户方式 多种多样。预留多个类型字段,提供可扩展性
  2. 考虑到未来需要新增多个产品线接入。在用户群组表、关系表都预留产品线字段,虽然此处不起眼,但是如若不然,未来新的产品线接入时,这个小小的字段工作量可是非常大。系统建设初期,预留产品线字段,成本小,收益大。
  3. 考虑到未来个性化的需求,预留了 json 的扩展字段。
  4. 考虑到大数据量的读写需求,预留了归档字段清理过期数据。优化存储结构支持大数据量的写入和查询。
  5. 考虑到未来用户群组可能需要实时更新,也预留了实时更新类型字段。
  6. 考虑到一个活动可选择多个用户群组的潜在需求,预留了 parentUserGroupId,把多个用户群组聚合为 1 个群组。

以上的方案设计不是小美闭门造车,闷头搞出来的,而是和 leader 王哥、组内同事无数次工位旁沟通的结果。新入职的小美,工作状态渐入佳境。即便新入职一切都不熟悉,但是方案设计时,她积极和领导同事沟通交流,不闷头搞,时刻从领导那里得到信息输入,毫无疑问:他的leader 王哥更加了解系统未来可能要支持的场景。

小美的思路渐渐清晰,她在和 leader 王哥沟通多次后,用户群组的系统模型终于确定了。小美心里长舒一口气,她相信方案评审时一定会很顺利,领导肯定不会提出一堆问题和 TODO(事实也是如此)。

后来的方案评审时,王哥对小美说:”用户群组是营销系统必需的系统能力,目前没有其他专门团队在做。我们这样设计,未来足以满足营销系统所需。"。王哥呢,是敢拼敢干、非常自信的程序员,说起话来,自然也有大多数男生的毛病,爱吹牛。

但美丽可爱的小美可是十分谦虚,她笑着对王哥说:“希望公司日渐壮大,业务越来越复杂多样。到时候某人可能会被无情的打脸。”

“那可太好了,求之不得”,王哥得意的笑着说。 小美不知道的是,王哥入职早,几年来,已经有了数量可观的期权。他就等着上市买房,财务自由的那一天呢。

后来公司的业务发展确实迅速,小美因为设计过用户群组,来来被紧急抽调到新组建的社交类产品线,主导设计社交产品用户关注关系的设计。在那里小美将继续挑战大数据量高并发场景的关联关系设计。毫无疑问,用户群组的设计经验,让小美有了更多的锻炼机会和人生机遇。

当前用户群组的设计暂时告一段落,接下来的小美又遇到什么问题呢?为什么后来王哥对小美说: 你太心急了?

各位兄弟姐妹们,如果觉得本文没有注水,关注、点赞、收藏转发,怎么方便怎么来,小弟在此拜谢了。你的一次点赞就足够让我开心一上午。比心