当前位置:   article > 正文

druid+mybatis 多数据源及读写分离的处理_druiddatasource 读写分离

druiddatasource 读写分离

现如今复杂的业务系统,一般会将数据库按业务拆开,比如产品系统的数据库放在product db中,订单系统的数据库放在order db中...,然后,如果量大了,可能每个库还要考虑做读、写分离,以进一步提高系统性能,下面就来看看如何处理:

核心思路:配置多个数据源,然后利用RoutingDataSource结合AOP来动态切不同的库。

 存在一下几个问题:

  1、配置文件中,多数据源的配置节点如何设计?

  1. #yml文件中进行如下配置
  2. druid:
  3. type: com.alibaba.druid.pool.DruidDataSource
  4. study:
  5. master: #study库的主库名
  6. url: jdbc:mysql://localhost:3306/study?useSSL=false&characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true
  7. driver-class-name: com.mysql.cj.jdbc.Driver
  8. username: root
  9. password: 1234 #加密很好
  10. initial-size: 5
  11. min-idle: 1
  12. max-active: 20
  13. test-on-borrow: true
  14. slave: #study库的从库名
  15. url: jdbc:mysql://localhost:3306/study_slave?useSSL=false&characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true
  16. driver-class-name: com.mysql.cj.jdbc.Driver
  17. username: root
  18. password: A1b2c3@def.com
  19. initial-size: 5
  20. min-idle: 1
  21. max-active: 20
  22. test-on-borrow: true
  23. product:
  24. master: #product库的主库名
  25. url: jdbc:mysql://localhost:3306/product?useSSL=false&characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true
  26. driver-class-name: com.mysql.cj.jdbc.Driver
  27. username: root
  28. password: 1234 #加密
  29. initial-size: 5
  30. min-idle: 1
  31. max-active: 20
  32. test-on-borrow: true
  33. slave: #product库的从库
  34. url: jdbc:mysql://localhost:3306/product_slave?useSSL=false&characterEncoding=UTF-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&useUnicode=true
  35. driver-class-name: com.mysql.cj.jdbc.Driver
  36. username: root
  37. password: 1234
  38. initial-size: 5
  39. min-idle: 1
  40. max-active: 20
  41. test-on-borrow: true

上面的配置写法供参数,如果slave节点数要扩展,按这个格式,改造成slave1,slave2... 自行扩展。

  1. package com.aspire.sh.db.config;
  2. import com.aspire.sh.db.datasource.DbContextHolder;
  3. import com.aspire.sh.db.datasource.MasterSlaveRoutingDataSource;
  4. import org.springframework.beans.factory.annotation.Value;
  5. import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
  6. import org.springframework.boot.context.properties.ConfigurationProperties;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. import org.springframework.context.annotation.Primary;
  10. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  11. import org.springframework.transaction.annotation.EnableTransactionManagement;
  12. import javax.sql.DataSource;
  13. import java.util.HashMap;
  14. import java.util.Map;
  15. @Configuration
  16. @EnableTransactionManagement
  17. public class DataSourceConfiguration {
  18. @Value("${druid.type}")
  19. private Class<? extends DataSource> dataSourceType;
  20. @Bean(name = "studyMasterDataSource")
  21. @ConfigurationProperties(prefix = "druid.study.master")
  22. public DataSource studyMasterDataSource() {
  23. return DataSourceBuilder.create().type(dataSourceType).build();
  24. }
  25. @Bean(name = "studySlaveDataSource")
  26. @ConfigurationProperties(prefix = "druid.study.slave")
  27. public DataSource studySlaveDataSource1() {
  28. return DataSourceBuilder.create().type(dataSourceType).build();
  29. }
  30. @Bean(name = "productMasterDataSource")
  31. @ConfigurationProperties(prefix = "druid.product.master")
  32. public DataSource productMasterDataSource() {
  33. return DataSourceBuilder.create().type(dataSourceType).build();
  34. }
  35. @Bean(name = "productSlaveDataSource")
  36. @ConfigurationProperties(prefix = "druid.product.slave")
  37. public DataSource productSlaveDataSource1() {
  38. return DataSourceBuilder.create().type(dataSourceType).build();
  39. }
  40. @Bean(name = "dataSource")
  41. @Primary
  42. public AbstractRoutingDataSource dataSource() {
  43. MasterSlaveRoutingDataSource proxy = new MasterSlaveRoutingDataSource();
  44. Map<Object, Object> targetDataResources = new HashMap<>();
  45. targetDataResources.put(DbContextHolder.DbType.PRODUCT_MASTER, productMasterDataSource());
  46. targetDataResources.put(DbContextHolder.DbType.PRODUCT_SLAVE, productSlaveDataSource1());
  47. targetDataResources.put(DbContextHolder.DbType.STUDY_MASTER, studyMasterDataSource());
  48. targetDataResources.put(DbContextHolder.DbType.STUDY_SLAVE, studySlaveDataSource1());
  49. proxy.setDefaultTargetDataSource(productMasterDataSource());
  50. proxy.setTargetDataSources(targetDataResources);
  51. proxy.afterPropertiesSet();
  52. return proxy;
  53. }
  54. }

