ShardingJDBC第一篇:分库分表

Posted 毛奇志

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ShardingJDBC第一篇:分库分表相关的知识,希望对你有一定的参考价值。

文章目录

一、前言

本文从高并发出发,主要介绍了ShardingJDBC分库分表。

本文代码如下:sharding-jdbc-split分库分表demo工程代码

二、关系型数据库层面的高并发优化

2.1 mysql海量数据带来的性能问题

根据阿里开发手册, 单表行数超过500W或者单表数据容量超过2G,需要考虑水平分表;根据mysql官网,一个表中列数超过1017列,开发者记住1000列就好,需要考虑垂直分表。即行数和数据容量不够,水平分表,列数不够,垂直分表,如下:

eg: 水平有水平分表和水平分库,垂直有垂直分表(商品表、商品明细表)和 垂直分库(订单库、商品库、用户库)

水平分表涉及的问题有三个:

第一个问题是确定分片策略:水平分表存在一个无法避免的问题是,需要决定根据哪个字段将一个表拆分为多个表,这就是分片字段,决定使用哪个分片字段、决定如何对目标表分片的就是分片策略,常见的分片策略方案包括:哈希取模分片算法、一致性哈希分片算法(哈希环偏斜)、范围分片算法。

第二个问题是确定分布式ID:需要额外自定义一个字段来存储全局唯一的主键,即分布式ID,因为mysql的物理主键只能保证在一个表内唯一,水平分表将一个表变为多个表,而这个数据库的水平分表对前端来说是透明的,前端需要做 select * from tablename where 主键列= ‘xxx’ 这种类型操作,不能再用id列,常见的方式是水平分表的每个表增加一个额外的列,用uuid或者雪花算法保证唯一。都能保证唯一性,但是用雪花算法比uuid要好,因为uuid没有任何意义,雪花算法可以存储时间戳

第三个问题是引入依赖:需要让后端Java代码中像操作一个表一样,去操作水平分表之后的多个表,要么在服务端代码中引入Sharding-JDBC依赖,要么是在linux上独立安装ShardingSphere这个中间件,两者的原理是一样的,只是一个在服务端代码层面实现,一个在数据库层面实现。

2.2 分片策略

哈希取模分片:通过表中的某一个字段进行hash算法得到一个哈希值,然后通过取模运算确定数据应该放在哪个分片中,这种方式非常适合随机读写的场景中,它能够很好的将一个大表的数据随机分散到多个小表,前提是哈希算法要设计的好,就是要均衡,如下:

哈希取模运算最大的优点是简单,“先哈希再取模”就好了,最大的缺点是无法适应业务需求的变化,假设根据当前数据表的量以及增长情况,我们把一个大表拆分成了4个小表,看起来满足目前的需求,但是经过一段时间的运行后,发现四个表不够,需要再增加4个表来存储,就是一共需要8个表,这种情况下,就需要对原来的数据进行整体迁移,这个过程非常麻烦。即一旦目标表或者数据库发生数量上的变化,就会导致所有数据都需要进行迁移,为了减少这种大规模的数据影响,所以引入了一致性hash算法,这也是现实开发中更常见的一种做法。

一致性哈希算法:将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数的值空间为 0 ~ 2^ 32 -1 ,就是我们通过 0 ~ 2^ 32 -1 的数字组成一个虚拟的圆环,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到2^ 32 -1,也就是说0点左侧的第一个点代表2^ 32
-1。我们把这个由2的32次方个点组成的圆环称为hash环。

假设现在水平分表出四个表,table_1、table_2、table_3、table_4,在一致性hash算法中,取模运算不是直接对这四个表来完成,而是对2^ 32来实现。hash(table编号)%2^ 32 通过上述公式算出的结果一定是一个0到2^ 32-1之间的一个整数,然后在这个数对应的位置标注目标表,四个表通过hash取模之后分别落在hash环的某个位置上,如下:


当添加一条数据时,同样通过hash和hash环取模运算得到一个目标值,然后根据目标值所在的hash环的位置顺时针查找最近的一个目标表,把数据存储到这个目标表中即可,所以,哈希环的作用也是帮助我们确定某条记录最终要插入到哪个表中,需要到哪个表中找到某条记录,如下:


