概述 分库分表是什么 随着公司业务快速发展,数据库中的数据量猛增,访问性能也变慢了,优化迫在眉睫。分析一下问题出现在哪儿呢? 关系型数据库本身比较容易成为系统瓶颈,单机存储容量、连接数、处理能力都有限。当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从库、优化索引,做很多操作时性能仍下降严重。
方案1
:
通过提升服务器硬件能力来提高数据处理能力,比如增加存储容量 、CPU等,这种方案成本很高,并且如果瓶颈在MySQL本身那么提高硬件也是有限的。
方案2
:
把数据分散在不同的数据库中,使得单一数据库的数据量变小来缓解单一数据库的性能问题,从而达到提升数据库性能的目的
。
如下图:将电商数据库拆分为若干独立的数据库
,并且对于大表也拆分为若干小表
,通过这种数据库拆分的方法来解决数据库的性能问题。
分库分表
就是为了解决由于数据量过大而导致数据库性能降低
的问题,将原来独立的数据库
拆分成若干数据库
组成,将数据大表
拆分成若干数据表
组成,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的。
分库分表的方式 分库分表包括分库和分表两个部分,在生产中通常包括:垂直分库
、垂直分表
、水平分库
、水平分表
四种方式。
垂直分表 通常在商品列表中是不显示商品详情信息的,如下图:
用户在浏览商品列表时,只有对某商品感兴趣时才会查看该商品的详细描述。因此,商品信息中商品描述字段访问频次较低,且该字段存储占用空间较大,访问单个数据IO时间较长
;商品信息中商品名称、商品图片、商品价格等其他字段数据访问频次较高
。
由于可将访问频次低的商品描述信息单独存放在一张表中,访问频次较高的商品基本信息单独放在一张表中。
商品列表可采用以下sql:
SELECT p.* ,r.[地理区域名称],s.[店铺名称],s.[信誉]FROM [商品信息] pLEFT JOIN [地理区域] r ON p.[产地] = r.[地理区域编码]LEFT JOIN [店铺信息] s ON p.id = s.[所属店铺]WHERE...ORDER BY...LIMIT...
需要获取商品描述时,再通过以下sql获取:
SELECT * FROM [商品描述]WHERE [商品ID] = ?
这一步优化,就叫垂直分表
。
垂直分表是指将一个表按照字段分成多表,每个表存储其中一部分字段。
带来的提升
是:
为了避免IO争抢并减少锁表的几率,查看详情的用户与商品信息浏览互不影响
充分发挥热门数据的操作效率,商品信息的操作的高效率不会被商品描述的低效率所拖累。
一般来说,某业务实体中的各个数据项的访问频次是不一样的,部分数据项可能是占用存储空间比较大的BLOB或是TEXT。例如上例中的商品描述。所以,当表数据量很大时,可以将表按字段切开,将热门字段、冷门字段分开放置在不同库中,这些库可以放在不同的存储设备上,避免IO争抢。垂直切分带来的性能提升主要集中在热门数据的操作效率上,而且磁盘争用情况减少。
通常按以下原则进行垂直拆分
:
把不常用的字段单独放在一张表;
把text,blob等大字段拆分出来放在附表中;
经常组合查询的列放在一张表中;
垂直分库 通过垂直分表性能得到了一定程度的提升,但是还没有达到要求,并且磁盘空间也快不够了,因为数据还是始终限制在一台服务器,库内垂直分表只解决了单一表数据量过大的问题,但没有将表分布到不同的服务器上,因此每个表还是竞争同一个物理机的CPU、内存、网络IO、磁盘。
经过思考,把原有的SELLER_DB(卖家库)
,分为了PRODUCT_DB(商品库)
和STORE_DB(店铺库)
,并把这两个库分散到不同服务器,如下图:
由于商品信息与商品描述业务耦合度较高
,因此一起被存放在PRODUCT_DB(商品库)
;而店铺信息相对独立
,因此单独被存放在STORE_DB(店铺库)
。
这一步优化,就叫垂直分库
。
垂直分库是指按照业务
将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用
。
它带来的提升
是:
解决业务层面的耦合,业务清晰
能对不同业务的数据进行分级管理、维护、监控、扩展等
高并发场景下,垂直分库一定程度的提升IO、数据库连接数、降低单机硬件资源的瓶颈
垂直分库通过将表按业务分类
,然后分布在不同数据库,并且可以将这些数据库部署在不同服务器上,从而达到多个服务器共同分摊压力的效果,但是依然没有解决单表数据量过大的问题。
水平分库 经过垂直分库后,数据库性能问题得到一定程度的解决,但是随着业务量的增长,PRODUCT_DB(商品库)单库
存储数据已经超出预估。假设目前有8w店铺,每个店铺平均150个不同规格的商品,再算上增长,那商品数量得往1500w+上预估,并且PRODUCT_DB(商品库)属于访问非常频繁的资源,单台服务器已经无法支撑。此时该如何优化?
再次分库?但是从业务角度分析,目前情况已经无法再次垂直分库。
尝试水平分库,将店铺ID为单数的和店铺ID为双数
的商品信息分别放在两个库中。
也就是说,要操作某条数据,先分析这条数据所属的店铺ID。如果店铺ID为双数,将此操作映射至RRODUCT_DB1(商品库1)
;如果店铺ID为单数,将操作映射至RRODUCT_DB2(商品库2)
。此操作要访问数据库名称的表达式为RRODUCT_DB[店铺ID%2 + 1]
。
这一步优化,就叫水平分库
。
水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上
。
它带来的提升
是:
解决了单库大数据,高并发的性能瓶颈。
提高了系统的稳定性及可用性。
当一个应用难以再细粒度的垂直切分
,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈
,这时候就需要进行水平分库了,经过水平切分的优化,往往能解决单库存储量及性能瓶颈。但由于同一个表被分配在不同的数据 库,需要额外进行数据操作的路由工作,因此大大提升了系统复杂度
。
水平分表 按照水平分库的思路把PRODUCT_DB_X(商品库)
内的表也可以进行水平拆分,其目的也是为解决单表数据量大的问题,如下图:
与水平分库的思路类似,不过这次操作的目标是表,商品信息及商品描述被分成了两套表。如果商品ID为双数,将此操作映射至商品信息1表
;如果商品ID为单数,将操作映射至商品信息2表
。此操作要访问表名称的表达式为商品信息[商品ID%2 + 1]
。
这一步优化,就叫水平分表
。
水平分表是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中
。
它带来的提升
是:
优化单一表数据量过大而产生的性能问题
避免IO争抢并减少锁表的几率
库内的水平分表,解决了单一表数据量过大的问题,分出来的小表中只包含一部分数据,从而使得单个表的数据量变小,提高检索性能。
小结
垂直分表
:可以把一个宽表的字段按访问频次、是否是大字段的原则拆分为多个表,这样既能使业务清晰,还能提升部分性能。拆分后,尽量从业务角度避免联查,否则性能方面将得不偿失
。
垂直分库
:可以把多个表按业务耦合松紧归类,分别存放在不同的库,这些库可以分布在不同服务器,从而使访问压力被多服务器负载,大大提升性能,同时能提高整体架构的业务清晰度,不同的业务库可根据自身情况定制优化方案。但是它需要解决跨库带来的所有复杂问题
。
水平分库
:可以把一个表的数据(按数据行)分到多个不同的库,每个库只有这个表的部分数据,这些库可以分布在不同服务器,从而使访问压力被多服务器负载,大大提升性能。它不仅需要解决跨库带来的所有复杂问题
,还要解决数据路由的问题
。
水平分表
:可以把一个表的数据(按数据行)分到多个同一个数据库的多张表中,每个表只有这个表的部分数据,这样做能小幅提升性能,它仅仅作为水平分库的一个补充优化
。
一般来说,在系统设计阶段就应该根据业务耦合松紧来确定垂直分库,垂直分表方案,在数据量及访问压力不是特别大的情况,首先考虑缓存
、读写分离
、索引技术
等方案。若数据量极大,且持续增长,再考虑水平分库水平分表方案。
分库分表带来的问题 分库分表能有效的缓解了单机和单库带来的性能瓶颈和压力,突破网络IO、硬件资源、连接数的瓶颈,同时也带来了一些问题。
事务一致性问题 由于分库分表把数据分布在不同库甚至不同服务器,不可避免会带来分布式事务
问题。
跨节点关联查询 在没有分库前,检索商品时可以通过以下SQL对店铺信息进行关联查询:
SELECT p.* ,r.[地理区域名称],s.[店铺名称],s.[信誉]FROM [商品信息] pLEFT JOIN [地理区域] r ON p.[产地] = r.[地理区域编码]LEFT JOIN [店铺信息] s ON p.id = s.[所属店铺]WHERE...ORDER BY...LIMIT...
但垂直分库后[商品信息]
和[店铺信息]
不在一个数据库,甚至不在一台服务器,无法进行关联查询。
可将原关联查询分为两次查询
,第一次查询的结果集中找出关联数据id,然后根据id发起第二次请求得到关联数据,最后将获得到的数据进行拼装。
跨节点分页、排序函数 跨节点多库进行查询时,limit分页、order by排序等问题,就变得比较复杂了。需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序
。
如,进行水平分库后的商品库,按ID倒序排序分页,取第一页:
以上流程是取第一页的数据,性能影响不大,但由于商品信息的分布在各数据库的数据可能是随机的,如果是取第N页,需要将所有节点前N页数据都取出来合并,再进行整体的排序,操作效率可想而知。所以请求页数越大,系统的性能也会越差。
在使用Max、Min、Sum、Count之类的函数进行计算的时候,与排序分页同理,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回。
主键避重 在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库生成的ID无法保证全局唯一
。因此需要单独设计全局主键
,以避免跨库主键重复问题。
公共表 实际的应用场景中,参数表、数据字典表等都是数据量较小,变动少,而且属于高频联合查询的依赖表。例子中地理区域表也属于此类型。 可以将这类表在每个数据库都保存一份,所有对公共表的更新操作都同时发送到所有分库执行。
由于分库分表之后,数据被分散在不同的数据库、服务器。因此,对数据的操作也就无法通过常规方式完成,并且它还带来了一系列的问题。
Sharding-JDBC 简介 Sharding-JDBC是当当网研发的开源分布式数据库中间件,从 3.0 开始Sharding-JDBC被包含在 Sharding-Sphere 中
,之后该项目进入进入Apache孵化器,4.0版本之后的版本为Apache版本。
ShardingSphere
是一套开源的分布式数据库中间件
解决方案组成的生态圈,它由Sharding-JDBC
、Sharding- Proxy
和Sharding-Sidecar
这3款相互独立的产品组成。 他们均提供标准化的数据分片
、分布式事务
和数据库治理
功能,可适用于如Java同构、异构语言、容器、云原生等各种多样化的应用场景。
Sharding-JDBC定位为轻量级Java框架
,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。
Sharding-JDBC的核心
功能为数据分片
和读写分离
,通过Sharding-JDBC,应用可以透明的使用jdbc访问已经分库分表、读写分离的多个数据源,而不用关心数据源的数量以及数据如何分布。
适用于任何基于Java的ORM框架
,如: Hibernate
、Mybatis
、Spring JDBC Template
或直接使用JDBC
。
基于任何第三方的数据库连接池
,如:DBCP
、C3P0
、BoneCP
、 Druid
、HikariCP
等。
支持任意实现JDBC规范的数据库
。目前支持MySQL
、Oracle
、SQLServer
和PostgreSQL
。
上图展示了Sharding-Jdbc的工作方式,使用Sharding-Jdbc前需要人工对数据库进行分库分表
,在应用程序中加入Sharding-Jdbc的Jar包,应用程序通过Sharding-Jdbc操作分库分表后的数据库和数据表,由于Sharding-Jdbc是对Jdbc驱动的增强,使用Sharding-Jdbc就像使用Jdbc驱动一样,在应用程序中是无需指定具体要操作的分库和分表。
性能对比
性能损耗测试:服务器资源充足、并发数相同,比较JDBC和Sharding-JDBC性能损耗,Sharding-JDBC相对JDBC损耗不超过7%。
性能对比测试:服务器资源使用到极限,相同的场景JDBC与Sharding-JDBC的吞吐量相当。
性能对比测试:服务器资源使用到极限,Sharding-JDBC采用分库分表后,Sharding-JDBC吞吐量较JDBC不分表有接近2倍的提升。
快速入门 需求说明 使用Sharding-JDBC完成对订单表的水平分表,快速体验Sharding-JDBC的使用方法。
人工创建两张表,t_order_1
和t_order_2
,这两张表是订单表拆分后的表。
通过Sharding-Jdbc向订单表插入数据
,按照一定的分片规则,主键为偶数的进入t_order_1
,另一部分数据进入t_order_2
;
通过Sharding-Jdbc 查询数据
,根据 SQL语句的内容从t_order_1
或t_order_2
查询数据;
创建数据库表 创建订单库
order_db
CREATE DATABASE `order_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci' ;
在order_db中创建t_order_1
、t_order_2
表
DROP TABLE IF EXISTS `t_order_1`; CREATE TABLE `t_order_1` (`order_id` bigint (20 ) NOT NULL COMMENT '订单id' , `price` decimal (10 , 2 ) NOT NULL COMMENT '订单价格' , `user_id` bigint (20 ) NOT NULL COMMENT '下单用户id' , `status` varchar (50 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单状态' , PRIMARY KEY (`order_id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic ;
DROP TABLE IF EXISTS `t_order_2`; CREATE TABLE `t_order_2` (`order_id` bigint (20 ) NOT NULL COMMENT '订单id' , `price` decimal (10 , 2 ) NOT NULL COMMENT '订单价格' , `user_id` bigint (20 ) NOT NULL COMMENT '下单用户id' , `status` varchar (50 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '订单状态' , PRIMARY KEY (`order_id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic ;
引入maven依赖 <dependency > <groupId > org.apache.shardingsphere</groupId > <artifactId > sharding‐jdbc‐spring‐boot‐starter</artifactId > <version > 4.0.0‐RC1</version > </dependency >
编写程序 分片规则配置 分片规则
配置是sharding-jdbc进行对分库分表操作的重要依据,配置内容包括:数据源
、主键生成策略
、分片策略
等。在application.properties中
配置
# 此配置为水平分表 server.port=56081 spring.application.name = sharding‐jdbc‐simple‐demo server.servlet.context‐path = /sharding‐jdbc‐simple‐demo spring.http.encoding.enabled = true spring.http.encoding.charset = UTF‐8 spring.http.encoding.force = true spring.main.allow‐bean‐definition‐overriding = true mybatis.configuration.map‐underscore‐to‐camel‐case = true # 以下是分片规则配置 # 定义数据源(数据源 m1,并对m1进行实际的参数配置) spring.shardingsphere.datasource.names = m1 spring.shardingsphere.datasource.m1.type = com.alibaba.druid.pool.DruidDataSource spring.shardingsphere.datasource.m1.driver‐class‐name = com.mysql.jdbc.Driver spring.shardingsphere.datasource.m1.url = jdbc:mysql://localhost:3306/order_db?useUnicode=true spring.shardingsphere.datasource.m1.username = root spring.shardingsphere.datasource.m1.password = root # 指定t_order表的数据分布情况,配置数据节点(指定t_order表的数据分布在m1.t_order_1、m1.t_order_2) spring.shardingsphere.sharding.tables.t_order.actual‐data‐nodes = m1.t_order_$‐>{1..2} # 指定t_order表的主键生成策略为SNOWFLAKE(SNOWFLAKE是一种分布式自增算法,保证id全局唯一) spring.shardingsphere.sharding.tables.t_order.key‐generator.column=order_id spring.shardingsphere.sharding.tables.t_order.key‐generator.type=SNOWFLAKE # 指定t_order表的分片策略,分片策略包括分片键和分片算法(order_id为偶数的数据落在t_order_1,为奇数的落在t_order_2,分表策略的表达式为t_order_$->{order_id % 2 + 1}) spring.shardingsphere.sharding.tables.t_order.table‐strategy.inline.sharding‐column = order_id spring.shardingsphere.sharding.tables.t_order.table‐strategy.inline.algorithm‐expression = t_order_$‐>{order_id % 2 + 1} # 打开sql输出日志 spring.shardingsphere.props.sql.show = true swagger.enable = true logging.level.root = info logging.level.org.springframework.web = info logging.level.cn.goitman.dbsharding = debug logging.level.druid.sql = debug
数据操作 @Mapper @Component public interface OrderDao { @Insert("insert into t_order(price,user_id,status) value(#{price},#{userId},#{status})") int insertOrder (@Param("price") BigDecimal price, @Param("userId") Long userId, @Param("status") String status) ; @Select({"<script>" + "select " + " * " + " from t_order t" + " where t.order_id in " + "<foreach collection='orderIds' item='id' open='(' separator=',' close=')'>" + " #{id} " + "</foreach>"+ "</script>"}) List<Map> selectOrderbyIds (@Param("orderIds") List<Long> orderIds) ; }
测试 @RunWith(SpringRunner.class) @SpringBootTest(classes = {ShardingJdbcSimpleDemoBootstrap.class}) public class OrderDaoTest { @Autowired private OrderDao orderDao; @Test public void testInsertOrder () { for (int i = 0 ; i<10 ; i++){ orderDao.insertOrder(new BigDecimal ((i+1 )*5 ),1L ,"WAIT_PAY" ); } @Test public void testSelectOrderbyIds () { List<Long> ids = new ArrayList <>(); ids.add(370969501279191040L ); ids.add(370969500767485953L ); List<Map> maps = orderDao.selectOrderbyIds(ids); System.out.println(maps); } }
执行testInsertOrder:
通过日志可以发现order_id为奇数的被插入到t_order_2表,为偶数的被插入到t_order_1表,达到预期目标。
执行testSelectOrderbyIds:
通过日志可以发现,根据传入order_id的奇偶不同,sharding-jdbc分别去不同的表检索数据,达到预期目标。
流程分析 通过日志分析,Sharding-JDBC在拿到用户要执行的sql之后做了一下操作:
解析sql,获取片键值,在本例中是order_id
Sharding-JDBC通过规则配置 t_order_$->{order_id % 2 + 1}
,当order_id为偶数时,应该往t_order_1表插数据,为奇数时,往t_order_2插数据。
于是Sharding-JDBC根据order_id的值改写sql语句,改写后的SQL语句是真实所要执行的SQL语句。
执行改写后的真实sql语句
将所有真正执行sql的结果进行汇总合并,返回。
其他集成方式 Sharding-JDBC除了支持上述application.properties配置外,还支持以下三种集成方式(共支持四种集成方式
)
Yaml 配置 Spring Boot定义application.yml,内容如下:
server: port: 56081 servlet: context‐path: /sharding‐jdbc‐simple‐demo spring: application: name: sharding‐jdbc‐simple‐demo http: encoding: enabled: true charset: utf‐8 force: true main: allow‐bean‐definition‐overriding: true shardingsphere: datasource: names: m1 m1: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/order_db?useUnicode=true username: root password: mysql sharding: tables: t_order: actualDataNodes: m1.t_order_$‐>{1..2} tableStrategy: inline: shardingColumn: order_id algorithmExpression: t_order_$‐>{order_id % 2 + 1 } keyGenerator: type: SNOWFLAKE column: order_id props: sql: show: true mybatis: configuration: map‐underscore‐to‐camel‐case: true swagger: enable: true logging: level: root: info org.springframework.web: info com.itheima.dbsharding: debug druid.sql: debug
如果使用application.yml则需要屏蔽原来的application.properties文件
Java 配置 @Configuration public class ShardingJdbcConfig { Map<String, DataSource> createDataSourceMap () { DruidDataSource dataSource1 = new DruidDataSource (); dataSource1.setDriverClassName("com.mysql.jdbc.Driver" ); dataSource1.setUrl("jdbc:mysql://localhost:3306/order_db?useUnicode=true" ); dataSource1.setUsername("root" ); dataSource1.setPassword("root" ); Map<String, DataSource> result = new HashMap <>(); result.put("m1" , dataSource1); return result; } private static KeyGeneratorConfiguration getKeyGeneratorConfiguration () { KeyGeneratorConfiguration result = new KeyGeneratorConfiguration ("SNOWFLAKE" , "order_id" ); return result; } TableRuleConfiguration getOrderTableRuleConfiguration () { TableRuleConfiguration result = new TableRuleConfiguration ("t_order" , "m1.t_order_$‐> {1..2}" ); result.setTableShardingStrategyConfig(new InlineShardingStrategyConfiguration ("order_id" , "t_order_$‐>{order_id % 2 + 1}" )); result.setKeyGeneratorConfig(getKeyGeneratorConfiguration()); return result; } @Bean DataSource getShardingDataSource () throws SQLException { ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration (); shardingRuleConfig.getTableRuleConfigs().add(getOrderTableRuleConfiguration()); Properties properties = new Properties (); properties.put("sql.show" , "true" ); return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig, properties); } }
由于采用了配置类
所以需要屏蔽原来application.properties文件中spring.shardingsphere开头的配置信息
。还需要在SpringBoot启动类中屏蔽使用spring.shardingsphere配置项的类
:
@SpringBootApplication(exclude = {SpringBootConfiguration.class}) public class ShardingJdbcSimpleDemoBootstrap {....}
Spring命名空间配置(不推荐) <?xml version="1.0" encoding="UTF‐8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema‐instance" xmlns:p ="http://www.springframework.org/schema/p" xmlns:context ="http://www.springframework.org/schema/context" xmlns:tx ="http://www.springframework.org/schema/tx" xmlns:sharding ="http://shardingsphere.apache.org/schema/shardingsphere/sharding" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring‐beans.xsd http://shardingsphere.apache.org/schema/shardingsphere/sharding http://shardingsphere.apache.org/schema/shardingsphere/sharding/sharding.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring‐context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring‐tx.xsd" > <context:annotation‐config /> <!‐‐定义多个数据源‐‐> <bean id ="m1" class ="com.alibaba.druid.pool.DruidDataSource" destroy ‐method ="close" > <property name ="driverClassName" value ="com.mysql.jdbc.Driver" /> <property name ="url" value ="jdbc:mysql://localhost:3306/order_db_1?useUnicode=true" /> <property name ="username" value ="root" /> <property name ="password" value ="root" /> </bean > <!‐‐定义分库策略‐‐> <sharding:inline‐strategy id="tableShardingStrategy" sharding‐column="order_id" algorithm‐ expression="t_order_$‐>{order_id % 2 + 1}" /> <!‐‐定义主键生成策略‐‐> <sharding:key‐generator id="orderKeyGenerator" type="SNOWFLAKE" column="order_id" /> <!‐‐定义sharding‐Jdbc数据源‐‐> <sharding:data‐source id="shardingDataSource"> <sharding:sharding‐rule data‐source‐names="m1"> <sharding:table‐rules> <sharding:table‐rule logic‐table="t_order" table‐strategy‐ref="tableShardingStrategy" key‐generator‐ref="orderKeyGenerator" /> </sharding:table‐rules> </sharding:sharding‐rule> </sharding:data‐source> </beans >
基本概念 如何分库分表 逻辑表 水平拆分的数据表的总称
。例:订单数据表根据主键%2拆分为2张表,分别是t_order_0
、t_order_1
,他们的逻辑表名为t_order
。
真实表 在分片的数据库中真实存在的物理表
。即上个示例中的t_order_0
到t_order_1
。
数据节点 数据分片的最小物理单元
。由数据源名称和数据表组成,例:ds_0.t_order_0
。
绑定表 指分片规则一致的主表和子表
。例如:t_order
表和t_order_item
表,均按照order_id
分片,绑定表之间的分区键要完全相同,则此两张表互为绑定表关系
。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升
。
举例说明,如果SQL为:
SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id= i.order_id WHERE o.order_id in (10 , 11 );
在不配置绑定表关系
时,假设分片键order_id
将数值10路由至第0片,将数值11路由至第1片,那么路由后的SQL应该为4条,它们呈现为笛卡尔积:
SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id= i.order_id WHERE o.order_id in (10 , 11 );SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id= i.order_id WHERE o.order_id in (10 , 11 );SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id= i.order_id WHERE o.order_id in (10 , 11 );SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id= i.order_id WHERE o.order_id in (10 , 11 );
在配置绑定表关系
后,路由的SQL应该为2条:
SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id= i.order_id WHERE o.order_id in (10 , 11 );SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id= i.order_id WHERE o.order_id in (10 , 11 );
广播表 指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致
。适用于数据量不大
且需要与海量数据的表进行关联查询的场景
,例如:字典表
。
数据分库+读写分离
分布式唯一主键 概述 在分布式环境中,分库分表之后,不同表生成全局唯一的Id是非常棘手的问题。因为在不同表之间的自增键是无法互相感知的,会造成重复Id的生成。当然可以通过约束表生成键的规则(设置不同的起始和步长)来达到数据的不重复,但是数据库节点变更会使框架缺乏扩展性。
目前有许多第三方解决方案可以完美解决这个问题,如:
UUID/GUID(一般应用程序和数据库均支持)
Redis| increment
Mongo DB ObjectID( 类似UUID的方式 )
Zookeeper分布式锁
Twitter的 Snowflake( 雪花算法 )
Ticket server(数据库生存方式, Flick采用的就是这种方式)
而 ShardingSphere不仅提供了内置
的分布式主键生成器,例如UUID
、 Snowflake
。还抽离出分布式主键生成器的接口
(io.shardingsphere.core.keygen.KeyGenerator),方便用户自行实现自定义的自增主键生成器。
雪花算法 snowflake算法是一款本地生成的(ID生成过程不依赖任何中间件,无网络通信),保证ID全局唯一,并且ID总体有序递增,
大致由:首位无效符
、时间戳差值
,机器(进程)编码
,序列号
四部分组成,雪花算法生成的ID是纯数字且具有时间顺序
的。
1 bit
:不用
,因为二进制里第一个bit为如果是1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0
时间位
:可以根据时间进行排序,有助于提高查询速度。41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1个毫秒值,换算成年就是表示 69 年的时间。
机器id位
:适用于分布式环境下对多节点的各个节点进行标识,可以具体根据节点数和部署情况设计,划分机器位10位长度,如划分5位表示进程位等,这个服务最多可以部署在2^10 台机器上,也就是 1024 台机器。
序列号位
:是一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号
时间自增排序
适合分布式场景,整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分)效率较高,一个节点每毫秒4096个ID序号,服务最大每毫秒409.6万个序列号
雪花算法在单机系统上ID是递增的,但是在分布式系统多节点的情况下不是绝对递增,所有节点的时钟(System.currentTimeMillis())并不能保证不完全同步
,所以有可能会出现不是全局递增的情况
不能在一台服务器上部署多个分布式ID服务;
时钟回拨问题;
使用方法 ShardingSphere 在分片规则配置模块可配置每个表的主键生成策略,默认使用为雪花算法
(io.shardingsphere.core.keygen.DefaultKeyGenerator)
# 主键生成 sharding jdbc 默认主键算法是 64位雪花算法 sharding.jdbc.config.sharding.tables.t_order.key-generator-class-name=io.shardingsphere.core.keygen.DefaultKeyGenerator sharding.jdbc.config.sharding.tables.t_order.key-generator-column-name=id
使用DefaultKeyGenerator 类获取
DefaultKeyGenerator generator = new DefaultKeyGenerator ();generator.generateKey();
private boolean waitTolerateTimeDifferenceIfNeed (long currentMilliseconds) throws Throwable { try { if (this .lastMilliseconds <= currentMilliseconds) { return false ; } else { long timeDifferenceMilliseconds = this .lastMilliseconds - currentMilliseconds; Preconditions.checkState(timeDifferenceMilliseconds < (long ) maxTolerateTimeDifferenceMilliseconds, "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds" , new Object []{this .lastMilliseconds, currentMilliseconds}); Thread.sleep(timeDifferenceMilliseconds); return true ; } } catch (Throwable var5) { throw var5; } }
服务器时钟回拨会导致产生重复序列
,因此默认分布式主键生成器提供了一个最大容忍的时钟回拨毫秒数。如果时钟回拨的时间超过最大容忍的毫秒数值,则程序报错;如果在可容忍的范围内,默认分布式主键生成器会等待(Thread.sleep)时钟同步到最后一次主键生成的时间后再继续工作
。最大容忍的时钟回拨毫秒数的默认值为0,可通过调用静态方法 Defaultkey Generator setMaxTolerate Time DifferenceMilliseconds
设置
public synchronized Number generateKey () { long currentMilliseconds = timeService.getCurrentMillis(); if (this .waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) { currentMilliseconds = timeService.getCurrentMillis(); } if (this .lastMilliseconds == currentMilliseconds) { if (0L == (this .sequence = this .sequence + 1L & 4095L )) { currentMilliseconds = this .waitUntilNextTime(currentMilliseconds); } } else { this .vibrateSequenceOffset(); this .sequence = (long )this .sequenceOffset; } this .lastMilliseconds = currentMilliseconds; return currentMilliseconds - EPOCH << 22 | workerId << 12 | this .sequence; }
核心代码如下,几个实现的关键点:
synchronized保证线程安全;
如果出现时间回拨,判断时钟回拨的时间是否超过最大容忍的毫秒数值,如果超过抛出异常;
如果当前时间和上一次是同一秒时间,那么sequence自增。如果同一秒内sequence自增值超过2^13-1
,那么就会自旋等待下一秒;
如果是新的一秒,那么sequence重新从0开始;
分片规则
随机分片
:hashcode 取模 (user_id % 片数量 ,取模结果为 0 ,insert t_user0,取模结果为 1,insert t_user1)
连续分片
: 时间范围int,可能会造成数据倾斜(每个库数据量不平衡)
根据业务需求来决定分片键,在业务之初分片键确定之后一般情况不更换,如果更换,对数据迁移,数据维护 非常的麻烦
分片算法 Sharding提供了以下4种算法接口:
精确分片算法
PreciseShardingAlgorithm:用于处理使用单一键
作为分片键的 = 与 IN 进行分片的场景
。需要配合StandardShardingStrategy使用。
范围分片算法
RangeShardingAlgorithm:用于处理使用单一键
作为分片键的BETWEEN AND进行分片的场景
。需要配合StandardShardingStrategy使用。如果需要使用RangeShardingAlgorithm,必须和PreciseShardingAlgorithm配套使用
,否则会报错
Hint分片算法
HintShardingAlgorithm
复合分片算法
ComplexKeysShardingAlgorithm
分片策略 Sharding-JDBC中的分片策略有两个维度:
数据源分片策略
(DatabaseShardingStrategy):数据被分配的目标数据源
表分片策略
(TableShardingStrategy):数据被分配的目标表
两种分片策略API完全相同,但是表分片策略是依赖于数据源分片策略的(即:先分库,然后才有分表)
Sharding分片策略继承自ShardingStrategy,提供如下5种分片策略
由于分片算法和业务实现紧密相关,因此Sharding-JDBC并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。
注:Sharding-jdbc在使用分片策略的时候,与分片算法是成对出现的,每种策略都对应一到两种分片算法(不分片策略NoneShardingStrategy除外)
标准分片策略(StandardShardingStrategy) 概述
提供对SQL语句中的=、IN和BETWEEN AND
的分片操作支持
StandardShardingStrategy只支持单分片键
,提供PreciseShardingAlgorithm(精准分片)
和RangeShardingAlgorithm(范围分片)
两个分片算法
PreciseShardingAlgorithm是必选
的,用于处理 =
和 IN
的分片
RangeShardingAlgorithm是可选
的,用于处理BETWEEN AND
分片,如果不配置RangeShardingAlgorithm
,SQL中的BETWEEN AND
将按照全库路由
处理
如果需要使用RangeShardingAlgorithm,必须和PreciseShardingAlgorithm配套使用
配置实现
application.properties 配置
配置主要分为三个部分:1. 配置数据源
、2. 分库配置
、3. 分表配置
# standard.precise-algorithm 标准策略 + 精准分片算法 SQL 就是 =、in # standard.range-algorithm 标准策略 + 范围分片算法 (主要是between and ) sharding.jdbc.datasource.names=ds0,ds1 #分库配置 sharding.jdbc.config.sharding.default-database-strategy.standard.sharding-column=user_id sharding.jdbc.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource sharding.jdbc.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds0.url=jdbc:mysql://127.0.0.1:5306/ds0?useUnicode=yes&characterEncoding=utf8 sharding.jdbc.datasource.ds0.username=root sharding.jdbc.datasource.ds0.password=root sharding.jdbc.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource sharding.jdbc.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds1.url=jdbc:mysql://127.0.0.1:5306/ds1?useUnicode=yes&characterEncoding=utf8 sharding.jdbc.datasource.ds1.username=root sharding.jdbc.datasource.ds1.password=root # standard.precise-algorithm 标准策略下分片算法包含2个 precise + range,range是可选的,但是如果使用 range 就必须同 precise 配套一起使用 # 买precise赠送 range,可以选择不要赠品,但是你不能不买还想白嫖赠品 sharding.jdbc.config.sharding.default-database-strategy.standard.precise-algorithm-class-name=类路径.PreciseShardingDBAlgorithm sharding.jdbc.config.sharding.default-database-strategy.standard.range-algorithm-class-name=类路径.RangeShardingDBAlgorithm # 设置绑定表 sharding.jdbc.config.sharding.binding-tables=t_order,t_order_item # t_order分表配置 # 如果分片键相同,可以直接在后面凭拼接 例如 :ds$->{0..1}.t_order$->{0..1},ds$->{0..1}.t_order_item$->{0..1} sharding.jdbc.config.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order$->{0..1} sharding.jdbc.config.sharding.tables.t_order.table-strategy.standard.sharding-column=order_id sharding.jdbc.config.sharding.tables.t_order.table-strategy.standard.precise-algorithm-class-name=类路径.PreciseShardingTableAlgorithm sharding.jdbc.config.sharding.tables.t_order.table-strategy.standard.range-algorithm-class-name=类路径.RangeShardingTableAlgorithm # t_order_item分表配置 sharding.jdbc.config.sharding.tables.t_order_item.actual-data-nodes=ds$->{0..1}.t_order_item$->{0..1} sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.standard.sharding-column=order_id sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.standard.precise-algorithm-class-name=类路径.PreciseShardingTableAlgorithm sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.standard.range-algorithm-class-name=类路径.RangeShardingTableAlgorithm
精准分库PreciseShardingDBAlgorithm
import io.shardingsphere.api.algorithm.sharding.PreciseShardingValue;import io.shardingsphere.api.algorithm.sharding.standard.PreciseShardingAlgorithm;import java.util.Collection; public class PreciseShardingDBAlgorithm implements PreciseShardingAlgorithm <Integer> { @Override public String doSharding (Collection<String> databaseNames, PreciseShardingValue<Integer> shardingValue) { for (String each : databaseNames) { if (each.endsWith(String.valueOf(shardingValue.getValue() % databaseNames.size()))) { System.out.println("each" +each); return each; } } throw new UnsupportedOperationException (); } }
范围分库RangeShardingDBAlgorithm
import io.shardingsphere.api.algorithm.sharding.RangeShardingValue;import io.shardingsphere.api.algorithm.sharding.standard.RangeShardingAlgorithm;import java.util.Collection;import java.util.LinkedHashSet;import java.util.Set; public class RangeShardingDBAlgorithm implements RangeShardingAlgorithm <Integer> { @Override public Collection<String> doSharding (final Collection<String> databaseNames, final RangeShardingValue<Integer> shardingValue) { Set<String> result = new LinkedHashSet <>(); int lower = shardingValue.getValueRange().lowerEndpoint(); int upper = shardingValue.getValueRange().upperEndpoint(); for (int i = lower; i <= upper; i++) { for (String each : databaseNames) { if (each.endsWith(i % databaseNames.size() + "" )) { result.add(each); } } } return result; } }
精准分表PreciseShardingTableAlgorithm
import io.shardingsphere.api.algorithm.sharding.PreciseShardingValue;import io.shardingsphere.api.algorithm.sharding.standard.PreciseShardingAlgorithm;import java.util.Collection; public class PreciseShardingTableAlgorithm implements PreciseShardingAlgorithm <Long> { @Override public String doSharding (Collection<String> tableNames, PreciseShardingValue<Long> shardingValue) { for (String key : tableNames) { if (key.endsWith(String.valueOf(shardingValue.getValue() % tableNames.size()))) { System.out.println("key" +key); return key; } } throw new UnsupportedOperationException (); } }
范围分表RangeShardingTableAlgorithm
import com.google.common.collect.Range;import io.shardingsphere.api.algorithm.sharding.RangeShardingValue;import io.shardingsphere.api.algorithm.sharding.standard.RangeShardingAlgorithm;import java.util.Collection;import java.util.LinkedHashSet;import java.util.Set; public class RangeShardingTableAlgorithm implements RangeShardingAlgorithm <Integer> { @Override public Collection<String> doSharding (final Collection<String> tableNames, final RangeShardingValue<Integer> shardingValue) { Set<String> result = new LinkedHashSet <>(); if (Range.closed(2000000 , 7000000 ).encloses(shardingValue.getValueRange())) { for (String each : tableNames) { if (each.endsWith("0" )) { result.add(each); } } } else { throw new UnsupportedOperationException (); } return result; } }
复合分片策略(ComplexShardingStrategy) 概述
提供对SQL语句中的=、IN和BETWEEN AND
的分片操作支持
ComplexShardingStrategy支持多分片键
由于多分片键之间的关系复杂,因此Sharding-JDBC并未做过多的封装,而是直接将分片键值组合
以及分片操作符
交于算法接口
,完全由应用开发者实现,提供最大的灵活度
配置实现
application.properties 配置
配置主要分为三个部分:1. 配置数据源
、2. 分库配置
、3. 分表配置
# 复合分片 sharding.jdbc.datasource.names=ds0,ds1 sharding.jdbc.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource sharding.jdbc.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds0.url=jdbc:mysql://127.0.0.1:5306/ds0?useUnicode=yes&characterEncoding=utf8 sharding.jdbc.datasource.ds0.username=root sharding.jdbc.datasource.ds0.password=root sharding.jdbc.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource sharding.jdbc.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds1.url=jdbc:mysql://127.0.0.1:5306/ds1?useUnicode=yes&characterEncoding=utf8 sharding.jdbc.datasource.ds1.username=root sharding.jdbc.datasource.ds1.password=root # 分库配置 (行表达式分片策略 + 行表达式分片算法) sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=user_id sharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds$->{user_id % 2} sharding.jdbc.config.sharding.binding-tables=t_order,t_order_item # t_order分表配置 (复合分片策略) sharding.jdbc.config.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order$->{0..1}_$->{0..1} sharding.jdbc.config.sharding.tables.t_order.table-strategy.complex.sharding-columns=user_id,order_id sharding.jdbc.config.sharding.tables.t_order.table-strategy.complex.algorithm-class-name=类路径.ComplexShardingAlgorithm # t_order_item分表配置 (复合分片策略) sharding.jdbc.config.sharding.tables.t_order_item.actual-data-nodes=ds$->{0..1}.t_order_item$->{0..1}_$->{0..1} # 标准 和 inline 都是单分片键 ,复合分片策略可以配置则多分片键 sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.complex.sharding-columns=user_id,order_id # 自定义算法,让使用者根据业务自定义实现(开发性接口更灵活方便) sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.complex.algorithm-class-name=类路径.ComplexShardingAlgorithm # 定义广播表 sharding.jdbc.config.sharding.broadcast-tables=t_province sharding.jdbc.config.props.sql.show=true
自定义ComplexShardingAlgorithm
import io.shardingsphere.api.algorithm.sharding.ListShardingValue;import io.shardingsphere.api.algorithm.sharding.ShardingValue;import io.shardingsphere.api.algorithm.sharding.complex.ComplexKeysShardingAlgorithm;import java.util.ArrayList;import java.util.Collection;import java.util.Iterator;import java.util.List; public class ComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm { @Override public Collection<String> doSharding (Collection<String> collection, Collection<ShardingValue> shardingValues) { System.out.println("collection:" + collection + ",shardingValues:" + shardingValues); Collection<Integer> orderIdValues = getShardingValue(shardingValues, "order_id" ); Collection<Integer> userIdValues = getShardingValue(shardingValues, "user_id" ); List<String> shardingSuffix = new ArrayList <>(); for (Integer userId : userIdValues) { for (Integer orderId : orderIdValues) { String suffix = userId % 2 + "_" + orderId % 2 ; for (String s : collection) { if (s.endsWith(suffix)) { shardingSuffix.add(s); } } } } return shardingSuffix; } private Collection<Integer> getShardingValue (Collection<ShardingValue> shardingValues, final String key) { Collection<Integer> valueSet = new ArrayList <>(); Iterator<ShardingValue> iterator = shardingValues.iterator(); while (iterator.hasNext()) { ShardingValue next = iterator.next(); if (next instanceof ListShardingValue) { ListShardingValue value = (ListShardingValue) next; if (value.getColumnName().equals(key)) { return value.getValues(); } } } return valueSet; } }
行表达式分片策略(InlineShardingStrategy) 概述
使用Groovy的Inline表达式,提供对SQL语句中的=
和IN
的分片操作支持。
InlineShardingStrategy只支持单分片键
对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发
,如: tuser${user_id % 8} 表示t_user表按照user_id按8取模分成8个表,表名称为t_user_0到t_user_7
配置实现
application.properties 配置
配置主要分为三个部分:1. 配置数据源
、2. 分库配置
、3. 分表配置
#数据源配置,有多少个数据库,就配置多少个数据源(库多的时候比较繁琐,可以采用数据治理),相比于Mycat 配置还是简单很多 #数据源名字随意,但是配置数据源时必须名字能对应 sharding.jdbc.datasource.names=ds0,ds1 sharding.jdbc.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource sharding.jdbc.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds0.url=jdbc:mysql://127.0.0.1:5306/ds0?useUnicode=yes&characterEncoding=utf8 sharding.jdbc.datasource.ds0.username=root sharding.jdbc.datasource.ds0.password=root sharding.jdbc.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource sharding.jdbc.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds1.url=jdbc:mysql://127.0.0.1:5306/ds1?useUnicode=yes&characterEncoding=utf8 sharding.jdbc.datasource.ds1.username=root sharding.jdbc.datasource.ds1.password=root # ----------------------分库配置-------------------------- # database-strategy.inline 库分片策略 + 指定分库的分片键 sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=user_id # database-strategy.inline.algorithm-expression 分片算法表达式 sharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds$->{user_id % 2} # 设置绑定表 t_order,t_order_item sharding.jdbc.config.sharding.binding-tables=t_order,t_order_item # ---------------------- t_order分表配置---------------------- # t_order 分库分表后真实的数据节点(逻辑表 -> 真实表) sharding.jdbc.config.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order$->{0..1} # 分片键设置 sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.sharding-column=order_id sharding.jdbc.config.sharding.tables.t_order.table-strategy.inline.algorithm-expression=t_order$->{order_id % 2} # 主键生成 sharding jdbc 默认主键算法是 64位雪花算法 # sharding.jdbc.config.sharding.tables.t_order.key-generator-class-name=io.shardingsphere.core.keygen.DefaultKeyGenerator # sharding.jdbc.config.sharding.tables.t_order.key-generator-column-name=id # ---------------------- 绑定表t_order_item分表配置 ---------------------- sharding.jdbc.config.sharding.tables.t_order_item.actual-data-nodes=ds$->{0..1}.t_order_item$->{0..1} # 分片键设置 sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.inline.sharding-column=order_id sharding.jdbc.config.sharding.tables.t_order_item.table-strategy.inline.algorithm-expression=t_order_item$->{order_id % 2} # 定义广播表 sharding.jdbc.config.sharding.broadcast-tables=t_province sharding.jdbc.config.props.sql.show=true
强制路由分片策略(HintShardingStrategyhint) 概述 在分库分区中,有些特定的SQL,Sharding-jdbc、Mycat、Vitess都不支持,如:
insert into table1 select * from table2 where ....
这种SQL 路由很麻烦,需要解析table2的路由(是在ds0/ds1,table2_0/table_1),结果集归并,insert语句也需要同样的路由解析。这种情况Sharding-jdbc可以使用Hint分片策略来实现各种Sharding-jdbc不支持语法的限制
通过Hint而非SQL解析的方式分片的策略
。对于分片字段非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段
Hint分片策略是绕过SQL解析的
,所以对于这些比较复杂的需要分片的查询,采用Hint分片策略性能可能会更好
在读写分离数据库中,Hint可以通过HintManager.setMasterRouteOnly()
方法,强制读主库
(主从复制存在一定延时,但在某些特定的业务场景中,可能更需要保证数据的实时性)
配置实现
application.properties 配置
配置主要分为三个部分:1. 配置数据源
、2. 分库配置
、3. 分表配置
# Hint 强制路由分片策略 sharding.jdbc.datasource.names=ds0,ds1 sharding.jdbc.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource sharding.jdbc.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds0.url=jdbc:mysql://127.0.0.1:5306/ds0?useUnicode=yes&characterEncoding=utf8 sharding.jdbc.datasource.ds0.username=root sharding.jdbc.datasource.ds0.password=root sharding.jdbc.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource sharding.jdbc.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver sharding.jdbc.datasource.ds1.url=jdbc:mysql://127.0.0.1:5306/ds1?useUnicode=yes&characterEncoding=utf8 sharding.jdbc.datasource.ds1.username=root sharding.jdbc.datasource.ds1.password=root # 分库配置 sharding.jdbc.config.sharding.default-database-strategy.inline.sharding-column=user_id sharding.jdbc.config.sharding.default-database-strategy.inline.algorithm-expression=ds$->{user_id % 2} # t_order强制分片配置 sharding.jdbc.config.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order$->{0..1} # 和其他3种不同的是,Hint 需要指定分片表 的数据库分片算法 + 表分片算法 sharding.jdbc.config.sharding.tables.t_order.database-strategy.hint.algorithm-class-name=类路径.HintShardingKeyAlgorithm sharding.jdbc.config.sharding.tables.t_order.table-strategy.hint.algorithm-class-name=类路径.HintShardingKeyAlgorithm sharding.jdbc.config.props.sql.show=true
自定义HintShardingKeyAlgorithm
import com.alibaba.druid.util.StringUtils;import io.shardingsphere.api.algorithm.sharding.ListShardingValue;import io.shardingsphere.api.algorithm.sharding.ShardingValue;import io.shardingsphere.api.algorithm.sharding.hint.HintShardingAlgorithm;import java.util.ArrayList;import java.util.Collection;import java.util.List; public class HintShardingKeyAlgorithm implements HintShardingAlgorithm { @Override public Collection<String> doSharding (Collection<String> availableTargetNames, ShardingValue shardingValue) { System.out.println("shardingValue=" + shardingValue); System.out.println("availableTargetNames=" + availableTargetNames); List<String> shardingResult = new ArrayList <>(); for (String targetName : availableTargetNames) { String suffix = targetName.substring(targetName.length() - 1 ); if (StringUtils.isNumber(suffix)) { ListShardingValue<Integer> tmpSharding = (ListShardingValue<Integer>) shardingValue; for (Integer value : tmpSharding.getValues()) { if (value % 2 == Integer.parseInt(suffix)) { shardingResult.add(targetName); } } } } return shardingResult; } }
编写测试类HintApplicationTests
import ai.yunxi.sharding.service.OrderService;import io.shardingsphere.api.HintManager;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest(classes = VipShardingApplication.class) public class HintApplicationTests { @Autowired private OrderService orderService; @Test public void test () { HintManager hintManager = HintManager.getInstance(); hintManager.addDatabaseShardingValue("t_order" , 0 ); hintManager.addTableShardingValue("t_order" , 1 ); System.out.println(orderService.findHint()); } }
不分片策略(NoneShardingStrategy) 不分片的策略。和直接不使用Sharding-JDBC 效果相同
主键如何生成 通过在服务端生成自增主键替换以数据库原生自增主键的方式
,做到分布式主键无重复。
执行原理
SQL解析 SQL解析引擎在 parsing包下:
Lexer:词法解析器
Parser:SQL解析器
两者都是解析器,区别在于Lexer只做词法的解析
,不关注上下文。将字符串拆解成N个词法,而Perser在 Lexer的基础上,还需要理解SQL再进行解析
语法树 解析过程分为词法
解析和语法
解析。词法解析器用于将SQL拆解为不可再分的原子符号,称为Token
。并根据不同数据库方言所提供的字典,将其归类为关键字,表达式,字面量和操作符。再使用语法解析器将SQL转换为抽象语法树
。如以下SQL:
SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18
抽象语法树中的关键字
的Token用绿色
表示,变量
的Token用红色
表示,灰色
表示需要进一步拆分
。
最后,通过对抽象语法树的遍历去提炼分片所需的上下文,并标记有可能需要改写的位置。 供分片使用的解析上下文包含:
查询选择项(Select Items)
表信息(Table)
分片条件(Sharding Condition)
自增主键信息(Auto increment Primary Key)
排序信息(Order By)
分组信息(Group By)
分页信息(Limit、Rownum、Top)
SQL的一次解析过程是不可逆的,一个个Token的按SQL原本的顺序依次进行解析,性能很高。
SQL路由
SQL路由:就是把针对逻辑表
的数据操作映射到对数据结点操作的过程。
根据解析上下文匹配数据库和表的分片策略,并生成路由路径。 对于携带分片键的SQL,根据分片键操作符不同可以划分为单片路由(分片键的操作符是等号)
、多片路由(分片键的操作符是IN)
和范围路由(分片键的操作符是BETWEEN)
,不携带分片键的SQL则采用广播路由
。
分片路由 用于原生SQL中有包含有分片键的场景,又细分为直接路由
、标准路由
和笛卡尔积
路由这3种类型。执行的性能也依次减弱
。
直接路由 满足直接路由
的条件相对苛刻,它需要通过Hint强制路由
的(使用HintAPI直接指定路由至库表)方式分片,并且是只分库不分表的前提下
,则可以避免SQL解析和之后的结果归并
。因此它的兼容性最好,可以执行包括子查询、自定义函数等复杂情况的任意SQL。直接路由还可以用于分片键不在SQL中的某些特殊场景。例如,设置用于数据库分片的键为3
hintManager.setDatabaseShardingValue(3 );
假如路由算法为order_id % 2
,当一个逻辑库t_order对应2个真实库ds_0和ds_1时,路由后SQL将在ds_1上执行。下方是使用API的代码样例:
String sql = "SELECT * FROM t_order" ;try ( HintManager hintManager = HintManager.getInstance(); Connection conn = dataSource.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { hintManager.setDatabaseShardingValue(3 ); try (ResultSet rs = pstmt.executeQuery()) { while (rs.next()) { } } }
标准路由 标准路由
是ShardingSphere最为推荐
使用的分片方式
,它的适用范围
是不包含关联查询
或仅包含绑定表和广播表之间关联查询
的SQL。当分片运算符是等于号
时,路由结果将落入单库(表)
,当分片运算符是BETWEEN或IN
时,则路由结果不一定落入唯一的库(表)
,因此一条逻辑SQL最终可能被拆分为多条用于执行的真实SQL。举例说明,如果按复合分片规则按照order_id的奇数和偶数进行分库,user_id的奇数和偶数进行分表时,一个单表查询的SQL如下:
SELECT * FROM t_order WHERE order_id IN (1 , 2 ) AND user_id IN (1 ,2 ) ;
那么路由的结果应为:
SELECT * FROM t_order WHERE ds0.order_id_0 IN (1 , 2 ) AND user_id IN (1 ,2 ) ;SELECT * FROM t_order WHERE ds0.order_id_1 IN (1 , 2 ) AND user_id IN (1 ,2 ) ;SELECT * FROM t_order WHERE ds1.order_id_0 IN (1 , 2 ) AND user_id IN (1 ,2 ) ;SELECT * FROM t_order WHERE ds1.order_id_1 IN (1 , 2 ) AND user_id IN (1 ,2 ) ;
绑定表和广播表的join关联查询与单表查询复杂度和性能相当
广播表在每个分库中都有相同的一份数据,例如原生SQLjion t_order_item
,每个分库都存在一张t_order_item
表,不需要特殊处理就可以查询 ;
绑定表之间的数据是一一对应的,order_id为奇数的数据只会存储在相同库的一个分表中,有很强的关联性,SQL拆分后的数目与单表是一致的;
笛卡尔路由 笛卡尔路由是最复杂的情况,它无法根据绑定表的关系定位分片规则,因此非绑定表之间的关联查询需要拆解为笛卡尔积组合执行
。
不管是mycat还是sharding,所有的分库分表对非绑定表和广播表之间的关联查询都无法做到有效优化支持,无法根据分片规则计算出SQL应该在哪个数据库、哪个表上执行,那么结果就是把所有分库分表中关联使用到的表交叉查询。笛卡尔路由查询性能较低,需谨慎使用
。
SELECT * FROM t_order AS t1 LFTE JION t_user AS t2 ON t1.user_id = t2.user_id WHERE t1.order_id IN (1 , 2 )
广播路由 对于不携带分片键的SQL,则采取广播路由的方式
。根据SQL类型又可以划分为全库表路由
、全库路由
、全实例路由
、单播路由
和阻断路由
这5种类型。
全库表路由 全库表路由
用于处理对数据库中与其逻辑表相关的所有真实表的操作
,主要包括不带分片键的DQL和DML,以及DDL等
。例如:
SELECT * FROM t_order WHERE good_prority IN (1 , 10 );
则会遍历所有数据库中的所有表,逐一匹配逻辑表和真实表名,能够匹配得上则执行。路由后成为
SELECT * FROM ds0.t_order_0 WHERE good_prority IN (1 , 10 );SELECT * FROM ds0.t_order_1 WHERE good_prority IN (1 , 10 );SELECT * FROM ds1.t_order_0 WHERE good_prority IN (1 , 10 );SELECT * FROM ds1.t_order_1 WHERE good_prority IN (1 , 10 );
全库路由 全库路由
用于处理对数据库的操作,包括用于库设置的SET类型的数据库管理命令,以及TCL这样的事务控制语句
。在这种情况下,会根据逻辑库的名字遍历所有符合名字匹配的真实库,并在真实库中执行该命令,例如:
# 在t_order中执行时有2 个真实库。则实际会在ds0和ds1上都执行这个命令。 SET autocommit= 0 ;
全实例路由 全实例路
由用于DCL操作,授权语句针对的是数据库的实例
。无论一个实例中包含多少个Schema,每个数据库的实例只执行一次
。例如:
# 这个命令将在所有的真实数据库实例中执行,以确保customer用户可以访问每一个实例。 CREATE USER customer@127 .0 .0 .1 identified BY '123' ;
单播路由 单播路由
用于获取某一真实表信息的场景,它仅需要从任意库中的任意真实表中获取数据即可
。例如:
# ds0 和 但ds1 上的的两个真实表t_order_0,t_order_1的描述结构相同,所以这个命令在任意真实表上选择执行一次。 DESCRIBE t_order;
阻断路由 阻断路由用于屏蔽SQL对数据库的操作,例如:
# 这个命令不会在真实数据库中执行,因为ShardingSphere采用的是逻辑Schema的方式,无需将切换数据库Schema的命令发送至数据库中。 USE order_db;
结果归并 将从各个数据节点获取的多数据结果集,组合成为一个结果集并正确的返回至请求客户端,称为结果归并
。
ShardingSphere支持的结果归并从功能
上分为遍历
、排序
、分组
、分页
和聚合
5种类型,它们是组合而非互斥的关系
。从结构
划分,可分为流式归并
、内存归并
和装饰者归并
。流式归并和内存归并是互斥的
,装饰者归并可以在流式归并和内存归并之上做进一步的处理
。
流式归并
是指每一次从结果集中获取到的数据,都能够通过逐条获取的方式返回正确的单条数据,它与数据库原生的返回结果集的方式最为契合。遍历
、排序
以及流式分组
都属于流式归并
的一种。
内存归并
则是需要将结果集的所有数据都遍历并存储在内存中,再通过统一的分组、排序以及聚合等计算之后,再将其封装成为逐条访问的数据结果集返回。
装饰者归并
是对所有的结果集归并进行统一的功能增强,目前装饰者归并有分页归并
和聚合归并
这2种类型。
遍历归并 最为简单的归并方式。在返回的结果集只有一个或者没有使用到排序条件的场景中使用,因为不涉及到排序,只需将多个数据结果集合并为一个单向链表即可。在遍历完成链表中当前数据结果集之后,将链表元素后移一位,继续遍历下一个数据结果集即可。
排序归并 在 查询SQL中,使用order by 但是没有group by + 聚合函数 的情况下使用,由于在SQL中存在ORDER BY语句,因此每个数据结果集自身是有序的,但各个数据结果集之间是无序的
。因此只需要将数据结果集当前游标指向的数据值进行排序即可。 这相当于对多个有序的数组进行排序,归并排序是最适合此场景的排序算法。
ShardingSphere在对排序的查询进行归并时,将每个结果集的当前数据值进行比较(通过实现Java的Comparable接口完成),并将其放入优先级队列。 每次获取下一条数据时,只需将队列顶端结果集的游标下移,并根据新游标重新进入优先级排序队列找到自己的位置即可。
如当前有三个数据结果集,将各个数据结果集的当前游标指向的数据值进行排序,并放入优先级队列。t_score_0
的第一个数据值最大,t_score_2
的第一个数据值次之,t_score_1
的第一个数据值最小,因此优先级队列根据t_score_0
,t_score_2
和t_score_1
的方式排序队列。结果如下所示:
调用next()方法,排在优先级队列首位的t_score_0
将会被弹出队列,并且将当前游标指向的数据值返回至查询客户端,并且将游标下移一位之后重新放入优先级队列从新进行优先级队列排序
可以看到,对于每个数据结果集中的数据有序,而多数据结果集整体无序的情况下,ShardingSphere无需将所有的数据都加载至内存即可排序。它使用的是流式归并的方式,每次next仅获取唯一正确的一条数据,极大的节省了内存的消耗
。
从另一个角度来说,ShardingSphere的排序归并,是在维护数据结果集的纵轴和横轴这两个维度的有序性。纵轴是指每个数据结果集本身,它是天然有序的,它通过包含ORDER BY的SQL所获取
。横轴是指每个数据结果集当前游标所指向的值,它需要通过优先级队列来维护其正确顺序
。每一次数据结果集当前游标的下移,都需要将该数据结果集重新放入优先级队列排序,而只有排列在队列首位的数据结果集才可能发生游标下移的操作。
分组归并 分组归并的情况最为复杂,它分为流式分组归并和内存分组归并。 流式分组归并要求SQL的排序项与分组项的字段以及排序类型(ASC或DESC)必须保持一致
,否则只能通过内存归并才能保证其数据的正确性。
流式分组归并 在分组项与排序项完全一致的情况下,取得的数据是连续的
,分组所需的数据全数存在于各个数据结果集的当前游标所指向的数据值,因此可以采用流式归并。
举例说明,假设根据科目分片,表结构中包含考生的姓名(为了简单起见,不考虑重名的情况)和分数。通过SQL获取每位考生的总分,可通过如下SQL:
SELECT name, SUM (score) FROM t_score GROUP BY name ORDER BY name;
进行归并时,逻辑与排序归并类似,下图展现了进行next调用的时候,流式分组归并是如何进行的。
通过图中我们可以看到,当进行第一次next调用时,排在队列首位的t_score_java将会被弹出队列,并且将分组值同为“Jetty”的其他结果集中的数据一同弹出队列。 在获取了所有的姓名为“Jetty”的同学的分数之后,进行累加操作,那么,在第一次next调用结束后,取出的结果集是“Jetty”的分数总和。
与此同时,所有的数据结果集中的游标都将下移至数据值“Jetty”的下一个不同的数据值,并且根据数据结果集当前游标指向的值进行重排序。因此,包含名字顺着第二位的“John”的相关数据结果集则排在的队列的前列。
流式分组归并与排序归并的区别仅仅在于两点:
它会一次性的将多个数据结果集中的分组项相同的数据全数取出。
它需要根据聚合函数的类型进行聚合计算。
内存分组归并 对于分组项与排序项不一致的情况,由于需要获取分组的相关的数据值并非连续的
,因此无法使用流式归并,需要将所有的结果集数据加载至内存中进行分组和聚合。 例如,若通过以下SQL获取每位考生的总分并按照分数从高至低排序,是无法进行流式归并的,只能将结果集的所有数据都遍历并存储在内存中,再通过统一的分组、排序以及聚合等计算之后,再将其封装成为逐条访问的数据结果集返回
:
SELECT name, SUM (score) FROM t_score GROUP BY name ORDER BY score DESC ;
注:当SQL中只包含分组语句时,根据不同数据库的实现,其排序的顺序不一定与分组顺序一致。但由于排序语句的缺失,则表示此SQL并不在意排序顺序。 因此,ShardingSphere通过SQL优化的改写,自动增加与分组项一致的排序项,使其能够从消耗内存的内存分组归并方式转化为流式分组归并方案。
聚合归并 聚合归并
是在之前介绍的归并类的之上追加的归并能力,即装饰者模式的一种
。
无论是流式分组归并还是内存分组归并,对聚合函数的处理都是一致的。 除了分组的SQL之外,不进行分组的SQL也可以使用聚合函数。聚合函数可以归类为比较
、累加
和求平均值
这3种类型
比较
类型的聚合函数是指MAX
和MIN
。它们需要对每一个同组的结果集数据进行比较,并且直接返回其最大或最小值即可。
累加
类型的聚合函数是指SUM
和COUNT
。它们需要将每一个同组的结果集数据进行累加。
求平均值
的聚合函数只有AVG
。
分页归并 上文所述的所有归并类型都可能进行分页。分页
也是追加在其他归并类型之上的装饰器
,ShardingSphere通过装饰者模式来增加对数据结果集进行分页的能力。 分页归并负责将无需获取的数据过滤掉。
ShardingSphere 执行分页的处理是通过对SQL的改写来实现的。例如:
SELECT id FROM t_user WHERE age > 18 LIMIT 10000 ,10
为了保证返回数据的准确性,在SQL改写阶段修改为:
SELECT id FROM t_user WHERE age > 18 LIMIT 0 ,10000
SQL执行 Sharding-JDBC采用一套自动化的执行引擎,负责将路由和改写完成之后的真实SQL安全且高效发送到底层数据源执行。它不是简单地将SQL通过JDBC直接发送至数据源执行;也并非直接将执行请求放入线程池去并发执行。它更关注平衡数据源连接创建以及内存占用所产生的消耗,以及最大限度地合理利用并发等问题
。执行引擎的目标是自动化的平衡资源控制与执行效率
。
内存限制模式 使用此模式的前提是,Sharding-JDBC对一次操作所耗费的数据库连接数量不做限制
。 如果实际执行的SQL需要对某数据库实例中的200张表做操作,则对每张表创建一个新的数据库连接,并通过多线程的方式并发处理,以达成执行效率最大化
。
连接限制模式 使用此模式的前提是,Sharding-JDBC严格控制对一次操作所耗费的数据库连接数量
。 如果实际执行的SQL需要对某数据库实例中的200张表做操作,那么只会创建唯一的数据库连接
,并对其200张表串行处理
。 如果一次操作中的分片散落在不同的数据库,仍然采用多线程处理对不同库的操作,但每个库的每次操作仍然只创建一个唯一的数据库连接。
适用场景
内存限制
模式适用于OLAP
操作,可以通过放宽对数据库连接的限制提升系统吞吐量;
连接限制
模式适用于OLTP
操作,OLTP通常带有分片键,会路由到单一的分片,因此严格控制数据库连接,以保证在线系统数据库资源能够被更多的应用所使用,是明智的选择。
OLAP(OnLine Analytical Processing) 和 OLTP(OnLine Transaction Processing) 分别代表什么?
总结
基础概念
:逻辑表,真实表,数据节点,绑定表,广播表,分片键,分片算法,分片策略,主键生成策略
核心功能
:数据分片,读写分离
执行流程
: SQL解析 => 查询优化 => SQL路由 => SQL改写 => SQL执行 => 结果归并