经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring » 查看文章
SpringBoot 项目优雅实现读写分离
来源:cnblogs  作者:京东云技术团队  时间:2023/11/15 9:23:08  对本文有异议

一、读写分离介绍

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

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

二、实现读写分离-基础

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

  1. # 主库配置
  2. spring.datasource.master.jdbc-url=jdbc:mysql://ip:port/master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
  3. spring.datasource.master.username=master
  4. spring.datasource.master.password=123456
  5. spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver
  6. # 从库配置
  7. spring.datasource.slave.jdbc-url=jdbc:mysql://ip:port/slave?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
  8. spring.datasource.slave.username=slave
  9. spring.datasource.slave.password=123456
  10. spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driver

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

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

主库数据源配置

  1. @Configuration
  2. @ConditionalOnProperty("spring.datasource.master.jdbc-url")
  3. public class MasterDataSourceConfiguration {
  4. @Bean("masterDataSource")
  5. @ConfigurationProperties(prefix = "spring.datasource.master")
  6. public DataSource masterDataSource() {
  7. return DataSourceBuilder.create().build();
  8. }
  9. }

从库数据源配置

  1. @Configuration
  2. @ConditionalOnProperty("spring.datasource.slave.jdbc-url")
  3. public class SlaveDataSourceConfiguration {
  4. @Bean("slaveDataSource")
  5. @ConfigurationProperties(prefix = "spring.datasource.slave")
  6. public DataSource slaveDataSource() {
  7. return DataSourceBuilder.create().build();
  8. }
  9. }

3. 创建主从数据源枚举

  1. public enum DataSourceTypeEnum {
  2. /**
  3. * 主库
  4. */
  5. MASTER,
  6. /**
  7. * 从库
  8. */
  9. SLAVE,
  10. ;
  11. }

4. 创建动态路由数据源

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

  1. @Slf4j
  2. public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
  3. @Value("${DB_RW_SEPARATE_SWITCH:false}")
  4. private boolean dbRwSeparateSwitch;
  5. @Override
  6. protected Object determineCurrentLookupKey() {
  7. if(dbRwSeparateSwitch && DataSourceTypeEnum.SLAVE.equals(DataSourceContextHolder.getDataSourceType())) {
  8. log.info("DynamicRoutingDataSource 切换数据源到从库");
  9. return DataSourceTypeEnum.SLAVE;
  10. }
  11. log.info("DynamicRoutingDataSource 切换数据源到主库");
  12. // 根据需要指定当前使用的数据源,这里可以使用ThreadLocal或其他方式来决定使用主库还是从库
  13. return DataSourceTypeEnum.MASTER;
  14. }
  15. }

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

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

  1. @Configuration
  2. @ConditionalOnProperty("spring.datasource.master.jdbc-url")
  3. public class DynamicDataSourceConfiguration {
  4. @Bean("dataSource")
  5. @Primary
  6. public DataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
  7. Map<Object, Object> targetDataSources = new HashMap<>();
  8. targetDataSources.put(DataSourceTypeEnum.MASTER, masterDataSource);
  9. targetDataSources.put(DataSourceTypeEnum.SLAVE, slaveDataSource);
  10. DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
  11. dynamicDataSource.setTargetDataSources(targetDataSources);
  12. dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
  13. return dynamicDataSource;
  14. }
  15. }

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

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

  1. public class DataSourceContextHolder {
  2. private static final ThreadLocal<DataSourceTypeEnum> contextHolder = new ThreadLocal<>();
  3. public static void setDataSourceType(DataSourceTypeEnum dataSourceType) {
  4. contextHolder.set(dataSourceType);
  5. }
  6. public static DataSourceTypeEnum getDataSourceType() {
  7. return contextHolder.get();
  8. }
  9. public static void clearDataSourceType() {
  10. contextHolder.remove();
  11. }
  12. }

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

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(ElementType.METHOD)
  3. public @interface MasterDataSource {
  4. }
  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(ElementType.METHOD)
  3. public @interface SlaveDataSource {
  4. }

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

  1. @Aspect
  2. @Component
  3. public class DataSourceAspect {
  4. @Before("@annotation(xxx.MasterDataSource)")
  5. public void setMasterDataSource(JoinPoint joinPoint) {
  6. DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.MASTER);
  7. }
  8. @Before("@annotation(xxx.SlaveDataSource)")
  9. public void setSlaveDataSource(JoinPoint joinPoint) {
  10. DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.SLAVE);
  11. }
  12. @After("@annotation(xxx.MasterDataSource) || @annotation(xxx.SlaveDataSource)")
  13. public void clearDataSource(JoinPoint joinPoint) {
  14. DataSourceContextHolder.clearDataSourceType();
  15. }
  16. }

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

  1. @Service
  2. public class TestService {
  3. @Autowired
  4. private TestDao testDao;
  5. @SlaveDataSource
  6. public Test test() {
  7. return testDao.queryByPrimaryKey(11L);
  8. }
  9. }

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

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

  1. SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

