Sharding-JDBC实现读写分离

目录

前言

1.为什么要做读写分离

2.主从复制存在的问题

3.读写分离实现效果

4.读写分离配置

        4.1:Maven依赖

        4.2:application.yml

        4.3:Controller

        4.4:Service

        4.5:Mapper

        4.6:启动类

        4.7:启动成功示例图(控制台输出)

5.读写分离测试

        5.1:写入测试

        5.2:查询测试

6.读操作强制路由到主节点原因

        6.1:强制路由原理

7.实现读操作强制走主节点

        7.1:代码层面实现

        7.2:使用AOP定义注解实现

8.模拟测试

        8.1:一主两从模拟某个从节点挂掉系统运行情况


前言

        前面已经实现了Mysql数据库的主从复制功能,光有数据库主从复制还不够,还需要实现对数据库的读写分离操作。

Mysql主从复制https://mp.csdn.net/mp_blog/creation/editor/128456196

Sharding-JDBC官方文档https://shardingsphere.apache.org/document/4.1.1/cn/overview/#sharding-jdbc

1.为什么要做读写分离

Sharding-JDBC官方说法:

面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。

通过一主多从的配置方式,可以将查询请求均匀的分散到多个数据副本,能够进一步的提升系统的处理能力。 使用多主多从的方式,不但能够提升系统的吞吐量,还能够提升系统的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行。

Sharding-JDBC读写分离是根据SQL语义的分析,将读操作和写操作分别路由至主库与从库。

2.主从复制存在的问题

        在主从复制中数据在主节点写入从节点读出,主从节点数据同步可能存在一定的延时,这导致主节点和从节点的数据不一致。对于时效性比较高的查询,一般都会强制走主节点查询。

3.读写分离实现效果

        配置一个主节点,两个从节点。增/删/改操作交由主节点完成(对于时效性高的查询也可强制路由到主节点),查询操作交由从节点完成。这样就将数据库的查询压力分担出去了。

4.读写分离配置

        4.1:Maven依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.6.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- AOP -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- sharding-jdbc -->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-core-api</artifactId>
            <version>4.0.0-RC2</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>4.0.0-RC2</version>
        </dependency>

        <!-- Mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <!-- Mybatis plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
            <exclusions>
                <!-- 排除默认的 HikariCP 数据源,使用druid的数据源 -->
                <exclusion>
                    <groupId>com.zaxxer</groupId>
                    <artifactId>HikariCP</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.9</version>
        </dependency>

        <!-- 测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

        4.2:application.yml

spring:
  # sharding-jdbc配置
  shardingsphere:
    # 是否开启SQL显示
    props:
      sql:
        show: true
    #  数据源配置
    datasource:
      names: ds-master,ds-slave1,ds-slave2 # 主从节点名称
      # 主节点配置
      ds-master:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3307/student?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
        username: root
        password: 123456
      # 从节点一配置
      ds-slave1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3308/student?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
        username: root
        password: 123456
      # 从节点二配置
      ds-slave2:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3309/student?useUnicode=true&characterEncoding=utf8&tinyInt1isBit=false&useSSL=false&serverTimezone=GMT
        username: root
        password: 123456
    # 读写分离配置
    sharding:
      master-slave-rules:
        ds-master:
          # 主库
          masterDataSourceName: ds-master
          # 从库
          slaveDataSourceNames:
            - ds-slave1
            - ds-slave2
          # 从库查询数据的负载均衡算法 目前有2种算法 round_robin(轮询)和 random(随机)
          loadBalanceAlgorithmType: round_robin

# Mybatis Plus 配置
mybatis-plus:
  configuration:
    # 控制台打印完整带参数SQL语句
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 设置当查询结果值为null时,同样映射该查询字段给实体(Mybatis-Plus默认会忽略查询为空的实体字段返回)。
    call-setters-on-nulls: true

        4.3:Controller

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/stu")
public class StudentController {
    @Autowired
    private IStudentService stuService;

    /** 查询入口 */
    @GetMapping("/loadStu")
    public Object loadOneStu(){
        return stuService.loadOneStu();
    }

    /** 新增入口 */
    @PostMapping("/saveStu")
    public void saveStu(){
        stuService.saveOneStu();
    }
}

        4.4:Service

               4.4.1: Service:

import java.util.Map;
public interface IStudentService {
    /** 查询 */
    Map loadOneStu();

    /** 新增 */
    void saveOneStu();
}

                4.4.2:ServiceImpl:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;

@Service
public class StudentServiceImpl implements IStudentService {
    @Autowired
    private StudentMapper stuMapper;

    /** 查询 */
    @Override
    public Map loadOneStu() {
        return stuMapper.loadOneStu();
    }

    /** 新增 */
    @Override
    public void saveOneStu() {
        stuMapper.saveOneStu();
    }
}

        4.5:Mapper

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.Map;

@Mapper
public interface StudentMapper {
    /** 查询 */
    @Select("SELECT * FROM stu LIMIT 1")
    Map loadOneStu();

    /** 新增 */
    @Insert("INSERT INTO stu(id,name) VALUES (2,'zs')")
    void saveOneStu();
}

        4.6:启动类

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("cn.cdjs.mapper")
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class);
    }
}

        4.7:启动成功示例图(控制台输出)

5.读写分离测试

        5.1:写入测试

                POST请求调用:​http://localhost:8080/stu/saveStu

        5.2:查询测试

                GET请求调用:​http://localhost:8080/stu/loadStu

    多次调用GET请求查询数据,发现从节点在轮询交替执行查询。

