基本概念
ShardingJDBC可以理解为是一个以Jar包形式内嵌于Java应用内在JDBC层面提供额外服务并且完全兼容各种ORM的轻量级Java框架。
应用中业务代码执行的逻辑SQL根据ShardingJDBC的分片规则最终可能会生成在多个数据库表中执行的多条真实SQL以实现数据分库分表的目的。
ShardingJDBC与ShardingProxy区别
核心概念
核心概念 | 描述 |
---|---|
逻辑表 | 水平拆分的数据库相同逻辑和数据结构表的总称 |
真实表 | 在分片的数据库中真实存在的物理表 |
数据节点 | 数据分片的最小单元,由数据源名称和数据表组成 |
绑定表 | 分片规则一致的主表和子表 |
广播表 | 又称公共表,指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中都完全一致,如字典表 |
分片键 | 指用于分片的数据库字段,是将数据库(表)进行水平拆分的关键字段,SQL中若没有分片字段将会执行全路由,性能较差 |
分片算法 | 数据分片的算法,支持通过=、Between和in分片,需要由开发者自行实现 |
分片策略 | 分片键+分片算法即为分片策略,ShardingJDBC中一般采用基于Groovy表达式的inline分片策略,通过包含分片键的算法表达式来制定分片策略,如t_$ ->{id%2}即根据id%2分成t_0和t_1的2张表 |
核心原理
ShardingJDBC接收一条SQL指令时会依次进行 SQL解析->查询优化->SQL路由->SQL改写->SQL执行->结果归并 操作。
解析
该阶段会对SQL进行词法语法解析。
词法解析是将SQL语句解析为一个个不可分割的原子符号(Token),再根据各个数据库的方言字典归类为关键字、字面量、表达式和操作符等;
语法解析是将SQL转换为抽象语法树,再对抽象语法树遍历提取分片上下文(查询选项、表信息、分片主键信息、分片条件、分组信息、排序信息及分页信息等)并标记需要改写的位置。
路由
该阶段是将逻辑表的数据操作映射到真实数据节点的数据操作过程。
根据解析上下文匹配数据库和数据表的分片策略并生成路由路径。
对于携带分片键的SQL根据分片键操作符的不同分为单片路由(匹配=)、多片路由(匹配IN)和范围路由(Between);【分片键路由也分为直接路由、标准路由和笛卡尔路由等】
对于不携带分片键的SQL则采用广播路由的方式;【根据SQL类型又分为全库表路由、全库路由、全实例路由、单播路由和阻断路由】
标准路由(推荐)
适用范围:不包含关联查询或仅包含绑定表间的关联查询
一条逻辑SQL最终可能被拆分为多条可执行的真实SQL,另外绑定表关联查询与单表查询的SQL拆分数目相同,性能相当且不会出现笛卡尔积
笛卡尔路由
适用场景:非绑定表间的关联查询
非绑定表间关联查询不像绑定表能确定分片规则,需要拆分为笛卡尔积组合执行,因此效率性能较低
改写
该阶段是将逻辑SQL改写为在真实数据节点可执行的SQL。
如:select * from order where oid=1;
改写为:select * from order_1 where oid=1;
执行
该阶段采用自动化执行引擎,将路由和改写完成后的真实SQL安全且高效发送到底层数据源执行,执行引擎目标是自动化平衡资源控制与执行效率(能在内存限制模式与连接限制模式间自适应切换)
内存限制模式
前提:对一次SQL操作所耗费的数据库连接数量不做限制
举例:比如一个实际执行的SQL需要操作数据库实例的10张表,内存限制模式会创建10个数据库连接,然后通过多线程并发处理达到最高效率
总结:内存限制适用于OLAP操作,可通过放宽对数据库连接的限制提高系统吞吐量。
连接限制模式
前提:对一次SQL操作严格控制所耗费的数据库连接数量
举例:比如一个实际执行的SQL需要操作数据库实例的10张表,连接限制模式只会创建1个数据库连接,然后串行处理这10张关联的表;若某个数据操作分片到不同的库中,则依然会采用多线程并发处理,但是每个库的每次操作都是使用唯一的数据库连接。
归并
该阶段是收集各个真实数据节点匹配的数据组合成一个数据集返回给客户端的过程,即归并结果。
结果归并从功能上分为遍历、分页、排序、分组与聚合五种类型;从结构上分为内存归并、流式归并和装饰者归并,内存归并与流式归并互斥,装饰者归并可以在前两者基础上加以处理。
内存归并
指各分片的结果集都加载进内存中,然后通过统一的分组排序聚合等操作封装为结果集返回
流式归并
指每一次从数据库结果集中获取到的数据都能通过游标逐条获取的方式返回正确的单条数据。
多个分片结果集各自都是排好序的,并且分片结果集之间是无序的。(局部有序,全局无序)
归并原理:以降序为例,先将多个已排序的分片结果集的当前游标对应的数据值并放入到优先级队列(最大堆)中进行排序,队首元素对应最大数据值,当进行next调用时排在队首元素被弹出返回客户端展示,接着游标下移一位将对应数据值放入优先级队列,以此类推,当一个结果集无数据时则无需放入队列。
分库分表实战演示
一、分片键
package com.itjeffrey.shardingsphere.test.sharding;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicLong;
/**
* 使用shardingsphere的KeyGenerator接口 实现分片Id生成器
*/
@Slf4j
public class CustomShardingKeyGenerator implements ShardingKeyGenerator {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MMddHHmmss");
private final AtomicLong atl = new AtomicLong(0);
private Properties properties;
@Override
public Comparable<?> generateKey() {
long year;
try {
year = Long.parseLong(this.properties.getProperty("param.year", "2020"));
} catch (NumberFormatException e) {
year = 2020;
}
return Long.valueOf(year + formatter.format(LocalDateTime.now()) + atl.getAndIncrement());
}
//对应yml中的key-generator.type
@Override
public String getType() {
return "CUSTOM";
}
@Override
public Properties getProperties() {
return this.properties;
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
}
二、分片策略
Inline策略
基于一个分片键并且支持Groovy表达式的内联分片策略
Standard策略
支持一个分片键精确分片策略与范围分片策略
package com.itjeffrey.shardingsphere.test.sharding;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import java.util.Collection;
/**
* 自定义精确分片算法:Precise处理=与IN的路由
*
* @From: Jeffrey
*/
@Slf4j
public class CustomPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
String result = null;
String offset = String.valueOf(preciseShardingValue.getValue()).substring(14);
for (String str : collection) {
if (str.startsWith("shardingsphere")) {
//db1-0,1,4,5,8,9...
//db2-2,3,6,7,10,11...
int ext = (int) (Long.parseLong(offset) % 4);
if ((ext == 0 || ext == 1) && Integer.parseInt(str.split("-")[1]) == 1) {
result = str;
break;
}else if((ext == 2 || ext == 3) && Integer.parseInt(str.split("-")[1]) == 2){
result = str;
break;
}
} else if (str.startsWith("course")) {
//table1-0,2,4,6,8...
//table2-1,3,5,7,9...
int ext = (int) (Long.parseLong(offset) % 2);
if (Integer.parseInt(str.split("_")[1]) == (ext + 1)) {
result = str;
break;
}
}
}
if(null == result){
result = collection.iterator().next();
log.warn("no route info matched! switched to the default route[{}].", result);
}
return result;
}
}
package com.itjeffrey.shardingsphere.test.sharding;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;
import java.util.Collection;
import java.util.HashSet;
/**
* 自定义范围分片算法:Range处理Between、AND、>、<、>=、<=路由
*
* @From: Jeffrey
*/
@Slf4j
public class CustomRangeShardingAlgorithm implements RangeShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
Collection<String> result = new HashSet<>(2);
for (long cid = rangeShardingValue.getValueRange().lowerEndpoint(); cid <= rangeShardingValue.getValueRange().upperEndpoint(); cid++) {
String offset = String.valueOf(cid).substring(14);
for (String str : collection) {
if (result.contains(str)) continue;
if(str.startsWith("shardingsphere")){
int ext = (int) Long.parseLong(offset) % 4;
if((ext == 0 || ext == 1) && Integer.parseInt(str.split("-")[1]) == 1){
result.add(str);
}else if((ext == 2 || ext == 3) && Integer.parseInt(str.split("-")[1]) == 2){
result.add(str);
}
}else if(str.startsWith("course")){
int ext = (int) (Long.parseLong(offset) % 2);
if (Integer.parseInt(str.split("_")[1]) == (ext + 1)) {
result.add(str);
}
}
}
}
if(CollectionUtils.isEmpty(result)){
result = collection;
log.warn("no route info matched! switched to the default route{ {} }.", JSON.toJSONString(result));
}
return result;
}
}
Complex策略
支持多个分片键的复杂分片策略
package com.itjeffrey.shardingsphere.test.sharding;
import com.google.common.collect.Range;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
/**
* 自定义复合分片算法:支持多个分片键
*
* @From: Jeffrey
*/
public class CustomComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> collection, ComplexKeysShardingValue<Long> complexKeysShardingValue) {
Collection<String> result = new HashSet<>(2);
Map<String, Collection<Long>> shardingValuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
Map<String, Range<Long>> rangeValuesMap = complexKeysShardingValue.getColumnNameAndRangeValuesMap();
if(MapUtils.isNotEmpty(shardingValuesMap)){
Long cid = getShardingValue(shardingValuesMap, "cid");
//Long userId = getShardingValue(shardingValuesMap, "user_id");
String offset = String.valueOf(cid).substring(14);
for (String str : collection) {
if (str.startsWith("shardingsphere")) {
//db1-0,1,4,5,8,9...
//db2-2,3,6,7,10,11...
int ext = (int) (Long.parseLong(offset) % 4);
if ((ext == 0 || ext == 1) && Integer.parseInt(str.split("-")[1]) == 1) {
result.add(str);
break;
}else if((ext == 2 || ext == 3) && Integer.parseInt(str.split("-")[1]) == 2){
result.add(str);
break;
}
} else if (str.startsWith("course")) {
//table1-0,2,4,6,8...
//table2-1,3,5,7,9...
int ext = (int) (Long.parseLong(offset) % 2);
if (Integer.parseInt(str.split("_")[1]) == (ext + 1)) {
result.add(str);
break;
}
}
}
}else if(MapUtils.isNotEmpty(rangeValuesMap)){
//TODO
}
return result;
}
private Long getShardingValue(Map<String, Collection<Long>> shardingValuesMap, String cid) {
for (Map.Entry<String, Collection<Long>> entry : shardingValuesMap.entrySet()) {
if(StringUtils.equals(cid, entry.getKey())){
return entry.getValue().iterator().next();
}
}
return null;
}
}
Hint策略
支持外部分片键的强制分片策略
package com.itjeffrey.shardingsphere.test.sharding;
import lombok.extern.slf4j.Slf4j;
import org.apache.shardingsphere.api.sharding.hint.HintShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.hint.HintShardingValue;
import java.util.Collection;
import java.util.Collections;
/**
* 自定义强制分片算法:分片键不与SQL语句关联,使用外部分片值进行数据分片,分片键线程隔离
* 可以通过编程的方式向 HintManager 中添加分片值,该分片值仅在当前线程内生效
* 使用限制:
* 1.不支持多层子查询
* 2.不支持UNION
* 3.不支持函数计算
*
* @From: Jeffrey
*/
@Slf4j
public class CustomHintShardingAlgorithm implements HintShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> collection, HintShardingValue<Long> hintShardingValue) {
Collection<Long> values = hintShardingValue.getValues();
if(values.contains("666")){
return Collections.singleton("user_1");
}
return collection;
}
}
//Hint插入
HintManager hintManager = HintManager.getInstance();
hintManager.addTableShardingValue("user", "666");
userMapper.insert(user);
//清除ThreadLocal
hintManager.close();