概述

分库分表是什么

随着公司业务快速发展,数据库中的数据量猛增,访问性能也变慢了,优化迫在眉睫。分析一下问题出现在哪儿呢? 关系型数据库本身比较容易成为系统瓶颈,单机存储容量、连接数、处理能力都有限。当单表的数据量达到1000W或100G以后,由于查询维度较多,即使添加从库、优化索引,做很多操作时性能仍下降严重。

方案1

通过提升服务器硬件能力来提高数据处理能力,比如增加存储容量 、CPU等,这种方案成本很高,并且如果瓶颈在MySQL本身那么提高硬件也是有限的。

方案2

把数据分散在不同的数据库中,使得单一数据库的数据量变小来缓解单一数据库的性能问题,从而达到提升数据库性能的目的

如下图:将电商数据库拆分为若干独立的数据库,并且对于大表也拆分为若干小表,通过这种数据库拆分的方法来解决数据库的性能问题。

分库分表就是为了解决由于数据量过大而导致数据库性能降低的问题,将原来独立的数据库拆分成若干数据库组成,将数据大表拆分成若干数据表组成,使得单一数据库、单一数据表的数据量变小,从而达到提升数据库性能的目的。

分库分表的方式

分库分表包括分库和分表两个部分,在生产中通常包括:垂直分库垂直分表水平分库水平分表四种方式。

垂直分表

通常在商品列表中是不显示商品详情信息的,如下图:

用户在浏览商品列表时,只有对某商品感兴趣时才会查看该商品的详细描述。因此,商品信息中商品描述字段访问频次较低,且该字段存储占用空间较大,访问单个数据IO时间较长;商品信息中商品名称、商品图片、商品价格等其他字段数据访问频次较高

由于可将访问频次低的商品描述信息单独存放在一张表中,访问频次较高的商品基本信息单独放在一张表中。

商品列表可采用以下sql:

SELECT p.*,r.[地理区域名称],s.[店铺名称],s.[信誉]
FROM [商品信息] p
LEFT JOIN [地理区域] r ON p.[产地] = r.[地理区域编码]
LEFT JOIN [店铺信息] s ON p.id = s.[所属店铺]
WHERE...ORDER BY...LIMIT...

需要获取商品描述时,再通过以下sql获取:

SELECT  *
FROM [商品描述]
WHERE [商品ID] = ?

这一步优化,就叫垂直分表

垂直分表是指将一个表按照字段分成多表,每个表存储其中一部分字段。

带来的提升是:

  1. 为了避免IO争抢并减少锁表的几率,查看详情的用户与商品信息浏览互不影响
  2. 充分发挥热门数据的操作效率,商品信息的操作的高效率不会被商品描述的低效率所拖累。

一般来说,某业务实体中的各个数据项的访问频次是不一样的,部分数据项可能是占用存储空间比较大的BLOB或是TEXT。例如上例中的商品描述。所以,当表数据量很大时,可以将表按字段切开,将热门字段、冷门字段分开放置在不同库中,这些库可以放在不同的存储设备上,避免IO争抢。垂直切分带来的性能提升主要集中在热门数据的操作效率上,而且磁盘争用情况减少。

通常按以下原则进行垂直拆分:

  1. 把不常用的字段单独放在一张表;
  2. 把text,blob等大字段拆分出来放在附表中;
  3. 经常组合查询的列放在一张表中;

垂直分库

通过垂直分表性能得到了一定程度的提升,但是还没有达到要求,并且磁盘空间也快不够了,因为数据还是始终限制在一台服务器,库内垂直分表只解决了单一表数据量过大的问题,但没有将表分布到不同的服务器上,因此每个表还是竞争同一个物理机的CPU、内存、网络IO、磁盘。

经过思考,把原有的SELLER_DB(卖家库),分为了PRODUCT_DB(商品库)STORE_DB(店铺库),并把这两个库分散到不同服务器,如下图:

