这是一个面试题:
十亿用户的系统,用户可以用手机号、账号、邮箱、昵称等登录,这样的表结构应该怎样设计?登录流程大致是怎样的?
好家伙!十亿用户的系统……
在我看来,这道面试题主要是想考察两点:
面试者是否做过相关业务或者有非常扎实的数据库表设计能力?
面试者是否真的了解在高并发下应该怎么分库分表?
我们先看第一个问题:
当出现多种登录方式的时候,就意味着一个用户对应的账号可能会有若干个。现在可能用手机和昵称登录,以后可能用邮箱登录,甚至将来还可能通过微信、QQ、微博等第三方渠道登录。
首先,直觉上,咱们第一个冒出的念头是什么?
对我个人来说,就是本能地想着,如果是多种登录类型,就在存储用户信息的表上加多个字段。
比如,支持手机号登录,就加一个手机号字段,支持邮箱登录,就加一个邮箱字段等等。
表结构类似下面这样:
id|name|phone|email|nick_name|desc
但是仔细一想,这种设计存在问题:
当用户登录的时候,我们需要根据用户的登录类型,先要知道去查找用户表的哪个字段才可以进行登录逻辑判断。例如,用户登录用手机号了,我们就要知道去表里查找对应的 phone 字段去校验登录;登录用邮箱了,我们就要知道去表里查找对应的 email 字段才可以。这样做,代码逻辑会很复杂。
再增加一种登录方式的时候,我们还得给数据库的表里再增加一个字段,同时还得修改登录的代码。这种修改一不小心,还很容易修改不完善,造成线上 bug。每增加一种登录方式,就搞一次这种流程,成本有点过高了。
因此,我们最好能找到另一种更灵活的办法。
更灵活的办法就意味着,我们设计的表,必须易扩展。怎么叫易扩展?
加记录比加字段要更容易扩展。
这样的话,我们只能想想是不是考虑列变行的思路了,即添加字段变成添加行记录来解决。
因此方案如下:
创建一张授权表,专门用来处理登录。当新增登录类型的时候,只需要考虑增加一条记录即可:记录登录类型、登录名称以及相关密码,同时有个 user_id 字段,去和用户表做关联。
用户表就存储一些非登录相关的额外信息即可。像这样:
这样设计后,很明显就做到了易扩展。
假如我自己有两种登录方式,授权表(Author)的数据:
id | user_name | type | passwd | user_id |
---|---|---|---|---|
10001 | siyuanwai | 01 | xxxxx | 1 |
10002 | siyuanwai@xxx.com | 02 | xxxxx | 1 |
用户表(User)的数据:
id | nick_name | logo_url | user_number | user_names |
---|---|---|---|---|
1 | 四猿外 | /pic/xyz.png | xxxxx | siyuanwai,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 得到表的编号。
于是,当我们登录的时候,流程如下:
这道题到此为止,算是回答完毕了。
但是值得注意的是,面试题这里,其实是限制死了,只让我们考虑数据库。但是在实际工作当中呢,对于这种高并发、大数据的解决,方向往往是多重索引,加外置缓存。
因为,在面试题中,我们只需要考虑登录问题。而在实际工作里,我们往往还需要考虑数据重用、资源耗费等问题。
所以,实际上,很多这种高并发、大数据的登录,我们根据手头的资源,虽然依然会使用分库分表,但是,往往还会采用 ElasticSearch 缓存一些用户基本信息和用户数据所在的数据库和表的地址信息,将它们作为索引,去真正地做相关登录业务行为。并根据用户字段的使用热度,会在登录时,把一些用户关键字段读取出来,放到外置的 Redis 缓存中,供以后重用。
这样做的好处就是,分库分表我们可以根据资源随意增加减少,只需要到时候修改下 ElasticSearch 中的索引信息即可。同时,有了 Redis,也能减少后面分库分表资源的消耗。
作者:四猿外
本文为 @ 四猿外 创作并授权 21CTO 发布,未经许可,请勿转载。
内容授权事宜请您联系 webmaster@21cto.com或关注 21CTO 公众号。
该文观点仅代表作者本人,21CTO 平台仅提供信息存储空间服务。