三、实现读写分离-进阶

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

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

  1. # 主库配置
  2. spring.datasource.master.jdbc-url=jdbc:mysql://ip:port/master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
  3. spring.datasource.master.username=master
  4. spring.datasource.master.password=123456
  5. spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver
  6. spring.datasource.master.type=com.zaxxer.hikari.HikariDataSource
  7. spring.datasource.master.hikari.name=master
  8. spring.datasource.master.hikari.minimum-idle=5
  9. spring.datasource.master.hikari.idle-timeout=30
  10. spring.datasource.master.hikari.maximum-pool-size=10
  11. spring.datasource.master.hikari.auto-commit=true
  12. spring.datasource.master.hikari.pool-name=DatebookHikariCP
  13. spring.datasource.master.hikari.max-lifetime=1800000
  14. spring.datasource.master.hikari.connection-timeout=30000
  15. spring.datasource.master.hikari.connection-test-query=SELECT 1
  16. # 从库配置
  17. spring.datasource.slave.jdbc-url=jdbc:mysql://ip:port/slave?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
  18. spring.datasource.slave.username=root
  19. spring.datasource.slave.password=123456
  20. spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driver
  21. spring.datasource.slave.type=com.zaxxer.hikari.HikariDataSource
  22. spring.datasource.slave.hikari.name=master
  23. spring.datasource.slave.hikari.minimum-idle=5
  24. spring.datasource.slave.hikari.idle-timeout=30
  25. spring.datasource.slave.hikari.maximum-pool-size=10
  26. spring.datasource.slave.hikari.auto-commit=true
  27. spring.datasource.slave.hikari.pool-name=DatebookHikariCP
  28. spring.datasource.slave.hikari.max-lifetime=1800000
  29. spring.datasource.slave.hikari.connection-timeout=30000
  30. spring.datasource.slave.hikari.connection-test-query=SELECT 1

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

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

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

  1. @Intercepts({
  2. @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
  3. })
  4. @Component
  5. public class WriteInterceptor implements Interceptor {
  6. @Override
  7. public Object intercept(Invocation invocation) throws Throwable {
  8. // 获取 SQL 类型
  9. DataSourceTypeEnum dataSourceType = DataSourceContextHolder.getDataSourceType();
  10. if(DataSourceTypeEnum.SLAVE.equals(dataSourceType)) {
  11. DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.MASTER);
  12. }
  13. try {
  14. // 执行 SQL
  15. return invocation.proceed();
  16. } finally {
  17. // 恢复数据源 考虑到写入后可能会反查,后续都走主库
  18. // DataSourceContextHolder.setDataSourceType(dataSourceType);
  19. }
  20. }
  21. }

作者:京东健康 苏曼

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

原文链接:https://www.cnblogs.com/jingdongkeji/p/17828805.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号