一、Sharding-JDBC简介
Sharding-JDBC是Sharding-Sphere下的一个子产品。Sharding-Sphere 是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(计划中) 这3款相互独立的产品组成。他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如Java同构、异构语言、容器、 云原生等各种多样化的应用场景。ShardingSphere 旨在充分合理地在分布式的场景下利用关系型数据库的计算和存储能力, 而并非实现一个全新的关系型数据库。
其中,Sharding-JDBC 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务, 无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。
- 适用于任何基于Java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
- 基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
- 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer和PostgreSQL。
面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。
通过一主多从的配置方式,可以将查询请求均匀的分散到多个数据副本,能够进一步的提升系统的处理能力。 使用多主多从的方式,不但能够提升系统的吞吐量,还能够提升系统的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行。与将数据根据分片键打散至各个数据节点的水平分片不同,读写分离则是根据SQL语义的分析,将读操作和写操作分别路由至主库与从库。
二、代码实践
下面介绍如何在实际开发中使用Sharding-JDBC实现读写分离
1.添加依赖
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
2.配置读写分离
2.1 Java代码中配置
第一步:在配置文件添加数据源参数
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
master:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/practice?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&generateSimpleParameterMetadata=true&autoReconnect=true&useSSL=true
username: root
password:
slave:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/practice_admin?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&generateSimpleParameterMetadata=true&autoReconnect=true&useSSL=true
username: root
password:
druid-pool:
#连接池的最大数据库连接数。设为0表示无限制
max-active: 20
#初始化数量
initial-size: 2
#最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制
max-wait: 60000
#最小空闲连接:连接池中容许保持空闲状态的最小连接数量,低于这个数量将创建新的连接
min-idle: 2
第二步:在Java代码中配置对应的主从数据源和读写分离规则
@Configuration
public class MasterSlaveDataSourceConfig {
@Value("${spring.datasource.type}")
private Class<? extends DataSource> dataSourceType;
@Value("${spring.datasource.druid-pool.initial-size}")
private int initialSize;
@Value("${spring.datasource.druid-pool.max-active}")
private int maxActive;
@Value("${spring.datasource.druid-pool.max-wait}")
private int maxWait;
@Value("${spring.datasource.druid-pool.min-idle}")
private int minIdle;
// 配置主库
@Bean("masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
DruidDataSource druidDataSource = (DruidDataSource) DataSourceBuilder.create().type(dataSourceType).build();
druidPoolConfig(druidDataSource);
return druidDataSource;
}
// 配置从库(可配置多个)
@Bean("slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
DruidDataSource druidDataSource = (DruidDataSource) DataSourceBuilder.create().type(dataSourceType).build();
druidPoolConfig(druidDataSource);
return druidDataSource;
}
private void druidPoolConfig(DruidDataSource druidDataSource) {
druidDataSource.setMaxActive(maxActive);
druidDataSource.setMaxWait(maxWait);
druidDataSource.setInitialSize(initialSize);
druidDataSource.setMinIdle(minIdle);
}
// 配置读写分离规则,并获取数据源
@Primary
@Bean("masterSlaveDataSource")
@DependsOn({"masterDataSource", "slaveDataSource"})
public DataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource) throws SQLException {
Map<String, DataSource> dataSourceMap = new HashMap<>();
dataSourceMap.put("masterDataSource", masterDataSource);
dataSourceMap.put("slaveDataSource", slaveDataSource);
// 配置读写分离规则
MasterSlaveRuleConfiguration ruleConfiguration = new MasterSlaveRuleConfiguration("msRule", "masterDataSource", Collections.singletonList("slaveDataSource"), new RandomMasterSlaveLoadBalanceAlgorithm());
// 初始化数据源
return MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, ruleConfiguration, new HashMap<String, Object>(), null);
}
}
MasterSlaveDataSourceFactory
是 ShardingSphere 的一个工厂类,用于创建主从数据源(Master-Slave DataSource)。它通过配置主从数据源和负载均衡策略,实现读写分离,提高数据库的性能和可靠性。其使用步骤为:
- 配置数据源:首先,需要配置主库和从库的基本信息。
- 配置读写分离规则:通过
MasterSlaveRuleConfiguration
配置读写分离规则,指定主库和从库,以及负载均衡策略。 - 初始化数据源:通过
MasterSlaveDataSourceFactory.createDataSource
方法,传入数据源和读写分离规则,创建一个支持读写分离的数据源。
通过以上配置,可以利用 ShardingSphere 的 MasterSlaveDataSourceFactory
实现数据库的读写分离,从而提高应用的性能和可靠性。
如果整合使用了Mybatis-Plus,还需要添加Mybatis-Plus的相关配置,否则会报错
@Configuration
@MapperScan(basePackages = {"com.practice.mapper.*"}, sqlSessionTemplateRef = "sqlTemplateDefault")
public class MybatisConfig {
@Bean(name = "sqlFactoryDefault")
public SqlSessionFactory sqlSessionFactory(@Qualifier("masterSlaveDataSource") DataSource masterSlaveDataSource)
throws Exception {
MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
factoryBean.setDataSource(masterSlaveDataSource);
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
factoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/*/*.xml"));
MybatisConfiguration mybatisConfiguration = new MybatisConfiguration();
mybatisConfiguration.setLogImpl(StdOutImpl.class);
factoryBean.setConfiguration(mybatisConfiguration);
return factoryBean.getObject();
}
@Bean(name = "sqlTemplateDefault")
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlFactoryDefault") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
2.2 在YAML文件中进行配置
在SpringBoot中,可以直接在YAML配置文件里配置主从数据源及对应的分离规则
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
master:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/practice?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&generateSimpleParameterMetadata=true&autoReconnect=true&useSSL=true
username: root
password:
slave:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/practice_admin?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&generateSimpleParameterMetadata=true&autoReconnect=true&useSSL=true
username: root
password:
druid-pool:
#连接池的最大数据库连接数。设为0表示无限制
max-active: 20
#初始化数量
initial-size: 2
#最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制
max-wait: 60000
#最小空闲连接:连接池中容许保持空闲状态的最小连接数量,低于这个数量将创建新的连接
min-idle: 2
shardingsphere:
enabled: true
props:
sql-show: true
datasource:
# 数据源名称,多个以逗号隔开
names: master,slave
master:
type: ${spring.datasource.type}
driver-class-name: ${spring.datasource.master.driver-class-name}
url: ${spring.datasource.master.url}
username: ${spring.datasource.master.username}
password: ${spring.datasource.master.password}
slave:
type: ${spring.datasource.type}
driver-class-name: ${spring.datasource.slave.driver-class-name}
url: ${spring.datasource.slave.url}
username: ${spring.datasource.slave.username}
password: ${spring.datasource.slave.password}
rules:
#读写分离配置
readwrite-splitting:
data-sources:
ms: # 逻辑数据源名字 不要乱写名字,否则读写分离不生效
type: STATIC #读写分离类型
props:
# 主库
write-data-source-name: master
# 从库
read-data-source-names: slave
# 负载均衡算法名称
load-balancer-name: round
# 负载均衡算法
load-balancers:
round: # 负载均衡算法名称
type: ROUND_ROBIN #负载均衡算法类型轮询算法
3.使用验证
创建一张sys_user表,并分别在两个表中新增了一条不同的用户数据,创建一个查询接口一个新增接口
注:当然在实际的开发场景中,我们需要配置数据库的主从复制,从而保证数据的一致性,我们这里为了方便就没有实现这一步
mysql> use practice;
Database changed
mysql> select * from sys_user \G;
*************************** 1. row ***************************
id: 1
user_cd: A0001
username: 小明
password: dfefegegww3
salt: gegsgegewxda
status: 1
email: 184719579@qq.com
phone: 12562658345
address: 北京市-海淀区
remark: NULL
del_flag: 0
create_time: 2024-07-10 15:52:12
update_time: 2024-07-10 15:52:12
1 row in set (0.00 sec)
mysql> use practice_admin;
Database changed
mysql> select * from sys_user \G;
*************************** 1. row ***************************
id: 1
user_cd: A0001
username: 小刚
password: dasfsjefie
salt: fejoijgefe
status: 1
email: 184719579@qq.com
phone: 17573458375
address: 北京市-朝阳区
remark: NULL
del_flag: 0
create_time: 2024-07-10 15:50:00
update_time: 2024-07-10 15:50:00
1 row in set (0.00 sec)
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private SysUserMapper sysUserMapper;
@GetMapping("/user")
public APIResponse getMasterUser(@RequestParam Integer userId) {
return new APIResponse(sysUserMapper.selectById(userId));
}
@PostMapping("/user")
public APIResponse addUser(@RequestBody UserReq userReq) {
SysUser sysUser = new SysUser();
BeanUtils.copyProperties(userReq, sysUser);
return new APIResponse(sysUserMapper.insert(sysUser));
}
}
调用查询接口,返回值如下,查到的是practice_admin.sys_user中的数据,也就是读的从库
{
"code": 0,
"message": "SUCCESS",
"data": {
"id": 1,
"userCd": "A0001",
"username": "小刚",
"password": "dasfsjefie",
"salt": "fejoijgefe",
"status": 1,
"email": "184719579@qq.com",
"phone": "17573458375",
"address": "北京市-朝阳区",
"remark": null,
"delFlag": 0
}
}
调用新增接口,入参为:
{
"address": "北京市-西城区",
"email": "233635239@qq.com",
"password": "dfasdagagsde",
"phone": "1758937523",
"remark": null,
"salt": "afdasgegewgew",
"status": 0,
"userCd": "A0002",
"username": "小红"
}
分别查询主从库中sys_user表的数据如下:
mysql> use practice;
Database changed
mysql> select * from sys_user \G;
*************************** 1. row ***************************
id: 1
user_cd: A0001
username: 小明
password: dfefegegww3
salt: gegsgegewxda
status: 1
email: 184719579@qq.com
phone: 12562658345
address: 北京市-海淀区
remark: NULL
del_flag: 0
create_time: 2024-07-10 15:52:12
update_time: 2024-07-10 15:52:12
*************************** 2. row ***************************
id: 2
user_cd: A0002
username: 小红
password: dfasdagagsde
salt: afdasgegewgew
status: 0
email: 233635239@qq.com
phone: 1758937523
address: 北京市-西城区
remark: NULL
del_flag: 0
create_time: 2024-07-11 10:44:30
update_time: 2024-07-11 10:44:30
2 rows in set (0.00 sec)
mysql> use practice_admin;
Database changed
mysql> select * from sys_user \G;
*************************** 1. row ***************************
id: 1
user_cd: A0001
username: 小刚
password: dasfsjefie
salt: fejoijgefe
status: 1
email: 184719579@qq.com
phone: 17573458375
address: 北京市-朝阳区
remark: NULL
del_flag: 0
create_time: 2024-07-10 15:50:00
update_time: 2024-07-10 15:50:00
1 row in set (0.00 sec)
可以看到,我们数据插入到了主库practice.sys_user,而从库practice_admin.sys_user中的数据并没有发生变化。综上,我们就成功实现了主从数据的读写分离操作。