注:@Primary一定要在动态数据源上,否则事务回滚无效!

3、根据什么来切换db?

a、用约定的方法前缀,比如:get/query/list开头的约定为读从库,其它为主库,但是这样还要考虑不同业务库的切换(即:何时切换到product库,何时切换到order库,可以再用不同的Scanner来处理,略复杂)

b、用自定义注解来处理,比如 @ProductMaster注解,表示切换到product的master库,这样同时把业务库,以及主还是从,一次性解决了,推荐这种。

这里,我定义了4个注解,代表product,study二个库的主及从。

4、aop在哪里拦截,如何拦截?

service层和mapper层都可以拦截,推荐在服务层拦截,否则如果一个业务方法里,即有读又有写,还得考虑如果遇到事务,要考虑的东西更多。

当然,如果拦截特定的注解,就不用过多考虑在哪个层,只认注解就行(当然,注解还是建议打在服务层上)。

dubbo-starter的一个小坑:spring boot中,只有managed bean才能用aop拦截,而dubbo-starter中的@service注解不是spring中的注解(是阿里package下的自定义注解),生成的service provider实例,aop拦截不到,解决办法,再加一个注解让spring认识它.

Aop拦截类的参考代码如下:

  1. package com.aspire.sh.db.aspect;
  2. import com.aspire.sh.db.annotation.ProductMaster;
  3. import com.aspire.sh.db.annotation.ProductSlave;
  4. import com.aspire.sh.db.annotation.StudyMaster;
  5. import com.aspire.sh.db.annotation.StudySlave;
  6. import com.aspire.sh.db.datasource.DbContextHolder;
  7. import org.aspectj.lang.ProceedingJoinPoint;
  8. import org.aspectj.lang.annotation.Around;
  9. import org.aspectj.lang.annotation.Aspect;
  10. import org.slf4j.Logger;
  11. import org.slf4j.LoggerFactory;
  12. import org.springframework.core.Ordered;
  13. import org.springframework.stereotype.Component;
  14. @Aspect
  15. @Component
  16. public class MasterSlaveAspect implements Ordered {
  17. public static final Logger logger = LoggerFactory.getLogger(MasterSlaveAspect.class);
  18. /**
  19. * 切换到product主库
  20. *
  21. * @param proceedingJoinPoint
  22. * @param productMaster
  23. * @return
  24. * @throws Throwable
  25. */
  26. @Around("@annotation(productMaster)")
  27. public Object proceed(ProceedingJoinPoint proceedingJoinPoint, ProductMaster productMaster) throws Throwable {
  28. try {
  29. logger.info("set database connection to product-master only");
  30. DbContextHolder.setDbType(DbContextHolder.DbType.PRODUCT_MASTER);
  31. Object result = proceedingJoinPoint.proceed();
  32. return result;
  33. } finally {
  34. DbContextHolder.clearDbType();
  35. logger.info("restore database connection");
  36. }
  37. }
  38. /**
  39. * 切换到product从库
  40. *
  41. * @param proceedingJoinPoint
  42. * @param productSlave
  43. * @return
  44. * @throws Throwable
  45. */
  46. @Around("@annotation(productSlave)")
  47. public Object proceed(ProceedingJoinPoint proceedingJoinPoint, ProductSlave productSlave) throws Throwable {
  48. try {
  49. logger.info("set database connection to product-slave only");
  50. DbContextHolder.setDbType(DbContextHolder.DbType.PRODUCT_SLAVE);
  51. Object result = proceedingJoinPoint.proceed();
  52. return result;
  53. } finally {
  54. DbContextHolder.clearDbType();
  55. logger.info("restore database connection");
  56. }
  57. }
  58. /**
  59. * 切换到study主库
  60. *
  61. * @param proceedingJoinPoint
  62. * @param studyMaster
  63. * @return
  64. * @throws Throwable
  65. */
  66. @Around("@annotation(studyMaster)")
  67. public Object proceed(ProceedingJoinPoint proceedingJoinPoint, StudyMaster studyMaster) throws Throwable {
  68. try {
  69. logger.info("set database connection to study-master only");
  70. DbContextHolder.setDbType(DbContextHolder.DbType.STUDY_MASTER);
  71. Object result = proceedingJoinPoint.proceed();
  72. return result;
  73. } finally {
  74. DbContextHolder.clearDbType();
  75. logger.info("restore database connection");
  76. }
  77. }
  78. /**
  79. * 切换到study从库
  80. *
  81. * @param proceedingJoinPoint
  82. * @param studySlave
  83. * @return
  84. * @throws Throwable
  85. */
  86. @Around("@annotation(studySlave)")
  87. public Object proceed(ProceedingJoinPoint proceedingJoinPoint, StudySlave studySlave) throws Throwable {
  88. try {
  89. logger.info("set database connection to study-slave only");
  90. DbContextHolder.setDbType(DbContextHolder.DbType.STUDY_SLAVE);
  91. Object result = proceedingJoinPoint.proceed();
  92. return result;
  93. } finally {
  94. DbContextHolder.clearDbType();
  95. logger.info("restore database connection");
  96. }
  97. }
  98. @Override
  99. public int getOrder() {
  100. return 0;
  101. }
  102. }

