17611538698
webmaster@21cto.com

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

作者 四猿外 分类 架构 02月28日


这是一个面试题:

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

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

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

  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,也能减少后面分库分表资源的消耗。


作者:四猿外

评论