slot deposit pulsa slot mahjong slot gacor slot gacor slot gacor resmi slot gacor 2025 slot gacor terpercaya slot gacor 2025 slot gacor hari ini slot gacor hari ini slot gacor hari ini
十亿用户的系统,该如何设计?
17611538698
webmaster@21cto.com

十亿用户的系统,该如何设计?

架构 0 2159 2022-02-28 11:31:54


这是一个面试题:

十亿用户的系统,用户可以用手机号、账号、邮箱、昵称等登录,这样的表结构应该怎样设计?登录流程大致是怎样的?

好家伙!十亿用户的系统……

在我看来,这道面试题主要是想考察两点:

  1. 面试者是否做过相关业务或者有非常扎实的数据库表设计能力?

  2. 面试者是否真的了解在高并发下应该怎么分库分表?

我们先看第一个问题:

表结构怎么设计?

当出现多种登录方式的时候,就意味着一个用户对应的账号可能会有若干个。现在可能用手机和昵称登录,以后可能用邮箱登录,甚至将来还可能通过微信、QQ、微博等第三方渠道登录。

首先,直觉上,咱们第一个冒出的念头是什么?

对我个人来说,就是本能地想着,如果是多种登录类型,就在存储用户信息的表上加多个字段。

比如,支持手机号登录,就加一个手机号字段,支持邮箱登录,就加一个邮箱字段等等。

表结构类似下面这样:

id|name|phone|email|nick_name|desc


但是仔细一想,这种设计存在问题:

  1. 当用户登录的时候,我们需要根据用户的登录类型,先要知道去查找用户表的哪个字段才可以进行登录逻辑判断。例如,用户登录用手机号了,我们就要知道去表里查找对应的 phone 字段去校验登录;登录用邮箱了,我们就要知道去表里查找对应的 email 字段才可以。这样做,代码逻辑会很复杂。

  2. 再增加一种登录方式的时候,我们还得给数据库的表里再增加一个字段,同时还得修改登录的代码。这种修改一不小心,还很容易修改不完善,造成线上 bug。每增加一种登录方式,就搞一次这种流程,成本有点过高了。

因此,我们最好能找到另一种更灵活的办法。

更灵活的办法就意味着,我们设计的表,必须易扩展。怎么叫易扩展?

加记录比加字段要更容易扩展。

这样的话,我们只能想想是不是考虑列变行的思路了,即添加字段变成添加行记录来解决。

因此方案如下:

创建一张授权表,专门用来处理登录。当新增登录类型的时候,只需要考虑增加一条记录即可:记录登录类型、登录名称以及相关密码,同时有个 user_id 字段,去和用户表做关联。

图片

用户表就存储一些非登录相关的额外信息即可。像这样:

图片

这样设计后,很明显就做到了易扩展。

假如我自己有两种登录方式,授权表(Author)的数据:

iduser_nametypepasswduser_id
10001siyuanwai01xxxxx1
10002siyuanwai@xxx.com02xxxxx1

用户表(User)的数据:

idnick_namelogo_urluser_numberuser_names
1四猿外/pic/xyz.pngxxxxxsiyuanwai,siyuanwai@xxx.com

这种方案的缺点就是,改密码的时候,得一起改动。需要注意。

说完表结构后,再来说下一个问题:

十亿用户系统的登录流程

乍一看,这道题里有十亿用户,那基本可以算是高并发、大数据了。

因为十亿用户,哪怕有百分之一的活跃用户,也是千万级别的。所以,在这样的情况下,必然需要考虑分库分表。分库是为了应对高并发,分表则是为了应对大数据。

以 MySQL 为例, 一般来讲,在 4 核 CPU / 8G 内存 / RAID10 的普通硬盘的服务器配置下,一台 MYSQL 库能一直可靠运行的可承载压力是 1000TPS 左右。一张通常的 20 个字段以内的表,能保证查询性能没有大的下降的话,可承载的数据量大致是 1000 万条数据左右。

所以,咱们分表的时候就要尽量控制表数据不超过一千万条数据。也因此,十亿用户,分表就是分 100 张表。

同时呢,咱们说了,一个库大概能承载的可靠运行并发数是 1000TPS 左右。分库一般来说,100 张表分 10 个库,每个库 10 张表,就很绰绰有余了。

好,现在问题来了,分库分表的策略是什么呢?就是按什么分呢?

一般是按照 user_id 分。假如我们要分 10 个库 100 张表, 一般来说就是先通过 user_id mod 10 去定义好库,再通过 user_id mod 100 定义好表。

比如 user_id mod 10 = 3,user_id mod 100 = 33,那么这个用户的数据就被定位到了数据库 3 中的 33 号表。

注意,这里又来了一个问题。

假设一个 user_id = 100 会怎么样?user_id  mod 10 = 0,user_id mod 100 = 0。它会被分在 0 号库,0 号表。

那如果我想分到 1 号库,0 号表呢,有对应的 user_id 吗?是没有的。为什么呢?

因为当一个 user_id mod 100 = 0 时,这个 user_id mod 10 也一定为 0 。所以,不会存在 1 号库,0 号表的情况。

所以,我们还需要对库进行调整,要把库变成 11 个库,然后呢,每个库有 100 张表。原因就是:

库数和表数之间不能存在公约数,也就是它们需要互质,只有这样,我们分配数据的时候,才会尽量均匀。

好了,当 user_id 分完之后,你会发现,按照咱们的设计,只能解决 User 表的问题。那登录在哪里?该怎么办?

咱们继续看,前面说了,登录逻辑是靠 Author 表来验证。那 Author 表数据大,也得分库分表啊?它怎么分?

其实挺简单,分库分表的时候,我们根据 Author 表的 user_name 的 Hash 去分。

假设有个用户的 user_name 是 abc,然后将这个 abc 进行下 hash,再除以库的数量。现在是 11 个库,所以就是 hash(abc) mod 11 这样得到库的编号,然后再 hash(abc) mod 100 得到表的编号。

于是,当我们登录的时候,流程如下:

  1. 输入 abc 和密码;
  2. 验证出账号类型;
  3. 将信息传递给服务器;
  4. 服务器在数据库层之上会有一个路由层,根据 hash(abc) mod 11/100 定位数据库和表;
  5. 查询 Author 表验证。

这道题到此为止,算是回答完毕了。

但是值得注意的是,面试题这里,其实是限制死了,只让我们考虑数据库。但是在实际工作当中呢,对于这种高并发、大数据的解决,方向往往是多重索引,加外置缓存。

因为,在面试题中,我们只需要考虑登录问题。而在实际工作里,我们往往还需要考虑数据重用、资源耗费等问题。

所以,实际上,很多这种高并发、大数据的登录,我们根据手头的资源,虽然依然会使用分库分表,但是,往往还会采用 ElasticSearch 缓存一些用户基本信息和用户数据所在的数据库和表的地址信息,将它们作为索引,去真正地做相关登录业务行为。并根据用户字段的使用热度,会在登录时,把一些用户关键字段读取出来,放到外置的 Redis 缓存中,供以后重用。

这样做的好处就是,分库分表我们可以根据资源随意增加减少,只需要到时候修改下 ElasticSearch 中的索引信息即可。同时,有了 Redis,也能减少后面分库分表资源的消耗。


作者:四猿外

评论