写点什么

SpringBoot 项目优雅实现读写分离 | 京东云技术团队

  • 2023-11-13
    北京
  • 本文字数:4244 字

    阅读完需:约 14 分钟

SpringBoot 项目优雅实现读写分离 | 京东云技术团队

一、读写分离介绍

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


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

二、实现读写分离-基础

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

# 主库配置spring.datasource.master.jdbc-url=jdbc:mysql://ip:port/master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=falsespring.datasource.master.username=masterspring.datasource.master.password=123456spring.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=falsespring.datasource.slave.username=slavespring.datasource.slave.password=123456spring.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. 创建动态路由数据源

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


@Slf4jpublic 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@Componentpublic 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 层的方法上使用自定义注解标记查询数据源

@Servicepublic 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=falsespring.datasource.master.username=masterspring.datasource.master.password=123456spring.datasource.master.driver-class-name=com.mysql.jdbc.Driverspring.datasource.master.type=com.zaxxer.hikari.HikariDataSourcespring.datasource.master.hikari.name=masterspring.datasource.master.hikari.minimum-idle=5spring.datasource.master.hikari.idle-timeout=30spring.datasource.master.hikari.maximum-pool-size=10spring.datasource.master.hikari.auto-commit=truespring.datasource.master.hikari.pool-name=DatebookHikariCPspring.datasource.master.hikari.max-lifetime=1800000spring.datasource.master.hikari.connection-timeout=30000spring.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=falsespring.datasource.slave.username=rootspring.datasource.slave.password=123456spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driverspring.datasource.slave.type=com.zaxxer.hikari.HikariDataSourcespring.datasource.slave.hikari.name=masterspring.datasource.slave.hikari.minimum-idle=5spring.datasource.slave.hikari.idle-timeout=30spring.datasource.slave.hikari.maximum-pool-size=10spring.datasource.slave.hikari.auto-commit=truespring.datasource.slave.hikari.pool-name=DatebookHikariCPspring.datasource.slave.hikari.max-lifetime=1800000spring.datasource.slave.hikari.connection-timeout=30000spring.datasource.slave.hikari.connection-test-query=SELECT 1
复制代码

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

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


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


@Intercepts({        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),})@Componentpublic 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);        }    }}
复制代码


作者:京东健康 苏曼

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

发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
SpringBoot 项目优雅实现读写分离 | 京东云技术团队_数据库_京东科技开发者_InfoQ写作社区