zoukankan      html  css  js  c++  java
  • 分库分表基础知识总结

    为什么要分区,分表和分库?

    随着互联网产品在体量和规模上日益膨胀,无论是Oracle还是MySQL,都会第一时间面临来自磁盘、CPU和内存等单机瓶颈,为此,产品方除了需要不断购买成本难以控制的高规格服务器,还要面临不断迭代的在线数据迁移。在这种情况下,无论是海量的结构化数据还是快速成长的业务规模,都迫切需要一种水平扩展的方法将存储成本分摊到成本可控的商用服务器上。同时,也希望通过线性扩容降低全量数据迁移对线上服务带来的影响,分库分表方案便应运而生。

    数据库中的数据量不一定是可控的,在未进行分库分表的情况下,随着时间和业务的发展,库中的表会越来越多,表中的数据量也会越来越大,相应地,数据操作,增删改查的开销也会越来越大;另外,一台服务器的资源(CPU、磁盘、内存、IO等)是有限的,最终数据库所能承载的数据量、数据处理能力都将遭遇瓶颈。

    当一张的数据达到几百万时,查询一次所花的时间会变多,如果有联合查询的话,有可能会死在那儿了。分表的目的就在于此,减小数据库的负担,缩短查询时间。

    单机数据库可能出现的性能瓶颈有:

    • 单个表数据量越大,读写锁,插入操作重新建立索引效率越低
    • 单个库数据量太大(一个数据库数据量到1T-2T就是极限)
    • 单个数据库服务器压力过大
    • 读写速度遇到瓶颈(并发量几百)

    主从复制读写分离分离在一定程度上可以缓解数据库压力,但是主从复制也带来其他一系列性能瓶颈问题:

    • 写入无法扩展
    • 写入无法缓存
    • 复制延时
    • 锁表率上升
    • 表变大,缓存率下降

    存储演进

    单库单表

    单库单表是最常见的数据库设计,例如,有一张用户(user)表放在数据库db中,所有的用户都可以在db库中的user表中查到。

    单库多表

    随着用户数量的增加,user表的数据量会越来越大,当数据量达到一定程度的时候对user表的查询会渐渐的变慢,从而影响整个DB的性能。如果使用mysql, 还有一个更严重的问题是,当需要添加一列的时候,mysql会锁表,期间所有的读写操作只能等待。

    可以通过某种方式将user进行水平的切分,产生两个表结构完全一样的user_0000,user_0001等表,user_0000 + user_0001 + …的数据刚好是一份完整的数据。

    多库多表

    随着数据量增加也许单台DB的存储空间不够,随着查询量的增加单台数据库服务器已经没办法支撑。这个时候可以再对数据库进行水平拆分。

    什么是分区,分表和分库

    分区

    将一张表的数据分成N个区块,在逻辑上看最终只是一张表,但底层是由N个物理区块组成的。这些区块可以在同一个磁盘上,也可以在不同的磁盘上

    分表

    将一张表按一定的规则分解成N个具有独立存储空间的实体表。系统读写时需要根据定义好的规则得到对应的表名,然后操作它。数据库分表可以解决单表海量数据的查询性能问题。分表分为水平分表和垂直分表。将一个表结构分为多个表,然后,可以再同一个库里,也可以放到不同的库。

    分库

    一旦分表,一个库中的表会越来越多,这时需要分成多个数据库,把不同表放到不同的库中。

     

    分库分表往往是业务层实施的,分库分表后,为了满足某些特定业务功能,往往需要修改代码。传统的分库分表都是通过应用层逻辑实现的,对于数据库层面来说,都是普通的表和库。

    为什么要分区?

    一张表的查询速度已经慢到影响使用的时候:

    • sql经过优化
    • 数据量大
    • 表中的数据是分段的
    • 对数据的操作往往只涉及一部分数据,而不是所有的数据

    分区解决的问题

    • 主要可以提升查询效率
    • 分区的实现方式(简单)

    分区的优点体现在以下方面:

    • 和单个磁盘或者文件系统相比,可以存储更多数据
    • 优化查询,在Where子句中包含分区条件时,可以只扫描必要的一个或者多个分区来提高查询效率;像sum和count这类聚集函数的查询时,可以很容易地在每个分区上并行地处理,最终汇总所有分区的结果
    • 对于已经过期的数据,可以通过删除与这些数据有关的分区来快速删除数据
    • 跨多个磁盘来分散数据查询,以获得更大的查询吞吐量

    MySQL 5开始支持分区功能,使用场景:

    对于这种数据库比较多,但是并发不是很多的情况下,可以采用表分区。

    对于数据量比较大的,但是并发也比较高的情况下,可以采用分表和分区相结合。

    分区的其他知识

    分区是指根据一定的规则,把一个表分解成多个更小更易管理的部分,逻辑上只有一个表或一个索引,但是实际上该表可能由数个物理分区对象组成,每个分区都是一个独立的对象,每个分区可以独自处理,也可以作为表的一部分处理。分区对应用是完全透明的。

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

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

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

    也就是说,对于原表分区后,对于应用层来说可以不做变化,无需改变原有的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官方文档也为此限制特意做了举例和解释

    MySQL中分区类型

    RANGE分区

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

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

    注意,按上面的语法paitition by range (id)中,括号内的字段必须为整数类型,但是如果是时间类型的怎么办?可以用函数YEAR()和TO_DAYS()来转换,例如:

    create table emp ( 
      id int(11), 
      name varchar(100), 
      hired date
    ) 
    partition by range (YEAR(hired)) ( 
    partition p0 values less than (2017), 
    partition p1 values less than (2018), 
    partition p2 values less than MAXVALUE); 

    List分区

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

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

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

    可以使用IGNORE关键字忽略出错的数据,这样其他符合条件的数据会全部插入不受影响。与Range分区相同,添加COLUMNS关键字可支持非整数和多列。

    List分区时根据离散的值列表告诉数据库特定的值属于哪个分区,SQL定义语法:

    create table good ( 
      id int, 
      name varchar(255), 
      category int 
    ) 
    partition by list (category) ( 
    partition p0 values in (1,3,5), 
    partition p1 values in (2,4,6) 
    );

    按上述定义后,若插入category=7的记录,那么是会报错的,你必须把所有可能值都枚举完,不像range分区有MAXVALUE可以用。

    MySQL5.5及其之后的版本中List分区也支持非整数列。

    Hash分区

    Hash分区主要用来确保数据在预先确定数目的分区中平均分布,Hash括号内只能是整数列或返回确定整数的函数,实际上就是使用返回的整数对分区数取模。Hash分区也存在与传统Hash分表一样的问题,可扩展性差。MySQL也提供了一个类似于一致Hash的分区方法-线性Hash分区,只需要在定义分区时添加LINEAR关键字,如果对实现原理感兴趣,可以查看官方文档

    Hash分区主要用来分散热点读,确保数据在预先确定个数的分区中尽可能平均分布。对一个表执行Hash分区时,MySQL会对分区键应用一个散列函数,依以此确定数据应当放在分区中的哪个分区中。

    MySQL支持两种Hash分区:

    • 常规Hash分区:使用取模算法
    • 线性Hash分区(Linear Hash):使用一个线性的2 的幂的运算规则

    常规Hash分区语法:

    create table emp ( 
      id int, 
      name varchar(100), 
      hired date 
    ) engine = innodb 
    partition by hash(id) partitions 3;

    id为0的将在p0分区,id为1的将在p1分区....以此类推。

    常规Hash不适合需要灵活变动分区的需求,因为分区数增加或减少后都得重新计算分区,否则数据就错位了。

    为此我们可以用线性Hash分区:

    1 create table emp ( 
    2   id int, 
    3   name varchar(100), 
    4   hired date 
    5 ) engine = innodb 
    6 partition by linear hash(id) partitions 3; 

    可以看到,数据往哪个分区的计算方法稍有区别:假如有N个分区,要插入的数据的id为Q,那么找到下一个大于等于N的2的幂,记为M,那么分区下标X=Q&(M-1)。其实说简单点,原来常规Hash是直接对分区数取模,但是线性Hash就不是这样,而是找出最小的大于等于分区数的2的幂,对该数进行取模,比如分区数为6,那么模数为8,要插入的记录的d为10的话,那么分区下标就为 10%8 = 2。这样的好处就是,分区数增加或减少不一定要重新计算分区,因为改变的分区数可能并没有使模数发生变化,因此一条记录原来往哪个分区存,还是往哪个分区存,但是如果分区数变化使得模数变化(比如分区数从7变成9,模数从8变为16),那么是依然需要重新计算分区的。

    线性hash分区的优点是,在分区维护(增加、删除、合并和拆分分区)时,MySQL能够处理得更加迅速;缺点是,对比常规Hash分区,线性Hash分区各个分区数据分布并不是很均匀。

    Key分区

    按照KEY进行分区类似于按照HASH分区,除了HASH分区使用的用户定义的表达式,而KEY分区的哈希函数是由MySQL服务器提供。MySQL 簇(Cluster)使用函数MD5()来实现KEY分区;对于使用其他存储引擎的表,服务器使用其自己内部的哈希函数,这些函数是基于与PASSWORD()一样的运算法则。Key分区与Hash分区很相似,只是Hash函数不同,定义时把Hash关键字替换成Key即可,同样Key分区也有对应与线性Hash的线性Key分区方法。

    Key分区类似于Hash分区,只不过Hash分区允许使用用户自定义得表达式,而Key分区不允许,需要使用MySQL服务器提供的Hash函数;同时Hash分区只支持整数分区,而Key分区支持除了Blob和Text类型外的所有类型作为分区键。在有主键或者非空唯一键的情况下,创建Key分区时可以不指定分区键,MySQL会默认使用主键作为分区键,若没有主键则使用非空唯一键作为分区键。

    create table emp ( 
      id int, 
      name varchar(100), 
      hired date 
    ) engine = innodb 
    partition by key (id) partitions 4;

    Columns分区

    Columns分区是MySQL5.5引入的新的分区类型,解决了Range分区和List分区只支持整数列的问题,Columns分区支持的分区列类型为:

    整数:tinyint、smallint、mediumint、int和bigint

    日期时间:date和datetime

    字符串类型:char、varchar、binary和varbinary

    Columns分区仅支持一个或多个字段名作为分区键,不再支持表达式作为分区键。

    create table emp ( 
      id int, 
      name varchar(100), 
      hired date 
    ) 
    partition by range columns (hired) ( 
    partition p0 values less than ('2017-01-01'), 
    partition p1 values less than ('2018-01-01'), 
    partition p2 values less than MAXVALUE); 

    Columns分区支持多列分区:  

    create table aa ( 
      a int, 
      b int 
    ) 
    partition by range columns(a,b) ( 
    partition p0 values less than (0,10), 
    partition p1 values less than (10,10), 
    partition p2 values less than (10,20), 
    partition p3 values less than (10,35), 
    partition p4 values less than (10,MAXVALUE), 
    partition p5 values less than (MAXVALUE,MAXVALUE) 
    );

    分区键包含多列时,其比较是基于元组的比较,也就是基于字段组的比较,假设有两个元组(A1,B1)和(A2,B2),那么其比较规则为:

    (A1 < A2) OR ((A1 == A2) && (B1 < B2)) 

    子分区

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

    分区管理命令

    MySQL提供了添加、删除、重定义、合并和拆分分区的命令,这些操作都可以用ALTER TABLE命令来实现

    删除分区

    当你要删除一个分区的数据时,只需:

    ALTER TABLE table_name DROP PARTITION partition_name 

    即可删除该分区的数据,比delete高效

    其他知识点

    查询时,MySQL会根据条件判断需要扫描的分区,而不是全表扫描:

    在MySQL5.1版本中,Range、List、Hash分区都要求分区键必须是INT类型,或者通过表达式返回一个INT类型的数值,也就是说5.1只支持整数分区,唯一的例外就是分区类型为Key分区时,可以使用其它类型的列(BLOB和TEXT除外)作为分区键。如果一个表有主键/非空唯一键,那么分区键必须使用该主键/非空唯一键;如果没有主键/非空唯一键,那只要该列类型被分区类型允许,就可以使用。

    其他注意的点:

    • 普通的hash分区增加风区后,需要重新计算
    • 线性hash分区(了解)增加分区后,还是在原来的分区
    • 线性hash相对于hash分区没有那么均匀
    • Key分区用的比较少,也是hash分区

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

    对于所有分区,MySQL都允许使用Null值,再Range分区中,Null值被当作最小值来处理;再List分区中,Null值必须被枚举出来,否则就不允许使用插入Null值;再Hash/Key分区中,Null值被当作零值来处理。

    分区的查询和优化

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

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

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

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

    查询优化

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

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

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

    分区缺点

    所有数据还在一个表中,但物理存储根据一定的规则放在不同的文件中。这个是MySQL支持的功能,业务rd代码无需改动。

    看上去分区表还挺好的,为什么大部分互联网还是更多的选择自己分库分表来水平扩展呢?

    1. 分区表,分区键设计不太灵活,如果不走分区键,很容易出现全表锁
    2. 一旦数据量并发量上来,如果在分区表实施关联,就是一个灾难
    3. 自己分库分表,自己掌控业务场景与访问模式,可控。分区表,研发写了一个sql,都不确定mysql是怎么玩的,不太可控
    4. 运维的坑

    为什么要分表?

    分表的原因

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

    什么时候考虑分表?

    • 一张表的查询速度已经慢到影响使用的时候
    •  sql经过优化
    • 数据量大
    • 当频繁插入或者联合查询时,速度变慢
    •  一张表的查询速度已经慢到影响使用的时候。
    • 当频繁插入或者联合查询时,速度变慢。

    分表解决的问题

    • 分表后,单表的并发能力提高了,磁盘I/O性能也提高了,写操作效率提高了
    • 查询一次的时间短了
    • 数据分布在不同的文件,磁盘I/O性能提高
    • 读写锁影响的数据量变小
    • 插入数据库需要重新建立索引的数据减少
    • 分表的实现方式(复杂)
    • 需要业务系统配合迁移升级,工作量较大

    分表的实现需要业务结合实现和迁移,较为复杂。

    分表策略

    merge分表

    CREATE TABLE IF NOT EXISTS `alluser` ( 
      `id` int(11) NOT NULL AUTO_INCREMENT, 
      `name` varchar(50) DEFAULT NULL, 
      `sex` int(1) NOT NULL DEFAULT '0', 
      INDEX(id) 
    ) TYPE=MERGE UNION=(user1,user2) INSERT_METHOD=LAST AUTO_INCREMENT=1;

    这样就成功的将一张user表,分成了二个表,这个时候有一个问题,代码中的sql语句怎么办,以前是一张表,现在变成二张表了,代码改动很大,这样给程序员带来了很大的工作量,有没有好的办法解决这一点呢?办法是把以前的user表备份一下,然后删除掉,上面的操作中我建立了一个alluser表,只把这个alluser表的表名改成user就行了。但是,不是所有的mysql操作都能用的

    a,如果使用 alter table 来把 merge 表变为其它表类型,到底层表的映射就被丢失了。取而代之的,来自底层 myisam 表的行被复制到已更换的表中,该表随后被指定新类型。

    b,网上看到一些说replace不起作用,试了一下可以起作用的。

    c,一个 merge 表不能在整个表上维持 unique 约束。当你执行一个 insert,数据进入第一个或者最后一个 myisam 表(取决于 insert_method 选项的值)。mysql 确保唯一键值在那个 myisam 表里保持唯一,但不是跨集合里所有的表。

    d,当你创建一个 merge 表之时,没有检查去确保底层表的存在以及有相同的机构。当 merge 表被使用之时,mysql 检查每个被映射的表的记录长度是否相等,但这并不十分可靠。如果你从不相似的 myisam 表创建一个merge表,你非常有可能撞见奇怪的问题。

    优点:扩展性好,并且程序代码改动的不是很大

    缺点:这种方法的效果比第二种要差一点

    垂直拆分表

    何谓垂直切分,即将表按照功能模块、关系密切程度划分出来,部署到不同的库上。将本来可以在同一个表的内容,人为划分为多个表。所谓的本来,是指按照关系型数据库的第三范式要求,是应该在同一个表的。例如,我们会建立定义数据库workDB、商品数据库payDB、用户数据库userDB、日志数据库logDB等,分别用于存储项目数据定义表、商品定义表、用户数据表、日志数据表等。

    如userid,name,addr一个表,为了防止表过大,分成2个表。

    userid,name

    userid,addr

    原因:

    • Innodb主索引叶子节点存储着当前行的所有信息,所以减少字段可使内存加载更多行数据,有利于查询。
    • 受限于操作系统中的文件大小限制。

    原则:

    • 把大字段独立存储到一张表中
    • 把不常用的字段单独拿出来存储到一张表
    • 把经常在一起使用的字段可以拿出来单独存储到一张表
    • 把不常用或业务逻辑不紧密或存储内容比较多的字段分到新的表中可使表存储更多数据
    • 根据数据的活跃度进行分离,(因为不同活跃的数据,处理方式是不同的)

    垂直拆分标准:

    • 表的体积大于2G并且行数大于1千万
    • 表中包含有text,blob,varchar(1000)以上

    数据有时效性的,可以单独拿出来归档处理

     

    优势:降低高并发情况下,对于表的锁定。

    不足:对于单表来说,随着数据库的记录增多,读写压力将进一步增大。

    案例:

    对于一个博客系统,文章标题,作者,分类,创建时间等,是变化频率慢,查询次数多,而且最好有很好的实时性的数据,我们把它叫做冷数据。而博客的浏览量,回复数等,类似的统计信息,或者别的变化频率比较高的数据,我们把它叫做活跃数据。所以,在进行数据库结构设计的时候,就应该考虑分表,首先是纵向分表的处理。

    这样垂直分表后:

    首先存储引擎的使用不同,冷数据使用MyIsam 可以有更好的查询数据。活跃数据,可以使用Innodb ,可以有更好的更新速度。

    其次,对冷数据进行更多的从库配置,因为更多的操作是查询,这样来加快查询速度。对热数据,可以相对有更多的主库的横向分表处理。

    其实,对于一些特殊的活跃数据,也可以考虑使用memcache,redis之类的缓存,等累计到一定量再去更新数据库。或者mongodb一类的nosql 数据库,这里只是举例,就先不说这个。

    水平拆分表

    如果单表的IO压力大,可以考虑用水平分割,其原理就是通过hash算法,将一张表分为N多页,并通过一个新的表(总表),记录着每个页的的位置。假如一 个门户网站,它的数据库表已经达到了1000万条记录,那么此时如果通过select去查询,必定会效率低下(不做索引的前提下)。为了降低单表的读写 IO压力,通过水平分割,将这个表分成10个页,同时生成一个总表,记录各个页的信息,那么假如我查询一条id=100的记录,它不再需要全表扫描,而是 通过总表找到该记录在哪个对应的页上,然后再去相应的页做检索,这样就降低了IO压力。

    水平分表技术就是将一个表拆成多个表,比较常见的方式就是将表中的记录按照某种HASH算法进行拆分,同时,这种分区方法也必须对前端的应用程序中的 SQL进行修改方能使用,而且对于一个SQL语句,可能会修改两个表,那么你必须要修改两个SQL语句来完成你这个逻辑的事务,会使得逻辑判断越来越复 杂,这样会增加程序的维护代价,所以我们要避免这样的情况出现。

    何谓水平切分,当一个表中的数据量过大时,我们可以把该表的数据按照某种规则,例如userID散列、按性别、按省,进行划分,然后存储到多个结构相同的表,和不同的库上。例如,我们的userDB中的用户数据表中,每一个表的数据量都很大,就可以把userDB切分为结构相同的多个userDB:part0DB、part1DB等,再将userDB上的用户数据表userTable,切分为很多userTable:userTable0、userTable1等,然后将这些表按照一定的规则存储到多个userDB上。字面意思,就可以看出来,是把大的表结构,横向切割为同样结构的不同表,如,用户信息表,user_1,user_2 等。表结构是完全一样,但是,根据某些特定的规则来划分的表,如根据用户ID来取模划分。

    原因:

    • 根据数据量的规模来划分,保证单表的容量不会太大,从而来保证单表的查询等处理能力。
    • 随着数据量的增大,table行数巨大,查询的效率越来越低。
    • 同样受限于操作系统中的文件大小限制,数据量不能无限增加,当到达一定容量时,需要水平切分以降低单表(文件)的大小。

    切分原则:

    增量区间或散列或其他业务逻辑。

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

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

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

    全局主键问题:

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

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

    一致性Hash:

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

    水平分表遇到的问题:

    1. 跨表直接连接查询无法进行
    2. 我们需要统计数据的时候
    3. 如果数据持续增长,达到现有分表的瓶颈,需要增加分表,此时会出现数据重新排列的情况

    案例:同上面的例子,博客系统。当博客的量达到很大时候,就应该采取横向分割来降低每个单表的压力,来提升性能。例如博客的冷数据表,假如分为100个表,当同时有100万个用户在浏览时,如果是单表的话,会进行100万次请求,而现在分表后,就可能是每个表进行1万个数据的请求(因为,不可能绝对的平均,只是假设),这样压力就降低了很多很多。

    为什么要分库

    分表和分区都是基于同一个数据库里的数据分离技巧,对数据库性能有一定提升,对MySQL数据库的吞吐量无质的变化。一旦分表,一个库中的表会越来越多。分表能够解决单表数据量过大带来的查询效率下降的问题,但是,却无法给数据库的并发处理能力带来质的提升。面对高并发的读写访问,当数据库master服务器无法承载写操作压力时,不管如何扩展slave服务器,此时都没有意义了。因此,我们必须换一种思路,对数据库进行拆分,从而提高数据库写入能力,这就是所谓的分库。当业务系统的数据容量接近或超过单台X86服务器的容量、QPS/TPS接近或超过单个MySQL数据库实例的处理极限等,则重点在于扩展MySQL数据库的吞吐量和数据处理量。此时,往往是采用垂直和水平结合的数据拆分方法,把数据服务和数据存储分布到多台MySQL数据库服务器上。

    分库只是一个通俗说法,更标准名称是数据分片,采用类似分布式数据库理论指导的方法实现,对应用程序达到数据服务的全透明和数据存储的全透明。

    分库可以解决单台数据库的并发访问压力问题

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

    database不是文件,只起到namespace的作用,所以MySQL对database大小当然也是没有限制的,而且对里面的表数量也没有限制。为了解决单台服务器的性能问题,当单台数据库服务器无法支撑当前的数据量时,就需要根据业务逻辑紧密程度把表分成几撮,分别放在不同的数据库服务器中以降低单台服务器的负载。

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

    比如一个论坛系统的数据库因当前服务器性能无法满足需要进行分库。先垂直切分,按业务逻辑把用户相关数据表比如用户信息、积分、用户间私信等放入user数据库;论坛相关数据表比如板块,帖子,回复等放入forum数据库,两个数据库放在不同服务器上。拆分后表往往不可能完全无关联,比如帖子中的发帖人、回复人这些信息都在user数据库中。未拆分前可能一次联表查询就能获取当前帖子的回复、发帖人、回复人等所有信息,拆分后因为跨数据库无法联表查询,只能多次查询获得最终数据。

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

    与分表策略相似,分库可以采用通过一个关键字取模的方式,来对数据访问进行路由。

    分库策略

    分库策略与分表策略的实现很相似,最简单的都是可以通过取模的方式进行路由。

    分库也可以按照业务分库,比如订单表和库存表在两个库,要注意处理好跨库事务。

    分表和分库同时实现。

    分库分表的策略相对于前边两种复杂一些,一种常见的路由策略如下:

    1. 中间变量=user_id%(库数量*每个库的表数量)
    2. 库序号=取整(中间变量/每个库的表数量)
    3. 表序号=中间变量%每个库的表数量

    例如:数据库有256个,每一个库中有1024个数据表,用户的user_id=262145,按照上述的路由策略,可得:

    1. 中间变量=262145%(256*1024)=1
    2. 库序号=取整(1/1024)= 0
    3. 表序号=1%1024=1

    这样的话,对于user_id=262145,将被路由到第0个数据库的第1个表中。

    垂直拆分

    将系统中不存在关联关系或者需要join的表可以放在不同的数据库不同的服务器中。

    按照业务垂直划分。比如:可以按照业务分为资金、会员、订单三个数据库。需要解决的问题:跨数据库的事务、join查询等问题。

    分数据库设计,将可能从压力性能上会提升几个档次,当然单次执行效率不会比单数据库来的高的,毕竟存在着数据库切换的效率问题。分库以及主从数据库搭 配是可以比较好改善数据库并发瓶颈的方案。原则:大数据量,分库;大访问量,主从。很多时候,都是这两者并行 ( 本文不讨论 cache) 。

    如果要实现分库以及主从关系,那么数据库服务器数量将是非常可观,在应用程序中随时切换到某一台服务器,将是非常头痛的问题,配置更换,变量名称,是不是会有一大堆呢?如何寻找更好的解决方案将是本文谈论的话题。

    首先是分库使得数据库颇多的问题。什么情况下分库?或许有些人还搞不明白为什么要分库,我就简要说一下自己的经验猜测。比如一个博客程序,一般设计是 将日志存放在一张日志表中。假设是一个多用户博客,那么将会关联一个 uid ,如果数据量不大,这样设计是没有问题的,但是当日志量巨大,一天有几十万条日 志记录录入的时候,而且访问量也比较可观的时候,我想不可能每个用户来访问日志列表,都去从这包含几千万条日志记录的数据表中去找那么几条,效率可见一 斑。这个时候就该考虑到分库的问题。如何分?有一个很简单的分表方法,即,根据 uid 段,将日志记录在各个数据库中,当然,这个分布还是需要根据以往统计 结果做出调整的,因为用户日志分布肯定不是均匀的。设置好 uid 段,然后根据 uid 索引到指定数据库配置,创建一个数据库对象即可。配置信息可能如下:

    垂直分库指的是根据应用来分数据库,比如博客一个数据库,论坛一个数据库。水平分库是指,根据某些规则,将同一个应用 / 表的数据分布在不同的库上。比如根据用户 ID 把用户的博客文章分布在 5 个数据库上。

    面对当今大数据存储,设想当mysql中一个表的总记录超过1000W,会出现性能的大幅度下降吗?

    答案是肯定的,一个表的总记录超过1000W,在操作系统层面检索也是效率非常低的

    水平拆分

    例如,大部分的站点。数据都是和用户有关,那么可以根据用户,将数据按照用户水平拆分。

    按照规则划分,一般水平分库是在垂直分库之后的。比如每天处理的订单数量是海量的,可以按照一定的规则水平划分。需要解决的问题:数据路由、组装。

    MySQL 使用自增ID主键和UUID 作为主键的优劣比较详细过程(从百万到千万表记录测试)

    单实例或者单节点组:

    经过500W、1000W的单机表测试,自增ID相对UUID来说,自增ID主键性能高于UUID,磁盘存储费用比UUID节省一半的钱。所以在单实例上或者单节点组上,使用自增ID作为首选主键。

    分布式架构场景:

    20个节点组下的小型规模的分布式场景,为了快速实现部署,可以采用多花存储费用、牺牲部分性能而使用UUID主键快速部署;

    20到200个节点组的中等规模的分布式场景,可以采用自增ID+步长的较快速方案。

    200以上节点组的大数据下的分布式场景,可以借鉴类似twitter雪花算法构造的全局自增ID作为主键。

    分区,分表,分库选择哪一个?

    如果你的单机性能很低了,那可以尝试分库。分库,业务透明,在物理实现上分成多个服务器,不同的分库在不同服务器上。分区可以把表分到不同的硬盘上,但不能分配到不同服务器上。一台机器的性能是有限制的,用分库可以解决单台服务器性能不够,或者成本过高问题。

    当分区之后,表还是很大,处理不过来,这时候可以用分库。

    应该使用哪一种方式来实施数据库分库分表,这要看数据库中数据量的瓶颈所在,并综合项目的业务类型进行考虑。

    如果数据库是因为表太多而造成海量数据,并且项目的各项业务逻辑划分清晰、低耦合,那么规则简单明了、容易实施的垂直切分必是首选。

    而如果数据库中的表并不多,但单表的数据量很大、或数据热度很高,这种情况之下就应该选择水平切分,水平切分比垂直切分要复杂一些,它将原本逻辑上属于一体的数据进行了物理分割,除了在分割时要对分割的粒度做好评估,考虑数据平均和负载平均,后期也将对项目人员及应用程序产生额外的数据管理负担。

    在现实项目中,往往是这两种情况兼而有之,这就需要做出权衡,甚至既需要垂直切分,又需要水平切分。我们的游戏项目便综合使用了垂直与水平切分,我们首先对数据库进行垂直切分,然后,再针对一部分表,通常是用户数据表,进行水平切分。

    总结

    总的来说,优先考虑分区  -->  当分区不能满足需求时,开始考虑分表,合理的分表对效率的提升会优于分区 --> 最后才是分库。

    分区和分表的区别与联系

    分区和分表的目的都是减少数据库的负担,提高表的增删改查效率。分区只是一张表中的数据的存储位置发生改变,分表是将一张表分成多张表。当访问量大,且表数据比较大时,两种方式可以互相配合使用。当访问量不大,但表数据比较多时,可以只进行分区。

    常见分区分表的规则策略(类似)

    • Range(范围)
    • Hash(哈希)
    • 按照时间拆分
    • Hash之后按照分表个数取模

    在认证库中保存数据库配置,就是建立一个DB,这个DB单独保存user_id到DB的映射关系

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

    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.

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

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

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

    实现方式上

    a)mysql的分表是真正的分表,一张表分成很多表后,每一个小表都是完正的一张表,都对应三个文件,一个.MYD数据文件,.MYI索引文件,.frm表结构文件。

    [root@BlackGhost test]# ls |grep user

    alluser.MRG

    alluser.frm

    user1.MYD

    user1.MYI

    user1.frm

    user2.MYD

    user2.MYI

    user2.frm 

    简单说明一下,上面的分表是利用了merge存储引擎(分表的一种),alluser是总表,下面有二个分表,user1,user2。他们二个都是独立的表,取数据的时候,我们可以通过总表来取。这里总表是没有.MYD,.MYI这二个文件的,也就是说,总表他不是一张表,没有数据,数据都放在分表里面。我们来看看.MRG到底是什么东西

    [root@BlackGhost test]# cat alluser.MRG |more

    user1

    user2

    #INSERT_METHOD=LAST

    从上面我们可以看出,alluser.MRG里面就存了一些分表的关系,以及插入数据的方式。可以把总表理解成一个外壳,或者是联接池。

    b)分区不一样,一张大表进行分区后,他还是一张表,不会变成二张表,但是他存放数据的区块变多了。

    [root@BlackGhost test]# ls |grep aa

    aa#P#p1.MYD

    aa#P#p1.MYI

    aa#P#p3.MYD

    aa#P#p3.MYI

    aa.frm

    aa.par

    从上面我们可以看出,aa这张表,分为二个区,p1和p3,本来是三个区,被我删了一个区。我们都知道一张表对应三个文件.MYD,.MYI,.frm。分区呢根据一定的规则把数据文件和索引文件进行了分割,还多出了一个.par文件,打开.par文件后你可以看出他记录了,这张表的分区信息,根分表中的.MRG有点像。分区后,还是一张,而不是多张表。

    如orderid,userid,ordertime,.....

    ordertime<2015-01-01 #p0

    ordertime<2015-04-01 #p1

    ordertime<2015-07-01 #p2

    ordertime<2015-10-01 #p3

    ordertime<2016-01-01 #p4

    按照时间分区。大部分只查询最近的订单数据,那么大部分只访问一个分区,比整个表小多了,数据库可以更加好的缓存,性能也提高了。这个是数据库分的,应用程序透明,无需修改。

    数据处理上

    a)分表后,数据都是存放在分表里,总表只是一个外壳,存取数据发生在一个一个的分表里面。看下面的例子:

    select * from alluser where id='12'表面上看,是对表alluser进行操作的,其实不是的。是对alluser里面的分表进行了操作。

    b)分区呢,不存在分表的概念,分区只不过把存放数据的文件分成了许多小块,分区后的表呢,还是一张表。数据处理还是由自己来完成。

    提高性能上

    a)分表后,单表的并发能力提高了,磁盘I/O性能也提高了。并发能力为什么提高了呢,因为查寻一次所花的时间变短了,如果出现高并发的话,总表可以根据不同的查询,将并发压力分到不同的小表里面。磁盘I/O性能怎么搞高了呢,本来一个非常大的.MYD文件现在也分摊到各个小表的.MYD中去了。

    b)mysql提出了分区的概念,我觉得就想突破磁盘I/O瓶颈,想提高磁盘的读写能力,来增加mysql性能。

    在这一点上,分区和分表的测重点不同,分表重点是存取数据时,如何提高mysql并发能力上;而分区呢,如何突破磁盘的读写能力,从而达到提高mysql性能的目的。

    实现的难易度上

    a)分表的方法有很多,用merge来分表,是最简单的一种方式。这种方式根分区难易度差不多,并且对程序代码来说可以做到透明的。如果是用其他分表方式就比分区麻烦了。

    b)分区实现是比较简单的,建立分区表,根建平常的表没什么区别,并且对开代码端来说是透明的。

    mysql分表和分区有什么联系呢:

    1. 都能提高mysql的性高,在高并发状态下都有一个良好的表面。
    2. 分表和分区不矛盾,可以相互配合的,对于那些大访问量,并且表数据比较多的表,我们可以采取分表和分区结合的方式(如果merge这种分表方式,不能和分区配合的话,可以用其他的分表试),访问量不大,但是表数据很多的表,我们可以采取分区的方式等。

     

    分库、分表带来的后遗症

    分库、分表会带来很多的后遗症,会使整个系统架构变的复杂。分的好与不好最关键就是如何寻找那个Sharding key,如果这个Sharding key刚好是业务维度上的分界线就会直接提升性能和改善复杂度,否则就会有各种脚手架来支撑,系统也就会变得复杂。

    比如订单系统中的用户__ID__、订单__type__、商家__ID__、渠道__ID__,优惠券系统中的批次__ID__、渠道__ID__、机构__ID__ 等,这些都是潜在的Sharding key。

    如果刚好有这么一个Sharding key存在后面处理路由(routing)就会很方便,否则就需要一些大而全的索引表来处理OLAP的查询。

    一旦Sharding之后首先要面对的问题就是查询时排序分页问题。

    跨库join的问题

    在拆分之前,系统中很多列表和详情页所需的数据是可以通过sql join来完成的。而拆分后,数据库可能是分布式在不同实例和不同的主机上,join将变得非常麻烦。而且基于架构规范,性能,安全性等方面考虑,一般是禁止跨库join的。那该怎么办呢?首先要考虑下垂直分库的设计问题,如果可以调整,那就优先调整。如果无法调整的情况,下面将结合以往的实际经验,总结几种常见的解决思路,并分析其适用场景。

    全局表

    所谓全局表,就是有可能系统中所有模块都可能会依赖到的一些表。比较类似我们理解的“数据字典”。为了避免跨库join查询,我们可以将这类表在其他每个数据库中均保存一份。同时,这类数据通常也很少发生修改(甚至几乎不会),所以也不用太担心“一致性”问题。

    字段冗余

    这是一种典型的反范式设计,在互联网行业中比较常见,通常是为了性能来避免join查询。

    举个电商业务中很简单的场景:

    “订单表”中保存“卖家Id”的同时,将卖家的“Name”字段也冗余,这样查询订单详情的时候就不需要再去查询“卖家用户表”。

    字段冗余能带来便利,是一种“空间换时间”的体现。但其适用场景也比较有限,比较适合依赖字段较少的情况。最复杂的还是数据一致性问题,这点很难保证,可以借助数据库中的触发器或者在业务代码层面去保证。当然,也需要结合实际业务场景来看一致性的要求。就像上面例子,如果卖家修改了Name之后,是否需要在订单信息中同步更新呢?

    数据同步

    A库中的tab_a表和B库中tbl_b有关联,可以定时将指定的表做同步。当然,同步本来会对数据库带来一定的影响,需要性能影响和数据时效性中取得一个平衡。这样来避免复杂的跨库查询。

    系统层组装

    在系统层面,通过调用不同模块的组件或者服务,获取到数据并进行字段拼装。说起来很容易,但实践起来可真没有这么简单,尤其是数据库设计上存在问题但又无法轻易调整的时候。具体情况通常会比较复杂。组装的时候要避免循环调用服务,循环RPC,循环查询数据库,最好一次性返回所有信息,在代码里做组装。

    在执行了分库分表之后,难以避免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上,这时,表的关联操作将受到限制,我们无法join位于不同分库的表,也无法join分表粒度不同的表,结果原本一次查询能够完成的业务,可能需要多次查询才能完成。

    跨库事务(分布式事务)的问题

    分布式全局唯一ID

    我们往往直接使用数据库自增特性来生成主键ID,这样确实比较简单。而在分库分表的环境中,数据分布在不同的分片上,不能再借助数据库自增长特性直接生成,否则会造成不同分片上的数据表主键会重复。简单介绍几种ID生成算法。

    Twitter的Snowflake(又名“雪花算法”)

    UUID/GUID(一般应用程序和数据库均支持)

    MongoDB ObjectID(类似UUID的方式)

    Ticket Server(数据库生存方式,Flickr采用的就是这种方式)

    其中,Twitter 的Snowflake算法生成的是64位唯一Id(由41位的timestamp+ 10位自定义的机器码+ 13位累加计数器组成)。

    分片字段该如何选择

    在开始分片之前,我们首先要确定分片字段(也可称为“片键”)。很多常见的例子和场景中是采用ID或者时间字段进行拆分。这也并不绝对的,我的建议是结合实际业务,通过对系统中执行的sql语句进行统计分析,选择出需要分片的那个表中最频繁被使用,或者最重要的字段来作为分片字段。

    归并排序

    原来在一个数据库表中处理排序分页是比较方便的,Sharding之后就会存在多个数据源,这里我们将多个数据源统称为分片。

    想要实现多分片排序分页就需要将各个片的数据都汇集起来进行排序,就需要用到归并排序算法。这些数据在各个分片中可以做到有序的(输出有序),但是整体上是无序的。

    我们看个简单的例子:

    shard node 1: {1、3、5、7、9}

    shard node 2: {2、4、6、8、10}

    这是做奇偶Sharding 的两个分片,我们假设分页参数设置为每页4条,当前第1页,参数如下:

    pageParameter:pageSize:4、currentPage:1

    最乐观情况下我们需要分别读取两个分片节点中的前两条:

    shard node 1: {1、3}

    shard node 2: {2、4}

    排序完刚好是{1、2、3、4},但是这种场景基本上不太可能出现,假设如下分片节点数据:

    shard node 1: {7、9、11、13、15}

    shard node 2: {2、4、6、8、10、12、14}

    我们还是按照读取每个节点前两条肯定是错误的,因为最悲观情况下(也是最真实的情况)就是排序完后所有的数据都来自一个分片。所以我们需要读取每个节点的pageSize大小的数据出来才有可能保证数据的正确性。

    这个例子只是假设我们的查询条件输出的数据刚好是均等的,真实的情况一定是各种各样的查询条件筛选出来的数据集合,此时这个数据一定不是这样的排列方式,最真实的就是最后者这种结构。

    我们以此类推,如果我们的currentPage:1000,那么会出现什么问题?我们需要每个Sharding node读取 4000(1000*4=4000) 条数据出来排序,因为最悲观情况下有可能所有的数据均来自一个Sharding node 。

    这样无限制的翻页下去,处理排序分页的机器肯定会内存撑爆,就算不撑爆一定会触发性能瓶颈。

    这个简单的例子用来说明分片之后,排序分页带来的现实问题,这也有助于我们理解分布式系统在做多节点排序分页时为什么有最大分页限制。

    深分页性能问题

    面对这种问题,我们需要改变查询条件重新分页。一个庞大的数据集会通过多种方式进行数据拆分,按机构、按时间、按渠道等等,拆分在不同的数据源中。一般的深分页问题我们可以通过改变查询条件来平滑解决,但是这种方案并不能解决所有的业务场景。

    比如,我们有一个订单列表,从C端用户来查询自己的订单列表数据量不会很大,但是运营后台系统可能面对全平台的所有订单数据量,所以数据量会很大。

    改变查询条件有两种:

    第一种条件是显示设置,尽量缩小查询范围,这种设置一般都会优先考虑如时间范围、支付状态、配送状态等等,通过多个叠加条件就可以横竖过滤出很小一部分数据集;

    第二种条件为隐式设置,比如订单列表通常是按照订单创建时间来排序,那么当翻页到限制的条件时,我们可以改变这个时间。

    Sharding node 1:orderID createDateTime

    100000  2018-01-10 10:10:10

    200000  2018-01-10 10:10:11

    300000  2018-01-10 10:10:12

    400000  2018-01-10 10:10:13

    500000  2018-01-20 10:10:10

    600000  2018-01-20 10:10:11

    700000  2018-01-20 10:10:12

    Sharding node 2:orderID createDateTime

    110000  2018-01-11 10:10:10

    220000  2018-01-11 10:10:11

    320000  2018-01-11 10:10:12

    420000  2018-01-11 10:10:13

    520000  2018-01-21 10:10:10

    620000  2018-01-21 10:10:11

    720000  2018-01-21 10:10:12

    我们假设上面是一个订单列表,orderID订单号大家就不要在意顺序性了。因为Sharding之后所有的orderID都会由发号器统一发放,多个集群多个消费者同时获取,但是创建订单的速度是不一样的,所以顺序性已经不存在了。

    上面的两个Sharding node基本上订单号是交叉的,如果按照时间排序node 1和node 2是要交替获取数据。

    比如我们的查询条件和分页参数:

    where createDateTime>'2018-01-11 00:00:00'

    pageParameter:pageSize:5、currentPage:1

    获取的结果集为:

    orderID createDateTime

    100000  2018-01-10 10:10:10

    200000  2018-01-10 10:10:11

    300000  2018-01-10 10:10:12

    400000  2018-01-10 10:10:13

    110000  2018-01-11 10:10:10

    前面4条记录来自node 1后面1条数据来自node 2 ,整个排序集合为:

    Sharding node 1:orderID createDateTime

    100000  2018-01-10 10:10:10

    200000  2018-01-10 10:10:11

    300000  2018-01-10 10:10:12

    400000  2018-01-10 10:10:13

    500000  2018-01-20 10:10:10

    Sharding node 2:orderID createDateTime

    110000  2018-01-11 10:10:10

    220000  2018-01-11 10:10:11

    320000  2018-01-11 10:10:12

    420000  2018-01-11 10:10:13

    520000  2018-01-21 10:10:10

    按照这样一直翻页下去每翻页一次就需要在node 1 、node 2多获取5条数据。这里我们可以通过修改查询条件来让整个翻页变为重新查询。

    where createDateTime>'2018-01-11 10:10:13'

    因为我们可以确定在‘2018-01-11 10:10:13’时间之前所有的数据都已经查询过,但是为什么时间不是从‘2018-01-21 10:10:10’开始,因为我们要考虑并发情况,在1s内会有多个订单进来。

    这种方式是实现最简单,不需要借助外部的计算来支撑。这种方式有一个问题就是要想重新计算分页的时候不丢失数据就需要保留原来一条数据,这样才能知道开始的时间在哪里,这样就会在下次的分页中看到这条时间。但是从真实的深分页场景来看也可以忽略,因为很少有人会一页一页一直到翻到500页,而是直接跳到最后几页,这个时候就不存在那个问题。

    如果非要精准控制这个偏差就需要记住区间,或者用其他方式来实现了,比如全量查询表、Sharding索引表、最大下单tps值之类的,用来辅助计算。(可以利用数据同步中间件建立单表多级索引、多表多维度索引来辅助计算。我们使用到的数据同步中间件有datax、yugong、otter、canal可以解决全量、增量同步问题)。

    事务问题

    在执行分库分表之后,由于数据存储到了不同的库上,数据库事务管理出现了困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。

    额外的数据管理负担和数据运算压力

    额外的数据管理负担,最显而易见的就是数据的定位问题和数据的增删改查的重复执行问题,这些都可以通过应用程序解决,但必然引起额外的逻辑运算,例如,对于一个记录用户成绩的用户数据表userTable,业务要求查出成绩最好的100位,在进行分表之前,只需一个order by语句就可以搞定,但是在进行分表之后,将需要n个order by语句,分别查出每一个分表的前100名用户数据,然后再对这些数据进行合并计算,才能得出结果。

    一些注意事项

    1、在现有项目中集成Sharding-JDBC有一些小问题,Sharding-JDBC不支持批量插入,如果项目中已经使用了大量的批量插入语句就需要改造,或者使用辅助hash计算物理表名,再批量插入。

    2、原有项目数据层使用Druid + MyBatis,集成了Sharding-JDBC之后Sharding-JDBC包装了Druid ,所以一些Sharding-JDBC不支持的SQL语句基本就过不去了。

    3、使用Springboot集成Sharding-JDBC的时候,在bean加载的时候我需要设置 IncrementIdGenerator ,但是出现classloader问题。

    IncrementIdGenerator incrementIdGenerator = this.getIncrementIdGenerator(dataSource);

    ShardingRule ShardingRule = ShardingRuleConfiguration.build(dataSourceMap);

    ((IdGenerator) ShardingRule.getDefaultKeyGenerator()).setIncrementIdGenerator(incrementIdGenerator);

    private IncrementIdGenerator getIncrementIdGenerator(DataSource druidDataSource) {

    ...

    }

    后来发现Springboot的类加载器使用的是restartclassloader,所以导致转换一直失败。只要去掉spring-boot-devtools package即可,restartclassloader是为了热启动。

    4、dao.xml逆向工程问题,我们使用的很多数据库表MyBatis生成工具生成的时候都是物理表名,一旦我们使用了Sharding-JDCB之后都是用的逻辑表名,所以生成工具需要提供选项来设置逻辑表名。

    5、为MyBatis提供的SqlSessionFactory需要在Druid的基础上用Sharding-JDCB包装下。

    6、Sharding-JDBC DefaultkeyGenerator默认采用是snowflake算法,但是我们不能直接用我们需要根据datacenterid-workerid自己配合Zookeeper来设置 workerId 段。

    (snowflake workId 10 bit 十进制 1023,dataCenterId 5 bit 十进制 31 、WorkId 5 bit 十进制 31)

    7、由于我们使用的是mysql com.mysql.jdbc.ReplicationDriver自带的实现读写分离,所以处理读写分离会方便很多。如果不是使用的这种就需要手动设置Datasource Hint来处理。

    8、在使用MyBatis dao mapper的时候需要多份逻辑表,因为有些数据源数据表是不需要走Sharding的,自定义ShardingStragety来处理分支逻辑。

    9、全局ID几种方法:

    如果使用 Zookeeper来做分布式ID,就要注意session expired可能会存在重复workid问题,加锁或者接受一定程度的并行(有序列号保证一段时间空间)。

    采用集中发号器服务,在主DB中采用预生成表+incrment 插件(经典取号器实现,InnoDB存储引擎中的TRX_SYS_TRX_ID_STORE 事务号也是这种方式)。

    定长发号器、业务规则发号器,这种需要业务上下文的发号器实现都需要预先配置,然后每次请求带上获取上下文来说明获取业务类型。

    10、在项目中有些地方使用了自增ID排序,数据表拆分之后就需要进行改造,因为ID大小顺序已经不存在了。根据数据的最新排序时使用了ID排序需要改造成用时间字段排序。

    可供选择的框架

    待续

    参考

    参考了很多博客和文献,这个只用于自己的学习目的

  • 相关阅读:
    机器学习是什么
    Computer Vision的尴尬---by林达华
    机器学习算法与Python实践之(四)支持向量机(SVM)实现
    机器学习算法与Python实践之(三)支持向量机(SVM)进阶
    Hortonworks HDP Sandbox定制(配置)开机启动服务(组件)
    GCC单独编译host/examples/ tx_waveforms.cpp
    GDAL1.11版本号对SHP文件索引加速測试
    Tcl 简单介绍及特性
    Hardwood Species
    java整合easyui进行的增删改操作
  • 原文地址:https://www.cnblogs.com/tuhooo/p/9582223.html
Copyright © 2011-2022 走看看