从官网上可以看到
Apache ShardingSphere 是一款开源分布式数据库生态项目,旨在碎片化的异构数据库上层构建生态,在最大限度的复用数据库原生存算能力的前提下,进一步提供面向全局的扩展和叠加计算能力。其核心采用可插拔架构,以数据库协议及 SQL 方式提供诸多增强功能,包括数据分片、访问路由、数据安全等。
Apache ShardingSphere 由 JDBC、Proxy 和 Sidecar(规划中) 3 款产品组成。 项目的线路规划和时间历程如下图
简单讲:Shardingshpere 本来是一款分库分表框架,后来在大量社区贡献者的不断迭代下,功能也逐渐完善,核心功能也变得多元化起来,已经不仅限于分库分表了,慢慢发展为开源分布式数据库生态项目。
分库分表
关系型数据库以 MySQL 为例,单机的存储能力、连接数是有限的,它自身就很容易会成为系统的瓶颈。当单表数据量在百万以内时,我们还可以通过添加从库、优化索引提升性能。
一旦数据量朝着千万以上趋势增长,再怎么优化数据库,很多操作性能仍下降严重。为了减少数据库的负担,提升数据库响应速度,缩短查询时间,这时候就需要进行分库分表,可分为垂直拆分和水平拆分。
分库分表的目的就是要将大量数据分散到多个数据库中,使每个数据库中数据量小响应速度快,以此来提升数据库整体性能。
分库
分表
读写分离
同分库分表一样,读写分离也是因为单机数据库已经很难满足业务的需要,一般在分库分表前会考虑读写分离。基本的原理是让主数据库处理事务性增、改、删操作( INSERT、UPDATE、 DELETE) ,而从数据库处理 SELECT 查询操作。数据库复制被用来把事务性操作导致的变更同步到集群中的从数据库。
读写分离,只是分担了访问的压力,但是存储的压力没有解决,当数据量大大增加时,还是应该考虑分库分表
分片是指数据分片,从概念上讲就是按照一定的规则,将数据集划分成相对独立的数据子集,然后将数据子集分布到不同的节点上,这个节点可以是逻辑上节点,也可以是物理上的节点。 具体操作就是将数据分库分表
分片键
用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段。 SQL 中如果无分片字段,将执行全路由,性能较差。 除了对单分片字段的支持,ShardingSphere 也支持根据多个字段进行分片。
分片算法( ShardingAlgorithm)
分片算法就是将数据按照某种方式分配给数据库或表,这种方式就是分片算法。下表是 ShardingSphere 内部提供的分片算法
已知实现类 | 详细说明 |
---|---|
BoundaryBasedRangeShardingAlgorithm | 基于分片边界的范围分片算法 |
VolumeBasedRangeShardingAlgorithm | 基于分片容量的范围分片算法 |
ComplexInlineShardingAlgorithm | 基于行表达式的复合分片算法 |
AutoIntervalShardingAlgorithm | 基于可变时间范围的分片算法 |
ClassBasedShardingAlgorithm | 基于自定义类的分片算法 |
HintInlineShardingAlgorithm | 基于行表达式的 Hint 分片算法 |
IntervalShardingAlgorithm | 基于固定时间范围的分片算法 |
HashModShardingAlgorithm | 基于哈希取模的分片算法 |
InlineShardingAlgorithm | 基于行表达式的分片算法 |
ModShardingAlgorithm | 基于取模的分片算法 |
常用有以下 5 种
行内表达式分片算法 ( InlineShardingAlgorithm )
精确分片算法 ( PreciseShardingAlgorithm )
用于处理使用单一键作为分片键的 = 与 IN 进行分片的场景。
spring:
shardingsphere:
sharding:
tables:
sku:
# 这个配置是告诉sharding有多少个表
actual-data-nodes: ds1.sku_$->{0..1}
# 配置其分片策略和分片算法
table-strategy:
standard:
sharding-column: sku_id
precise-algorithm-class-name: com.xulei.spring.shardConfig.MyPreciseShardingAlgorithm
@Slf4j
public class MyPreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
/**
* Sharding.
* 实现标准分片策略,通过计算分到不同的表来实现分表策略。
* @param collection available data sources or tables's names
* @param preciseShardingValue sharding value
* @return sharding result for data source or table's name
*/
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<String> preciseShardingValue) {
Comparable value = preciseShardingValue.getValue();
String columnName = preciseShardingValue.getColumnName();
String logicTableName = preciseShardingValue.getLogicTableName();
log.info("collection:" + JSON.toJSONString(collection) + ",preciseShardingValue:" + JSON.toJSONString(preciseShardingValue));
String data = value.toString();
int size = collection.size();
for(String tableName: collection){
int index = Math.abs(data.hashCode() % size);
if(tableName.endsWith(index+"")){
return tableName;
}
}
return null;
}
}
范围分片算法 ( RangeShardingAlgorithm )
用于处理使用单一键作为分片键的 BETWEEN AND、>、<、>=、<= 进行分片的场景。
public class MyDBRangeShardingAlgorithm implements RangeShardingAlgorithm<Integer> {
@Override
public Collection<String> doSharding(Collection<String> databaseNames, RangeShardingValue<Integer> rangeShardingValue) {
Set<String> result = new LinkedHashSet<>();
// between and 的起始值
int lower = rangeShardingValue.getValueRange().lowerEndpoint();
int upper = rangeShardingValue.getValueRange().upperEndpoint();
// 循环范围计算分库逻辑
for (int i = lower; i <= upper; i++) {
for (String databaseName : databaseNames) {
if (databaseName.endsWith(i % databaseNames.size() + "")) {
result.add(databaseName);
}
}
}
return result;
}
}
复合分片算法 ( ComplexKeysShardingAlgorithm )
用于处理使用多键作为分片键进行分片的场景,多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。
public class MyDBComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm<Integer> {
@Override
public Collection<String> doSharding(Collection<String> databaseNames, ComplexKeysShardingValue<Integer> complexKeysShardingValue) {
// 得到每个分片健对应的值
Collection<Integer> orderIdValues = this.getShardingValue(complexKeysShardingValue, "order_id");
Collection<Integer> userIdValues = this.getShardingValue(complexKeysShardingValue, "user_id");
List<String> shardingSuffix = new ArrayList<>();
// 对两个分片健同时取模的方式分库
for (Integer userId : userIdValues) {
for (Integer orderId : orderIdValues) {
String suffix = userId % 2 + "_" + orderId % 2;
for (String databaseName : databaseNames) {
if (databaseName.endsWith(suffix)) {
shardingSuffix.add(databaseName);
}
}
}
}
return shardingSuffix;
}
private Collection<Integer> getShardingValue(ComplexKeysShardingValue<Integer> shardingValues, final String key) {
Collection<Integer> valueSet = new ArrayList<>();
Map<String, Collection<Integer>> columnNameAndShardingValuesMap = shardingValues.getColumnNameAndShardingValuesMap();
if (columnNameAndShardingValuesMap.containsKey(key)) {
valueSet.addAll(columnNameAndShardingValuesMap.get(key));
}
return valueSet;
}
}
Hint分片算法 ( HintShardingAlgorithm )
用于处理使用 Hint 行分片的场景。对于分片字段非 SQL 决定,而由其他外置条件决定的场景,可使用 SQL Hint 灵活的注入分片字段。例:内部系统,按照员工登录主键分库,而数据库中并无此字段。SQL Hint 支持通过 Java API 和 SQL 注释两种方式使用。
public class MyTableHintShardingAlgorithm implements HintShardingAlgorithm<String> {
@Override
public Collection<String> doSharding(Collection<String> tableNames, HintShardingValue<String> hintShardingValue) {
Collection<String> result = new ArrayList<>();
for (String tableName : tableNames) {
for (String shardingValue : hintShardingValue.getValues()) {
if (tableName.endsWith(String.valueOf(Long.valueOf(shardingValue) % tableNames.size()))) {
result.add(tableName);
}
}
}
return result;
}
}
分片策略
包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前提供5种分片策略。
标准分片策略( StandardShardingStrategy )
提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy 只支持单分片键,提供PreciseShardingAlgorithm 和 RangeShardingAlgorithm 两个分片算法。PreciseShardingAlgorithm 是必选的,用于处理=和IN的分片。RangeShardingAlgorithm 是可选的,用于处理 BETWEEN AND, >, <, >=, <= 分片,如果不配置RangeShardingAlgorithm ,SQL 中的 BETWEEN AND 将按照全库路由处理。
复合分片策略( ComplexShardingStrategy )
提供对SQL语句中的 =, >, <, >=, <=, IN 和 BETWEEN AND 的分片操作支持。ComplexShardingStrategy 支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。
行表达式分片策略 ( InlineShardingStrategy )
使用 Groovy 的表达式,提供对 SQL 语句中的=和IN的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$->{u_id % 8}
表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0
到t_user_7
。
Hint分片策略 ( HintShardingStrategy )
通过Hint指定分片值而非从 SQL 中提取分片值的方式进行分片的策略。
不分片策略 ( NoneShardingStrategy )
不分片的策略。
下面会演示 shardingsphere 和 mybatisplus 一起使用
<dependency>
<groupId>org.apache.shardingspheregroupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starterartifactId>
<version>5.1.1version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.5.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
准备三个数据库,也就是三个数据源,一个 master 和两个 slave
DROP DATABASE IF EXISTS demo_dev_master;
create database demo_dev_master;
DROP DATABASE IF EXISTS demo_dev_slave_0;
create database demo_dev_slave_0;
DROP DATABASE IF EXISTS demo_dev_slave_1;
create database demo_dev_slave_1;
use demo_dev_master;
DROP TABLE IF EXISTS `t_address`;
CREATE TABLE `t_address` (
`address_id` bigint(20) NOT NULL,
`address_name` varchar(100) NOT NULL,
PRIMARY KEY (`address_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
`order_id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_type` int(11) DEFAULT NULL,
`user_id` int(11) NOT NULL,
`address_id` bigint(20) NOT NULL,
`status` varchar(50) DEFAULT NULL,
PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `t_order_0`;
create table `t_order_0` like `t_order`;
DROP TABLE IF EXISTS `t_order_1`;
create table `t_order_1` like `t_order`;
DROP TABLE IF EXISTS `t_order_2`;
create table `t_order_2` like `t_order`;
DROP TABLE IF EXISTS `t_order_3`;
create table `t_order_3` like `t_order`;
DROP TABLE IF EXISTS `t_order_4`;
create table `t_order_4` like `t_order`;
DROP TABLE IF EXISTS `t_order_item`;
CREATE TABLE `t_order_item` (
`order_item_id` bigint(20) NOT NULL AUTO_INCREMENT,
`order_id` bigint(20) DEFAULT NULL,
`user_id` int(11) NOT NULL,
`phone` varchar(50) DEFAULT NULL,
`status` varchar(50) DEFAULT NULL,
PRIMARY KEY (`order_item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DROP TABLE IF EXISTS `t_order_item_0`;
create table `t_order_item_0` like `t_order_item`;
DROP TABLE IF EXISTS `t_order_item_1`;
create table `t_order_item_1` like `t_order_item`;
DROP TABLE IF EXISTS `t_order_item_2`;
create table `t_order_item_2` like `t_order_item`;
DROP TABLE IF EXISTS `t_order_item_3`;
create table `t_order_item_3` like `t_order_item`;
DROP TABLE IF EXISTS `t_order_item_4`;
create table `t_order_item_4` like `t_order_item`;
将上面的 master 数据库复制两份 slave ,可以用下面的方法
mysqldump -u root --password=<pwd> demo_dev_master | mysql -u root -p demo_dev_slave_0
mysqldump -u root --password=<pwd> demo_dev_master | mysql -u root -p demo_dev_slave_1
@TableName("t_address")
@Data
@Builder
public class Address {
@TableId
private Long addressId;
private String addressName;
}
@TableName("t_order")
@Data
@Builder
public class Order {
@TableId
private Long orderId;
private Integer orderType;
private Integer userId;
private Long addressId;
private String status;
}
@TableName("t_order_item")
@Data
@Builder
public class OrderItem {
@TableId
private Long orderItemId;
private Long orderId;
private Integer userId;
private String phone;
private String status;
}
@Mapper
public interface AddressDao extends BaseMapper<Address> {
}
@Mapper
public interface OrderDao extends BaseMapper<Order> {
}
@Mapper
public interface OrderItemDao extends BaseMapper<OrderItem> {
}
将 demo_dev_slave_0 和 demo_dev_slave_1 用作只分库的数据源
一个简单的分库配置文件,将 user_id 为偶数的数据分配给slave_0, 奇数则给slave_1. 具体 yml 配置文件 application-sharding-database.yml 如下所示
spring:
shardingsphere:
datasource:
names: dev_slave_0, dev_slave_1
dev_slave_0:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo_dev_slave_0?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password:
dev_slave_1:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo_dev_slave_1?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password:
props:
sql-show: true
rules:
sharding:
default-database-strategy:
standard:
sharding-algorithm-name: database-inline
sharding-column: user_id
sharding-algorithms:
database-inline:
type: INLINE
props:
algorithm-expression: dev_slave_$->{user_id % 2}
tables:
t_order:
actual-data-nodes: dev_slave_$->{0..1}.t_order
t_order_item:
actual-data-nodes: dev_slave_$->{0..1}.t_order_item
测试
@SpringBootTest
// @ActiveProfiles("readwrite-splitting")
@ActiveProfiles("sharding-database")
class DemoShardingSphereApplicationTests {
private static final Logger logger = LoggerFactory.getLogger(DemoShardingSphereApplicationTests.class);
@Autowired
private OrderDao orderDao;
@Autowired
private OrderItemDao orderItemDao;
@Autowired
private AddressDao addressDao;
@BeforeEach
void setUp() {
addressDao.delete(null);
orderDao.delete(null);
orderItemDao.delete(null);
Address address1 = Address.builder().addressId(1L).addressName("北京朝阳区").build();
Address address2 = Address.builder().addressId(2L).addressName("海南三亚市").build();
addressDao.insert(address1);
addressDao.insert(address2);
for (int i = 0; i < 50; i++) {
Order order = Order.builder().orderType(1).userId(i % 2).addressId( i % 2L).status("发货中").build();
orderDao.insert(order);
for (int j = 0; j < 2; j++) {
OrderItem orderItem = OrderItem.builder().orderId(order.getOrderId()).phone("1333333333").userId(i % 2).status("发货中").build();
orderItemDao.insert(orderItem);
}
}
}
@Test
void test() {
List<Order> orders = orderDao.selectList(null);
logger.info("{}", orders);
}
}
最终数据库结果展示
将 demo_dev_slave_0 用作只分表的数据源。
一个简单的分表配置文件,将 order_id 对 5 取余,分配给 t_order_{0…4}。具体 yml 配置文件 application-sharding-table.yml 如下所示。
spring:
shardingsphere:
datasource:
names: dev_slave_0
dev_slave_0:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo_dev_slave_0?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password: root
props:
sql-show: true
rules:
sharding:
tables:
t_order:
actual-data-nodes: dev_slave_0.t_order_${0..4}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: t_order_inline
t_order_item:
actual-data-nodes: dev_slave_0.t_order_item_${0..4}
table-strategy:
standard:
sharding-column: order_id
sharding-algorithm-name: t_order_item_inline
binding-tables: t_order,t_order_item
broadcast-tables: t_address
sharding-algorithms:
t_order_inline:
type: INLINE
props:
algorithm-expression: t_order_$->{order_id % 5}
t_order_item_inline:
type: INLINE
props:
algorithm-expression: t_order_item_$->{order_id % 5}
测试代码如分库所示,只不过要修改 profile 为 sharding-table
数据库结果如下:t_order_{0…4} 都有数据
对任意t_order_{0…4}表 order_id 进行取余运算,发现满足规则
将 demo_dev_slave_0 和 demo_dev_slave_1 用作分库分表的数据源
一个简单的分库分表配置文件,将 上述两种情况整合.。具体 yml 配置文件 application-sharding-database-table.yml 如下所示
spring:
shardingsphere:
datasource:
names: dev_slave_0, dev_slave_1
dev_slave_0:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo_dev_slave_0?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password:
dev_slave_1:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo_dev_slave_1?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password:
props:
sql-show: true
rules:
sharding:
default-database-strategy:
standard:
sharding-algorithm-name: database-inline
sharding-column: user_id
binding-tables: t_order,t_order_item
broadcast-tables: t_address
tables:
t_order:
actual-data-nodes: dev_slave_$->{0..1}.t_order_$->{0..4}
table-strategy:
standard:
sharding-algorithm-name: order_mod
sharding-column: order_id
t_order_item:
actual-data-nodes: dev_slave_$->{0..1}.t_order_item_$->{0..4}
table-strategy:
standard:
sharding-algorithm-name: order_item_mod
sharding-column: order_id
sharding-algorithms:
auto-mod:
type: MOD
props:
sharding-count: 2
database-inline:
type: INLINE
props:
algorithm-expression: dev_slave_$->{user_id % 2}
order_mod:
type: INLINE
props:
algorithm-expression: t_order_$->{order_id % 5}
order_item_mod:
type: INLINE
props:
algorithm-expression: t_order_item_$->{order_id % 5}
读写分离会将读写的 sql 应用到主从数据库中,主库和从库的数据复制需要自己实现,shardingSphere 并不提供这样的功能。 具体 yml 配置文件 application-readwrite-splitting.yml 如下所示
spring:
shardingsphere:
datasource:
names: dev_master, dev_slave_0, dev_slave_1
dev_master:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo_dev_master?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password: root
dev_slave_0:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo_dev_slave_0?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password: root
dev_slave_1:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo_dev_slave_1?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password: root
props:
sql-show: true
rules:
readwrite-splitting:
data-sources:
read_write_db:
type: Static
props:
write-data-source-name: dev_master
read-data-source-names: dev_slave_0, dev_slave_1
load-balancer-name: round_robin
load-balancers:
round_robin:
type: ROUND_ROBIN
因为从库有多个,所以我们需要根据一定的策略将请求分发到不同的数据库上,防止单节点的压力过大或者空闲,ShardingSphere 内置了多种负载均衡算法,如果我们想实现自己的 算法,那么可以实现 ReadQueryLoadBalanceAlgorithm
接口
ROUND_ROBIN
基于轮询的读库负载均衡算法,原理是每一次读库的请求轮流分配,从1开始,直到N(从库个数),然后重新开始循环。
例子如上所示
RANDOM
基于随机的读库负载均衡算法,原理就是随机分配
spring:
shardingsphere:
rules:
readwrite-splitting:
data-sources:
read_write_db:
type: Static
props:
write-data-source-name: dev_master
read-data-source-names: dev_slave_0, dev_slave_1
load-balancer-name: random
loadBalancers:
random:
type: RANDOM
WEIGHT
基于权重的读库负载均衡算法
spring:
shardingsphere:
rules:
readwrite-splitting:
data-sources:
read_write_db:
type: Static
props:
write-data-source-name: dev_master
read-data-source-names: dev_slave_0, dev_slave_1
load-balancer-name: weight
loadBalancers:
weight:
type: WEIGHT
props:
dev_slave_0: 2
dev_slave_1: 1