前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot读写分离

SpringBoot读写分离

作者头像
CBeann
发布2023-12-25 18:45:00
1510
发布2023-12-25 18:45:00
举报
文章被收录于专栏:CBeann的博客CBeann的博客

前提

  • 2台mysql

①可以开两个docker ②也可以在一个MySQL服务器中用2个数据库

代码语言:javascript
复制
 docker run -d -p 3307:3306 --name mysql01 -e MYSQL_ROOT_PASSWORD=123456  docker.io/mysql
 docker run -d -p 3308:3306 --name mysql02 -e MYSQL_ROOT_PASSWORD=123456  docker.io/mysql
  • SpringBoot

代码

代码下载: Demooo/springboot-readwrite-separation-deno at master · cbeann/Demooo · GitHub

目录结构

maven依赖

代码语言:javascript
复制
    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>provided</scope>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

    </dependencies>

配置文件application.yml

代码语言:javascript
复制
spring:
  datasource:
    master:
      jdbc-url: jdbc:mysql://202.204.124.110:3306/readwritedemo
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
    slave:
      jdbc-url: jdbc:mysql://202.204.124.110:3306/readwritedemo
      username: root   # 只读账户
      password: root
      driver-class-name: com.mysql.jdbc.Driver

数据源配置

DataSourceConfig

此处设计了两个数据源,一个是master,一个是slave

代码语言:javascript
复制
package com.example.config;

/**
 * @author chaird
 * @create 2020-12-30 21:27
 */
import com.example.enums.DBTypeEnum;
import com.example.bean.MyRoutingDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 关于数据源配置,参考SpringBoot官方文档第79章《Data Access》 79. Data Access 79.1 Configure a Custom DataSource 79.2
 * Configure Two DataSources
 */
@Configuration
public class DataSourceConfig {

  @Bean
  @ConfigurationProperties("spring.datasource.master")
  public DataSource masterDataSource() {
    return DataSourceBuilder.create().build();
  }

  @Bean
  @ConfigurationProperties("spring.datasource.slave")
  public DataSource slaveDataSource() {
    return DataSourceBuilder.create().build();
  }

  @Bean
  public DataSource myRoutingDataSource(
      @Qualifier("masterDataSource") DataSource masterDataSource,
      @Qualifier("slaveDataSource") DataSource slave1DataSource) {
    Map<Object, Object> targetDataSources = new HashMap<>(2);
    targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
    targetDataSources.put(DBTypeEnum.SLAVE, slave1DataSource);
    MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
    myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
    myRoutingDataSource.setTargetDataSources(targetDataSources);
    return myRoutingDataSource;
  }
}
MyBatisConfig

MyBatis配置xml位置,事务管理器等

代码语言:javascript
复制
package com.example.config;

/**
 * @author chaird
 * @create 2020-12-30 21:27
 */
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.annotation.Resource;
import javax.sql.DataSource;

@EnableTransactionManagement
@Configuration
public class MyBatisConfig {

  @Resource(name = "myRoutingDataSource")
  private DataSource myRoutingDataSource;

  @Bean
  public SqlSessionFactory sqlSessionFactory() throws Exception {
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
    sqlSessionFactoryBean.setMapperLocations(
        new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
    return sqlSessionFactoryBean.getObject();
  }

  @Bean
  public PlatformTransactionManager platformTransactionManager() {
    return new DataSourceTransactionManager(myRoutingDataSource);
  }
}
DBTypeEnum 枚举类
代码语言:javascript
复制
package com.example.enums;

/**
 * @author chaird
 * @create 2020-12-30 21:28
 */
public enum DBTypeEnum {
  MASTER,
  SLAVE;
}
MyRoutingDataSource

AbstractRoutingDataSource 一个神奇的接口,自己在determineCurrentLookupKey方法debug就清楚了。

代码语言:javascript
复制
package com.example.bean;

/**
 * @author chaird
 * @create 2020-12-30 21:37
 */
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;

public class MyRoutingDataSource extends AbstractRoutingDataSource {
  @Nullable
  @Override
  protected Object determineCurrentLookupKey() {
    return DBContextHolder.get();
  }
}

AOP设置

DataSourceAop

设置切面和切面方法

代码语言:javascript
复制
package com.example.aop;

/**
 * @author chaird
 * @create 2020-12-30 21:30
 */
import com.example.bean.DBContextHolder;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/** 切面表达式和方法 */
@Aspect
@Component
public class DataSourceAop {

  /** 读切面(条件1&&条件2)
   * 条件1:没有Master注解 条件2:com.example.service包下 任意类 的select* 或者get*方法
   * */
  @Pointcut(
      "!@annotation(com.example.annotation.Master) "
          + "&& (execution(* com.example.service..*.select*(..)) "
          + "|| execution(* com.example.service..*.get*(..)))")
  public void readPointcut() {}


    /** 写切面
     * 类似上面,不做解释
     * */
  @Pointcut(
      "@annotation(com.example.annotation.Master) "
          + "|| execution(* com.example.service..*.insert*(..)) "
          + "|| execution(* com.example.service..*.add*(..)) "
          + "|| execution(* com.example.service..*.update*(..)) "
          + "|| execution(* com.example.service..*.edit*(..)) "
          + "|| execution(* com.example.service..*.delete*(..)) "
          + "|| execution(* com.example.service..*.remove*(..))")
  public void writePointcut() {}

