首页 > 其他分享 >SpringBoot 项目优雅实现读写分离

SpringBoot 项目优雅实现读写分离

时间:2023-11-13 11:56:48浏览次数:58  
标签:slave SpringBoot spring 读写 优雅 master datasource 数据源 public

一、读写分离介绍

当使用Spring Boot开发数据库应用时,读写分离是一种常见的优化策略。读写分离将读操作和写操作分别分配给不同的数据库实例,以提高系统的吞吐量和性能。

读写分离实现主要是通过动态数据源功能实现的,动态数据源是一种通过在运行时动态切换数据库连接的机制。它允许应用程序根据不同的条件或配置选择不同的数据源,以实现更灵活和可扩展的数据库访问。

二、实现读写分离-基础

1. 配置主数据库和从数据库的连接信息

# 主库配置
spring.datasource.master.jdbc-url=jdbc:mysql://ip:port/master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.master.username=master
spring.datasource.master.password=123456
spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver

# 从库配置
spring.datasource.slave.jdbc-url=jdbc:mysql://ip:port/slave?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.slave.username=slave
spring.datasource.slave.password=123456
spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driver 

2. 创建主数据库和从数据库的数据源配置类

通过不同的条件限制和配置文件前缀可以完成不同数据源的创建工作,不止是主从也可以是多个不同的数据库

主库数据源配置

@Configuration
@ConditionalOnProperty("spring.datasource.master.jdbc-url")
public class MasterDataSourceConfiguration {
    @Bean("masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
}

从库数据源配置

@Configuration
@ConditionalOnProperty("spring.datasource.slave.jdbc-url")
public class SlaveDataSourceConfiguration {
    @Bean("slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
}



3. 创建主从数据源枚举

public enum DataSourceTypeEnum {
    /**
     * 主库
     */
    MASTER,

    /**
     * 从库
     */
    SLAVE,
   ;
  
}

4. 创建动态路由数据源

这儿做了一个开关,可以控制读写分离的开启和关闭工作,可以讲操作全部切换到主库进行。然后根据上下文中的数据源类型来返回不同的数据源类型枚举

@Slf4j
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {

    @Value("${DB_RW_SEPARATE_SWITCH:false}")
    private boolean dbRwSeparateSwitch;
    @Override
    protected Object determineCurrentLookupKey() {
        if(dbRwSeparateSwitch && DataSourceTypeEnum.SLAVE.equals(DataSourceContextHolder.getDataSourceType())) {
            log.info("DynamicRoutingDataSource 切换数据源到从库");
            return DataSourceTypeEnum.SLAVE;
        }
        log.info("DynamicRoutingDataSource 切换数据源到主库");
        // 根据需要指定当前使用的数据源,这里可以使用ThreadLocal或其他方式来决定使用主库还是从库
        return DataSourceTypeEnum.MASTER;
    }
}

5. 创建动态数据源配置类

将主数据库和从数据库的数据源添加到动态数据源中,并可以通过枚举创建一个数据源 map,这样就可以通过上面的路由返回的枚举来切换数据源

@Configuration
@ConditionalOnProperty("spring.datasource.master.jdbc-url")
public class DynamicDataSourceConfiguration {
    @Bean("dataSource")
    @Primary
    public DataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceTypeEnum.MASTER, masterDataSource);
        targetDataSources.put(DataSourceTypeEnum.SLAVE, slaveDataSource);

        DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        return dynamicDataSource;
    }
}

6. 创建DatasourceContextHolder类使用ThreadLocal存储当前线程的数据源类型

注意这儿有个潜在风险就是创建新的线程时会导致 ThreadLocal 中的数据无法正确读取,如果涉及到在开启新线程可以使用 TransmittableThreadLocal 来进行父子线程数据的同步,git 地址: https://github.com/alibaba/transmittable-thread-local

