zoukankan      html  css  js  c++  java
  • Spring Boot 实现读写分离,还有谁不会??

    来源:www.liaoxuefeng.com

    第一步:配置多数据源

    Spring Boot 基础就不介绍了,推荐下这个实战教程:
    https://github.com/javastacks/spring-boot-best-practice

    首先,我们在 SpringBoot 中配置两个数据源,其中第二个数据源是ro-datasource

    spring:
      datasource:
        jdbc-url: jdbc:mysql://localhost/test
        username: rw
        password: rw_password
        driver-class-name: com.mysql.jdbc.Driver
        hikari:
          pool-name: HikariCP
          auto-commit: false
          ...
      ro-datasource:
        jdbc-url: jdbc:mysql://localhost/test
        username: ro
        password: ro_password
        driver-class-name: com.mysql.jdbc.Driver
        hikari:
          pool-name: HikariCP
          auto-commit: false
          ...
    

    在开发环境下,没有必要配置主从数据库。只需要给数据库设置两个用户,一个rw具有读写权限,一个ro只有 SELECT 权限,这样就模拟了生产环境下对主从数据库的读写分离。

    在 SpringBoot 的配置代码中,我们初始化两个数据源:

    @SpringBootApplication
    public class MySpringBootApplication {
        /**
         * Master data source.
         */
        @Bean("masterDataSource")
        @ConfigurationProperties(prefix = "spring.datasource")
        DataSource masterDataSource() {
           logger.info("create master datasource...");
            return DataSourceBuilder.create().build();
        }
    
        /**
         * Slave (read only) data source.
         */
        @Bean("slaveDataSource")
        @ConfigurationProperties(prefix = "spring.ro-datasource")
        DataSource slaveDataSource() {
            logger.info("create slave datasource...");
            return DataSourceBuilder.create().build();
        }
    
        ...
    }
    

    第二步:编写 RoutingDataSource

    然后,我们用 Spring 内置的 RoutingDataSource,把两个真实的数据源代理为一个动态数据源:

    public class RoutingDataSource extends AbstractRoutingDataSource {
    
        @Override
        protected Object determineCurrentLookupKey() {
            return "masterDataSource";
        }
    }
    

    对这个RoutingDataSource,需要在 SpringBoot 中配置好并设置为主数据源:

    @SpringBootApplication
    public class MySpringBootApplication {
        @Bean
        @Primary
        DataSource primaryDataSource(
                @Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
                @Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource
        ) {
            logger.info("create routing datasource...");
            Map<Object, Object> map = new HashMap<>();
            map.put("masterDataSource", masterDataSource);
            map.put("slaveDataSource", slaveDataSource);
            RoutingDataSource routing = new RoutingDataSource();
            routing.setTargetDataSources(map);
            routing.setDefaultTargetDataSource(masterDataSource);
            return routing;
        }
        ...
    }
    

    现在,RoutingDataSource 配置好了,但是,路由的选择是写死的,即永远返回"masterDataSource"

    现在问题来了:如何存储动态选择的 key 以及在哪设置 key?

    在 Servlet 的线程模型中,使用 ThreadLocal 存储 key 最合适,因此,我们编写一个 RoutingDataSourceContext,来设置并动态存储 key:

    public class RoutingDataSourceContext implements AutoCloseable {
    
        // holds data source key in thread local:
        static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>();
    
        public static String getDataSourceRoutingKey() {
            String key = threadLocalDataSourceKey.get();
            return key == null ? "masterDataSource" : key;
        }
    
        public RoutingDataSourceContext(String key) {
            threadLocalDataSourceKey.set(key);
        }
    
        public void close() {
            threadLocalDataSourceKey.remove();
        }
    }
    

    然后,修改 RoutingDataSource,获取 key 的代码如下:

    public class RoutingDataSource extends AbstractRoutingDataSource {
        protected Object determineCurrentLookupKey() {
            return RoutingDataSourceContext.getDataSourceRoutingKey();
        }
    }
    

    这样,在某个地方,例如一个 Controller 的方法内部,就可以动态设置 DataSource 的 Key:

    @Controller
    public class MyController {
        @Get("/")
        public String index() {
            String key = "slaveDataSource";
            try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {
                // TODO:
                return "html... www.liaoxuefeng.com";
            }
        }
    }
    

    到此为止,我们已经成功实现了数据库的动态路由访问。

    这个方法是可行的,但是,需要读从数据库的地方,就需要加上一大段try (RoutingDataSourceContext ctx = ...) {}代码,使用起来十分不便。有没有方法可以简化呢?

    有!

    我们仔细想想,Spring 提供的声明式事务管理,就只需要一个@Transactional()注解,放在某个 Java 方法上,这个方法就自动具有了事务。

    我们也可以编写一个类似的@RoutingWith("slaveDataSource")注解,放到某个 Controller 的方法上,这个方法内部就自动选择了对应的数据源。代码看起来应该像这样:

    @Controller
    public class MyController {
        @Get("/")
        @RoutingWith("slaveDataSource")
        public String index() {
            return "html... www.liaoxuefeng.com";
        }
    }
    

    这样,完全不修改应用程序的逻辑,只在必要的地方加上注解,自动实现动态数据源切换,这个方法是最简单的。

    想要在应用程序中少写代码,我们就得多做一点底层工作:必须使用类似 Spring 实现声明式事务的机制,即用 AOP 实现动态数据源切换。

    实现这个功能也非常简单,编写一个RoutingAspect,利用 AspectJ 实现一个Around拦截:

    @Aspect
    @Component
    public class RoutingAspect {
        @Around("@annotation(routingWith)")
        public Object routingWithDataSource(ProceedingJoinPoint joinPoint, RoutingWith routingWith) throws Throwable {
            String key = routingWith.value();
            try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {
                return joinPoint.proceed();
            }
        }
    }
    

    注意方法的第二个参数RoutingWith是 Spring 传入的注解实例,我们根据注解的value()获取配置的 key。编译前需要添加一个 Maven 依赖:

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

    到此为止,我们就实现了用注解动态选择数据源的功能。最后一步重构是用字符串常量替换散落在各处的"masterDataSource""slaveDataSource"

    使用限制

    受 Servlet 线程模型的局限,动态数据源不能在一个请求内设定后再修改,也就是@RoutingWith不能嵌套。此外,@RoutingWith@Transactional混用时,要设定 AOP 的优先级。

    本文代码需要 SpringBoot 支持,JDK 1.8 编译并打开-parameters编译参数。

    近期热文推荐:

    1.1,000+ 道 Java面试题及答案整理(2021最新版)

    2.劲爆!Java 协程要来了。。。

    3.玩大了!Log4j 2.x 再爆雷。。。

    4.Spring Boot 2.6 正式发布,一大波新特性。。

    5.《Java开发手册(嵩山版)》最新发布,速速下载!

    觉得不错,别忘了随手点赞+转发哦!

  • 相关阅读:
    hdu4841 圆桌问题[STL vector]
    hdu1002 A + B Problem II[大数加法]
    hdu1501 Zipper[简单DP]
    C语言学习之结构体
    C++学习之从C到C++
    一键自动格式化你的代码
    C标准库string.h中几个常用函数的使用详解
    Jlink使用技巧系列教程索引
    Jlink使用技巧之合并烧写文件
    Jlink使用技巧之烧写SPI Flash存储芯片
  • 原文地址:https://www.cnblogs.com/javastack/p/15715772.html
Copyright © 2011-2022 走看看