6.读操作强制路由到主节点原因

        主从复制,主节点写入从节点读取,由于主节点写入从节点复制有一定延迟。对于一些时效性要求很高的查询,比如我写入了立马就要去查,假如此时从库还未复制主库这条数据从库就不会查询到。而解决此类问题一般都是对时效性比较高的查询强制路由到主节点进行查询(主节点写入当然主节点也能立马查询出来)。当然Sharding-JDBC也支持“强制路由”这个功能。

        6.1:强制路由原理

强制路由 - 基于暗示数据分片https://shardingsphere.apache.org/document/4.1.1/cn/manual/sharding-jdbc/usage/hint/

基于暗示的数据分片,ShardingSphere在进行Routing时,如果发现LogicTable的TableRule采用了 Hint的分片算法,将会从HintManager中获取分片值进行路由操作。

7.实现读操作强制走主节点

        7.1:代码层面实现

Controller

    /** 强制路由主节点查询入口 */
    @GetMapping("/coerceRouteMasterNodeSelect")
    public Object coerceRouteMasterNodeSelect(){
        return stuService.coerceRouteMasterNodeSelect();
    }

ServiceImpl

    /** 强制路由主节点查询入口 */
    @Override
    public Map coerceRouteMasterNodeSelect() {
        HintManager.clear();

        try(HintManager hintManager = HintManager.getInstance()){
            // 设置走主库
            hintManager.setMasterRouteOnly();
            return stuMapper.loadOneStu();
        }
    }

        测试:

        7.2:使用AOP定义注解实现

每次强制路由到主节点都需要写上面的代码这是很繁琐的,通过AOP + 注解 我们可以轻松实现强制路由到主节点功能。

7.2.1:定义注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 定义注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface DataSourceMaster {
}

7.2.2:定义AOP切面

import org.apache.shardingsphere.api.hint.HintManager;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Objects;

@Component
@Aspect
public class DataSourceMasterAop {
    /** 作用范围 */
    @Around("execution(* cn.cdjs.service.impl.*.*(..))")
    public Object master(ProceedingJoinPoint joinPoint){
        // 获取执行方法参数
        Object[] args = joinPoint.getArgs();
        Object ret = null;

        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 获取执行方法
        Method method = methodSignature.getMethod();
        // 获取方法上自定义的“DataSourceMaster”注解
        DataSourceMaster shardingJdbcMaster = method.getAnnotation(DataSourceMaster.class);

        HintManager hintManager = null;
        try {
            if (Objects.nonNull(shardingJdbcMaster)) { // 当前方法上有自定义的DataSourceMaster注解
                HintManager.clear();
                hintManager = HintManager.getInstance();
                // 强制路由到主节点
                hintManager.setMasterRouteOnly();
            }
            ret = joinPoint.proceed(args); // 执行目标方法并获取响应对象
        }catch (Throwable ex){
            System.out.println("Exception Err " + ex);
        }finally {
            if (Objects.nonNull(shardingJdbcMaster) && Objects.nonNull(hintManager)) {
                // 关闭
                hintManager.close();
            }
        }
        return ret;
    }
}

7.2.3:Controller

    /** AOP + 注解方式 强制路由主节点查询入口 */
    @GetMapping("/coerceRouteMasterNodeSelectByAop")
    public Object coerceRouteMasterNodeSelectByAop(){
        return stuService.coerceRouteMasterNodeSelectByAop();
    }

7.2.4:ServiceImpl

    /** AOP + 注解方式 强制路由主节点查询入口 */
    @DataSourceMaster // 自定义的注解
    @Override
    public Object coerceRouteMasterNodeSelectByAop() {
        return stuMapper.loadOneStu();
    }

7.2.5:测试

8.模拟测试

        8.1:一主两从模拟某个从节点挂掉系统运行情况

8.1.1:关闭其中一个从节点模拟从节点故障宕机

8.1.2:多次请求查询接口(http://localhost:8080/stu/loadStu)

从节点采用的是轮询的方式进行负载均衡,原正常情况下从节点是交替执行,但现在有一个从节点故障了,多次调用发现当轮询到故障的那台从节点上时程序一直拿取不到结果(即不会自动跳过宕机从节点)。

        

其它说明:

        多个从节点负载均衡策略为“轮询”时,不要在Junit单元测试中执行查询方法,这样测试出来的结果一直都是只有一个从节点在执行查询。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ShardingSphere:SpringBoot2+MybatisPlus+读写分离+分库分表课程目标快速的掌握读写分离+分表的实战,即插即用适用人群IT从业人员,开发人员,Java从业者,互联网从业者,性能调优人群课程简介ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈。它由Sharding-JDBCSharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成,shardingSphere定位为关系型数据库中间件。 Sharding-JDBCSharding-JDBCSharding-Sphere的第一个产品,也是Sharding-Sphere的前身,是当当网开源的一个产品。定位为轻量级的Java框架,在Java的JDBC层提供额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。他们均提供标准化的数据分片、读写分离、柔性事务和数据治理功能,可适用于如Java同构、异构语言、容器、云原生等各种多样化的应用场景。Sharding-JDBC可以通过Java,YAML,Spring命名空间和Spring Boot Starter四种方式配置,开发者可根据场景选择适合的配置方式。课程特色 本章节以尽量短的时间,为使用者提供最简单的ShardingSphere的快速入门。课程说明该课程属于系列课程,分为读写分离,分库不分表,不分库分表,分库分表,读写分离+分库分表共5个回合。本课程属于其中一个回合,请各位小哥哥们注意,课程的标题哦~

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值