5、其它事项

启用类上,一定要排除spring-boot自带的datasource配置,即:

  1. @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
  2. @EnableAspectJAutoProxy
  3. @ComponentScan("com.aspire.sh")
  4. @MapperScan(basePackages = "com.aspire.sh.dao.mapper")
  5. public class ServiceProvider {
  6. public static void main(String[] args) {
  7. SpringApplication.run(ServiceProvider.class, args);
  8. }
  9. }

6、日志中如何输出格式化且带参数值的sql?

是不是更友好!

方法:加一个mybtais的拦截器即可  (代码块在最前面)

这里面还用了hibernate的一个小工具,用于格式化sql(代码块在最前面)

接下来,把这个拦截器配置在mybatis-config.xml里

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  3. "http://mybatis.org/dtd/mybatis-3-config.dtd">
  4. <configuration>
  5. <settings>
  6. <setting name="cacheEnabled" value="true"/>
  7. </settings>
  8. <plugins>
  9. <plugin interceptor="com.aspire.sh.db.interceptor.MybatisInterceptor">
  10. </plugin>
  11. </plugins>
  12. </configuration>

最后在application.yml里指定mybatis-config.xml所在的路径:

注意:包路径可能会有些差异com.cnblogs.yjmyzz与com.aspire.sh二者取其一

实现方式二:

在pom.xml中增加相关依赖:

  1. <!-- aop -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-aop</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework</groupId>
  8. <artifactId>spring-context-support</artifactId>
  9. </dependency>
  10. <!-- mysql -->
  11. <dependency>
  12. <groupId>mysql</groupId>
  13. <artifactId>mysql-connector-java</artifactId>
  14. <scope>runtime</scope>
  15. </dependency>
  16. <dependency>
  17. <groupId>org.springframework</groupId>
  18. <artifactId>spring-jdbc</artifactId>
  19. </dependency>
  20. <!-- druid -->
  21. <dependency>
  22. <groupId>com.alibaba</groupId>
  23. <artifactId>druid-spring-boot-starter</artifactId>
  24. <version>1.1.10</version>
  25. </dependency>
  26. <!--如果不添加此依赖,自定义druid属性则会绑定失败-->
  27. <dependency>
  28. <groupId>log4j</groupId>
  29. <artifactId>log4j</artifactId>
  30. <version>1.2.17</version>
  31. </dependency>
  32. <!-- mybatis -->
  33. <dependency>
  34. <groupId>org.mybatis.spring.boot</groupId>
  35. <artifactId>mybatis-spring-boot-starter</artifactId>
  36. <version>1.3.2</version>
  37. </dependency>
  38. <!-- slf4j -->
  39. <dependency>
  40. <groupId>org.slf4j</groupId>
  41. <artifactId>slf4j-api</artifactId>
  42. </dependency>