public class DataSourceContextHolder {
    private static final ThreadLocal<DataSourceTypeEnum> contextHolder = new ThreadLocal<>();

    public static void setDataSourceType(DataSourceTypeEnum dataSourceType) {
        contextHolder.set(dataSourceType);
    }

    public static DataSourceTypeEnum getDataSourceType() {
        return contextHolder.get();
    }

    public static void clearDataSourceType() {
        contextHolder.remove();
    }
}

7. 创建自定义注解,用于标记主和从数据源

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MasterDataSource {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SlaveDataSource {
}

8. 创建切面类,拦截数据库操作,并根据注解设置切换数据源参数

@Aspect
@Component
public class DataSourceAspect {

    @Before("@annotation(xxx.MasterDataSource)")
    public void setMasterDataSource(JoinPoint joinPoint) {
        DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.MASTER);
    }

    @Before("@annotation(xxx.SlaveDataSource)")
    public void setSlaveDataSource(JoinPoint joinPoint) {
        DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.SLAVE);
    }

    @After("@annotation(xxx.MasterDataSource) || @annotation(xxx.SlaveDataSource)")
    public void clearDataSource(JoinPoint joinPoint) {
        DataSourceContextHolder.clearDataSourceType();
    }
}

9. 在Service层的方法上使用自定义注解标记查询数据源

@Service
public class TestService {
    @Autowired
    private TestDao testDao;

    @SlaveDataSource
    public Test test() {
        return testDao.queryByPrimaryKey(11L);
    }
}

10. 排除掉数据源自动配置类

如果不排除自动配置类会导致初始化多个 dataSource 对象导致出现问题

SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

三、实现读写分离-进阶

1. 使用链接池,以Hikari为例

修改链接配置,加入链接池相关配置即可

# 主库配置
spring.datasource.master.jdbc-url=jdbc:mysql://ip:port/master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.master.username=master
spring.datasource.master.password=123456
spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.master.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.master.hikari.name=master
spring.datasource.master.hikari.minimum-idle=5
spring.datasource.master.hikari.idle-timeout=30
spring.datasource.master.hikari.maximum-pool-size=10
spring.datasource.master.hikari.auto-commit=true
spring.datasource.master.hikari.pool-name=DatebookHikariCP
spring.datasource.master.hikari.max-lifetime=1800000
spring.datasource.master.hikari.connection-timeout=30000
spring.datasource.master.hikari.connection-test-query=SELECT 1

# 从库配置
spring.datasource.slave.jdbc-url=jdbc:mysql://ip:port/slave?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.slave.username=root
spring.datasource.slave.password=123456
spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.slave.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.slave.hikari.name=master
spring.datasource.slave.hikari.minimum-idle=5
spring.datasource.slave.hikari.idle-timeout=30
spring.datasource.slave.hikari.maximum-pool-size=10
spring.datasource.slave.hikari.auto-commit=true
spring.datasource.slave.hikari.pool-name=DatebookHikariCP
spring.datasource.slave.hikari.max-lifetime=1800000
spring.datasource.slave.hikari.connection-timeout=30000
spring.datasource.slave.hikari.connection-test-query=SELECT 1

2. 集成 mybatis 并在写入时强制切换到主库

不需要做任何配置,正常集成 mybatis 即可使用读写分离功能

可以通过 mybatis 的拦截器在写入操作时强制切换到主库

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})
@Component
public class WriteInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取 SQL 类型
        DataSourceTypeEnum dataSourceType = DataSourceContextHolder.getDataSourceType();
        if(DataSourceTypeEnum.SLAVE.equals(dataSourceType)) {
            DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.MASTER);
        }
        try {
            // 执行 SQL
            return invocation.proceed();
        } finally {
            // 恢复数据源  考虑到写入后可能会反查,后续都走主库
            // DataSourceContextHolder.setDataSourceType(dataSourceType);
        }
    }
}

作者:京东健康 苏曼

来源:京东云开发者社区 转发请注明来源

