Sharding Sphere
一、概念
什么是Sharding Sphere?
Apache ShardingSphere 是一款分布式的数据库生态系统, 可以将任意关系型数据库转换为分布式数据库,并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。
ShardingSphere-JDBC 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。
ShardingSphere-Proxy 定位为透明化的数据库代理端,通过实现数据库二进制协议,对异构语言提供支持。
什么是分库分表?
字面意思,将一个数据库拆分为多个数据库,将一个表拆分为多个表。其中又有水平切分和垂直切分两种。分库分表的出现是为了解决
垂直切分
垂直分库
将一个数据库中的多个表,根据不同的业务场景或者其他规则,切分为多个不同的数据库。
垂直分表
将一个表中的多个字段,根据不同的业务场景或者其他规则,拆分为多个不同的表。
水平切分
水平分库
将一个数据库拆分为多个具有相同表的数据库,数据按照一定的规则,存放到不同的数据库中。
水平分表
将一张表拆分为多个具有相同表结构的表,数据按照一定的规则,存放到不同的表中。
分库分表的应用和问题
应用
- 垂直切分:在数据库设计阶段的时候,就应该考虑好垂直分库和垂直分表的情况。因为随着系统功能的不断完善,后续在进行垂直切分的难度是指数级的。
- 水平切分:随着数据库数据量增加,不要马上考虑做水平切分,首先考虑通过索引、缓存、读写分离等手段处理,当上述方式都无法解决问题的时候,在考虑要不要进行水平分库或水平分表。
分库分表的问题
- 跨节点关联查询问题,分页、排序问题
- 多数据源管理的问题
- 分布式事务问题
各种分库分表方案和思考
Hash分表
使用某个字段的计算hash值,根据计算得到的hash值,将数据存储到对应的表。
没有热点数据问题,适合高并发场景,但扩容迁移数据痛苦
range分表(范围分表)
按照一定的范围划分存储数据,比如0-1000万存储到1表,后续每1000万存储一张表。
不需要迁移数据,但是有热点数据问题,高并发场景下可能 压力都在某个表里面
分库分表后的数据迁移
分库分表后大数据量的查询可以考虑
二、Sharding-JDBC
简介
1、Sharding-JDBC是一个开源的轻量级Java框架,在JDBC层提供服务,原先是当当网开发的,后来加入了Apache孵化器,成为了其中的顶级项目。
2、Sharding-JDBC提供了数据分片和读写分离等功能,简化了对分库分表之后数据的相关操作。
sharding-jdbc实现水平分表
假设在数据库sharding
中,你有两张表:t_order_0
和 t_order_1
,订单信息存放到这两张表中。
我们将根据订单ID 进行分表,使用以下算法:
当 order_id % 2 == 0
时,路由到 t_order_0
当 order_id % 2 == 1
时,路由到 t_order_1
1、sql
-- 创建数据库
CREATE DATABASE sharding;
USE sharding;
-- 创建 t_order_0 表
CREATE TABLE t_order_0 (
order_id BIGINT PRIMARY KEY COMMENT '订单ID,主键',
user_id INT NOT NULL COMMENT '用户ID',
status VARCHAR(50) NOT NULL COMMENT '订单状态',
create_time DATETIME NOT NULL COMMENT '订单创建时间'
) COMMENT='订单表0';
-- 创建 t_order_1 表
CREATE TABLE t_order_1 (
order_id BIGINT PRIMARY KEY COMMENT '订单ID,主键',
user_id INT NOT NULL COMMENT '用户ID',
status VARCHAR(50) NOT NULL COMMENT '订单状态',
create_time DATETIME NOT NULL COMMENT '订单创建时间'
) COMMENT='订单表1';
2、pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>5.0.0</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>5.0.0</version>
</dependency>
</dependencies>
3、application.yml
spring:
shardingsphere:
props:
sql-show: true
datasource:
names: ds
ds:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/sharding
username: root
password: taishang
rules:
sharding:
tables:
t_order:
actual-data-nodes: ds.t_order_$->{0..1}
# 分表策略
table-strategy:
standard:
# 分片算法名称
sharding-algorithm-name: order-inline
# 分片所用的列
sharding-column: order_id
# 主键生成策略
key-generate-strategy:
column: order_id
key-generator-name: snowflake
# 分片算法
sharding-algorithms:
order-inline:
type: INLINE
props:
algorithm-expression: t_order_$->{order_id % 2}
4、实体类
@Data
@TableName("t_order")
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 订单ID,主键
*/
@TableId
private Long orderId;
/**
* 用户ID
*/
private Integer userId;
/**
* 订单状态
*/
private String status;
/**
* 订单创建时间
*/
private Date createTime;
}
5、Mapper
public interface OrderMapper extends BaseMapper<Order> {}
6、测试类
我们不需要区分数据在哪张表上,sharding-jdbc会自动处理路由。
@SpringBootTest
public class Test1 {
@Autowired
private OrderMapper orderMapper;
@Test
void selectAll() {
List<Order> orders = orderMapper.selectList(null);
orders.forEach(System.out::println);
}
@Test
void selectById() {
QueryWrapper<Order> qw = new QueryWrapper<>();
qw.eq("order_id", "1058846700250071041");
Order order = orderMapper.selectOne(qw);
System.out.println(order);
}
@Test
void insert() {
for (int i = 0; i < 100; i++) {
Order order = new Order();
order.setUserId(i);
order.setStatus("1");
order.setCreateTime(new Date());
orderMapper.insert(order);
}
}
@Test
void update() {
QueryWrapper<Order> qw = new QueryWrapper<>();
qw.eq("user_id", 2);
Order order = orderMapper.selectOne(qw);
// 如果使用uer_id作为分片规则,需要将user_id放到where后面,并且保证update语句中两个值一致。否则修改将会报错。
order.setStatus("测试");
orderMapper.updateById(order);
}
@Test
void deleteAll() {
orderMapper.delete(null);
}
}
7、插入结果
sharding-jdbc实现水平分库
假设你有两个数据库:db0
和 db1
,用户表 user
分布在这两个数据库中。
我们将根据用户 ID 进行分库,使用以下算法:
当 user_id % 2 == 0
时,路由到 db0
当 user_id % 2 == 1
时,路由到 db1
1、sql
CREATE DATABASE db0;
CREATE DATABASE db1;
USE db0;
CREATE TABLE user (
user_id INT PRIMARY KEY COMMENT '用户ID',
username VARCHAR(100) COMMENT '用户名',
email VARCHAR(100) COMMENT '电子邮件',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) COMMENT='用户表';
USE db1;
CREATE TABLE user (
user_id INT PRIMARY KEY COMMENT '用户ID',
username VARCHAR(100) COMMENT '用户名',
email VARCHAR(100) COMMENT '电子邮件',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
) COMMENT='用户表';
2、pom.xml
同上面水平分表。
3、application.yaml
spring:
shardingsphere:
props:
sql-show: true
datasource:
names: db0,db1
db0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/db0
username: root
password: taishang
db1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/db1
username: root
password: taishang
rules:
sharding:
tables:
user:
actual-data-nodes: db$->{0..1}.user
#分库策略
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: db-inline
# 主键生成策略
key-generate-strategy:
column: user_id
key-generator-name: snowflake
# 分片算法
sharding-algorithms:
db-inline:
type: INLINE
props:
algorithm-expression: db$->{user_id % 2}
4、实体类
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
@TableId(value = "user_id")
private Long userId;
/**
* 用户名
*/
@TableField("username")
private String username;
/**
* 电子邮件
*/
@TableField("email")
private String email;
/**
* 创建时间
*/
@TableField("created_at")
private LocalDateTime createdAt;
}
5、mapper
public interface UserMapper extends BaseMapper<User> {}
6、测试类
@SpringBootTest
public class Test2 {
@Autowired
private UserMapper userMapper;
@Test
void selectAll() {
List<User> users = userMapper.selectList(null);
users.forEach(System.out::println);
}
@Test
void selectById() {
QueryWrapper<User> qw = new QueryWrapper<>();
qw.eq("user_id", "1058846700250071041");
User user = userMapper.selectOne(qw);
System.out.println(user);
}
@Test
void insert() {
for (int i = 0; i < 20; i++) {
User user = new User();
user.setUsername("测试" + i);
user.setEmail(i + "@qq.com");
user.setCreatedAt(LocalDateTime.now());
userMapper.insert(user);
}
}
@Test
void insert2() {
userMapper.insert(getUser(true));
}
@Test
void insert3() {
userMapper.insert(getUser(false));
}
User getUser(boolean isOdd) {
User user = new User();
int number = generateRandomNumber(100000, 1000000, isOdd);
user.setUsername("测试" + number);
user.setEmail(number + "@qq.com");
user.setCreatedAt(LocalDateTime.now());
return user;
}
/**
* 生成指定范围内的随机奇数或偶数
*
* @param min 最小值(包含)
* @param max 最大值(包含)
* @param isOdd 是否生成奇数
* @return 生成的随机数
*/
public int generateRandomNumber(int min, int max, boolean isOdd) {
if (min > max) {
throw new IllegalArgumentException("最小值不能大于最大值");
}
Random random = new Random();
int range = (max - min) + 1;
int randomNumber = random.nextInt(range) + min;
// 调整为奇数或偶数
if (isOdd) {
if (randomNumber % 2 == 0) {
randomNumber++;
}
} else {
if (randomNumber % 2 != 0) {
randomNumber++;
}
}
// 确保生成的数在范围内
if (randomNumber > max) {
randomNumber -= 2;
}
return randomNumber;
}
@Test
void update() {
QueryWrapper<User> qw = new QueryWrapper<>();
qw.eq("user_id", 2);
User user = userMapper.selectOne(qw);
}
@Test
void deleteAll() {
userMapper.delete(null);
}
}
7、插入结果
sharding-jdbc实现垂直分库、分表
对于sharding-jdbc来说,垂直分库、垂直分表其实就是多配置一些数据源和表。
公共表
# 配置公共表(所有数据源中都需要有这个表,并且增、删、改都同步操作所有数据源)
spring.shardingsphere.rules.sharding.tables.broadcast-tables=t_public
spring.shardingsphere.rules.sharding.tables.t_public.key-generate-strategy.column=public_id
spring.shardingsphere.rules.sharding.tables.t_public.key-generate-strategy.key-generator-name=snowflake
sharding-jdbc读写分离
sharding-jdbc具有读写分离功能,能够将读、写分别使用不同的数据源进行操作。目前支持单主库、多从库。
我们基于此实现一个简单的读写分离Demo。配置两个数据库:db0
和 db1
,其中db0作为主库,db1作为从库,两个数据库做好数据同步操作
。我们在此基础上实现读写分离的操作:
db0(主库)负责写操作(增删改)操作,db1(从库)负责查询操作。
1、sql
CREATE TABLE t_order (
order_id BIGINT PRIMARY KEY COMMENT '订单ID,主键',
user_id INT NOT NULL COMMENT '用户ID',
status VARCHAR(50) NOT NULL COMMENT '订单状态',
create_time DATETIME NOT NULL COMMENT '订单创建时间'
) COMMENT='订单表';
2、application.yaml
spring:
shardingsphere:
# 属性配置
props:
sql-show: true
# 数据源配置
datasource:
# 数据源名称不能使用下划线!不能使用下划线!!不能使用下划线!!!(只要是后面需要作为key的都不能使用下划线)
names: db-master,db-slave-1
# 主数据源,负责增、删、改,以及事物内的读操作
db-master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/sharding
username: root
password: taishang
# 从数据源,可以配置多个,负责读操作(查询操作)
db-slave-1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3308/sharding
username: taishang
password: taishang
# 规则配置
rules:
# 声明使用读写分离规则
readwrite-splitting:
data-sources:
readwrite_ds:
# 静态策略配置
write-data-source-name: db-master
read-data-source-names:
- db-slave-1
# 负载均衡算法名称
loadBalancerName: random
# 负载均衡算法配置
load-balancers:
random:
type: RANDOM
3、测试类及结果
写操作
增、删、改操作将会使用主库进行。
测试代码:
@Test
void writerTest() {
for (int i = 0; i < 10; i++) {
User user = new User();
user.setUsername("测试");
user.setEmail("12313@qq.com");
user.setCreatedAt(LocalDateTime.now());
userMapper.insert(user);
}
}
结果:
查询操作
普通查询操作将会使用从库进行。
测试代码:
@Test
void readTest() {
for (int i = 0; i < 10; i++) {
userMapper.selectList(null);
}
}
结果:
事务内查询操作
事务内的查询操作将会使用主库进行查询。
测试代码:
@Transactional
@Test
void transactionTest() {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
for (int i = 0; i < 10; i++) {
mapper.selectList(null);
// 清空一级缓存
sqlSession.clearCache();
}
}
结果:
数据加密
shardingsphere-jdbc-core-spring-boot-starter版本从5.0.0改为5.2.0,5.0.0设置加密算法类型后会报找不到key的错误,如下面的rc4加密算法后,会报rc4-key-value不能为空的错!!!!
不知道是傻逼官方埋的坑还是maven仓库的坑,我先截图保存记在笔记里,以后有空了再看是哪个傻逼埋的坑。如果后面发现是官方,我看看这个特么是谁写的!!!
妈的,刚准备开喷,改个版本号回来,莫名其妙的好了,不报错了!!!
按照如下配置,配置好相应字段后,即可实现加密存储,解密返回。但是如果使用非对称加密,则返回的是密文,因为非对称加密不能还原出原始数据。
spring:
shardingsphere:
# 属性配置
props:
sql-show: true
# 数据源配置
datasource:
# 数据源名称不能使用下划线!不能使用下划线!!不能使用下划线!!!(只要是后面需要作为key的都不能使用下划线)
names: db
# 主数据源,负责增、删、改,以及事物内的读操作
db:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/sharding
username: root
password: taishang
rules:
encrypt:
tables:
t_user:
columns:
password:
# 指定明文存储在哪个列
plain-column: password
# 指定加密后的数据存储在哪个列
cipher-column: password_encrypt
# 设置加密器
encryptor-name: md5-encryptor
# 设置查询辅助列
# assisted-query-column: password_query
# assisted-query-encryptor-name: pwd_query_encryptor
# query-with-cipher-column: true
username:
cipher-column: username
encryptor-name: rc4-encryptor
# query-with-cipher-column: false
# 配置加密器算法
encryptors:
# 使用MD5非对称加密,无法还原
md5-encryptor:
type: MD5
rc4-encryptor:
type: rc4
# 天坑!! 5.0.0使用该配置报aes-key-value不能为空,升级到5.2.1解决
props:
rc4-key-value: 1231231
# 使用加密列进行查询(开启后会自动将查询条件加密后查询)
# query-with-cipher-column: true
评论区