2、配置application.yml 

  1. server:
  2. port: 8818
  3. spring:
  4. application:
  5. name: read-write-separationp
  6. aop:
  7. proxy-target-class: true
  8. auto: true
  9. datasource:
  10. type: com.alibaba.druid.pool.DruidDataSourceC3P0Adapter
  11. druid:
  12. master:
  13. url: jdbc:mysql://207.148.33.32:3306/captain?useSSL=true&characterEncoding=UTF-8&serverTimezone=UTC
  14. username: captainLii
  15. password: Captain@1689
  16. slave:
  17. url: jdbc:mysql://45.32.120.84:3306/captain?useSSL=true&characterEncoding=UTF-8&serverTimezone=UTC
  18. username: captainLii
  19. password: Captain@1689
  20. # 配置初始化大小(默认0)、最小、最大(默认8
  21. initial-size: 1
  22. min-idle: 1
  23. max-active: 20
  24. # 配置获取连接等待超时的时间
  25. max-wait: 60000
  26. # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大。 默认为false
  27. pool-prepared-statements: true
  28. # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true
  29. max-open-prepared-statements: 20
  30. # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
  31. time-between-eviction-runs-millis: 60000
  32. # 配置一个连接在池中最小和最大生存的时间,单位是毫秒
  33. min-evictable-idle-time-millis: 300000
  34. max-evictable-idle-time-millis: 900000
  35. # 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'
  36. # 如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
  37. validation-query: SELECT 'X'
  38. # 申请连接时执行validationQuery检测连接是否有效 默认为true
  39. test-on-borrow: true
  40. # 归还连接时执行validationQuery检测连接是否有效 默认为false
  41. test-on-return: false
  42. # 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
  43. test-while-idle: true
  44. # Mybatis
  45. mybatis:
  46. mapper-locations: classpath:mapping/*.xml
  47. type-aliases-package: com.captain.readwriteseparation.entity

3、 定义数据源枚举类

  1. package com.captain.readwriteseparation.dbconfig;
  2. /**
  3. * @author
  4. * @description 数据源枚举
  5. */
  6. public enum DataSourceTypeEnum {
  7. master("master"), slave("slave");
  8. private String value;
  9. DataSourceTypeEnum(String value) {
  10. this.value = value;
  11. }
  12. public String getValue() {
  13. return value;
  14. }
  15. }

4、设置获取数据源

  1. package com.aspire.sh.dbconfig;
  2. /**
  3. * @description 设置获取数据源
  4. */
  5. public class DataSourceHolder {
  6. private static final ThreadLocal contextHolder = new ThreadLocal<>();
  7. /**
  8. * 设置数据源
  9. *
  10. * @param dbTypeEnum
  11. */
  12. public static void setDbType(DataSourceTypeEnum dbTypeEnum) {
  13. contextHolder.set(dbTypeEnum.getValue());
  14. }
  15. /**
  16. * 取得当前数据源
  17. *
  18. * @return
  19. */
  20. public static String getDbType() {
  21. return (String) contextHolder.get();
  22. }
  23. /**
  24. * 清除上下文数据
  25. */
  26. public static void clearDbType() {
  27. contextHolder.remove();
  28. }
  29. }

 5、数据源切换(切入点和切面)

  1. package com.captain.readwriteseparation.dbconfig;
  2. import org.aspectj.lang.annotation.Aspect;
  3. import org.aspectj.lang.annotation.Before;
  4. import org.slf4j.Logger;
  5. import org.slf4j.LoggerFactory;
  6. import org.springframework.stereotype.Component;
  7. /**
  8. * @author splendor.s
  9. * @description 数据源切换(切入点和切面)
  10. */
  11. @Aspect
  12. @Component
  13. public class DataSourceAop {
  14. static Logger logger = LoggerFactory.getLogger(DataSourceAop.class);
  15. @Before("execution(* com.captain.readwriteseparation.mapper.*.insert*(..)) || execution(* com.captain.readwriteseparation.mapper.*.update*(..)) || execution(* com.captain.readwriteseparation.mapper.*.delete*(..))")
  16. public void setWriteDataSourceType() {
  17. DataSourceHolder.setDbType(DataSourceTypeEnum.master);
  18. logger.info("change -------- write ------------");
  19. }
  20. @Before("execution(* com.captain.readwriteseparation.mapper.*.select*(..)) || execution(* com.captain.readwriteseparation.mapper.*.count*(..))")
  21. public void setReadDataSourceType() {
  22. DataSourceHolder.setDbType(DataSourceTypeEnum.slave);
  23. logger.info("change -------- read ------------");
  24. }
  25. }

