Apache ShardingSphere 是一套开源的分布式数据库解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款既可以独立部署,又支持混合部署配合使用的产品组成;接下来的几篇文章将重点分析ShardingSphere-JDBC,从数据分片,分布式主键,分布式事务,读写分离,弹性伸缩等几个方面来介绍。java
ShardingSphere-JDBC定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为加强版的 JDBC 驱动,彻底兼容 JDBC 和各类 ORM 框架。总体架构图以下(来自官网):mysql
ShardingSphere-JDBC包含了众多的功能模块包括数据分片,分布式主键,分布式事务,读写分离,弹性伸缩等等;做为一个数据库中间件最核心的功能当属数据分片了,ShardingSphere-JDBC提供了不少分库分表的策略和算法,接下来看看具体是如何使用这些策略的;面试
做为一个开发者咱们但愿中间件能够帮咱们屏蔽底层的细节,让咱们在面对分库分表的场景下,能够像使用单库单表同样简单;固然ShardingSphere-JDBC不会让你们失望,引入了分片数据源、逻辑表等概念;算法
t_order
,对于开发者来讲只须要使用逻辑表便可;ShardingDataSource
有了以上两个最基本的概念固然还不够,还须要分库分表策略算法帮助咱们作路由处理;可是这两个概念可让开发者有一种使用单库单表的感受,就像下面这样一个简单的实例:sql
DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties()); Connection conn = dataSource.getConnection(); String sql = "select id,user_id,order_id from t_order where order_id = 103"; PreparedStatement preparedStatement = conn.prepareStatement(sql); ResultSet set = preparedStatement.executeQuery();
以上根据真实数据源列表,分库分表策略生成了一个抽象数据源,能够简单理解就是ShardingDataSource
;接下来的操做和咱们使用jdbc操做正常的单库单表没有任何区别;数据库
ShardingSphere-JDBC在分片策略上分别引入了分片算法、分片策略两个概念,固然在分片的过程当中分片键也是一个核心的概念;在此能够简单的理解分片策略 = 分片算法 + 分片键
;至于为何要这么设计,应该是ShardingSphere-JDBC考虑更多的灵活性,把分片算法单独抽象出来,方便开发者扩展;apache
提供了抽象分片算法类:ShardingAlgorithm
,根据类型又分为:精确分片算法、区间分片算法、复合分片算法以及Hint分片算法;编程
PreciseShardingAlgorithm
类,主要用于处理 =
和 IN
的分片;RangeShardingAlgorithm
类,主要用于处理 BETWEEN AND
, >
, <
, >=
, <=
分片;ComplexKeysShardingAlgorithm
类,用于处理使用多键做为分片键进行分片的场景;HintShardingAlgorithm
类,用于处理使用 Hint
行分片的场景;以上全部的算法类都是接口类,具体实现交给开发者本身;安全
分片策略基本和上面的分片算法对应,包括:标准分片策略、复合分片策略、Hint分片策略、内联分片策略、不分片策略;微信
标准分片策略:对应StandardShardingStrategy
类,提供PreciseShardingAlgorithm
和RangeShardingAlgorithm
两个分片算法,PreciseShardingAlgorithm
是必须的,RangeShardingAlgorithm
可选的;
public final class StandardShardingStrategy implements ShardingStrategy { private final String shardingColumn; private final PreciseShardingAlgorithm preciseShardingAlgorithm; private final RangeShardingAlgorithm rangeShardingAlgorithm; }
复合分片策略:对应ComplexShardingStrategy
类,提供ComplexKeysShardingAlgorithm
分片算法;
public final class ComplexShardingStrategy implements ShardingStrategy { @Getter private final Collection<String> shardingColumns; private final ComplexKeysShardingAlgorithm shardingAlgorithm; }
能够发现支持多个分片键;
Hint分片策略:对应HintShardingStrategy
类,经过 Hint 指定分片值而非从 SQL 中提取分片值的方式进行分片的策略;提供HintShardingAlgorithm
分片算法;
public final class HintShardingStrategy implements ShardingStrategy { @Getter private final Collection<String> shardingColumns; private final HintShardingAlgorithm shardingAlgorithm; }
InlineShardingStrategy
类,没有提供分片算法,路由规则经过表达式来实现;NoneShardingStrategy
类,不分片策略;在使用中咱们并无直接使用上面的分片策略类,ShardingSphere-JDBC分别提供了对应策略的配置类包括:
StandardShardingStrategyConfiguration
ComplexShardingStrategyConfiguration
HintShardingStrategyConfiguration
InlineShardingStrategyConfiguration
NoneShardingStrategyConfiguration
有了以上相关基础概念,接下来针对每种分片策略作一个简单的实战,在实战前首先准备好库和表;
分别准备两个库:ds0
、ds1
;而后每一个库分别包含两个表:t_order0
,t_order1
;
CREATE TABLE `t_order0` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL, `order_id` bigint(20) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
咱们这里有两个数据源,这里都使用java代码的方式来配置:
// 配置真实数据源 Map<String, DataSource> dataSourceMap = new HashMap<>(); // 配置第一个数据源 BasicDataSource dataSource1 = new BasicDataSource(); dataSource1.setDriverClassName("com.mysql.jdbc.Driver"); dataSource1.setUrl("jdbc:mysql://localhost:3306/ds0"); dataSource1.setUsername("root"); dataSource1.setPassword("root"); dataSourceMap.put("ds0", dataSource1); // 配置第二个数据源 BasicDataSource dataSource2 = new BasicDataSource(); dataSource2.setDriverClassName("com.mysql.jdbc.Driver"); dataSource2.setUrl("jdbc:mysql://localhost:3306/ds1"); dataSource2.setUsername("root"); dataSource2.setPassword("root"); dataSourceMap.put("ds1", dataSource2);
这里配置的两个数据源都是普通的数据源,最后会把dataSourceMap交给ShardingDataSourceFactory
管理;
表规则配置类TableRuleConfiguration
,包含了五个要素:逻辑表、真实数据节点、数据库分片策略、数据表分片策略、分布式主键生成策略;
TableRuleConfiguration orderTableRuleConfig = new TableRuleConfiguration("t_order", "ds${0..1}.t_order${0..1}"); orderTableRuleConfig.setDatabaseShardingStrategyConfig( new StandardShardingStrategyConfiguration("user_id", new MyPreciseSharding())); orderTableRuleConfig.setTableShardingStrategyConfig( new StandardShardingStrategyConfiguration("order_id", new MyPreciseSharding())); orderTableRuleConfig.setKeyGeneratorConfig(new KeyGeneratorConfiguration("SNOWFLAKE", "id"));
真实数据节点:这里使用行表达式进行配置的,简化了配置;上面的配置就至关于配置了:
db0 ├── t_order0 └── t_order1 db1 ├── t_order0 └── t_order1
数据库分片策略:这里的库分片策略就是上面介绍的五种类型,这里使用的StandardShardingStrategyConfiguration
,须要指定分片键和分片算法,这里使用的是精确分片算法;
public class MyPreciseSharding implements PreciseShardingAlgorithm<Integer> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) { Integer index = shardingValue.getValue() % 2; for (String target : availableTargetNames) { if (target.endsWith(index + "")) { return target; } } return null; } }
这里的shardingValue就是user_id对应的真实值,每次和2取余;availableTargetNames可选择就是{ds0,ds1};看余数和哪一个库能匹配上就表示路由到哪一个库;
配置分片规则ShardingRuleConfiguration
,包括多种配置规则:表规则配置、绑定表配置、广播表配置、默认数据源名称、默认数据库分片策略、默认表分片策略、默认主键生成策略、主从规则配置、加密规则配置;
以上准备好,就能够操做数据库了,这里执行插入操做:
DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties()); Connection conn = dataSource.getConnection(); String sql = "insert into t_order (user_id,order_id) values (?,?)"; PreparedStatement preparedStatement = conn.prepareStatement(sql); for (int i = 1; i <= 10; i++) { preparedStatement.setInt(1, i); preparedStatement.setInt(2, 100 + i); preparedStatement.executeUpdate(); }
经过以上配置的真实数据源、分片规则以及属性文件建立分片数据源ShardingDataSource
;接下来就能够像使用单库单表同样操做分库分表了,sql中能够直接使用逻辑表,分片算法会根据具体的值就行路由处理;
通过路由最终:奇数入ds1.t_order1,偶数入ds0.t_order0;以上使用了最多见的精确分片算法,下面继续看一下其余几种分片算法;
上面的介绍的精确分片算法中,经过PreciseShardingValue
来获取当前分片键值,ShardingSphere-JDBC针对每种分片算法都提供了相应的ShardingValue
,具体包括:
用在区间查询的时候,好比下面的查询SQL:
select * from t_order where order_id>2 and order_id<9
以上两个区间值二、9会直接保存到RangeShardingValue
中,这里没有指定user_id用来作库路由,因此会访问两个库;
public class MyRangeSharding implements RangeShardingAlgorithm<Integer> { @Override public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Integer> shardingValue) { Collection<String> result = new LinkedHashSet<>(); Range<Integer> range = shardingValue.getValueRange(); // 区间开始和结束值 int lower = range.lowerEndpoint(); int upper = range.upperEndpoint(); for (int i = lower; i <= upper; i++) { Integer index = i % 2; for (String target : availableTargetNames) { if (target.endsWith(index + "")) { result.add(target); } } } return result; } }
能够发现会检查区间开始和结束中的每一个值和2取余,是否都能和真实的表匹配;
能够同时使用多个分片键,好比能够同时使用user_id和order_id做为分片键;
orderTableRuleConfig.setDatabaseShardingStrategyConfig( new ComplexShardingStrategyConfiguration("order_id,user_id", new MyComplexKeySharding())); orderTableRuleConfig.setTableShardingStrategyConfig( new ComplexShardingStrategyConfiguration("order_id,user_id", new MyComplexKeySharding()));
如上在配置分库分表策略时,指定了两个分片键,用逗号隔开;分片算法以下:
public class MyComplexKeySharding implements ComplexKeysShardingAlgorithm<Integer> { @Override public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Integer> shardingValue) { Map<String, Collection<Integer>> map = shardingValue.getColumnNameAndShardingValuesMap(); Collection<Integer> userMap = map.get("user_id"); Collection<Integer> orderMap = map.get("order_id"); List<String> result = new ArrayList<>(); // user_id,order_id分片键进行分表 for (Integer userId : userMap) { for (Integer orderId : orderMap) { int suffix = (userId+orderId) % 2; for (String s : availableTargetNames) { if (s.endsWith(suffix+"")) { result.add(s); } } } } return result; } }
在一些应用场景中,分片条件并不存在于 SQL,而存在于外部业务逻辑;能够经过编程的方式向 HintManager
中添加分片条件,该分片条件仅在当前线程内生效;
// 设置库表分片策略 orderTableRuleConfig.setDatabaseShardingStrategyConfig(new HintShardingStrategyConfiguration(new MyHintSharding())); orderTableRuleConfig.setTableShardingStrategyConfig(new HintShardingStrategyConfiguration(new MyHintSharding())); // 手动设置分片条件 int hitKey1[] = { 2020, 2021, 2022, 2023, 2024 }; int hitKey2[] = { 3020, 3021, 3022, 3023, 3024 }; DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties()); Connection conn = dataSource.getConnection(); for (int i = 1; i <= 5; i++) { final int index = i; new Thread(new Runnable() { @Override public void run() { try { HintManager hintManager = HintManager.getInstance(); String sql = "insert into t_order (user_id,order_id) values (?,?)"; PreparedStatement preparedStatement = conn.prepareStatement(sql); // 分别添加库和表分片条件 hintManager.addDatabaseShardingValue("t_order", hitKey1[index - 1]); hintManager.addTableShardingValue("t_order", hitKey2[index - 1]); preparedStatement.setInt(1, index); preparedStatement.setInt(2, 100 + index); preparedStatement.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } }).start(); }
以上实例中,手动设置了分片条件,分片算法以下所示:
public class MyHintSharding implements HintShardingAlgorithm<Integer> { @Override public Collection<String> doSharding(Collection<String> availableTargetNames, HintShardingValue<Integer> shardingValue) { List<String> shardingResult = new ArrayList<>(); for (String targetName : availableTargetNames) { String suffix = targetName.substring(targetName.length() - 1); Collection<Integer> values = shardingValue.getValues(); for (int value : values) { if (value % 2 == Integer.parseInt(suffix)) { shardingResult.add(targetName); } } } return shardingResult; } }
配置NoneShardingStrategyConfiguration
便可:
orderTableRuleConfig.setDatabaseShardingStrategyConfig(new NoneShardingStrategyConfiguration()); orderTableRuleConfig.setTableShardingStrategyConfig(new NoneShardingStrategyConfiguration());
这样数据会插入每一个库每张表,能够理解为广播表
面对多个数据库表须要有惟一的主键,引入了分布式主键功能,内置的主键生成器包括:UUID、SNOWFLAKE;
直接使用UUID.randomUUID()生成,主键没有任何规则;对应的主键生成类:UUIDShardingKeyGenerator
;
实现类:SnowflakeShardingKeyGenerator
;使⽤雪花算法⽣成的主键,⼆进制表⽰形式包含 4 部分,从⾼位到低位分表为:1bit 符号位、41bit 时间戳位、10bit ⼯做进程位以及 12bit 序列号位;来自官网的图片:
实现接口:ShardingKeyGenerator
,实现本身的主键生成器;
public interface ShardingKeyGenerator extends TypeBasedSPI { Comparable<?> generateKey(); }
使用也很简单,直接使用KeyGeneratorConfiguration
便可,配置对应的算法类型和字段名称:
orderTableRuleConfig.setKeyGeneratorConfig(new KeyGeneratorConfiguration("SNOWFLAKE", "id"));
这里使用雪花算法生成器,对应生成的字段是id;结果以下:
mysql> select * from t_order0; +--------------------+---------+----------+ | id | user_id | order_id | +--------------------+---------+----------+ | 589535589984894976 | 0 | 0 | | 589535590504988672 | 2 | 2 | | 589535590718898176 | 4 | 4 | +--------------------+---------+----------+
ShardingSphere-JDBC使用分布式事务和使用本地事务没什么区别,提供了透明化的分布式事务;支持的事务类型包括:本地事务、XA事务和柔性事务,默认是本地事务;
public enum TransactionType { LOCAL, XA, BASE }
根据具体使用XA事务仍是柔性事务,须要引入不一样的模块;
<dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-transaction-xa-core</artifactId> </dependency> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>shardingsphere-transaction-base-seata-at</artifactId> </dependency>
ShardingSphere-JDBC提供了分布式事务管理器ShardingTransactionManager
,实现包括:
SeataATShardingTransactionManager:基于 Seata 的分布式事务管理器;
XA 的分布式事务管理器具体实现包括:Atomikos、Narayana、Bitronix;默认是Atomikos;
默认的事务类型是TransactionType.LOCAL,ShardingSphere-JDBC天生面向多数据源,本地模式实际上是循环提交每一个数据源的事务,不能保证数据的一致性,因此须要使用分布式事务,具体使用也很简单:
//改变事务类型为XA TransactionTypeHolder.set(TransactionType.XA); DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties()); Connection conn = dataSource.getConnection(); try { //关闭自动提交 conn.setAutoCommit(false); String sql = "insert into t_order (user_id,order_id) values (?,?)"; PreparedStatement preparedStatement = conn.prepareStatement(sql); for (int i = 1; i <= 5; i++) { preparedStatement.setInt(1, i - 1); preparedStatement.setInt(2, i - 1); preparedStatement.executeUpdate(); } //事务提交 conn.commit(); } catch (Exception e) { e.printStackTrace(); //事务回滚 conn.rollback(); }
能够发现使用起来仍是很简单的,ShardingSphere-JDBC会根据当前的事务类型,在提交的时候判断是走本地事务提交,仍是使用分布式事务管理器ShardingTransactionManager
进行提交;
对于同一时刻有大量并发读操做和较少写操做类型的应用系统来讲,将数据库拆分为主库和从库,主库负责处理事务性的增删改操做,从库负责处理查询操做,可以有效的避免由数据更新致使的行锁,使得整个系统的查询性能获得极大的改善。
在上面章节介绍分片规则的时候,其中有说到主从规则配置,其目的就是用来实现读写分离的,核心配置类:MasterSlaveRuleConfiguration
:
public final class MasterSlaveRuleConfiguration implements RuleConfiguration { private final String name; private final String masterDataSourceName; private final List<String> slaveDataSourceNames; private final LoadBalanceStrategyConfiguration loadBalanceStrategyConfiguration; }
主从负载算法类:MasterSlaveLoadBalanceAlgorithm
,实现类包括:随机和循环;
RoundRobinMasterSlaveLoadBalanceAlgorithm
RandomMasterSlaveLoadBalanceAlgorithm
分别给ds0和ds1准备从库:ds01和ds11,分别配置主从同步;读写分离配置以下:
List<String> slaveDataSourceNames0 = new ArrayList<String>(); slaveDataSourceNames0.add("ds01"); MasterSlaveRuleConfiguration masterSlaveRuleConfiguration0 = new MasterSlaveRuleConfiguration("ds0", "ds0", slaveDataSourceNames0); shardingRuleConfig.getMasterSlaveRuleConfigs().add(masterSlaveRuleConfiguration0); List<String> slaveDataSourceNames1 = new ArrayList<String>(); slaveDataSourceNames1.add("ds11"); MasterSlaveRuleConfiguration masterSlaveRuleConfiguration1 = new MasterSlaveRuleConfiguration("ds1", "ds1", slaveDataSourceNames1); shardingRuleConfig.getMasterSlaveRuleConfigs().add(masterSlaveRuleConfiguration1);
这样在执行查询操做的时候会自动路由到从库,实现读写分离;
本文重点介绍了ShardingSphere-JDBC的数据分片功能,这也是全部数据库中间件的核心功能;固然分布式主键、分布式事务、读写分离等功能也是必不可少的;同时ShardingSphere还引入了弹性伸缩
的功能,这是一个很是亮眼的功能,由于数据库分片自己是有状态的,因此咱们在项目启动之初都固定了多少库多少表,而后经过分片算法路由到各个库表,可是业务的发展每每超乎咱们的预期,这时候若是想扩表扩库会很麻烦,目前看ShardingSphere官网弹性伸缩
处于alpha开发阶段,很是期待此功能。
https://shardingsphere.apache...
能够关注微信公众号「 回滚吧代码」,第一时间阅读,文章持续更新;专一Java源码、架构、算法和面试。