由于商品信息与商品描述业务耦合度较高,因此一起被存放在PRODUCT_DB(商品库);而店铺信息相对独立,因此单独被存放在STORE_DB(店铺库)

这一步优化,就叫垂直分库

垂直分库是指按照业务将表进行分类,分布到不同的数据库上面,每个库可以放在不同的服务器上,它的核心理念是专库专用

它带来的提升是:

  1. 解决业务层面的耦合,业务清晰
  2. 能对不同业务的数据进行分级管理、维护、监控、扩展等
  3. 高并发场景下,垂直分库一定程度的提升IO、数据库连接数、降低单机硬件资源的瓶颈

垂直分库通过将表按业务分类,然后分布在不同数据库,并且可以将这些数据库部署在不同服务器上,从而达到多个服务器共同分摊压力的效果,但是依然没有解决单表数据量过大的问题。

水平分库

经过垂直分库后,数据库性能问题得到一定程度的解决,但是随着业务量的增长,PRODUCT_DB(商品库)单库存储数据已经超出预估。假设目前有8w店铺,每个店铺平均150个不同规格的商品,再算上增长,那商品数量得往1500w+上预估,并且PRODUCT_DB(商品库)属于访问非常频繁的资源,单台服务器已经无法支撑。此时该如何优化?

再次分库?但是从业务角度分析,目前情况已经无法再次垂直分库。

尝试水平分库,将店铺ID为单数的和店铺ID为双数的商品信息分别放在两个库中。

也就是说,要操作某条数据,先分析这条数据所属的店铺ID。如果店铺ID为双数,将此操作映射至RRODUCT_DB1(商品库1);如果店铺ID为单数,将操作映射至RRODUCT_DB2(商品库2)。此操作要访问数据库名称的表达式为RRODUCT_DB[店铺ID%2 + 1]

这一步优化,就叫水平分库

水平分库是把同一个表的数据按一定规则拆到不同的数据库中,每个库可以放在不同的服务器上

它带来的提升是:

  1. 解决了单库大数据,高并发的性能瓶颈。
  2. 提高了系统的稳定性及可用性。

当一个应用难以再细粒度的垂直切分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时候就需要进行水平分库了,经过水平切分的优化,往往能解决单库存储量及性能瓶颈。但由于同一个表被分配在不同的数据 库,需要额外进行数据操作的路由工作,因此大大提升了系统复杂度

水平分表

按照水平分库的思路把PRODUCT_DB_X(商品库)内的表也可以进行水平拆分,其目的也是为解决单表数据量大的问题,如下图:

与水平分库的思路类似,不过这次操作的目标是表,商品信息及商品描述被分成了两套表。如果商品ID为双数,将此操作映射至商品信息1表;如果商品ID为单数,将操作映射至商品信息2表。此操作要访问表名称的表达式为商品信息[商品ID%2 + 1]

这一步优化,就叫水平分表

水平分表是在同一个数据库内,把同一个表的数据按一定规则拆到多个表中

它带来的提升是:

  1. 优化单一表数据量过大而产生的性能问题
  2. 避免IO争抢并减少锁表的几率

库内的水平分表,解决了单一表数据量过大的问题,分出来的小表中只包含一部分数据,从而使得单个表的数据量变小,提高检索性能。

小结

  • 垂直分表:可以把一个宽表的字段按访问频次、是否是大字段的原则拆分为多个表,这样既能使业务清晰,还能提升部分性能。拆分后,尽量从业务角度避免联查,否则性能方面将得不偿失

  • 垂直分库:可以把多个表按业务耦合松紧归类,分别存放在不同的库,这些库可以分布在不同服务器,从而使访问压力被多服务器负载,大大提升性能,同时能提高整体架构的业务清晰度,不同的业务库可根据自身情况定制优化方案。但是它需要解决跨库带来的所有复杂问题

  • 水平分库:可以把一个表的数据(按数据行)分到多个不同的库,每个库只有这个表的部分数据,这些库可以分布在不同服务器,从而使访问压力被多服务器负载,大大提升性能。它不仅需要解决跨库带来的所有复杂问题,还要解决数据路由的问题

  • 水平分表:可以把一个表的数据(按数据行)分到多个同一个数据库的多张表中,每个表只有这个表的部分数据,这样做能小幅提升性能,它仅仅作为水平分库的一个补充优化