6、动态数据源决策

  1. package com.captain.readwriteseparation.dbconfig;
  2. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  3. /**
  4. * @authorsplendor.s
  5. * @description 动态数据源决策
  6. */
  7. public class DynamicDataSource extends AbstractRoutingDataSource {
  8. @Override
  9. protected Object determineCurrentLookupKey() {
  10. return DataSourceHolder.getDbType();
  11. }
  12. }

7、数据库(源)配置

  1. package com.aspire.sh.dbconfig;
  2. import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
  3. import com.alibaba.druid.support.http.StatViewServlet;
  4. import com.alibaba.druid.support.http.WebStatFilter;
  5. import org.slf4j.Logger;
  6. import org.slf4j.LoggerFactory;
  7. import org.springframework.beans.factory.annotation.Qualifier;
  8. import org.springframework.beans.factory.annotation.Value;
  9. import org.springframework.boot.context.properties.ConfigurationProperties;
  10. import org.springframework.boot.web.servlet.FilterRegistrationBean;
  11. import org.springframework.boot.web.servlet.ServletRegistrationBean;
  12. import org.springframework.context.annotation.Bean;
  13. import org.springframework.context.annotation.Configuration;
  14. import org.springframework.context.annotation.Primary;
  15. import javax.sql.DataSource;
  16. import java.util.HashMap;
  17. import java.util.Map;
  18. /**
  19. * @author splendor.s
  20. * @description 数据库(源)配置
  21. */
  22. @Configuration
  23. public class DruidDataSourceConfig {
  24. static Logger logger = LoggerFactory.getLogger(DruidDataSourceConfig.class);
  25. @Value("${spring.datasource.type}")
  26. private Class<? extends DataSource> dataSourceType;
  27. @Bean
  28. public ServletRegistrationBean staViewServlet() {
  29. ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
  30. Map<String, String> initParams = new HashMap<>();
  31. //设置servlet初始化参数
  32. initParams.put("loginUsername", "admin");//登陆名
  33. initParams.put("loginPassword", "123456");//密码
  34. initParams.put("allow", "");//默认就是允许所有访问
  35. initParams.put("deny", "192.168.10.17");//拒绝相对应的id访问
  36. //加载到容器中
  37. bean.setInitParameters(initParams);
  38. return bean;
  39. }
  40. @Bean
  41. public FilterRegistrationBean filterRegistrationBean() {
  42. FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter());
  43. filterRegistrationBean.addUrlPatterns("/*");
  44. filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico," + "/druid/*");
  45. return filterRegistrationBean;
  46. }
  47. @Bean(name = "master")
  48. @ConfigurationProperties(prefix = "spring.datasource.druid.master")
  49. public DataSource master() {
  50. logger.info("-------------------- master init ---------------------");
  51. return DruidDataSourceBuilder.create().build();
  52. }
  53. @Bean(name = "slave")
  54. @ConfigurationProperties(prefix = "spring.datasource.druid.slave")
  55. public DataSource slaveOne() {
  56. logger.info("-------------------- slave init ---------------------");
  57. return DruidDataSourceBuilder.create().build();
  58. }
  59. // slave 多个时,可进行负载(另行处理)
  60. @Bean
  61. @Primary
  62. public DataSource multipleDataSource(@Qualifier("master") DataSource master,
  63. @Qualifier("slave") DataSource slave) {
  64. DynamicDataSource dynamicDataSource = new DynamicDataSource();
  65. Map<Object, Object> targetDataSources = new HashMap<>();
  66. targetDataSources.put(DataSourceTypeEnum.master.getValue(), master);
  67. targetDataSources.put(DataSourceTypeEnum.slave.getValue(), slave);
  68. dynamicDataSource.setTargetDataSources(targetDataSources);
  69. dynamicDataSource.setDefaultTargetDataSource(slave);
  70. return dynamicDataSource;
  71. }
  72. }

8、配置事务管理

  1. package com.aspire.sh.dbconfig;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
  5. import org.springframework.context.annotation.Bean;
  6. import org.springframework.context.annotation.Configuration;
  7. import org.springframework.transaction.annotation.EnableTransactionManagement;
  8. import javax.sql.DataSource;
  9. import javax.annotation.Resource;
  10. /**
  11. * @author splendor.s
  12. * @description 事务控制
  13. */
  14. @Configuration
  15. @EnableTransactionManagement
  16. public class DataSourceTransactionManager extends DataSourceTransactionManagerAutoConfiguration {
  17. static Logger logger = LoggerFactory.getLogger(DataSourceTransactionManager.class);
  18. @Resource(name = "master")
  19. private DataSource dataSource;
  20. /**
  21. * 自定义事务
  22. * MyBatis自动参与到spring事务管理中,无需额外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的数据源与DataSourceTransactionManager引用的数据源一致即可,否则事务管理会不起作用。
  23. *
  24. * @return
  25. */
  26. @Bean(name = "transactionManager")
  27. public org.springframework.jdbc.datasource.DataSourceTransactionManager transactionManagers() {
  28. logger.info("-------------------- transactionManager init ---------------------");
  29. return new org.springframework.jdbc.datasource.DataSourceTransactionManager(dataSource);
  30. }
  31. }

实现方式三:

这里配置了4个数据源,1个master,2两个slave,1个路由数据源。前3个数据源都是为了生成第4个数据源,而且后续我们只用这最后一个路由数据源。

  1. /**
  2. * 关于数据源配置,参考SpringBoot官方文档第79章《Data Access》
  3. * 79. Data Access
  4. * 79.1 Configure a Custom DataSource
  5. * 79.2 Configure Two DataSources
  6. * 这里配置了4个数据源,1个master,2两个slave,1个路由数据源。前3个数据源都是为了生成第4个数据源,而且后续我们只用这最后一个路由数据源。
  7. */
  8. @Configuration
  9. public class DataSourceConfig {
  10. @Bean
  11. @ConfigurationProperties("spring.datasource.master")
  12. public DataSource masterDataSource() {
  13. return DataSourceBuilder.create().build();
  14. }
  15. @Bean
  16. @ConfigurationProperties("spring.datasource.slave1")
  17. public DataSource slave1DataSource() {
  18. return DataSourceBuilder.create().build();
  19. }
  20. @Bean
  21. @ConfigurationProperties("spring.datasource.slave2")
  22. public DataSource slave2DataSource() {
  23. return DataSourceBuilder.create().build();
  24. }
  25. @Bean
  26. public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
  27. @Qualifier("slave1DataSource") DataSource slave1DataSource,
  28. @Qualifier("slave2DataSource") DataSource slave2DataSource) {
  29. Map<Object, Object> targetDataSources = new HashMap<>();
  30. targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
  31. targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource);
  32. targetDataSources.put(DBTypeEnum.SLAVE2, slave2DataSource);
  33. MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
  34. myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
  35. myRoutingDataSource.setTargetDataSources(targetDataSources);
  36. return myRoutingDataSource;
  37. }
  38. }

