使用 Sharding Jdbc 实现读写分离

发布时间 2023-05-02 12:04:27作者: 乔京飞

上一篇博客介绍了 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 工程,具体结构如下:

image

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 的接口

image

控制台打印的日志如下,由于原始控制台的信息换行过多,因此我复制到文本文件中进行展示:

image

从 Actual SQL:master 可以看出,添加操作是访问主库进行操作的。


(2)调用通过 id 获取 Employee 的接口

image

控制台打印的结果如下:

image

从 Actual SQL:slave 可以看出,读取数据是访问从库进行操作的。


(3)调用获取员工列表接口,代码中强制从主库中获取数据

由于主从复制需要一定的时间,尽管时间很短暂,为了能够实时获取数据,我们希望强制从主库获取数据,此时只需要添加一下代码,通过 HintManager 指定从主库获取即可。需要注意的是一定要提前调用 HintManager.clear() 方法。

HintManager.clear();
//HintManager 实现了 AutoCloseable 接口,因此使用 try 可以自动释放资源
try (HintManager hintManager = HintManager.getInstance()) {
    hintManager.setMasterRouteOnly();
    //自己需要编写的操作主库的业务代码...
}

使用 Postman 调用获取员工列表接口如下:

image

控制台打印的结果如下:

image

从上图中的 Actual SQL:master 可以发现,我们已经实现了从主库强制进行读取的操作。


本篇博客的源代码下载地址:https://files.cnblogs.com/files/blogs/699532/springboot_rw_demo.zip