一般来说,在系统设计阶段就应该根据业务耦合松紧来确定垂直分库,垂直分表方案,在数据量及访问压力不是特别大的情况,首先考虑缓存读写分离索引技术等方案。若数据量极大,且持续增长,再考虑水平分库水平分表方案。

分库分表带来的问题

分库分表能有效的缓解了单机和单库带来的性能瓶颈和压力,突破网络IO、硬件资源、连接数的瓶颈,同时也带来了一些问题。

事务一致性问题

由于分库分表把数据分布在不同库甚至不同服务器,不可避免会带来分布式事务问题。

跨节点关联查询

在没有分库前,检索商品时可以通过以下SQL对店铺信息进行关联查询:

SELECT p.*,r.[地理区域名称],s.[店铺名称],s.[信誉]
FROM [商品信息] p
LEFT 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-JDBCSharding- ProxySharding-Sidecar这3款相互独立的产品组成。 他们均提供标准化的数据分片分布式事务数据库治理功能,可适用于如Java同构、异构语言、容器、云原生等各种多样化的应用场景。

Sharding-JDBC定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。

Sharding-JDBC的核心功能为数据分片读写分离,通过Sharding-JDBC,应用可以透明的使用jdbc访问已经分库分表、读写分离的多个数据源,而不用关心数据源的数量以及数据如何分布。

  • 适用于任何基于Java的ORM框架,如: HibernateMybatisSpring JDBC Template或直接使用JDBC
  • 基于任何第三方的数据库连接池,如:DBCPC3P0BoneCPDruidHikariCP等。
  • 支持任意实现JDBC规范的数据库。目前支持MySQLOracleSQLServerPostgreSQL

上图展示了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_1t_order_2,这两张表是订单表拆分后的表。

  • 通过Sharding-Jdbc向订单表插入数据,按照一定的分片规则,主键为偶数的进入t_order_1另一部分数据进入t_order_2
  • 通过Sharding-Jdbc 查询数据,根据 SQL语句的内容从t_order_1t_order_2查询数据;

创建数据库表

创建订单库order_db