配置Mybatis指定数据源

  1. /**
  2. * 由于Spring容器中现在有4个数据源,所以我们需要为事务管理器和MyBatis手动指定一个明确的数据源。
  3. */
  4. @EnableTransactionManagement
  5. @Configuration
  6. public class MyBatisConfig {
  7. @Resource(name = "myRoutingDataSource")
  8. private DataSource myRoutingDataSource;
  9. @Bean
  10. public SqlSessionFactory sqlSessionFactory() throws Exception {
  11. SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
  12. sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
  13. sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
  14. return sqlSessionFactoryBean.getObject();
  15. }
  16. @Bean
  17. public PlatformTransactionManager platformTransactionManager() {
  18. return new DataSourceTransactionManager(myRoutingDataSource);
  19. }
  20. }

定义一个枚举类来代表这三个数据源

  1. /**
  2. * 定义一个枚举来代表这三个数据源
  3. */
  4. public enum DBTypeEnum {
  5. MASTER, SLAVE1, SLAVE2;
  6. }

通过ThreadLocal将数据源绑定到每个线程上下文中

  1. /**
  2. * 通过ThreadLocal将数据源设置到每个线程上下文中
  3. */
  4. public class DBContextHolder {
  5. private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
  6. private static final AtomicInteger counter = new AtomicInteger(-1);
  7. public static void set(DBTypeEnum dbType) {
  8. contextHolder.set(dbType);
  9. }
  10. public static DBTypeEnum get() {
  11. return contextHolder.get();
  12. }
  13. public static void master() {
  14. set(DBTypeEnum.MASTER);
  15. System.out.println("切换到master");
  16. }
  17. public static void slave() {
  18. // 轮询
  19. int index = counter.getAndIncrement() % 2;
  20. if (counter.get() > 9999) {
  21. counter.set(-1);
  22. }
  23. if (index == 0) {
  24. set(DBTypeEnum.SLAVE1);
  25. System.out.println("切换到slave1");
  26. }else {
  27. set(DBTypeEnum.SLAVE2);
  28. System.out.println("切换到slave2");
  29. }
  30. }
  31. }

