这篇文章,我们来谈谈如何设计一个支持数百万日活用户的高阶并行系统的数据库架构?看到这个题目,很多人的反应是:分库分表!但其实我想很多同学可能不明白数据库层面的分库分表是干什么用的,它们的不同功能是如何应对不同场景的。以一家创业公司的发展为背景来介绍如果我们现在是一家小型创业公司,注册用户20万,每天活跃用户1万,每天单表数据1000条,最大数量高峰期每秒并发请求数为10。我的上帝!这种体制,随便找个有几年工作经验的高级工程师,再带几个年轻的工程师,就可以为所欲为。因为这样的系统其实主要是前期快速开发业务功能,在一台服务器上部署一个单块系统,然后连接一个数据库。然后大家在一个项目中不停的填写各种业务代码,以尽快支持公司的业务。如下图所示:结果没想到,我们这么幸运,遇到了一位优秀的CEO,带领我们走上了致富之路!公司业务发展迅速。几个月下来,注册用户已经达到2000万!每天活跃用户数100万!单表每天新增数据量达到50万条!高峰期每秒请求量达到10000!与此同时,公司还进行了两轮融资,估值达到了惊人的数亿!一只欣欣向荣的年轻独角兽的节奏!那么,现在大家感觉有点压力,为什么呢?因为单表每天新增50万条数据,一个月就新增1500万条数据。一年后,单表将达到数亿条数据。经过一段时间的运行,现在我们的单表有20到3000万条数据,勉强支撑。但是眼看着系统访问数据库的性能越来越差,单表的数据量越来越大,拖累了一些复杂查询SQL的性能!那么现在峰值请求是10000个/秒,我们系统部署20台机器上线,平均每台机器支持500个请求/秒。这个还是能顶住的,问题不大。但是数据库级别呢?如果此时你还是一个支持每秒几万请求的数据库服务器,我可以很负责任的告诉你,在每个高峰期都会出现以下问题:你的数据库服务器的磁盘IO和网络带宽,CPU负载,内存消耗都会达到很高的水平,数据库所在服务器的整体负载会很重,甚至几乎不堪重负。高峰期,你单表数据量大,SQL性能不是很好。这时候,如果你的数据库服务器负载过高,性能下降,你会发现你的SQL性能更差。最明显的感觉就是在高峰时段你的系统所有功能运行起来都很慢,用户体验很差。单击按钮后可能需要几十秒才能得到结果。如果运气不好,数据库服务器的配置不是特别高,可能会因为负载太高,数据库压力太大,导致数据库宕机。多服务器分库支持高并发读写。首先,让我们考虑第一个问题。数据库应该如何支持每秒数万个并发请求?要理解这个问题,首先要理解通用数据库在服务器上部署在什么配置下。一般来说,如果使用普通配置的服务器来部署数据库,至少要有16核32G的机器配置。对于这种部署在很普通的机器配置上的数据库,网上一般的经验是:不要让它支持每秒超过2000个请求,一般控制在2000左右。控制在这个水平上,一般数据库负载相对合理的话,不会带来太大的压力,也没有太大的宕机风险。所以第一步是在上万并发请求的场景下部署5台服务器,在每台服务器上部署一个数据库实例。然后,在每个数据库实例中,创建相同的库,例如订单库。这时候五台服务器上都有一个订单库,名字可以类似:db_order_01、db_order_02等等。那么每个订单库都有同一张表。比如订单库中有一个订单信息表,那么五个订单库中都有一个订单信息表。比如db_order_01库中有一张tb_order_01表,db_order_02库中有一张tb_order_02表。这样就实现了一个分库分表的基本思路,原来的一个数据库服务器变成了五个数据库服务器,原来的一个数据库变成了五个数据库,原来的一个表变成了五个表。那么写数据的时候就需要用到数据库中间件,比如sharding-jdbc,或者mycat。可以根据orderid进行hash,然后对5取模。比如order表每天新增50万条数据,其中10万条会落到db_order_01数据库的tb_order_01表中,另外10万条数据会落入db_order_02数据库tb_order_02表,等等。这样就可以将数据平均分布在5台服务器上。查询的时候也可以使用orderid对model进行hash,到对应服务器上的数据库,从对应的表中查询数据。按照这个思路画出来的图如下。大家可以看看:做这一步有什么好处?第一个好处,比如订单表原来只有一个表,现在变成了五张表,则每张表的数据变为1/5。假设order表一年有1亿条数据,那么5张表每年都有2000万条数据。那么假设当前订单表中已经有2000万条数据。此时经过上面的拆分后,每张表只有400万条数据。而如果每天新增50万条数据,那么每张表只新增10万条数据。这是否初步缓解了单表数据量过大影响系统性能的问题?另外每秒10000个请求发送到5个数据库,每个数据库每秒承载2000个请求。是不是一下子把各个数据库服务器的并发请求降低到一个安全的范围内?这样既降低了数据库的峰值负载,又保证了高峰期的性能。大量分表,保证海量数据下的查询性能。但是上述的数据库架构还是存在一个问题,就是单表的数据量还是太大了。现在订单表分为5张表,如果一年有1亿个订单,每张表有2000万条,还是太大了。所以要继续分表,而且要大量分表。比如订单表可以拆分成1024张表。如果将1亿的数据量分布在每个表中,数据量只有10万量级。那么,这几千张表就可以分布在5个数据库中。向上。写数据的时候,需要做两条路由。先对orderidhash取模,再对数据库数取模。可以路由到一个数据库,然后对那个数据库上的表数取模来路由。到数据库中的一个表中。通过这一步,可以让每张表的数据量变得很小,每年增长1亿条数据,但每张表只增长10万条数据。这个系统运行10年,每个表可能只有**个数据量。这样一来,您可以一次性为系统以后的运行做好充分的准备。看下图,一起感受一下:如何生成一个全局唯一的id分库分表后,你必须面对的一个问题就是如何生成id?因为如果一个表分成多个表,每个表的id从1开始累加自增,那肯定是错的。比如你的订单表拆分成1024张订单表,每张表的id都是从1开始累加的,肯定有问题!你的系统无法根据表的主键查询订单,比如id=50这个订单存在于每个表中!所以这时候就需要一种分布式架构下的全局唯一id生成方案。分库分表后,对于插入数据库的核心id,表不能直接简单使用自增id需要在全局生成一个唯一的id,然后插入到每张表中,保证一个每个表中的id是全局唯一的。比如order表虽然被拆分成了1024张表,但是id=50的订单只会存在一张表中。那么如何实现全局唯一id呢?有几种方案:方案一:独立库自增id这种方案的意思是,你的系统每次生成一个id,在一个独立库中插入一个id到一个独立的表中是没有什么业务意义的数据,然后获取在数据库中自动递增的id。拿到id后,写入对应的分库分表。比如你有一个auto_id库,里面只有一张表,叫做auto_id表,一个id是自增的。然后每次要获取一个全局唯一id,直接往这张表中插入一条记录就可以获取一个全局唯一id,然后把这个全局唯一id插入到订单的分库分表中即可。这种解决方案的优点是方便简单,任何人都可以使用。缺点是单个库生成自增id。并发高了就会有瓶颈,因为auto_id库要承载每秒几万的并发肯定是不现实的。方案二:UUID大家应该都知道,就是用UUID生成一个全局唯一的id。优点是每个系统都是在本地生成的,而不是基于数据库。缺点是UUID太长,作为主键性能太差,不能作为主键使用。如果想随机生成一个文件名、编号等,可以使用UUID,但是不能用UUID作为主键。方案三:获取系统当前时间该方案的意思是获取当前时间作为全局唯一id。但是问题是当并发量很高的时候,比如每秒几千个并发,就会出现重复,肯定是不合适的。一般如果采用这种方案,会将当前时间和很多其他业务字段拼接为一个id。如果你觉得在商业上可以接受,那也是可以的。可以将其他业务字段值与当前时间拼接成一个全球唯一的编号,比如一个订单号:时间戳+用户id+业务含义码。方案四:SnowFlake算法思路分析SnowFlake算法是Twitter开源的分布式id生成算法。核心思想是:使用一个64位长的数字作为全局唯一的id。64位中,1位没有用,然后41位作为毫秒数,10位作为工作机id,12位作为序列号。我举个例子,比如下面的64位长数:第一部分是1位:0,没有意义。第二部分是41位:代表时间戳。第三部分5位:表示机房id,10001第四部分5位:表示机器id,11001第五部分12位:表示的序号是id的序号某机房某台机器一毫秒内同时生成,000000000000。①1位:没有用,为什么?因为二进制的第一位是1,那么就是负数,但是我们生成的ID都是正数,所以第一位统一为0。②41位:代表时间戳,单位是毫秒。41位最多可以表示2^41-1,即可以识别2^41-1个毫秒值,换算成年就是69年。③10位:记录workingmachineid,表示这个服务最多可以部署在2^10台机器上,即1024台机器。但是10位中的5位代表机房ID,5位代表机器ID。意思是最多可以代表2^5个机房(32个机房),每个机房可以代表2^5台机器(32台机器)。④12位:用于记录同一毫秒内产生的不同id。12位能表示的最大正整数是2^12-1=4096,也就是说这12位所表示的数在同一毫秒内可以区分4096个不同的id。简单来说,如果你的某个服务要生成一个全局唯一的id,那么你可以向部署了SnowFlake算法的系统发送一个请求,SnowFlake算法系统就会生成一个唯一的id。这个SnowFlake算法系统首先要知道它所在的机房和机器,比如机房id=17,machineid=12。SnowFlake算法系统收到这个请求后,首先会生成一个64位的long类型id是通过二进制位运算得到的,64位中的第一位是无意义的。然后41位,可以用当前的时间戳(单位到毫秒),然后后面5位设置机房id,5位设置机器id。***再判断一下,在当前机房本机的这一毫秒内,这是第一个请求,在请求中加上一个序号来生成本次的id,作为***的12位.最后一个64位的id出来了,类似于:这个算法可以保证在机房的一台机器上同一毫秒内生成一个唯一的id。一毫秒内可能会产生多个id,但最多有12位的序号来区分。下面简单看一下这个SnowFlake算法的一个代码实现。这是一个例子。如果理解了这个意思,以后可以尝试自己修改这个算法。简而言之,64位数字中的每一位用于设置不同的标志以区分每个id。SnowFlake算法的实现代码如下:publicclassIdWorker{privatelongworkerId;//这个代表机器idprivatelongdatacenterId;//这个代表机房idprivatelongsequence;//这个代表一毫秒内生成的多个id的***序号publicIdWorker(longworkerId,longdatacenterId,longsequence){//sanitycheckforworkerId//我这里刚刚查过,要求是你传入的机房id和机器id不能超过32,不能小于0if(workerId>maxWorkerId||workerId<0){thrownewIllegalArgumentException(String.format("workerIdcan'tbegreaterthan%dorlessthan0",maxWorkerId));}if(datacenterId>maxDatacenterId||datacenterId<0){thrownewIllegalArgumentException(String.format("datacenterIdcan'tbegreaterthan%dorlessthan0",maxDatacenterId));}这。workerId=workerId;this.datacenterId=datacenterId;this.sequence=sequence;}privatelongtwepoch=1288834974657L;privatelongworkerIdBits=5L;privatelongdatacenterIdBits=5L;//这是二进制运算,即5bit最多只能有31个数,也就是说机器的id只能在32以内privatelongmaxWorkerId=-1L^(-1L<