一致性哈希运算不是直接面向目标表,而是面向hash环,这样的好处就是当需要删除某张表或者增加表的时候,对于整个数据变化的影响是局部的,而不是全局。即插入和删除一个表,只会影响后面那个表。增加一个表,只需要将一些数据从新表的下一个表的移动到新表就可以了;删除一个表,只需要将删除表里面的数据移动到下一个表里就可以了。

理论上,各个表表是能够均衡的分布在整个hash环中,然后每个新插入的记录就是均衡的分布到水平分表的各个表里面,但实际情况如下:

也就是产生了hash环偏斜的现象,这种现象导致的问题就是大量的数据都会保存到同一个表中,导致数据分配极度不均匀,如上图,很多数据会被存入到表table_01中。

解决的办法是把这四个节点分别复制一份出来分散到这个hash环中,这个复制出来的节点叫虚拟节点,根据实际需要可以虚拟出多个节点出来,注意这里是按实际需要来,比如可以给table_02、table_03、table_04各虚拟出一个,也可以给table_02虚拟出三个。

范围分片:基于数据表的业务特性,按照某种范围拆分,这个范围的有很多含义,比如:

① 时间范围,比如我们按照数据创建时间,按照每一个月保存一个表。基于时间划分还可以用来做冷热数据分离,越早的数据访问频次越少。
② 区域范围,区域一般指的是地理位置,比如一个表里面存储了来自全国各地的数据,如果数据量较大的情况下,可以按照地域来划分多个表。
③ 数据范围,比如根据某个字段的数据区间来进行划分。

2.3 分布式ID

分布式ID的特性
① 唯一性:确保生成的ID是全局唯一的。
② 有序递增性:确保生成的ID是对于某个用户或者业务是按一定的数字有序递增的。
③ 高可用性:确保任何时候都能正确的生成ID。
④ 带时间:ID里面包含时间,一眼扫过去就知道哪天的数据

分布式id方案大概有:
① 数据库自增ID(定义全局表)
② UUID
③ Twitter-Snowflake算法

2.3.1 定义全局表

在数据库中专门创建一张序列表,利用数据库表中的自增ID来为其他业务的数据生成一个全局ID,那么每次要用ID的时候,直接从这个表中获取即可。

CREATE TABLE `uid_table` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `business_id` int(11) NOT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE (business_type) )

在应用程序中,每次调用下面这段代码,就可以持续获得一个递增的ID。

begin
replace into uid_table (business_id) values(2);
select last_insert_id();
commit;

其中,replace into是每次删除原来相同的数据,同时加1条,就能保证我们每次得到的就是一个自增的ID。

优点:
非常简单,利用现有数据库系统的功能实现,成本小,有DBA专业维护。
ID号单调自增,可以实现一些对ID有特殊要求的业务。
缺点:
强依赖DB,当DB异常时整个系统不可用,属于致命问题。配置主从复制可以尽可能的增加可用
性,但是数据一致性在特殊情况下难以保证。主从切换时的不一致可能会导致重复发号。
ID发号性能瓶颈限制在单台MySQL的读写性能。

2.3.2 UUID

UUID的格式是: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 8-4-4-4-12共36个字符,它是一个128bit的
二进制转化为16进制的32个字符,然后用4个 - 连接起来的字符串。

UUID的五种生成方式
① 基于时间的UUID(date-time & MAC address): 主要依赖当前的时间戳及机器mac地址,因此
可以保证全球唯一性。(使用了Mac地址,因此会暴露Mac地址和生成时间。)
② 分布式安全的UUID(date-time & group/user id)将版本1的时间戳前四位换为POSIX的UID或
GID。
③ 基于名字空间的UUID-MD5版(MD5 hash & namespace),基于指定的名字空间/名字生成MD5
散列值得到,标准不推荐。
④ 基于随机数的UUID(pseudo-random number):基于随机数或伪随机数生成。
⑤ 基于名字空间的UUID-SHA1版(SHA-1 hash & namespace):将版本3的散列算法改为SHA1。

在Java中,提供了基于MD5算法的UUID、以及基于随机数的UUID。

优点:本地生成,没有网络消耗,生成简单,没有高可用风险。