CREATE DATABASE `order_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

在order_db中创建t_order_1t_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 {

/**
* 新增订单
* @param price 订单价格
* @param userId 用户id
* @param status 订单状态
* @return
*/
@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);

/**
* 根据id列表查询多个订单
* @param orderIds 订单id列表
* @return
*/
@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之后做了一下操作:

  1. 解析sql,获取片键值,在本例中是order_id
  2. Sharding-JDBC通过规则配置 t_order_$->{order_id % 2 + 1},当order_id为偶数时,应该往t_order_1表插数据,为奇数时,往t_order_2插数据。
  3. 于是Sharding-JDBC根据order_id的值改写sql语句,改写后的SQL语句是真实所要执行的SQL语句。
  4. 执行改写后的真实sql语句
  5. 将所有真正执行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;
}

// 定义t_order表的分片策略
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;
}

// 定义sharding‐Jdbc数据源
@Bean
DataSource getShardingDataSource() throws SQLException {
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
shardingRuleConfig.getTableRuleConfigs().add(getOrderTableRuleConfiguration());
//spring.shardingsphere.props.sql.show = true
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" destroymethod="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_0t_order_1,他们的逻辑表名为t_order

真实表

在分片的数据库中真实存在的物理表。即上个示例中的t_order_0t_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不仅提供了内置的分布式主键生成器,例如UUIDSnowflake还抽离出分布式主键生成器的接口(io.shardingsphere.core.keygen.KeyGenerator),方便用户自行实现自定义的自增主键生成器。

雪花算法

snowflake算法是一款本地生成的(ID生成过程不依赖任何中间件,无网络通信),保证ID全局唯一,并且ID总体有序递增,

  • 组成结构

大致由:首位无效符时间戳差值机器(进程)编码序列号四部分组成,雪花算法生成的ID是纯数字且具有时间顺序的。

  1. 1 bit不用,因为二进制里第一个bit为如果是1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0
  2. 时间位:可以根据时间进行排序,有助于提高查询速度。41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1个毫秒值,换算成年就是表示 69 年的时间。
  3. 机器id位:适用于分布式环境下对多节点的各个节点进行标识,可以具体根据节点数和部署情况设计,划分机器位10位长度,如划分5位表示进程位等,这个服务最多可以部署在2^10 台机器上,也就是 1024 台机器。
  4. 序列号位:是一系列的自增id,可以支持同一节点同一毫秒生成多个ID序号,12位的计数序列号支持每个节点每毫秒产生4096个ID序号
  • 优点
  1. 时间自增排序
  2. 适合分布式场景,整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分)效率较高,一个节点每毫秒4096个ID序号,服务最大每毫秒409.6万个序列号
  • 缺点
  1. 雪花算法在单机系统上ID是递增的,但是在分布式系统多节点的情况下不是绝对递增,所有节点的时钟(System.currentTimeMillis())并不能保证不完全同步,所以有可能会出现不是全局递增的情况
  2. 不能在一台服务器上部署多个分布式ID服务;
  3. 时钟回拨问题;

使用方法

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();
  • 对Snowflake时钟回拨问题处理
private boolean waitTolerateTimeDifferenceIfNeed(long currentMilliseconds) throws Throwable {
try {
//lastMilliseconds 最后一次生成序列时间
if (this.lastMilliseconds <= currentMilliseconds) {
//如果lastMilliseconds
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;
}

核心代码如下,几个实现的关键点:

  1. synchronized保证线程安全;
  2. 如果出现时间回拨,判断时钟回拨的时间是否超过最大容忍的毫秒数值,如果超过抛出异常;
  3. 如果当前时间和上一次是同一秒时间,那么sequence自增。如果同一秒内sequence自增值超过2^13-1,那么就会自旋等待下一秒;
  4. 如果是新的一秒,那么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;


/**
- 自定义实现 精准分片算法(PreciseShardingAlgorithm)接口
- 数据库DB的精准分片
*/
public class PreciseShardingDBAlgorithm implements PreciseShardingAlgorithm<Integer> {

/**
*
* @param databaseNames 有效的数据源 或者 表 的名字 databaseNames 就为配置文件中的 配置的数据源信息 -> ds0 , ds1
* @param shardingValue SQL 分片列 对应的实际值
* @return
*/
@Override
public String doSharding(Collection<String> databaseNames,
PreciseShardingValue<Integer> shardingValue) {
/*
* 作用:散列到具体的哪个库里面去
* shardingValue : SQL -> SELECT * FROM t_order WHERE order _id IN(1,3,6)
* shardingValue = [1,3,6]
* */
for (String each : databaseNames) {
/**
* 此方法如果参数所表示的字符序列是由该对象表示的字符序列的后缀返回true, 否则为false;
* 请注意,如果参数是空字符串或等于此String对象由equals(Object)方法确定结果为 true。
* String Str = new String("This is really not immutable!!"); retVal = Str.endsWith( "immutable!!" )
* 为true
* ds0.endsWith("0") -> true ;
*/
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;


/**
* 自定义实现 范围分片算法(RangeShardingAlgorithm)接口
* 数据库DB的范围分片
*/
public class RangeShardingDBAlgorithm implements RangeShardingAlgorithm<Integer> {


@Override
public Collection<String> doSharding(final Collection<String> databaseNames,
final RangeShardingValue<Integer> shardingValue) {

/**
* 自定义SQL -> SELECT * FROM t_order WHERE order _id Between 2000 and 4000
* ds0.t_order: 1000 ~ 3000
* ds1.t_order: 3001 ~ 5000
* ds2.t_order: 5001 ~ 7000
*
* 执行路由后的SQL 应为:
* SELECT * FROM ds0.t_order WHERE order _id Between 2000 and 3000
* SELECT * FROM ds1.t_order WHERE order _id Between 3001 and 4000
*/
Set<String> result = new LinkedHashSet<>();
// 从sql 中获取 Between 2000 and 4000 的值,将2000 赋值给 lower, 4000 赋值给 upper
int lower = shardingValue.getValueRange().lowerEndpoint();
int upper = shardingValue.getValueRange().upperEndpoint();
for (int i = lower; i <= upper; i++) {
for (String each : databaseNames) { //ds0,ds1
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;


/**
* 自定义实现 精准分片算法(PreciseShardingAlgorithm)接口
* 数据表table的精准分片
*/
public class PreciseShardingTableAlgorithm implements PreciseShardingAlgorithm<Long> {

/**
* 注释键 PreciseShardingDBAlgorithm
* @param tableNames
* @param shardingValue
* @return
*/
@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;

/**
* 自定义实现 范围分片算法(RangeShardingAlgorithm)接口
* 数据表 table 的范围分片
*/
public class RangeShardingTableAlgorithm implements RangeShardingAlgorithm<Integer> {

@Override
public Collection<String> doSharding(final Collection<String> tableNames,
final RangeShardingValue<Integer> shardingValue) {
Set<String> result = new LinkedHashSet<>();
// 如果between 2000000 and 7000000
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 {

/**
*
* @param collection 在加载配置文件时,会解析表分片规则。将结果存储到 collection中,doSharding()参数使用
* @param shardingValues SQL中对应的
* @return
*/
@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<>();

// user_id,order_id分片键进行分表
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;
}

/**
* 例如: SELECT * FROM T_ORDER user_id = 100000 AND order_id = 1000009
* 循环 获取SQL 中 分片键列对应的value值
* @param shardingValues sql 中分片键的value值 -> 1000009
* @param key 分片键列名 -> user_id
* @return shardingValues 集合 -> [1000009]
*/
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;
// user_id,order_id分片键进行分表
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 {

/**
* 自定义Hint 实现算法
* 能够保证绕过Sharding-JDBC SQL解析过程
* @param availableTargetNames
* @param shardingValue 不再从SQL 解析中获取值,而是直接通过hintManager.addTableShardingValue("t_order", 1)参数指定
* @return
*/
@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)) {
// hint分片算法的ShardingValue有两种具体类型:
// ListShardingValue和RangeShardingValue
// 使用哪种取决于HintManager.addDatabaseShardingValue(String, String, ShardingOperator,...),ShardingOperator的类型
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() {
// Hint分片策略必须要使用 HintManager工具类
HintManager hintManager = HintManager.getInstance();
//
hintManager.addDatabaseShardingValue("t_order", 0);
hintManager.addTableShardingValue("t_order", 1);

// 直接指定对应具体的数据库
//hintManager.setDatabaseShardingValue(1);
//在读写分离数据库中,Hint 可以强制读主库(主从复制是存在一定延时,但在业务场景中,可能更需要保证数据的实时性)
//hintManager.setMasterRouteOnly();
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_0t_score_2t_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种类型

  • 比较类型的聚合函数是指MAXMIN。它们需要对每一个同组的结果集数据进行比较,并且直接返回其最大或最小值即可。
  • 累加类型的聚合函数是指SUMCOUNT。它们需要将每一个同组的结果集数据进行累加。
  • 求平均值的聚合函数只有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执行 => 结果归并