使用 Sharding Jdbc 实现读写分离
上一篇博客介绍了 MySQL 的主从复制的搭建,为实现读写分离创造了条件。对于一个网站来说,80% 来源于读操作,绝大多数情况下的网站宕机,都是由于过多的读操作导致的,因此在实际的生产环境中,经常会搭建一主多从的架构,主库只负责写操作,多个从库用来负责读操作,对于少量需要实时获取信息的读操作,可以从主库进行读取。
本篇博客将使用 Sharding Jdbc 在主从复制的基础上和已经开发好的项目上,只需要进行配置,不需要写任何代码就可以实现读写分离。只有当需要强制从主库读取数据时,才需要写极少量的代码指定从主库读取。在本篇博客的最后会提供源代码下载。
一、Sharding Jdbc 简单介绍
Sharding Jdbc 是轻量级的 Java 框架,在 Java 的 Jdbc 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 Jdbc 驱动,完全兼容 Jdbc 和各种 ORM 框架。可以在程序中轻松的实现读写分离。
Sharding Jdbc 具有以下几个特点:
-
适用于任何基于 Jdbc 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring Jdbc Template 或直接使用 Jdbc。
-
支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP 等。
-
支持任意实现 Jdbc 规范的数据库。支持 MySQL,Oracle,SQLServer,PostgreSQL 以及任何遵循 SQL92 标准的数据库。
当然 Sharding Jdbc 也可以实现物理上的分库分表,由于物理上的分库分表自身也存在很多缺点,主要体现在人工处理问题时比较麻烦,如测试环境出现 bug 或生产环境出现问题时,需要人工排查问题时很麻烦,所以 Sharding Jdbc 逐渐很少在分库分表上发挥作用了,现在大多数使用分布式数据库在逻辑上使用分区表,物理上还是一张表,如 OceanBase 数据库。
二、搭建工程
搭建一个 SpringBoot 工程,具体结构如下:
pom 文件中使用的 SpringBoot 版本和引入的 jar 包依赖如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.5</version> <relativePath/> </parent> <groupId>com.jobs</groupId> <artifactId>springboot_rw_demo</artifactId> <version>1.0-SNAPSHOT</version> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <scope>compile</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.20</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.2</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <!--sharding jdbc引入--> <dependency> <groupId>org.apache.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.1.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
从上面可以发现,如果想使用 sharding jdbc 只需要引入 sharding-jdbc-spring-boot-starter 即可,这里引入的是最新的 4.1.1 版本,需要注意的是:如果使用 durid 连接池的话,请使用 durid 包,不要使用 druid-spring-boot-starter 依赖包。
本篇博客做的是 Demo ,使用上篇博客搭建的 Mysql 一主一从结构作为演示,application.yml 配置如下:
server: port: 8888 spring: shardingsphere: datasource: # 数据源名称,以英文逗号分隔,需要跟下面的每个数据源配置对应上 names: master,slave # 主库连接信息 master: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.216.128:3306/rw_demo?characterEncoding=utf-8 username: root password: root # 从库连接信息 slave: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.216.129:3306/rw_demo?characterEncoding=utf-8 username: root password: root masterslave: # 从库负载均衡算法,可选值为:round_robin 和 random load-balance-algorithm-type: round_robin # 最终的数据源名称(可以随便指定) name: ds # 主库数据源名称 master-data-source-name: master # 从库数据源名称列表,多个逗号分隔 slave-data-source-names: slave props: sql: #开启SQL显示,默认false show: true mybatis-plus: configuration: #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射 map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: ASSIGN_ID
配置文件的内容很简单,注释也很详细,应该很容易看懂。最主要就是把主库和从库的数据库连接信息配置好即可。
默认情况下 sharding jdbc 所有的写操作都是在主库上执行,所有的读操作都是在从库上执行。
对于从库的选择,有两种负载均衡算法:轮询和随机。
到此为止,Sharding Jdbc 的读写分离,就已经全部搞定了,下面要编写的业务代码,跟 Sharding Jdbc 没有什么关系了,只有想要强制从主库读取数据时,才需要编写一点点代码。下面就让我们快速把用于测试效果的业务代码堆起来吧。
三、代码细节展示
创建了一个实体类 Employee 具体细节如下:
package com.jobs.entity; import lombok.Data; import java.io.Serializable; @Data public class Employee implements Serializable { private Long id; private String name; private Integer age; }
由于使用 Mybatis Plus 框架,因此对于 Mapper 和 Service 等都是自动生成的,具体如下:
package com.jobs.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.jobs.entity.Employee; import org.apache.ibatis.annotations.Mapper; @Mapper public interface EmployeeMapper extends BaseMapper<Employee> {}
package com.jobs.service; import com.baomidou.mybatisplus.extension.service.IService; import com.jobs.entity.Employee; public interface EmployeeService extends IService<Employee> {}
package com.jobs.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.jobs.entity.Employee; import com.jobs.mapper.EmployeeMapper; import com.jobs.service.EmployeeService; import org.springframework.stereotype.Service; @Service public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {}
下面就是用于对外提供接口测试的 EmployeeController 代码:
package com.jobs.controller; import com.jobs.entity.Employee; import com.jobs.service.EmployeeService; import org.apache.shardingsphere.api.hint.HintManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RequestMapping("/emp") @RestController public class EmployeeController { @Autowired private EmployeeService employeeService; //主库添加 @PostMapping public Employee addEmployee(@RequestBody Employee employee) { employeeService.save(employee); return employee; } //从库读取 @GetMapping("/{id}") public Employee getEmployee(@PathVariable("id") Long id) { Employee employee = employeeService.getById(id); return employee; } //如果必须要及时拿到最新结果的话,可以强制从主库读取 @GetMapping("/list") public List<Employee> getList() { HintManager.clear(); //HintManager 实现了 AutoCloseable 接口,因此使用 try 可以自动释放资源 try (HintManager hintManager = HintManager.getInstance()) { hintManager.setMasterRouteOnly(); List<Employee> list = employeeService.list(); return list; } } }
四、验证效果
我们使用 Postman 工具来请求结果,在 IDEA 的控制台中,查看日志结果来验证读写分离的效果。
(1)调用添加 Employee 的接口
控制台打印的日志如下,由于原始控制台的信息换行过多,因此我复制到文本文件中进行展示:
从 Actual SQL:master 可以看出,添加操作是访问主库进行操作的。
(2)调用通过 id 获取 Employee 的接口
控制台打印的结果如下:
从 Actual SQL:slave 可以看出,读取数据是访问从库进行操作的。
(3)调用获取员工列表接口,代码中强制从主库中获取数据
由于主从复制需要一定的时间,尽管时间很短暂,为了能够实时获取数据,我们希望强制从主库获取数据,此时只需要添加一下代码,通过 HintManager 指定从主库获取即可。需要注意的是一定要提前调用 HintManager.clear() 方法。
HintManager.clear(); //HintManager 实现了 AutoCloseable 接口,因此使用 try 可以自动释放资源 try (HintManager hintManager = HintManager.getInstance()) { hintManager.setMasterRouteOnly(); //自己需要编写的操作主库的业务代码... }
使用 Postman 调用获取员工列表接口如下:
控制台打印的结果如下:
从上图中的 Actual SQL:master 可以发现,我们已经实现了从主库强制进行读取的操作。
本篇博客的源代码下载地址:https://files.cnblogs.com/files/blogs/699532/springboot_rw_demo.zip
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构