标签:slave,SpringBoot,spring,读写,优雅,master,datasource,数据源,public
From: https://www.cnblogs.com/jingdongkeji/p/17828805.html

相关文章

  • springboot集成EasyPoi,实现Excel/Word的导入导出
    前言在日常工作中,我们经常需要进行Excel或Word文档的导入和导出。这需要我们编写大量的代码和复杂的操作,从而增加了我们工作的难度。而EasyPoi就是针对这种情况而开发出来的一个简单易用的Java导入导出工具。EasyPoi是一个开源的Java导入导出工具,它提供了非常简单易用的API,可以让......
  • SpringBoot: RestTemplate中文乱码
    1.问题原因RestTemplate的构造器创建了一个默认字符集为ISO-8859-1的StringHttpMessageConverter对象,它的默认编码格式为ISO-8859-1,所以需要修改这个StringHttpMessageConverter对象的编码格式即可2.解决方案@BeanpublicRestTemplategetRestTemplate(){......
  • SpringBoot后端流程
    1.idea创建springboot项目:2.7版本2.pom依赖导入<!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--mysql--><dependency......
  • springboot使用requestmapping创建xml响应体接口
    entity下创建类文件,类名分别为:ResponseXml,ResponseItemcontroller下创建xml响应体实现方法getResponseWithXml---------ResponseXmlStart-------importjavax.xml.bind.annotation.*;//根标签@XmlRootElement(name="test1")publicclassResponseXml{privateStringum......
  • Springboot内置的工具类之ObjectUtils
    在实际业务开发中,有时候经常需要判断对象是否为空、数组是否为空、两个对象是否相等,数组中是否包含某个元素,往数组中追加元素等这些操作,每次都手写太麻烦,然后很多人的选择是封装成util工具类,实际上类似这些东西,如果项目使用了spring的框架,根本不需要封装,org.springframework.util.O......
  • SpringBoot3.x使用Swagger
    SpringBoot3.x使用Swagger当前开发主流是前后端分离,有完整文档可以使团队配合更加流畅Spring生态中通常使用springfox,但是当前springfox并不支持SpringBoot3.x版本使用替代产品:Springdoc.orgSpringdoc在v1.7.0版本之后不支持SpringBoot2.x和1.x!!!项目启动后,Swagger默认地址......
  • SpringBoot创建非web项目
    从开始使用SpringBoot到现在,一直都是在用SpringBoot开发web服务(API服务),其实SpringBoot也支持非web项目。Spring诞生之初就不是为web项目定制的,SpringBoot无非是在spring核心项目的基础上添加了一些方便开发者使用的组件,所以使用SpringBoot开发非web项目也是可行的。依赖创建web......
  • SpringBoot @Async:魔法和陷阱
    来源:https://medium.com/@Async注解就像是springboot项目中性能优化的秘密武器。是的,我们也可以手动创建自己的执行器和线程池,但@Async使事情变得更简单、更神奇。@Async注释允许我们在后台运行代码,因此我们的主线程可以继续运行,而无需等待较慢的任务完成。但是,就像所有秘密武器一......
  • C++ 采用get()和put()读写文件
    在某些特殊的场景中,我们可能需要逐个读取文件中存储的字符,或者逐个将字符存储到文件中。这种情况下,就可以调用get()和put()成员方法实现。C++ostream::put()成员方法通过《C++cout.put()》一节的学习,读者掌握了如何通过执行cout.put()方法向屏幕输出单个字符。我们知道,fstr......
  • C++ 采用read()和write()读写二进制文件
    以文本形式读写文件和以二进制形式读写文件的区别,并掌握了用重载的>>和<<运算符实现以文本形式读写文件。在此基础上,本节继续讲解如何以二进制形式读写文件。举个例子,现在要做一个学籍管理程序,其中一个重要的工作就是记录学生的学号、姓名、年龄等信息。这意味着,我们需要用一个......