    /**
     * Before方法,设置ThreadLocal里的一个变量为slave
     */
  @Before("readPointcut()")
  public void read() {
    DBContextHolder.slave();
  }

    /**
     * Before方法,设置ThreadLocal里的一个变量为master
     */
  @Before("writePointcut()")
  public void write() {
    DBContextHolder.master();
  }

  /** 另一种写法:if...else... 判断哪些需要读从数据库,其余的走主数据库 */
  //    @Before("execution(* com.cjs.example.service.impl.*.*(..))")
  //    public void before(JoinPoint jp) {
  //        String methodName = jp.getSignature().getName();
  //
  //        if (StringUtils.startsWithAny(methodName, "get", "select", "find")) {
  //            DBContextHolder.slave();
  //        }else {
  //            DBContextHolder.master();
  //        }
  //    }
}
Master

自定义注解,有该注解的的service方法出现主库

代码语言:javascript
复制
package com.example.annotation;

/**
 * @author chaird
 * @create 2020-12-30 21:35
 * 加此注解的的方法查询主库
 */
public @interface Master {}
DBContextHolder

给threadLocal设置参数

代码语言:javascript
复制
package com.example.bean;

/**
 * @author chaird
 * @create 2020-12-30 21:30
 */
import com.example.enums.DBTypeEnum;

import java.util.concurrent.atomic.AtomicInteger;

public class DBContextHolder {

  private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();

  private static final AtomicInteger counter = new AtomicInteger(-1);

  public static void set(DBTypeEnum dbType) {
    contextHolder.set(dbType);
  }

  public static DBTypeEnum get() {
    return contextHolder.get();
  }

  public static void master() {
    set(DBTypeEnum.MASTER);
    System.out.println("切换到master");
  }

  public static void slave() {
    //  轮询
    set(DBTypeEnum.SLAVE);
    System.out.println("切换到slave2");
  }
}

entity层

代码语言:javascript
复制
package com.example.entity;

import java.io.Serializable;
import lombok.Data;

/**
 * student
 * @author 
 */
@Data
public class Student implements Serializable {
    private Integer id;

    private String name;

    private Integer age;

    private static final long serialVersionUID = 1L;
}

mapper、xml(省略)

service层

代码语言:javascript
复制
package com.example.service;

import com.example.annotation.Master;
import com.example.dao.StudentDao;
import com.example.entity.Student;
import com.example.entity.StudentExample;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author chaird
 * @create 2020-12-30 21:38
 */
@Service
public class StudentService {

  @Autowired private StudentDao studentDao;

  public List<Student> getAllSlave() {
    return studentDao.selectByExample(new StudentExample());
  }

  @Master
  public List<Student> getAllMaster() {
    return studentDao.selectByExample(new StudentExample());
  }

  public void add(Student student) {
    studentDao.insert(student);
  }
}

接口层

代码语言:javascript
复制
package com.example.controller;

import com.example.entity.Student;
import com.example.service.StudentService;
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.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author chaird
 * @create 2020-12-30 21:40
 */
@RestController
public class StudentController {

  @Autowired private StudentService studentService;

  @GetMapping("/getAllMaster")
  public Object getAllMaster() {
    return studentService.getAllMaster();
  }

  @GetMapping("/getAllSlave")
  public Object getAllSlave() {
    return studentService.getAllSlave();
  }

  @PostMapping("/save")
  public Object save(@RequestBody Student student) {
    studentService.add(student);
    return "success";
  }
}

启动类

代码语言:javascript
复制
package com.example;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.dao")//扫描DAO包
public class ReadwriteSepApp {

  public static void main(String[] args) {
    SpringApplication.run(ReadwriteSepApp.class, args);
  }
}

测试

查询走写库:http://localhost:8080/getAllMaster

查询走读库:http://localhost:8080/getAllSlave

增加走写库:http://localhost:8080/save

原理

注意:该原理流程图是根据我上面的代码写的

1)创建两个数据源,并放在AbstractRoutingDataSource里的一个属性map中,其中(master->dataSource1,slave->dataSource2)

2) 先执行方法的AOP的Before方法,根据方法名称getAllSlave()方法在ThreadLocal中设置为 slave

3) 执行查询的时候需要获取dataSource,获取dataSource时AbstractRoutingDataSource.determineCurrentLookupKey()方法,拿到了步骤2)中设置slave,

4)从步骤3)中拿到了slave属性,然后在步骤1)放入到的AbstractRoutingDataSource里的map里拿到dataSource

5)从而实现了变换dataSource

总结

1)ThreadLocal竟然可以用在此处,惊呆了,我的小伙伴

2)AOP也是秀

3)XXXHolder不知道出自哪,但是我看了几篇博客,都是这种编码风格,例如DBContextHolder

参考

SpringBoot+MyBatis+MySQL读写分离 - 废物大师兄 - 博客园

Spring 如何实现多数据源读写分离?

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-03-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前提
  • 代码
    • 目录结构
      • maven依赖
        • 配置文件application.yml
          • 数据源配置
            • DataSourceConfig
            • MyBatisConfig
            • DBTypeEnum 枚举类
            • MyRoutingDataSource
          • AOP设置
            • DataSourceAop
            • Master
            • DBContextHolder
          • entity层
            • mapper、xml(省略)
              • service层
                • 接口层
                  • 启动类
                    • 测试
                    • 原理
                    • 总结
                    • 参考
                    相关产品与服务
                    数据库
                    云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档