通过Aop的前置通知来设置要使用的路由key(数据源)

  1. /**
  2. * 默认情况下,所有的查询都走从库,插入/修改/删除走主库。我们通过方法名来区分操作类型(CRUD)
  3. *
  4. */
  5. @Aspect
  6. @Component
  7. public class DataSourceAop {
  8. @Pointcut("!@annotation(huaxin.annotation.Master) " +
  9. "&& (execution(* huaxin.service.*.select*(..)) " +
  10. "|| execution(* huaxin.service..*.find*(..)))")
  11. public void readPointcut() {
  12. }
  13. @Pointcut("@annotation(huaxin.annotation.Master) " +
  14. "|| execution(* huaxin.service..*.save*(..)) " +
  15. "|| execution(* huaxin.service..*.add*(..)) " +
  16. "|| execution(* huaxin.service..*.update*(..)) " +
  17. "|| execution(* huaxin.service..*.edit*(..)) " +
  18. "|| execution(* huaxin..*.delete*(..)) " +
  19. "|| execution(* huaxin..*.remove*(..))")
  20. public void writePointcut() {
  21. }
  22. @Before("readPointcut()")
  23. public void read() {
  24. DBContextHolder.slave();
  25. }
  26. @Before("writePointcut()")
  27. public void write() {
  28. DBContextHolder.master();
  29. }
  30. /**
  31. * 另一种写法:if...else... 判断哪些需要读从数据库,其余的走主数据库
  32. */
  33. // @Before("execution(* com.cjs.example.service.impl.*.*(..))")
  34. // public void before(JoinPoint jp) {
  35. // String methodName = jp.getSignature().getName();
  36. //
  37. // if (StringUtils.startsWithAny(methodName, "get", "select", "find")) {
  38. // DBContextHolder.slave();
  39. // }else {
  40. // DBContextHolder.master();
  41. // }
  42. // }
  43. }

获取当前线程上绑定的路由key

  1. /**
  2. * 获取路由key
  3. */
  4. public class MyRoutingDataSource extends AbstractRoutingDataSource {
  5. @Nullable
  6. @Override
  7. protected Object determineCurrentLookupKey() {
  8. return DBContextHolder.get();
  9. }
  10. }

特殊情况下我们需要强制读主库,针对这种情况,我们定义一个注解,用该注解标注的就读主库

public @interface Master {}

给查询所有添加@Master注解

启动入口加上@MapperScan("xx.xx")

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读