精华内容
下载资源
问答
  • 采用了读写分离主从复制(数据库读写分离方案,实现高性能数据库集群)的方案去应对,后来又面临了大并发写入的时候,系统数据库采用了分库分表的方案(数据库分库分表方案,优化大量并发写入所带来的性能问题),...

    上两篇讲到了我们的系统在面临大并发读取的时候,采用了读写分离主从复制(数据库读写分离方案,实现高性能数据库集群)的方案去应对,后来又面临了大并发写入的时候,系统数据库采用了分库分表的方案(数据库分库分表方案,优化大量并发写入所带来的性能问题),通过垂直拆分以及水平拆分的方式,将数据分到多个库和多个表中去应对的,即现在是这样的一套分布式存储结构。

     

    数据库分库分表那篇也讲到了,使用了分库分表势必会带来和我们之前使用不大相同的问题。今天,我将其中一个和我们开发息息相关的问题提出来进行讲解,也就是我们开发中所使用的的主键的问题。我们知道,以前我们单库的时候,主键唯一ID是自增的,现在好了,我们的数据被分到多个库的多个表里面了,如果我们还是使用之前的主键自增策略,那么这样就会出现两个数据插入到了两个不同的表会出现相同的ID值,这时我们该怎么去使用呢?

    对于什么是主键,主键该怎么选,今天不做讲解,我相信大家可能比我还精通,我们今天主要是讲唯一主键ID在分布式存储系统下怎么生成,保证ID的唯一性且符合我们业务需要,才是我们开发人员最关心的实战。

     

    UUID

    这个时候,你可能会说,自增用不了,那我就是用UUID嘛,这个UUID生成出来的就是唯一的。的确,在我以前在一个公司中的确接触到是使用UUID来生成唯一主键ID的,而且性能还可以。但是,我想提一点的就是,当这个ID和我们业务交集不相关的时候是可以使用UUID生成主键的。比如,一般我们业务是需要用来做查询的,而且最好是单调递增的,这样我们的UUID就很不适合了。

     

    主键ID单调递增有什么好处呢?

    1,就拿我们用户关注航班这个模块来说,我们查看某个航班关注用户按照时间的先后进行排序。因为现在的ID是时间上有序的,所以现在我们就可以按照ID来进行排序了,同时这样对于有些并不是要存储时间的业务来说,会减少不少的存储空间。

    2,有序的ID可以提升数据写入的性能

    我们知道主键其实在数据库中就是一种索引,而索引在MySql数据库的B+数据结构中是顺序存储的,所以每次插入的时候就是递增排序的,直接追加到后面就行。如果是无序的话,则每次插入数据之前还得查找它应该所在的位置,这无疑就会增加数据的异动等相关的开销,如下图:

     

    如上图所示,如果我们生成的ID是有序的,那这个 50 就直接插在尾部就行了,如果是无序的话,突然生成了一个 26,我们还得先找到 26 需要存放的位置,然后还要对其后面数据进行挪位置。

    3,UUID不具备业务相关性

    我们现在开发的项目都是依据公司业务开展的,而我们的唯一ID一般都是和业务有关系的,比如,有些订单ID中带上了时间的维度、机房的维度以及业务类型等维度。也就是为了我方便进行定位是那种业务的订单,才会这么设计的,是不是。

    而UUID是由32位的16进制数字组成的字符串,不仅在存储空间上造成浪费,更不具备我们业务相关性。那我们该怎么解决呢?其实twitter提出来的Snowflake 算法就能很好满足我们现在的要求,满足了主键ID的全局唯一性、单调递增性,也可以满足我们的业务相关。所以,我们现在使用的唯一ID生成方式就是使用Snowflake算法,这个算法其实很简单。下面我们来对其进行讲解,并对其相应改造使其能用到我们的开发业务中来。

     

    Snowflake 算法原理

    Snowflake 是由 64 比特bit二进制数字组成的,一共分为4大部分:

    • 1位默认不使用

    • 41位时间戳

    • 10位机器ID

    • 12位序列号

     

    1. 我们从上图中可以看出snowflake算法的第二部分的41位时间戳,大概可以支撑2^41/1000/60/60/24/365 年,也就是大约有69年。我们设计一个系统用69年应该是足够了吧。

    2. 10位的机器ID我们可以怎么使用呢?我们可以划分成大概2到3位IDC,也就是可以支撑4到8个IDC机房;然后划分7到 8 位的机器ID,即可以支撑128~256台机器。

    3. 12位的序号,就代表每个节点每毫秒可以生成4096个ID序号。

     

     

     

     

    如何改造

    我们现在已经知道了Snowflake 算法的核心原理,并且知道了其有64位的二进制数据,那我们就可以根据自己业务进行改造以更好的来为我们业务服务。一般不同的公司对其进行改造的方式都不尽相同,但是道理都是一样的。我们可以这么做:

    1. 我们是减少序列号的位数,增加机器ID的位数,是为了用来支撑我们单IDC的更多机器。

    2. 将我们业务ID加入进去用来区分我们不同的业务。比如,1位0 + 41位时间戳 + 6位IDC(64个IDC) + 6位业务信息(支撑64种业务) + 10位自增序列(每毫秒1024个ID)

     

    如此,我们就可以在单机房部署这么一个统一ID发号器,然后用Keeplive 保证高可用(对于高可用不熟悉的回去看看哈「高可用」你们服务器挂了怎么办,我们是这样做的)可以将不同的业务模块ID加入进去,这样的好处是即使哪个业务出问题了,我只看ID号我就分析出来,比如,我看到现在ID号有我的订单ID业务,我就去看订单模块。

     

    开发如何使用

    现在我们知道Snowflake 算法原理了,还知道了我们可以进行改造了。那我们开发人员该怎么去使用,来为我们业务生成统一的唯一ID呢?

    1,直接嵌入到业务代码

    嵌入业务代码的意思就是,这个snowflake算法就部署在和我们业务相同的服务器上,这样我们代码使用的时候,就不用了跨网络调用,性能相对比较好。但是也是有缺点的,因为我们的业务机器肯定是很多的,这就意味着我们发号器算法需要更多的机器ID位数。同时,太多的业务服务器我们会很难保证业务机器id的唯一性,这里就需要引用zookeeper一致性组件来保证每次机器重启都能能获得唯一的机器ID。

    2,独立部署成发号器服务

    也就是说,我们将其作为单独的服务部署到单独的机器上,已对外提供服务。这样就是多了网络的传输,不过影响不大,比如,我可以将其部署成一个主备的方式对外提供发号服务,机器ID可以用作序列号使用,这样也就是会有更多的自增序号,有部分大厂就是以这样单独的服务提供出来的。

    开发中避坑大法

    1,虽然snowflake很优秀,但是它是基于系统时间的,万一我们系统的时间不准怎么办,就会造成我们的ID会重复。那我们的做法就是,要利用系统的对时功能,一旦发现时间不一致,就暂停发号器,等到时钟准了在启用。

    2,还有一个坑比较关键,也是常发生的,就是当我们的QPS并发不高的时候,比如每毫秒只生成一个ID号,这样就是直接结果是,每次生成的ID末尾都是1,这样我们分库分表就会出现问题呀对吧,因为我们用这个ID去分库分表呀,会造成数据不均匀,是吧,忘记了去复习哈(数据库分库分表方案,优化大量并发写入所带来的性能问题)那我们怎么解决呢?

    我们可以将时间戳记录从毫秒记录改为秒记录,这样我一秒可以发好多个号了

    生成的序列号起始号随机启动,比如这一秒起始号是10,我下一秒随机了变成了28,这样就更加分散开了。

    总结,今天我们针对分库分表之后带来的第一个直接影响我们开发的问题,就是主键ID唯一性的问题,然后说到了使用Snowflake算法去解决,并且对其原理和使用进行了详细的讲解,同时,还将其在使用中遇到的坑给讲出来了,也对其进行了填坑分析,让大家直接避免遇到同样的问题。当然生成唯一ID有多种,我们根据业务选择合适我们自己的就好,你们是基于什么方式生成的可以也可以告诉大家。

    下一篇预告:继续分析分库分表后续问题

    在公众号菜单“成神之路”可获得专属架构视频资料,还有今年最新阿里面试,更多java、python、人工智能、小程序、大前端等可加作者,无私奉献

    往期精选

    Linux生产环境CPU使用率100%,教你定位到具体函数

    数据库分库分表方案,优化大量并发写入所带来的性能问题

    数据库读写分离方案,实现高性能数据库集群

    要想精通java,你必须得知道java的内存模型,不忽悠

    面试是不是经常被问到分布式系统核心问题,这一次没人难倒你

     

     

    关于  “架构师修炼”

    本号旨在分享一线互联网各种技术架构解决方案,分布式以及高并发等相关专题,同时会将作者的学习总结进行整理并分享

     

    展开全文
  • 你现在已经明白为啥要分库分表了,你也知道常用的分库分表中间件了,你也设计好你们如何分库分表的方案了(水平拆分、垂直拆分、分表),那问题来了,你接下来该怎么把你那个单库单表的系统给迁移到分库分表上去?...

    1、如何设计让系统从未分库分表动态切换到分库分表上

    1.1、问题

    你看看,你现在已经明白为啥要分库分表了,你也知道常用的分库分表中间件了,你也设计好你们如何分库分表的方案了(水平拆分、垂直拆分、分表),那问题来了,你接下来该怎么把你那个单库单表的系统给迁移到分库分表上去?

    假设,你现有有一个单库单表的系统,在线上在跑,假设单表有600万数据

    3个库,每个库里分了4个表,每个表要放50万的数据量

    假设你已经选择了一个分库分表的数据库中间件,sharding-jdbc,mycat,都可以

    你怎么把线上系统平滑地迁移到分库分表上面去

    sharding-jdbc:自己上官网,找一个官网最基本的例子,自己写一下,试一下,跑跑看,是非常简单的

    mycat:自己上官网,找一个官网最基本的例子,自己写一下,试一下看看

    1.2、解决方法

    (1)长时间停机分库分表:
    在这里插入图片描述
    (2)不停机双写方案:
    在这里插入图片描述

    (1)停机迁移方案

    我先给你说一个最low的方案,就是很简单,大家伙儿凌晨12点开始运维,网站或者app挂个公告,说0点到早上6点进行运维,无法访问。。。。。。

    接着到0点,停机,系统挺掉,没有流量写入了,此时老的单库单表数据库静止了。然后你之前得写好一个导数的一次性工具,此时直接跑起来,然后将单库单表的数据哗哗哗读出来,写到分库分表里面去。

    导数完了之后,就ok了,修改系统的数据库连接配置啥的,包括可能代码和SQL也许有修改,那你就用最新的代码,然后直接启动连到新的分库分表上去。

    验证一下,ok了,完美,大家伸个懒腰,看看看凌晨4点钟的北京夜景,打个滴滴回家吧

    但是这个方案比较low,谁都能干,我们来看看高大上一点的方案

    (2)双写迁移方案

    这个是我们常用的一种迁移方案,比较靠谱一些,不用停机,不用看北京凌晨4点的风景

    简单来说,就是在线上系统里面,之前所有写库的地方,增删改操作,都除了对老库增删改,都加上对新库的增删改,这就是所谓双写,同时写俩库,老库和新库。

    然后系统部署之后,新库数据差太远,用之前说的导数工具,跑起来读老库数据写新库,写的时候要根据gmt_modified这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。

    接着导万一轮之后,有可能数据还是存在不一致,那么就程序自动做一轮校验,比对新老库每个表的每条数据,接着如果有不一样的,就针对那些不一样的,从老库读数据再次写。反复循环,直到两个库每个表的数据都完全一致为止。

    接着当数据完全一致了,就ok了,基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。所以现在基本玩儿数据迁移之类的,都是这么干了。

    2、如何设计可以动态扩容缩容的分库分表方案?

    2.1、问题分析

    (1)选择一个数据库中间件,调研、学习、测试
    (2)设计你的分库分表的一个方案,你要分成多少个库,每个库分成多少个表,3个库每个库4个表
    (3)基于选择好的数据库中间件,以及在测试环境建立好的分库分表的环境,然后测试一下能否正常进行分库分表的读写
    (4)完成单库单表到分库分表的迁移,双写方案
    (5)线上系统开始基于分库分表对外提供服务
    (6)扩容了,扩容成6个库,每个库需要12个表,你怎么来增加更多库和表呢?

    这个是你必须面对的一个事儿,就是你已经弄好分库分表方案了,然后一堆库和表都建好了,基于分库分表中间件的代码开发啥的都好了,测试都ok了,数据能均匀分布到各个库和各个表里去,而且接着你还通过双写的方案咔嚓一下上了系统,已经直接基于分库分表方案在搞了。

    那么现在问题来了,你现在这些库和表又支撑不住了,要继续扩容咋办?这个可能就是说你的每个库的容量又快满了,或者是你的表数据量又太大了,也可能是你每个库的写并发太高了,你得继续扩容。

    2.2、解决方法

    在这里插入图片描述

    (1)停机扩容

    这个方案就跟停机迁移一样,步骤几乎一致,唯一的一点就是那个导数的工具,是把现有库表的数据抽出来慢慢倒入到新的库和表里去。但是最好别这么玩儿,有点不太靠谱,因为既然分库分表就说明数据量实在是太大了,可能多达几亿条,甚至几十亿,你这么玩儿,可能会出问题。

    从单库单表迁移到分库分表的时候,数据量并不是很大,单表最大也就两三千万

    写个工具,多弄几台机器并行跑,1小时数据就导完了

    3个库+12个表,跑了一段时间了,数据量都1亿~2亿了。光是导2亿数据,都要导个几个小时,6点,刚刚导完数据,还要搞后续的修改配置,重启系统,测试验证,10点才可以搞完

    (2)优化后的方案

    一开始上来就是32个库,每个库32个表,1024张表

    我可以告诉各位同学说,这个分法,第一,基本上国内的互联网肯定都是够用了,第二,无论是并发支撑还是数据量支撑都没问题

    每个库正常承载的写入并发量是1000,那么32个库就可以承载32 * 1000 = 32000的写并发,如果每个库承载1500的写并发,32 * 1500 = 48000的写并发,接近5万/s的写入并发,前面再加一个MQ,削峰,每秒写入MQ 8万条数据,每秒消费5万条数据。

    有些除非是国内排名非常靠前的这些公司,他们的最核心的系统的数据库,可能会出现几百台数据库的这么一个规模,128个库,256个库,512个库

    1024张表,假设每个表放500万数据,在MySQL里可以放50亿条数据

    每秒的5万写并发,总共50亿条数据,对于国内大部分的互联网公司来说,其实一般来说都够了

    谈分库分表的扩容,第一次分库分表,就一次性给他分个够,32个库,1024张表,可能对大部分的中小型互联网公司来说,已经可以支撑好几年了

    一个实践是利用32 * 32来分库分表,即分为32个库,每个库里一个表分为32张表。一共就是1024张表。根据某个id先根据32取模路由到库,再根据32取模路由到库里的表。

    刚开始的时候,这个库可能就是逻辑库,建在一个数据库上的,就是一个mysql服务器可能建了n个库,比如16个库。后面如果要拆分,就是不断在库和mysql服务器之间做迁移就可以了。然后系统配合改一下配置即可。

    比如说最多可以扩展到32个数据库服务器,每个数据库服务器是一个库。如果还是不够?最多可以扩展到1024个数据库服务器,每个数据库服务器上面一个库一个表。因为最多是1024个表么。

    这么搞,是不用自己写代码做数据迁移的,都交给dba来搞好了,但是dba确实是需要做一些库表迁移的工作,但是总比你自己写代码,抽数据导数据来的效率高得多了。

    哪怕是要减少库的数量,也很简单,其实说白了就是按倍数缩容就可以了,然后修改一下路由规则。

    对2 ^ n取模

    orderId 模 32 = 库
    orderId / 32 模 32 = 表

    259 3 8
    1189 5 5
    352 0 11
    4593 17 15

    1、设定好几台数据库服务器,每台服务器上几个库,每个库多少个表,推荐是32库 * 32表,对于大部分公司来说,可能几年都够了

    2、路由的规则,orderId 模 32 = 库,orderId / 32 模 32 = 表

    3、扩容的时候,申请增加更多的数据库服务器,装好mysql,倍数扩容,4台服务器,扩到8台服务器,16台服务器

    4、由dba负责将原先数据库服务器的库,迁移到新的数据库服务器上去,很多工具,库迁移,比较便捷

    5、我们这边就是修改一下配置,调整迁移的库所在数据库服务器的地址

    6、重新发布系统,上线,原先的路由规则变都不用变,直接可以基于2倍的数据库服务器的资源,继续进行线上系统的提供服务

    3、分库分表之后,id主键如何处理?

    3.1、问题

    在这里插入图片描述

    其实这是分库分表之后你必然要面对的一个问题,就是id咋生成?因为要是分成多个表之后,每个表都是从1开始累加,那肯定不对啊,需要一个全局唯一的id来支持。所以这都是你实际生产环境中必须考虑的问题。

    3.2、解决方法

    (1)数据库自增id

    这个就是说你的系统里每次得到一个id,都是往一个库的一个表里插入一条没什么业务含义的数据,然后获取一个数据库自增的一个id。拿到这个id之后再往对应的分库分表里去写入。

    这个方案的好处就是方便简单,谁都会用;缺点就是单库生成自增id,要是高并发的话,就会有瓶颈的;如果你硬是要改进一下,那么就专门开一个服务出来,这个服务每次就拿到当前id最大值,然后自己递增几个id,一次性返回一批id,然后再把当前最大id值修改成递增几个id之后的一个值;但是无论怎么说都是基于单个数据库。

    适合的场景:你分库分表就俩原因,要不就是单库并发太高,要不就是单库数据量太大;除非是你并发不高,但是数据量太大导致的分库分表扩容,你可以用这个方案,因为可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。

    并发很低,几百/s,但是数据量大,几十亿的数据,所以需要靠分库分表来存放海量的数据

    (2)uuid

    好处就是本地生成,不要基于数据库来了;不好之处就是,uuid太长了,作为主键性能太差了,不适合用于主键。

    适合的场景:如果你是要随机生成个什么文件名了,编号之类的,你可以用uuid,但是作为主键是不能用uuid的。

    UUID.randomUUID().toString().replace(“-”, “”) -> sfsdf23423rr234sfdaf

    (3)获取系统当前时间

    这个就是获取当前时间即可,但是问题是,并发很高的时候,比如一秒并发几千,会有重复的情况,这个是肯定不合适的。基本就不用考虑了。

    适合的场景:一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号,订单编号,时间戳 + 用户id + 业务含义编码

    (4)snowflake算法

    在这里插入图片描述

    twitter开源的分布式id生成算法,就是把一个64位的long型的id,1个bit是不用的,用其中的41 bit作为毫秒数,用10 bit作为工作机器id,12 bit作为序列号

    在这里插入图片描述
    给大家举个例子吧,比如下面那个 64 bit 的 long 型数字:

    • 第一个部分,是 1 个 bit:0,这个是无意义的。

    • 第二个部分是 41 个 bit:表示的是时间戳。

    • 第三个部分是 5 个 bit:表示的是机房 id,10001。

    • 第四个部分是 5 个 bit:表示的是机器 id,1 1001。

    • 第五个部分是 12 个 bit:表示的序号,就是某个机房某台机器上这一毫秒内同时生成的 id 的序号,0000 00000000。

    1 bit:不用,为啥呢?

    因为二进制里第一个bit为如果是1,那么都是负数,但是我们生成的id都是正数,所以第一个bit统一都是0

    41 bit:表示的是时间戳,单位是毫秒。

    41 bit可以表示的数字多达2^41 - 1,也就是可以标识2 ^ 41 - 1个毫秒值,换算成年就是表示69年的时间。

    10 bit:记录工作机器id,代表的是这个服务最多可以部署在2^10台机器上哪,也就是1024台机器。

    但是10 bit里5个bit代表机房id,5个bit代表机器id。意思就是最多代表2 ^ 5个机房(32个机房),每个机房里可以代表2 ^ 5个机器(32台机器)。

    12 bit:这个是用来记录同一个毫秒内产生的不同id

    12 bit可以代表的最大正整数是2 ^ 12 - 1 = 4096,也就是说可以用这个12bit代表的数字来区分同一个毫秒内的4096个不同的id

    简单来说,你的某个服务假设要生成一个全局唯一 id,那么就可以发送一个请求给部署了 SnowFlake 算法的系统,由这个 SnowFlake 算法系统来生成唯一 id。

    这个 SnowFlake 算法系统首先肯定是知道自己所在的机房和机器的,比如机房 id = 17,机器 id = 12。

    接着 SnowFlake 算法系统接收到这个请求之后,首先就会用二进制位运算的方式生成一个 64 bit 的 long 型 id,64 个 bit 中的第一个 bit 是无意义的。

    接着 41 个 bit,就可以用当前时间戳(单位到毫秒),然后接着 5 个 bit 设置上这个机房 id,还有 5 个 bit 设置上机器 id。

    最后再判断一下,当前这台机房的这台机器上这一毫秒内,这是第几个请求,给这次生成 id 的请求累加一个序号,作为最后的 12 个 bit。

    最终一个 64 个 bit 的 id 就出来了,类似于:

    0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000

    这个算法可以保证说,一个机房的一台机器上,在同一毫秒内,生成了一个唯一的 id。可能一个毫秒内会生成多个 id,但是有最后 12 个 bit 的序号来区分开来。

    下面我们简单看看这个 SnowFlake 算法的一个代码实现,这就是个示例,大家如果理解了这个意思之后,以后可以自己尝试改造这个算法。

    总之就是用一个 64 bit 的数字中各个 bit 位来设置不同的标志位,区分每一个 id。

    SnowFlake 算法的实现代码如下:

    64位的long型的id,64位的long -> 二进制

    0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000

    2018-01-01 10:00:00 -> 做了一些计算,再换算成一个二进制,41bit来放 -> 0001100 10100010 10111110 10001001 01011100 00

    机房id,17 -> 换算成一个二进制 -> 10001

    机器id,25 -> 换算成一个二进制 -> 11001

    snowflake算法服务,会判断一下,当前这个请求是否是,机房17的机器25,在2175/11/7 12:12:14时间点发送过来的第一个请求,如果是第一个请求

    假设,在2175/11/7 12:12:14时间里,机房17的机器25,发送了第二条消息,snowflake算法服务,会发现说机房17的机器25,在2175/11/7 12:12:14时间里,在这一毫秒,之前已经生成过一个id了,此时如果你同一个机房,同一个机器,在同一个毫秒内,再次要求生成一个id,此时我只能把加1

    0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000001

    比如我们来观察上面的那个,就是一个典型的二进制的64位的id,换算成10进制就是910499571847892992。

     
    public class IdWorker {
     
    	//因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
     
    	//机器ID  2进制5位  32位减掉1位 31个
    	private long workerId;
    	//机房ID 2进制5位  32位减掉1位 31个
    	private long datacenterId;
    	//代表一毫秒内生成的多个id的最新序号  12位 4096 -1 = 4095 个
    	private long sequence;
    	//设置一个时间初始值    2^41 - 1   差不多可以用69年
    	private long twepoch = 1585644268888L;
    	//5位的机器id
    	private long workerIdBits = 5L;
    	//5位的机房id
    	private long datacenterIdBits = 5L;
    	//每毫秒内产生的id数 2 的 12次方
    	private long sequenceBits = 12L;
    	// 这个是二进制运算,就是5 bit最多只能有31个数字,也就是说机器id最多只能是32以内
    	private long maxWorkerId = -1L ^ (-1L << workerIdBits);
    	// 这个是一个意思,就是5 bit最多只能有31个数字,机房id最多只能是32以内
    	private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
     
    	private long workerIdShift = sequenceBits;
    	private long datacenterIdShift = sequenceBits + workerIdBits;
    	private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    	private long sequenceMask = -1L ^ (-1L << sequenceBits);
    	//记录产生时间毫秒数,判断是否是同1毫秒
    	private long lastTimestamp = -1L;
    	public long getWorkerId(){
    		return workerId;
    	}
    	public long getDatacenterId() {
    		return datacenterId;
    	}
    	public long getTimestamp() {
    		return System.currentTimeMillis();
    	}
     
     
     
    	public IdWorker(long workerId, long datacenterId, long sequence) {
     
    		// 检查机房id和机器id是否超过31 不能小于0
    		if (workerId > maxWorkerId || workerId < 0) {
    			throw new IllegalArgumentException(
    					String.format("worker Id can't be greater than %d or less than 0",maxWorkerId));
    		}
     
    		if (datacenterId > maxDatacenterId || datacenterId < 0) {
     
    			throw new IllegalArgumentException(
    					String.format("datacenter Id can't be greater than %d or less than 0",maxDatacenterId));
    		}
    		this.workerId = workerId;
    		this.datacenterId = datacenterId;
    		this.sequence = sequence;
    	}
     
    	// 这个是核心方法,通过调用nextId()方法,让当前这台机器上的snowflake算法程序生成一个全局唯一的id
    	public synchronized long nextId() {
    		// 这儿就是获取当前时间戳,单位是毫秒
    		long timestamp = timeGen();
    		if (timestamp < lastTimestamp) {
     
    			System.err.printf(
    					"clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
    			throw new RuntimeException(
    					String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
    							lastTimestamp - timestamp));
    		}
     
    		// 下面是说假设在同一个毫秒内,又发送了一个请求生成一个id
    		// 这个时候就得把seqence序号给递增1,最多就是4096
    		if (lastTimestamp == timestamp) {
     
    			// 这个意思是说一个毫秒内最多只能有4096个数字,无论你传递多少进来,
    			//这个位运算保证始终就是在4096这个范围内,避免你自己传递个sequence超过了4096这个范围
    			sequence = (sequence + 1) & sequenceMask;
    			//当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
    			if (sequence == 0) {
    				timestamp = tilNextMillis(lastTimestamp);
    			}
     
    		} else {
    			sequence = 0;
    		}
    		// 这儿记录一下最近一次生成id的时间戳,单位是毫秒
    		lastTimestamp = timestamp;
    		// 这儿就是最核心的二进制位运算操作,生成一个64bit的id
    		// 先将当前时间戳左移,放到41 bit那儿;将机房id左移放到5 bit那儿;将机器id左移放到5 bit那儿;将序号放最后12 bit
    		// 最后拼接起来成一个64 bit的二进制数字,转换成10进制就是个long型
    		return ((timestamp - twepoch) << timestampLeftShift) |
    				(datacenterId << datacenterIdShift) |
    				(workerId << workerIdShift) | sequence;
    	}
     
    	/**
    	 * 当某一毫秒的时间,产生的id数 超过4095,系统会进入等待,直到下一毫秒,系统继续产生ID
    	 * @param lastTimestamp
    	 * @return
    	 */
    	private long tilNextMillis(long lastTimestamp) {
     
    		long timestamp = timeGen();
     
    		while (timestamp <= lastTimestamp) {
    			timestamp = timeGen();
    		}
    		return timestamp;
    	}
    	//获取当前时间戳
    	private long timeGen(){
    		return System.currentTimeMillis();
    	}
     
    	/**
    	 *  main 测试类
    	 * @param args
    	 */
    	public static void main(String[] args) {
    		System.out.println(1&4596);
    		System.out.println(2&4596);
    		System.out.println(6&4596);
    		System.out.println(6&4596);
    		System.out.println(6&4596);
    		System.out.println(6&4596);
    //		IdWorker worker = new IdWorker(1,1,1);
    //		for (int i = 0; i < 22; i++) {
    //			System.out.println(worker.nextId());
    //		}
    	}
    }
    

    SnowFlake算法的优点:

    (1)高性能高可用:生成时不依赖于数据库,完全在内存中生成。

    (2)容量大:每秒中能生成数百万的自增ID。

    (3)ID自增:存入数据库中,索引效率高。

    SnowFlake算法的缺点:

    依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成id冲突或者重复。

    实际中我们的机房并没有那么多,我们可以改进改算法,将10bit的机器id优化,成业务表或者和我们系统相关的业务。

    展开全文
  • 我们知道,如果我们使用mysql,当数据库数据量达到一定数据量之后,会考虑对数据库进行分库分表等操作,但是在什么情况下做怎么的切分,下面分表介绍。 一、分库 1 分库原因 首先,在单台数据库服务器性能足够的...

    我们知道,如果我们使用mysql,当数据库数据量达到一定数据量之后,会考虑对数据库进行分库分表等操作,但是在什么情况下做怎么的切分,下面分表介绍。

    一、分库

    1 分库原因

    首先,在单台数据库服务器性能足够的情况下,分库对于数据库性能是没有影响的。在数据库存储上,database只起到一个namespace的作用。database中的表文件存储在一个以database名命名的文件夹中。比如下面的employees数据库:

    mysql> show tables in employees;
    +---------------------+
    | Tables_in_employees |
    +---------------------+
    | departments         |
    | dept_emp            |
    | dept_manager        |
    | employees           |
    | salaries            |
    | titles              |
    +---------------------+
    

    在操作系统中看是这样的:

    # haitian at haitian-coder.local in /usr/local/var/mysql/employees on git:master ● [21:19:47]
    → ls  
    db.opt           dept_emp.frm     dept_manager.ibd salaries.frm     titles.ibd
    departments.frm  dept_emp.ibd     employees.frm    salaries.ibd
    departments.ibd  dept_manager.frm employees.ibd    titles.frm
    

    database不是文件,只起到namespace的作用,所以MySQLdatabase大小当然也是没有限制的,而且对里面的表数量也没有限制。

    所以,为什么要分库呢?

    答案是为了解决单台服务器的性能问题,当单台数据库服务器无法支撑当前的数据量时,就需要根据业务逻辑紧密程度把表分成几撮,分别放在不同的数据库服务器中以降低单台服务器的负载。

    分库一般考虑的是垂直切分,除非在垂直切分后,数据量仍然多到单台服务器无法负载,才继续水平切分。

    比如一个论坛系统的数据库因当前服务器性能无法满足需要进行分库。先垂直切分,按业务逻辑把用户相关数据表比如用户信息、积分、用户间私信等放入user数据库;论坛相关数据表比如板块,帖子,回复等放入forum数据库,两个数据库放在不同服务器上。

    拆分后表往往不可能完全无关联,比如帖子中的发帖人、回复人这些信息都在user数据库中。未拆分前可能一次联表查询就能获取当前帖子的回复、发帖人、回复人等所有信息,拆分后因为跨数据库无法联表查询,只能多次查询获得最终数据。

    所以总结起来,分库的目的是降低单台服务器负载,切分原则是根据业务紧密程度拆分,缺点是跨数据库无法联表查询。

    二、分表

    1 分表的原因

    当数据量超大的时候,B-Tree索引就无法起作用了。除非是索引覆盖查询,否则数据库服务器需要根据索引扫描的结果回表,查询所有符合条件的记录,如果数据量巨大,这将产生大量随机I/O,随之,数据库的响应时间将大到不可接受的程度。另外,索引维护(磁盘空间、I/O操作)的代价也非常高。

    2 垂直分表

    原因:

    1.根据MySQL索引实现原理及相关优化策略的内容我们知道Innodb主索引叶子节点存储着当前行的所有信息,所以减少字段可使内存加载更多行数据,有利于查询。

    2.受限于操作系统中的文件大小限制。

    切分原则: 把不常用或业务逻辑不紧密或存储内容比较多的字段分到新的表中可使表存储更多数据。。

    3 水平分表

    原因:

    1.随着数据量的增大,table行数巨大,查询的效率越来越低。

    2.同样受限于操作系统中的文件大小限制,数据量不能无限增加,当到达一定容量时,需要水平切分以降低单表(文件)的大小。

    切分原则: 增量区间或散列或其他业务逻辑。

    使用哪种切分方法要根据实际业务逻辑判断。

    比如对表的访问多是近期产生的新数据,历史数据访问较少,可以考虑根据时间增量把数据按照一定时间段(比如每年)切分。

    如果对表的访问较均匀,没有明显的热点区域,则可以考虑用范围(比如每500w一个表)或普通Hash或一致性Hash来切分。

    全局主键问题:

    原本依赖数据库生成主键(比如自增)的表在拆分后需要自己实现主键的生成,因为一般拆分规则是建立在主键上的,所以在插入新数据时需要确定主键后才能找到存储的表。

    实际应用中也已经有了比较成熟的方案。比如对于自增列做主键的表,flickr的全局主键生成方案很好的解决了性能和单点问题,具体实现原理可以参考这个帖子。除此之外,还有类似于uuid的全局主键生成方案,比如达达参考的Instagram的ID生成器

    一致性Hash:

    使用一致性Hash切分比普通的Hash切分可扩展性更强,可以实现拆分表的添加和删除。一致性Hash的具体原理可以参考这个帖子,如果拆分后的表存储在不同服务器节点上,可以跟帖子一样对节点名或ip取Hash;如果拆分后的表存在一个服务器中则可对拆分后的表名取Hash。

    三、MySQL的分区表

    上面介绍的传统的分库分表都是在应用层实现,拆分后都要对原有系统进行很大的调整以适应新拆分后的库或表,比如实现一个SQL中间件、原本的联表查询改成两次查询、实现一个全局主键生成器等等。

    而下面介绍的MySQL分区表是在数据库层面,MySQL自己实现的分表功能,在很大程度上简化了分表的难度。

    1 介绍

    对用户来说,分区表是一个独立的逻辑表,但是底层由多个物理子表实现。

    也就是说,对于原表分区后,对于应用层来说可以不做变化,我们无需改变原有的SQL语句,相当于MySQL帮我们实现了传统分表后的SQL中间件,当然,MySQL的分区表的实现要复杂很多。

    另外,在创建分区时可以指定分区的索引文件和数据文件的存储位置,所以可以把数据表的数据分布在不同的物理设备上,从而高效地利用多个硬件设备。

    一些限制:

    1.在5.6.7之前的版本,一个表最多有1024个分区;从5.6.7开始,一个表最多可以有8192个分区。

    2.分区表中无法使用外键约束。

    3.主表的所有唯一索引列(包括主键)都必须包含分区字段。MySQL官方文档中写的是:

    All columns used in the partitioning expression for a partitioned table must be part of every unique key that the table may have.

    这句话不是很好理解,需要通过例子才能明白,MySQL官方文档也为此限制特意做了举例和解释

    2 分区表类型

    RANGE分区

    根据范围分区,范围应该连续但是不重叠,使用PARTITION BY RANGEVALUES LESS THAN关键字。不使用COLUMNS关键字时RANGE括号内必须为整数字段名或返回确定整数的函数。

    根据数值范围:

    CREATE TABLE employees (
        id INT NOT NULL,
        fname VARCHAR(30),
        lname VARCHAR(30),
        hired DATE NOT NULL DEFAULT '1970-01-01',
        separated DATE NOT NULL DEFAULT '9999-12-31',
        job_code INT NOT NULL,
        store_id INT NOT NULL
    )
    PARTITION BY RANGE (store_id) (
        PARTITION p0 VALUES LESS THAN (6),
        PARTITION p1 VALUES LESS THAN (11),
        PARTITION p2 VALUES LESS THAN (16),
        PARTITION p3 VALUES LESS THAN MAXVALUE
    );
    

    根据TIMESTAMP范围:

    CREATE TABLE quarterly_report_status (
        report_id INT NOT NULL,
        report_status VARCHAR(20) NOT NULL,
        report_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    )
    PARTITION BY RANGE ( UNIX_TIMESTAMP(report_updated) ) (
        PARTITION p0 VALUES LESS THAN ( UNIX_TIMESTAMP('2008-01-01 00:00:00') ),
        PARTITION p1 VALUES LESS THAN ( UNIX_TIMESTAMP('2008-04-01 00:00:00') ),
        PARTITION p2 VALUES LESS THAN ( UNIX_TIMESTAMP('2008-07-01 00:00:00') ),
        PARTITION p3 VALUES LESS THAN ( UNIX_TIMESTAMP('2008-10-01 00:00:00') ),
        PARTITION p4 VALUES LESS THAN ( UNIX_TIMESTAMP('2009-01-01 00:00:00') ),
        PARTITION p5 VALUES LESS THAN ( UNIX_TIMESTAMP('2009-04-01 00:00:00') ),
        PARTITION p6 VALUES LESS THAN ( UNIX_TIMESTAMP('2009-07-01 00:00:00') ),
        PARTITION p7 VALUES LESS THAN ( UNIX_TIMESTAMP('2009-10-01 00:00:00') ),
        PARTITION p8 VALUES LESS THAN ( UNIX_TIMESTAMP('2010-01-01 00:00:00') ),
        PARTITION p9 VALUES LESS THAN (MAXVALUE)
    );
    

    添加COLUMNS关键字可定义非integer范围及多列范围,不过需要注意COLUMNS括号内只能是列名,不支持函数;多列范围时,多列范围必须呈递增趋势:

    根据DATEDATETIME范围:

    CREATE TABLE members (
        firstname VARCHAR(25) NOT NULL,
        lastname VARCHAR(25) NOT NULL,
        username VARCHAR(16) NOT NULL,
        email VARCHAR(35),
        joined DATE NOT NULL
    )
    PARTITION BY RANGE COLUMNS(joined) (
        PARTITION p0 VALUES LESS THAN ('1960-01-01'),
        PARTITION p1 VALUES LESS THAN ('1970-01-01'),
        PARTITION p2 VALUES LESS THAN ('1980-01-01'),
        PARTITION p3 VALUES LESS THAN ('1990-01-01'),
        PARTITION p4 VALUES LESS THAN MAXVALUE
    );
    

    根据多列范围:

    CREATE TABLE rc3 (
        a INT,
        b INT
    )
    PARTITION BY RANGE COLUMNS(a,b) (
        PARTITION p0 VALUES LESS THAN (0,10),
        PARTITION p1 VALUES LESS THAN (10,20),
        PARTITION p2 VALUES LESS THAN (10,30),
        PARTITION p3 VALUES LESS THAN (10,35),
        PARTITION p4 VALUES LESS THAN (20,40),
        PARTITION p5 VALUES LESS THAN (MAXVALUE,MAXVALUE)
     );
    

    List分区

    根据具体数值分区,每个分区数值不重叠,使用PARTITION BY LISTVALUES IN关键字。跟Range分区类似,不使用COLUMNS关键字时List括号内必须为整数字段名或返回确定整数的函数。

    CREATE TABLE employees (
        id INT NOT NULL,
        fname VARCHAR(30),
        lname VARCHAR(30),
        hired DATE NOT NULL DEFAULT '1970-01-01',
        separated DATE NOT NULL DEFAULT '9999-12-31',
        job_code INT,
        store_id INT
    )
    PARTITION BY LIST(store_id) (
        PARTITION pNorth VALUES IN (3,5,6,9,17),
        PARTITION pEast VALUES IN (1,2,10,11,19,20),
        PARTITION pWest VALUES IN (4,12,13,14,18),
        PARTITION pCentral VALUES IN (7,8,15,16)
    );
    

    数值必须被所有分区覆盖,否则插入一个不属于任何一个分区的数值会报错。

    mysql> CREATE TABLE h2 (
        ->   c1 INT,
        ->   c2 INT
        -> )
        -> PARTITION BY LIST(c1) (
        ->   PARTITION p0 VALUES IN (1, 4, 7),
        ->   PARTITION p1 VALUES IN (2, 5, 8)
        -> );
    Query OK, 0 rows affected (0.11 sec)
    
    mysql> INSERT INTO h2 VALUES (3, 5);
    ERROR 1525 (HY000): Table has no partition for value 3
    

    当插入多条数据出错时,如果表的引擎支持事务(Innodb),则不会插入任何数据;如果不支持事务,则出错前的数据会插入,后面的不会执行。

    可以使用IGNORE关键字忽略出错的数据,这样其他符合条件的数据会全部插入不受影响。

    mysql> TRUNCATE h2;
    Query OK, 1 row affected (0.00 sec)
    
    mysql> SELECT * FROM h2;
    Empty set (0.00 sec)
    
    mysql> INSERT IGNORE INTO h2 VALUES (2, 5), (6, 10), (7, 5), (3, 1), (1, 9);
    Query OK, 3 rows affected (0.00 sec)
    Records: 5  Duplicates: 2  Warnings: 0
    
    mysql> SELECT * FROM h2;
    +------+------+
    | c1   | c2   |
    +------+------+
    |    7 |    5 |
    |    1 |    9 |
    |    2 |    5 |
    +------+------+
    3 rows in set (0.00 sec)
    

    Range分区相同,添加COLUMNS关键字可支持非整数和多列。

    Hash分区

    Hash分区主要用来确保数据在预先确定数目的分区中平均分布,Hash括号内只能是整数列或返回确定整数的函数,实际上就是使用返回的整数对分区数取模。

    CREATE TABLE employees (
        id INT NOT NULL,
        fname VARCHAR(30),
        lname VARCHAR(30),
        hired DATE NOT NULL DEFAULT '1970-01-01',
        separated DATE NOT NULL DEFAULT '9999-12-31',
        job_code INT,
        store_id INT
    )
    PARTITION BY HASH(store_id)
    PARTITIONS 4;
    
    CREATE TABLE employees (
        id INT NOT NULL,
        fname VARCHAR(30),
        lname VARCHAR(30),
        hired DATE NOT NULL DEFAULT '1970-01-01',
        separated DATE NOT NULL DEFAULT '9999-12-31',
        job_code INT,
        store_id INT
    )
    PARTITION BY HASH( YEAR(hired) )
    PARTITIONS 4;
    

    Hash分区也存在与传统Hash分表一样的问题,可扩展性差。MySQL也提供了一个类似于一致Hash的分区方法-线性Hash分区,只需要在定义分区时添加LINEAR关键字,如果对实现原理感兴趣,可以查看官方文档

    CREATE TABLE employees (
        id INT NOT NULL,
        fname VARCHAR(30),
        lname VARCHAR(30),
        hired DATE NOT NULL DEFAULT '1970-01-01',
        separated DATE NOT NULL DEFAULT '9999-12-31',
        job_code INT,
        store_id INT
    )
    PARTITION BY LINEAR HASH( YEAR(hired) )
    PARTITIONS 4;
    

    Key分区

    按照KEY进行分区类似于按照HASH分区,除了HASH分区使用的用户定义的表达式,而KEY分区的 哈希函数是由MySQL 服务器提供。MySQL 簇(Cluster)使用函数MD5()来实现KEY分区;对于使用其他存储引擎的表,服务器使用其自己内部的 哈希函数,这些函数是基于与PASSWORD()一样的运算法则。

    Key分区与Hash分区很相似,只是Hash函数不同,定义时把Hash关键字替换成Key即可,同样Key分区也有对应与线性Hash的线性Key分区方法。

    CREATE TABLE tk (
        col1 INT NOT NULL,
        col2 CHAR(5),
        col3 DATE
    )
    PARTITION BY LINEAR KEY (col1)
    PARTITIONS 3;
    

    另外,当表存在主键或唯一索引时可省略Key括号内的列名,Mysql将按照主键-唯一索引的顺序选择,当找不到唯一索引时报错。

    子分区

    子分区是分区表中每个分区的再次分割。创建子分区方法:

    CREATE TABLE ts (id INT, purchased DATE)
        PARTITION BY RANGE( YEAR(purchased) )
        SUBPARTITION BY HASH( TO_DAYS(purchased) )
        SUBPARTITIONS 2 (
            PARTITION p0 VALUES LESS THAN (1990),
            PARTITION p1 VALUES LESS THAN (2000),
            PARTITION p2 VALUES LESS THAN MAXVALUE
        );
    

    CREATE TABLE ts (id INT, purchased DATE)
        PARTITION BY RANGE( YEAR(purchased) )
        SUBPARTITION BY HASH( TO_DAYS(purchased) ) (
            PARTITION p0 VALUES LESS THAN (1990) (
                SUBPARTITION s0
                    DATA DIRECTORY = '/disk0/data'
                    INDEX DIRECTORY = '/disk0/idx',
                SUBPARTITION s1
                    DATA DIRECTORY = '/disk1/data'
                    INDEX DIRECTORY = '/disk1/idx'
            ),
            PARTITION p1 VALUES LESS THAN (2000) (
                SUBPARTITION s2
                    DATA DIRECTORY = '/disk2/data'
                    INDEX DIRECTORY = '/disk2/idx',
                SUBPARTITION s3
                    DATA DIRECTORY = '/disk3/data'
                    INDEX DIRECTORY = '/disk3/idx'
            ),
            PARTITION p2 VALUES LESS THAN MAXVALUE (
                SUBPARTITION s4
                    DATA DIRECTORY = '/disk4/data'
                    INDEX DIRECTORY = '/disk4/idx',
                SUBPARTITION s5
                    DATA DIRECTORY = '/disk5/data'
                    INDEX DIRECTORY = '/disk5/idx'
            )
        );
    

    需要注意的是:每个分区的子分区数必须相同。如果在一个分区表上的任何分区上使用SUBPARTITION来明确定义任何子分区,那么就必须定义所有的子分区,且必须指定一个全表唯一的名字。

    分区表的使用及查询优化

    根据实际情况选择分区方法

    对现有表分区的原则与传统分表一样。

    传统的按照增量区间分表对应于分区的Range分区,比如对表的访问多是近期产生的新数据,历史数据访问较少,则可以按一定时间段(比如年或月)或一定数量(比如100万)对表分区,具体根据哪种取决于表索引结构。分区后最后一个分区即为近期产生的数据,当一段时间过后数据量再次变大,可对最后一个分区重新分区(REORGANIZE PARTITION)把一段时间(一年或一月)或一定数量(比如100万)的数据分离出去。

    传统的散列方法分表对应于分区的Hash/Key分区,具体方法上面已经介绍过。

    查询优化

    分区的目的是为了提高查询效率,如果查询范围是所有分区那么就说明分区没有起到作用,我们用explain partitions命令来查看SQL对于分区的使用情况。

    一般来说,就是在where条件中加入分区列。

    比如表salaries结构为:

    mysql> show create table salaries\G;
    *************************** 1. row ***************************
           Table: salaries
    Create Table: CREATE TABLE `salaries` (
      `emp_no` int(11) NOT NULL,
      `salary` int(11) NOT NULL,
      `from_date` date NOT NULL,
      `to_date` date NOT NULL,
      PRIMARY KEY (`emp_no`,`from_date`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    /*!50100 PARTITION BY RANGE (year(from_date))
    (PARTITION p1 VALUES LESS THAN (1985) ENGINE = InnoDB,
     PARTITION p2 VALUES LESS THAN (1986) ENGINE = InnoDB,
     PARTITION p3 VALUES LESS THAN (1987) ENGINE = InnoDB,
     PARTITION p4 VALUES LESS THAN (1988) ENGINE = InnoDB,
     PARTITION p5 VALUES LESS THAN (1989) ENGINE = InnoDB,
     PARTITION p6 VALUES LESS THAN (1990) ENGINE = InnoDB,
     PARTITION p7 VALUES LESS THAN (1991) ENGINE = InnoDB,
     PARTITION p8 VALUES LESS THAN (1992) ENGINE = InnoDB,
     PARTITION p9 VALUES LESS THAN (1993) ENGINE = InnoDB,
     PARTITION p10 VALUES LESS THAN (1994) ENGINE = InnoDB,
     PARTITION p11 VALUES LESS THAN (1995) ENGINE = InnoDB,
     PARTITION p12 VALUES LESS THAN (1996) ENGINE = InnoDB,
     PARTITION p13 VALUES LESS THAN (1997) ENGINE = InnoDB,
     PARTITION p14 VALUES LESS THAN (1998) ENGINE = InnoDB,
     PARTITION p15 VALUES LESS THAN (1999) ENGINE = InnoDB,
     PARTITION p16 VALUES LESS THAN (2000) ENGINE = InnoDB,
     PARTITION p17 VALUES LESS THAN (2001) ENGINE = InnoDB,
     PARTITION p18 VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */
    

    则下面的查询没有利用分区,因为partitions中包含了所有的分区:

    mysql> explain partitions select * from salaries where salary > 100000\G;
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: salaries
       partitions: p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11,p12,p13,p14,p15,p16,p17,p18
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 2835486
            Extra: Using where
    

    只有在where条件中加入分区列才能起到作用,过滤掉不需要的分区:

    mysql> explain partitions select * from salaries where salary > 100000 and from_date > '1998-01-01'\G;
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: salaries
       partitions: p15,p16,p17,p18
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 1152556
            Extra: Using where
    

    与普通搜索一样,在运算符左侧使用函数将使分区过滤失效,即使与分区函数想同也一样:

    mysql> explain partitions select * from salaries where salary > 100000 and year(from_date) > 1998\G;
    *************************** 1. row ***************************
               id: 1
      select_type: SIMPLE
            table: salaries
       partitions: p1,p2,p3,p4,p5,p6,p7,p8,p9,p10,p11,p12,p13,p14,p15,p16,p17,p18
             type: ALL
    possible_keys: NULL
              key: NULL
          key_len: NULL
              ref: NULL
             rows: 2835486
            Extra: Using where
    

    四、分区和分表的比较

    • 传统分表后,countsum等统计操作只能对所有切分表进行操作后之后在应用层再次计算得出最后统计数据。而分区表则不受影响,可直接统计。

    Queries involving aggregate functions such as SUM() and COUNT() can easily be parallelized. A simple example of such a query might be SELECT salesperson_id, COUNT(orders) as order_total FROM sales GROUP BY salesperson_id;. By “parallelized,” we mean that the query can be run simultaneously on each partition, and the final result obtained merely by summing the results obtained for all partitions.

    • 分区对原系统改动最小,分区只涉及数据库层面,应用层不需要做出改动。

    • 分区有个限制是主表的所有唯一字段(包括主键)必须包含分区字段,而分表没有这个限制。

    • 分表包括垂直切分和水平切分,而分区只能起到水平切分的作用。

    五、常用分库分表

    1 tddl介绍

    tddl主要分为三次,matrix、group、atom层;

    matrix层

    Sql解析->规则引擎计算->数据执行->合并结果

    group层

    读写分离、权重、写的HA切换、读的HA切换、slave节点

    atom层

    1 单个数据库的抽象

    2 jboss数据源, ip port 用户名密码都可 以动态修改

    3 Thread count模式,保护 业务的处理线程,超过指定值,保护启动。

    4 动态阻止某个SQL执行

    5 执行次数统计和限制

    tddl 唯一键生成方式

    目前基于tddl进行分库分表后,原本一个数据库上的自增id的结果,在分库分表下并不是全局唯一的. 所以,分库分表后需要有一种技术可以生成全局的唯一id.

    唯一键的生成方式必须具备:1)全局唯一;2)高可用;3)高性能;

    tddl主要使用数据库+内存的方式实现,在内存中进行分配 优势:简单高效 缺点:无法保证自增顺序如下,下面内步长1000:

    group value
    group_0 0
    group_1 1000
    group_2 2000
    group_3 3000

    当需要产生唯一键时,从上面4个group中随机选择一个,获取value+步长的id,例如,从group_1获取1000~1000+1000的id,批量获取,提高性能。获取之后,数据库的记录变为下面的格式:

    group value
    group_0 0
    group_1 5000
    group_2 2000
    group_3 3000
     

    每次获取之后,将对应group的值变为,value+group的个数*步长。

     

    展开全文
  • 当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从、优化索引,做很多操作时性能仍下降严重。此时就要考虑对其进行切分了,切分的目的就在于减少数据库的负担,缩短查询时间。 数据库分布式核心...

    一. 数据切分

    关系型数据库本身比较容易成为系统瓶颈,单机存储容量、连接数、处理能力都有限。当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从库、优化索引,做很多操作时性能仍下降严重。此时就要考虑对其进行切分了,切分的目的就在于减少数据库的负担,缩短查询时间。

    数据库分布式核心内容无非就是数据切分(Sharding),以及切分后对数据的定位、整合。数据切分就是将数据分散存储到多个数据库中,使得单一数据库中的数据量变小,通过扩充主机的数量缓解单一数据库的性能问题,从而达到提升数据库操作性能的目的。

    数据切分根据其切分类型,可以分为两种方式:垂直(纵向)切分和水平(横向)切分

     

    1、垂直(纵向)切分

    垂直切分常见有垂直分库和垂直分表两种。

    垂直分库就是根据业务耦合性,将关联度低的不同表存储在不同的数据库。做法与大系统拆分为多个小系统类似,按业务分类进行独立划分。与"微服务治理"的做法相似,每个微服务使用单独的一个数据库。如图:

    垂直分表是基于数据库中的"列"进行,某个表字段较多,可以新建一张扩展表,将不经常用或字段长度较大的字段拆分出去到扩展表中。在字段很多的情况下(例如一个大表有100多个字段),通过"大表拆小表",更便于开发与维护,也能避免跨页问题,MySQL底层是通过数据页存储的,一条记录占用空间过大会导致跨页,造成额外的性能开销。另外数据库以行为单位将数据加载到内存中,这样表中字段长度较短且访问频率较高,内存能加载更多的数据,命中率更高,减少了磁盘IO,从而提升了数据库性能。

    垂直切分的优点:

    • 解决业务系统层面的耦合,业务清晰

    • 与微服务的治理类似,也能对不同业务的数据进行分级管理、维护、监控、扩展等

    • 高并发场景下,垂直切分一定程度的提升IO、数据库连接数、单机硬件资源的瓶颈

    缺点:

    • 部分表无法join,只能通过接口聚合方式解决,提升了开发的复杂度

    • 分布式事务处理复杂

    • 依然存在单表数据量过大的问题(需要水平切分)

     

    2、水平(横向)切分

    当一个应用难以再细粒度的垂直切分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时候就需要进行水平切分了。

    水平切分分为库内分表和分库分表,是根据表内数据内在的逻辑关系,将同一个表按不同的条件分散到多个数据库或多个表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果。如图所示: 

     

    库内分表只解决了单一表数据量过大的问题,但没有将表分布到不同机器的库上,因此对于减轻MySQL数据库的压力来说,帮助不是很大,大家还是竞争同一个物理机的CPU、内存、网络IO,最好通过分库分表来解决。

    水平切分的优点:

    • 不存在单库数据量过大、高并发的性能瓶颈,提升系统稳定性和负载能力

    • 应用端改造较小,不需要拆分业务模块

    缺点:

    • 跨分片的事务一致性难以保证

    • 跨库的join关联查询性能较差

    • 数据多次扩展难度和维护量极大

    水平切分后同一张表会出现在多个数据库/表中,每个库/表的内容不同。几种典型的数据分片规则为:

    1、根据数值范围

    按照时间区间或ID区间来切分。例如:按日期将不同月甚至是日的数据分散到不同的库中;将userId为1~9999的记录分到第一个库,10000~20000的分到第二个库,以此类推。某种意义上,某些系统中使用的"冷热数据分离",将一些使用较少的历史数据迁移到其他库中,业务功能上只提供热点数据的查询,也是类似的实践。

    这样的优点在于:

    • 单表大小可控

    • 天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移

    • 使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题。

    缺点:

    • 热点数据成为性能瓶颈。连续分片可能存在数据热点,例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询

     

    2、根据数值取模

    一般采用hash取模mod的切分方式,例如:将 Customer 表根据 cusno 字段切分到4个库中,余数为0的放到第一个库,余数为1的放到第二个库,以此类推。这样同一个用户的数据会分散到同一个库中,如果查询条件带有cusno字段,则可明确定位到相应库去查询。

    优点:

    • 数据分片相对比较均匀,不容易出现热点和并发访问的瓶颈

    缺点:

    • 后期分片集群扩容时,需要迁移旧的数据(使用一致性hash算法能较好的避免这个问题)

    • 容易面临跨分片查询的复杂问题。比如上例中,如果频繁用到的查询条件中不带cusno时,将会导致无法定位数据库,从而需要同时向4个库发起查询,再在内存中合并数据,取最小集返回给应用,分库反而成为拖累。

    二. 分库分表带来的问题

    分库分表能有效的环节单机和单库带来的性能瓶颈和压力,突破网络IO、硬件资源、连接数的瓶颈,同时也带来了一些问题。下面将描述这些技术挑战以及对应的解决思路。 

    1、事务一致性问题

    分布式事务

    当更新内容同时分布在不同库中,不可避免会带来跨库事务问题。跨分片事务也是分布式事务,没有简单的方案,一般可使用"XA协议"和"两阶段提交"处理。

    分布式事务能最大限度保证了数据库操作的原子性。但在提交事务时需要协调多个节点,推后了提交事务的时间点,延长了事务的执行时间。导致事务在访问共享资源时发生冲突或死锁的概率增高。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平扩展的枷锁。

    最终一致性

    对于那些性能要求很高,但对一致性要求不高的系统,往往不苛求系统的实时一致性,只要在允许的时间段内达到最终一致性即可,可采用事务补偿的方式。与事务在执行中发生错误后立即回滚的方式不同,事务补偿是一种事后检查补救的措施,一些常见的实现方法有:对数据进行对账检查,基于日志进行对比,定期同标准数据来源进行同步等等。事务补偿还要结合业务系统来考虑。

     

    2、跨节点关联查询 join 问题

    切分之前,系统中很多列表和详情页所需的数据可以通过sql join来完成。而切分之后,数据可能分布在不同的节点上,此时join带来的问题就比较麻烦了,考虑到性能,尽量避免使用join查询。

    解决这个问题的一些方法:

    1)全局表

    全局表,也可看做是"数据字典表",就是系统中所有模块都可能依赖的一些表,为了避免跨库join查询,可以将这类表在每个数据库中都保存一份。这些数据通常很少会进行修改,所以也不担心一致性的问题。

    2)字段冗余

    一种典型的反范式设计,利用空间换时间,为了性能而避免join查询。例如:订单表保存userId时候,也将userName冗余保存一份,这样查询订单详情时就不需要再去查询"买家user表"了。

    但这种方法适用场景也有限,比较适用于依赖字段比较少的情况。而冗余字段的数据一致性也较难保证,就像上面订单表的例子,买家修改了userName后,是否需要在历史订单中同步更新呢?这也要结合实际业务场景进行考虑。

    3)数据组装

    在系统层面,分两次查询,第一次查询的结果集中找出关联数据id,然后根据id发起第二次请求得到关联数据。最后将获得到的数据进行字段拼装。

    4)ER分片

    关系型数据库中,如果可以先确定表之间的关联关系,并将那些存在关联关系的表记录存放在同一个分片上,那么就能较好的避免跨分片join问题。在1:1或1:n的情况下,通常按照主表的ID主键切分。如下图所示:

    这样一来,Data Node1上面的order订单表与orderdetail订单详情表就可以通过orderId进行局部的关联查询了,Data Node2上也一样。

     

    3、跨节点分页、排序、函数问题

    跨节点多库进行查询时,会出现limit分页、order by排序等问题。分页需要按照指定字段进行排序,当排序字段就是分片字段时,通过分片规则就比较容易定位到指定的分片;当排序字段非分片字段时,就变得比较复杂了。需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户。如图所示:

    上图中只是取第一页的数据,对性能影响还不是很大。但是如果取得页数很大,情况则变得复杂很多,因为各分片节点中的数据可能是随机的,为了排序的准确性,需要将所有节点的前N页数据都排序好做合并,最后再进行整体的排序,这样的操作时很耗费CPU和内存资源的,所以页数越大,系统的性能也会越差。

    在使用Max、Min、Sum、Count之类的函数进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回。如图所示:

     

    4、全局主键避重问题

    在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库自生成的ID无法保证全局唯一。因此需要单独设计全局主键,以避免跨库主键重复问题。有一些常见的主键生成策略:

    1)UUID

    UUID标准形式包含32个16进制数字,分为5段,形式为8-4-4-4-12的36个字符,例如:550e8400-e29b-41d4-a716-446655440000

    UUID是主键是最简单的方案,本地生成,性能高,没有网络耗时。但缺点也很明显,由于UUID非常长,会占用大量的存储空间;另外,作为主键建立索引和基于索引进行查询时都会存在性能问题,在InnoDB下,UUID的无序性会引起数据位置频繁变动,导致分页。

    2)结合数据库维护主键ID表

    在数据库中建立 sequence 表:

    复制代码

    
     
    1. CREATE TABLE `sequence` (  

    2.   `id` bigint(20) unsigned NOT NULL auto_increment,  

    3.   `stub` char(1) NOT NULL default '',  

    4.   PRIMARY KEY  (`id`),  

    5.   UNIQUE KEY `stub` (`stub`)  

    6. ) ENGINE=MyISAM;

    复制代码

    stub字段设置为唯一索引,同一stub值在sequence表中只有一条记录,可以同时为多张表生成全局ID。sequence表的内容,如下所示:

    +-------------------+------+  | id                | stub |  +-------------------+------+  | 72157623227190423 |    a |  +-------------------+------+

    使用 MyISAM 存储引擎而不是 InnoDB,以获取更高的性能。MyISAM使用的是表级别的锁,对表的读写是串行的,所以不用担心在并发时两次读取同一个ID值。

    当需要全局唯一的64位ID时,执行:

    
     
    1. REPLACE INTO sequence (stub) VALUES ('a');  

    2. SELECT LAST_INSERT_ID();

    这两条语句是Connection级别的,select last_insert_id() 必须与 replace into 在同一数据库连接下才能得到刚刚插入的新ID。

    使用replace into代替insert into好处是避免了表行数过大,不需要另外定期清理。

    此方案较为简单,但缺点也明显:存在单点问题,强依赖DB,当DB异常时,整个系统都不可用。配置主从可以增加可用性,但当主库挂了,主从切换时,数据一致性在特殊情况下难以保证。另外性能瓶颈限制在单台MySQL的读写性能。

    flickr团队使用的一种主键生成策略,与上面的sequence表方案类似,但更好的解决了单点和性能瓶颈的问题。

    这一方案的整体思想是:建立2个以上的全局ID生成的服务器,每个服务器上只部署一个数据库,每个库有一张sequence表用于记录当前全局ID。表中ID增长的步长是库的数量,起始值依次错开,这样能将ID的生成散列到各个数据库上。如下图所示:

    由两个数据库服务器生成ID,设置不同的auto_increment值。第一台sequence的起始值为1,每次步长增长2,另一台的sequence起始值为2,每次步长增长也是2。结果第一台生成的ID都是奇数(1, 3, 5, 7 ...),第二台生成的ID都是偶数(2, 4, 6, 8 ...)。

    这种方案将生成ID的压力均匀分布在两台机器上。同时提供了系统容错,第一台出现了错误,可以自动切换到第二台机器上获取ID。但有以下几个缺点:系统添加机器,水平扩展时较复杂;每次获取ID都要读写一次DB,DB的压力还是很大,只能靠堆机器来提升性能。

    可以基于flickr的方案继续优化,使用批量的方式降低数据库的写压力,每次获取一段区间的ID号段,用完之后再去数据库获取,可以大大减轻数据库的压力。如下图所示:

    还是使用两台DB保证可用性,数据库中只存储当前的最大ID。ID生成服务每次批量拉取6个ID,先将max_id修改为5,当应用访问ID生成服务时,就不需要访问数据库,从号段缓存中依次派发0~5的ID。当这些ID发完后,再将max_id修改为11,下次就能派发6~11的ID。于是,数据库的压力降低为原来的1/6。

    3)Snowflake分布式自增ID算法

    Twitter的snowflake算法解决了分布式系统生成全局ID的需求,生成64位的Long型数字,组成部分:

    • 第一位未使用

    • 接下来41位是毫秒级时间,41位的长度可以表示69年的时间

    • 5位datacenterId,5位workerId。10位的长度最多支持部署1024个节点

    • 最后12位是毫秒内的计数,12位的计数顺序号支持每个节点每毫秒产生4096个ID序列

    这样的好处是:毫秒数在高位,生成的ID整体上按时间趋势递增;不依赖第三方系统,稳定性和效率较高,理论上QPS约为409.6w/s(1000*2^12),并且整个分布式系统内不会产生ID碰撞;可根据自身业务灵活分配bit位。

    不足就在于:强依赖机器时钟,如果时钟回拨,则可能导致生成ID重复。

    综上

    结合数据库和snowflake的唯一ID方案,可以参考业界较为成熟的解法:Leaf——美团点评分布式ID生成系统,并考虑到了高可用、容灾、分布式下时钟等问题。

     

    5、数据迁移、扩容问题

    当业务高速发展,面临性能和存储的瓶颈时,才会考虑分片设计,此时就不可避免的需要考虑历史数据迁移的问题。一般做法是先读出历史数据,然后按指定的分片规则再将数据写入到各个分片节点中。此外还需要根据当前的数据量和QPS,以及业务发展的速度,进行容量规划,推算出大概需要多少分片(一般建议单个分片上的单表数据量不超过1000W)

    如果采用数值范围分片,只需要添加节点就可以进行扩容了,不需要对分片数据迁移。如果采用的是数值取模分片,则考虑后期的扩容问题就相对比较麻烦。

    三. 什么时候考虑切分

    下面讲述一下什么时候需要考虑做数据切分。

    1、能不切分尽量不要切分

    并不是所有表都需要进行切分,主要还是看数据的增长速度。切分后会在某种程度上提升业务的复杂度,数据库除了承载数据的存储和查询外,协助业务更好的实现需求也是其重要工作之一。

    不到万不得已不用轻易使用分库分表这个大招,避免"过度设计"和"过早优化"。分库分表之前,不要为分而分,先尽力去做力所能及的事情,例如:升级硬件、升级网络、读写分离、索引优化等等。当数据量达到单表的瓶颈时候,再考虑分库分表。

    2、数据量过大,正常运维影响业务访问

    这里说的运维,指:

    1)对数据库备份,如果单表太大,备份时需要大量的磁盘IO和网络IO。例如1T的数据,网络传输占50MB时候,需要20000秒才能传输完毕,整个过程的风险都是比较高的

    2)对一个很大的表进行DDL修改时,MySQL会锁住全表,这个时间会很长,这段时间业务不能访问此表,影响很大。如果使用pt-online-schema-change,使用过程中会创建触发器和影子表,也需要很长的时间。在此操作过程中,都算为风险时间。将数据表拆分,总量减少,有助于降低这个风险。

    3)大表会经常访问与更新,就更有可能出现锁等待。将数据切分,用空间换时间,变相降低访问压力

    3、随着业务发展,需要对某些字段垂直拆分

    举个例子,假如项目一开始设计的用户表如下:

    
     
    1. id                   bigint             #用户的ID

    2. name                 varchar            #用户的名字

    3. last_login_time      datetime           #最近登录时间

    4. personal_info        text               #私人信息

    5. .....                                   #其他信息字段

    在项目初始阶段,这种设计是满足简单的业务需求的,也方便快速迭代开发。而当业务快速发展时,用户量从10w激增到10亿,用户非常的活跃,每次登录会更新 last_login_name 字段,使得 user 表被不断update,压力很大。而其他字段:id, name, personal_info 是不变的或很少更新的,此时在业务角度,就要将 last_login_time 拆分出去,新建一个 user_time 表。

    personal_info 属性是更新和查询频率较低的,并且text字段占据了太多的空间。这时候,就要对此垂直拆分出 user_ext 表了。

    4、数据量快速增长

    随着业务的快速发展,单表中的数据量会持续增长,当性能接近瓶颈时,就需要考虑水平切分,做分库分表了。此时一定要选择合适的切分规则,提前预估好数据容量

    5、安全性和可用性

    鸡蛋不要放在一个篮子里。在业务层面上垂直切分,将不相关的业务的数据库分隔,因为每个业务的数据量、访问量都不同,不能因为一个业务把数据库搞挂而牵连到其他业务。利用水平切分,当一个数据库出现问题时,不会影响到100%的用户,每个库只承担业务的一部分数据,这样整体的可用性就能提高。

    四. 支持分库分表中间件

    站在巨人的肩膀上能省力很多,目前分库分表已经有一些较为成熟的开源解决方案:

    展开全文
  • 采用了读写分离主从复制(数据库读写分离方案,实现高性能数据库集群)的方案去应对,后来又面临了大并发写入的时候,系统数据库采用了分库分表的方案(数据库分库分表方案,优化大量并发写入所带来的性能问题),...
  • 怎么进行数据库分库分表

    千次阅读 2019-08-14 17:23:27
    当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从、优化索引,做很多操作时性能仍下降严重。此时就要考虑对其进行切分了,切分的目的就在于减少数据库的负担,缩短查询时间。 数据库分布式核心...
  • 数据库分库分表后可以带来很多好处,如解决数据库的水平扩展能力,提升读写能力。但是也带来了很多的限制,第一,查询时候,需要带上分区键,否则你找不到你要查询的数据在哪个库哪个表。第二,一些聚合类的查询...
  • 当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从、优化索引,做很多操作时性能仍下降严重。此时就要考虑对其进行切分了,切分的目的就在于减少数据库的负担,缩短查询时间。数据库分布式核心...
  • 现在将我们的各种经验以及架构实战分享出来,如果大家喜欢,就关注我,一起将技术学深学透,我会每一篇分享结束都会预告下一专题昨天我们讲解了数据库分库分表后我们怎么去生成主键唯一ID(数据库分库分表后,我们...
  • 我们需要接受失望,因为它是有限的;我们不会失去希望,因为它是无穷的。一、概述随着时间和业务的发展...二、需要解决问题2.1 原有事务由于分库分表之后,新表在另外一个数据库中,如何保证主库和分库的事务性是必...
  • 昨天我们分享了怎么不停机进行分库分表数据迁移(数据库分库分表,生产环境不停机数据迁移)后来有好多朋友问我,说他们的系统虽然也到了差不多分表的地步了,但是,不知道具体拆分多少张表,分多了又怕浪费公司资源...
  • 为什么分库分表 (1) Mysql提倡的表行数在百万量级,而实际我们经常遇到千万级和亿级数据的大表,使用分表可以提升查询效率。 (2) 在读写分离、一主多从的架构下,如果大量的写请求达到主库,当主库不堪重负时,使用...
  • 前言作为一个数据库,作为数据库中的...MyBatis实现分表最简单步骤既然文章的标题都这么写了,不如直接上干货来的比较实际,我们就先来看看如何实现最简单的分表。1、我们模拟用户表数据量超过千万(虽然实际不太可...
  • 采用了读写分离主从复制(数据库读写分离方案,实现高性能数据库集群)的方案去应对,后来又面临了大并发写入的时候,系统数据库采用了分库分表的方案(数据库分库分表方案,优化大量并发写入所带来的性能问题),...
  • 数据库怎么分库分表

    2020-11-11 15:30:36
    数据库瓶颈 不管是IO瓶颈还是CPU瓶颈,最终都会导致数据库的活跃连接数增加,进而逼近甚至达到数据库可承载的活跃连接数的阈值。在业务service来看, ...分库和垂直分表 第二种:网络IO瓶颈,请求的数
  • 数据库怎么分库分表数据库瓶颈 不管是IO瓶颈还是CPU瓶颈,最终都会导致数据库的活跃连接数增加,进而逼近甚至达到数据库可承载的活跃连接数的阈值。在业务service来看, 就是可用数据库连接少甚至无连接可用,...
  • 应聘者:“前后端分离啊,限流啊,分库分表啊。。” 面试官:"谈谈分库分表吧?" 应聘者:“bala。bala。bala。。” 面试官心理活动:这个仁兄讲的怎么这么像网上的博客抄的,容我再问问。 面试官:...
  • 主键唯一ID是自增的,现在好了,我们的数据被到多个的多个表里面了,如果我们还是使用之前的 主键自增策略 ,那么这样就会出现两个数据插入到了两个不同的表会出现相同的ID值,这时我们该怎么去使用呢?...
  • 读写分离主从复制(数据库读写分离方案,实现高性能数据库集群)的方案去应对,后来又面临了大并发写入的时候,系统数据库采用了分库分表的方案(数据库分库分表方案,优化大量并发写入所带来的性能问题),通过垂直...
  • 简单来说,数据的切分就是通过某种特定的条件,将我们存放在同一个数据库中的数据分散存放到多个数据库(主机)中,以达到分散单台设备负载的效果,即分库分表。 数据的切分根据其切分规则的类型,可以分...
  • 作者:尜尜人物https://www.cnblogs.com/littlecharacter/一、数据库瓶颈不管是IO瓶颈,还是CPU瓶颈,最终都会导致数据库的活跃连接数...
  • 来源:...在业务service来看, 就是可用数据库连接少甚至无连接可用,接下来就可以想象了(并发量、吞吐量、崩溃)。IO瓶颈第一种:磁盘读IO瓶颈,热点数据太多,数据库缓存放不下,每次...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 393
精华内容 157
关键字:

数据库分库分表怎么分的