缺点:
① 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
② 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅
丽莎病毒的制作者位置。
③ 无序查询效率低:由于生成的UUID是无序不可读的字符串,所以其查询效率低。
④ UUID不适合用来做数据库的唯一ID,如果用UUID做主键,无序的不递增,大家都知道,主键是有
索引的,然后mysql的索引是通过b+树来实现的,每一次新的UUID数据的插入,为了查询的优化,都会对索引底层的b+树进行修改,因为UUID数据是无序的,所以每一次UUID数据的插入都会对主键的b+树进行很大的修改,严重影响性能

2.3.3 雪花算法

SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型
的数字作为全局唯一 id。雪花算法的组成,一共64bit,这64个bit位由四个部分组成。

第一部分,1bit位,用来表示符号位,而ID一般是正数,所以这个符号位一般情况下是0。

第二部分,占41 个 bit:表示的是时间戳,是系统时间的毫秒数,但是这个时间戳不是当前系统的
时间,而是当前 系统时间-开始时间 ,即表示这个ID生成方案的使用的时间,时间戳是为了保证有序性,可读性,即开发者一看就能猜到ID是什么时候生成的。41位可以2^ 41 - 1表示个数字,可以表示的数值范围是:0 至 2^ 41-1,也就是说41位可以表示2^ 41-1个毫秒的值,转化成单位年则是(2^ 41-1)/1000 * 60 * 60 * 24*365=69年,也就是能容纳69年的时间。

第三部分,用来记录工作机器id,id包含10bit,意味着这个服务最多可以部署在 2^10 台机器上,
也就是 1024 台机器。其中这10bit又可以分成2个5bit,前5bit表示机房id、5bit表示机器id,意味着最多支持2^5个机房(32),每个机房可以支持32台机器。

第四部分,由12bit组成,表示一个递增序列,用来记录同毫秒内产生的不同id。如果是同一毫秒同一台机器来请求,需要使用序列号来保证唯一性,即保证同一毫秒内同一机器生成的ID是唯一的,这个其实就是为了满足我们ID的这个高并发,就是保证我同一毫秒进来的并发场景的唯一性。12位(bit)可以表示的最大正整数是2^12-1=4095,即可以用0、1、2、3、…4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。12位2进制,如果全部都是1的情况下,那么最终的值就是4095,也就是12bit能够存储的最大的数字是4095。

三、Sharding-JDBC分库分表

3.1 Sharding-JDBC内置算法

访问官网 https://shardingsphere.apache.org/document/current/cn/user-manual/shardingsphere-jdbc/builtin-algorithm/sharding/ ,主要包括四种

分片算法:用来分库分表,常见的就是取模
分布式序列算法:用来生成全局id,包括uuid和雪花算法
负载均衡算法:用来做读写分离,多个库中选择一个写库进行操作
加密算法:用来加密解密



这里将分库分表,所有主要看分片算法就好,分布式序列算法就直接用雪花算法。

3.2 取模算法

取模算法需要我们自己实现一个算法,直接取模操作,如下:

运行生成四个表,如下:

其实还有一种哈希取模分片算法,即定位到 HashModShardingAlgorithm 类的 doSharding 方法,先哈希,再取模,如下:

3.3 基于分片容量的范围分片算法

基于分片容量的范围分片算法就是VOLUME_RANGE,如下:



插入600条数据,且按照user_id分表,每个表就是200条,所以运行之后生成三个表,每个表中200条数据

3.4 基于分片边界的范围分片算法


运行生成四个表,按照userid字段值分表,如下:

3.5 自动时间段分片算法


运行生成13个表,如下:

3.6 同时分库分表

上面都只有将一个表的数据存放到同一个库的多个表中,其实我们可以将一个表存放到多个库多个表中,如下:



未水平分表,数据仅在一个表中,查询语句走 “索引字段”;
水平分表后,之后从查询语句要走 “分片字段+索引字段” 。

四、尾声

本文从高并发出发,主要介绍了ShardingJDBC分库分表,本文代码如下:sharding-jdbc-split分库分表demo工程代码

天天打码,天天进步!!

以上是关于ShardingJDBC第一篇:分库分表的主要内容,如果未能解决你的问题,请参考以下文章

ShardingJdbc-分表;分库分表;读写分离;一主多从+分表;一主多从+分库分表;公共表;数据脱敏;分布式事务

ShardingJDBC 分库分表详解

mysql数据库分库分表shardingjdbc

ShardingJDBC分库分表配置

分库分表ShardingJDBC最佳实践

ShardingSphere技术专题ShardingJDBC实现分库分表