概述:

  2018,在平(tou)静(lan)了一段时间后,开始找点事情来做。这一次准备开发一个个人博客,在开发过程之中完善一下自己的技术。本系列博客只会提出一些比较有价值的技术思路,不会像写流水账一样记录开发过程。

  技术栈方面,会采用Spring Boot 2.0 作为底层框架,主要为了后续能够接入Spring Cloud 进行学习拓展。并且Spring Boot 2.0基于Spring5,也可以提前预习一些Spring5的新特性。后续技术会在相应博客中提出。

  项目GitHub地址:https://github.com/jaycekon/Spring-Blog

  介绍一下目录结构:

  • Spring-Blog( Parent 项目)
  • Spring-Blog-common( Util 模块)
  • Spring-Blog-business(Repository模块)
  • Spring-Blog-api (Web 模块)
  • Spring-Blog-webflux (基于Spring Boot 2.0Web模块)

  为了让各位朋友能够更好理解这一模块的内容,演示代码将存放在Spring Boot 项目下:

  Github 地址:https://github.com/jaycekon/SpringBoot

1、DataSource

    在开始讲解前,我们需要先构建后我们的运行环境。Spring Boot 引入 Mybatis 的教程 可以参考 传送门 。这里我们不细述了,首先来看一下我们的目录结构:

  有使用过Spring Boot 的童鞋应该清楚,当我们在application.properties 配置好了我们的数据库连接信息后,Spring Boot 将会帮我们自动装载好 DataSource 。但如果我们需要进行读写分离操作是,如何配置自己的数据源,是我们必须掌握的。

  首先我们来看一下配置文件中的信息:

  1. spring.datasource.url=jdbc:mysql://localhost:3306/charles_blog2
  2. spring.datasource.username=root
  3. spring.datasource.password=root
  4. spring.datasource.driver-class-name=com.mysql.jdbc.Driver
  5.  
  6. #别名扫描目录
  7. mybatis.type-aliases-package=com.jaycekon.demo.model
  8. #Mapper.xml扫描目录
  9. mybatis.mapper-locations=classpath:mybatis-mappers/*.xml
  10.  
  11. #tkmapper 帮助工具
  12. mapper.mappers=com.jaycekon.demo.MyMapper
  13. mapper.not-empty=false
  14. mapper.identity=MYSQL

  

1.1 DataSourceBuilder

      我们首先来看一下使用 DataSourceBuilder 来构建出DataSource:

  1. @Configuration
  2. @MapperScan("com.jaycekon.demo.mapper")
  3. @EnableTransactionManagement
  4. public class SpringJDBCDataSource {
  5.  
  6. /**
  7. * 通过Spring JDBC 快速创建 DataSource
  8. * 参数格式
  9. * spring.datasource.master.jdbcurl=jdbc:mysql://localhost:3306/charles_blog
  10. * spring.datasource.master.username=root
  11. * spring.datasource.master.password=root
  12. * spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver
  13. *
  14. * @return DataSource
  15. */
  16. @Bean
  17. @ConfigurationProperties(prefix = "spring.datasource.master")
  18. public DataSource dataSource() {
  19. return DataSourceBuilder.create().build();
  20. }
  21. }

      从代码中我们可以看出,使用DataSourceBuilder 构建DataSource 的方法非常简单,但是需要注意的是:

  •     DataSourceBuilder 只能自动识别配置文件中的 jdbcurl,username,password,driver-class-name等命名,因此我们需要在方法体上加上 @ ConfigurationProperties 注解。
  •          数据库连接地址变量名需要使用 jdbcurl
  •               数据库连接池使用 com.zaxxer.hikari.HikariDataSource

    执行单元测试时,我们可以看到 DataSource 创建以及关闭的过程。

1.2 DruidDataSource

    除了使用上述的构建方法外,我们可以选择使用阿里提供的 Druid 数据库连接池创建 DataSource

  1. @Configuration
  2. @EnableTransactionManagement
  3. public class DruidDataSourceConfig {
  4.  
  5. @Autowired
  6. private DataSourceProperties properties;
  7.  
  8. @Bean
  9. public DataSource dataSoucre() throws Exception {
  10. DruidDataSource dataSource = new DruidDataSource();
  11. dataSource.setUrl(properties.getUrl());
  12. dataSource.setDriverClassName(properties.getDriverClassName());
  13. dataSource.setUsername(properties.getUsername());
  14. dataSource.setPassword(properties.getPassword());
  15. dataSource.setInitialSize(5);
  16. dataSource.setMinIdle(5);
  17. dataSource.setMaxActive(100);
  18. dataSource.setMaxWait(60000);
  19. dataSource.setTimeBetweenEvictionRunsMillis(60000);
  20. dataSource.setMinEvictableIdleTimeMillis(300000);
  21. dataSource.setValidationQuery("SELECT 'x'");
  22. dataSource.setTestWhileIdle(true);
  23. dataSource.setTestOnBorrow(false);
  24. dataSource.setTestOnReturn(false);
  25. dataSource.setPoolPreparedStatements(true);
  26. dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
  27. dataSource.setFilters("stat,wall");
  28. return dataSource;
  29. }
  30. }

    

    使用 DruidDataSource  作为数据库连接池可能看起来会比较麻烦,但是换一个角度来说,这个更加可控。我们可以通过  DataSourceProperties 来获取 application.properties 中的配置文件:

  1. spring.datasource.url=jdbc:mysql://localhost:3306/charles_blog2
  2. spring.datasource.username=root
  3. spring.datasource.password=root
  4. spring.datasource.driver-class-name=com.mysql.jdbc.Driver

    需要注意的是,DataSourceProperties 读取的配置文件 前缀是 spring.datasource ,我们可以进入到 DataSourceProperties 的源码中观察:

  1. @ConfigurationProperties(prefix = "spring.datasource")
  2. public class DataSourceProperties
  3. implements BeanClassLoaderAware, EnvironmentAware, InitializingBean

    可以看到,在源码中已经默认标注了前缀的格式。

    除了使用 DataSourceProperties 来获取配置文件 我们还可以使用通用的环境变量读取类:

  1. @Autowired
  2. private Environment env;
      
      
  1.     env.getProperty("spring.datasource.write")

 

  

2、多数据源配置

    配置多数据源主要需要以下几个步骤:

    2.1 DatabaseType 数据源名称

        这里直接使用枚举类型区分,读数据源和写数据源

  1. public enum DatabaseType {
  2. master("write"), slave("read");
  3.  
  4. DatabaseType(String name) {
  5. this.name = name;
  6. }
  7.  
  8. private String name;
  9.  
  10. public String getName() {
  11. return name;
  12. }
  13.  
  14. public void setName(String name) {
  15. this.name = name;
  16. }
  17.  
  18. @Override
  19. public String toString() {
  20. return "DatabaseType{" +
  21. "name='" + name + '\'' +
  22. '}';
  23. }
  24. }

    2.2 DatabaseContextHolder

      该类主要用于记录当前线程使用的数据源,使用 ThreadLocal 进行记录数据

  1. public class DatabaseContextHolder {
  2. private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>();
  3.  
  4. public static void setDatabaseType(DatabaseType type) {
  5. contextHolder.set(type);
  6. }
  7.  
  8. public static DatabaseType getDatabaseType() {
  9. return contextHolder.get();
  10. }
  11. }

    2.3 DynamicDataSource

     该类继承 AbstractRoutingDataSource 用于管理 我们的数据源,主要实现了 determineCurrentLookupKey 方法。

     后续细述这个类是如何进行多数据源管理的。

  1. public class DynamicDataSource extends AbstractRoutingDataSource {
  2.  
  3. @Nullable
  4. @Override
  5. protected Object determineCurrentLookupKey() {
  6. DatabaseType type = DatabaseContextHolder.getDatabaseType();
  7. logger.info("====================dataSource ==========" + type);
  8. return type;
  9. }
  10.  
  11. }

    2.4 DataSourceConfig

     最后一步就是配置我们的数据源,将数据源放置到 DynamicDataSource 中:

  1. @Configuration
  2. @MapperScan("com.jaycekon.demo.mapper")
  3. @EnableTransactionManagement
  4. public class DataSourceConfig {
  5.  
  6. @Autowired
  7. private DataSourceProperties properties;
  8.  
  9. /**
  10. * 通过Spring JDBC 快速创建 DataSource
  11. * 参数格式
  12. * spring.datasource.master.jdbcurl=jdbc:mysql://localhost:3306/charles_blog
  13. * spring.datasource.master.username=root
  14. * spring.datasource.master.password=root
  15. * spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver
  16. *
  17. * @return DataSource
  18. */
  19. @Bean(name = "masterDataSource")
  20. @Qualifier("masterDataSource")
  21. @ConfigurationProperties(prefix = "spring.datasource.master")
  22. public DataSource masterDataSource() {
  23. return DataSourceBuilder.create().build();
  24. }
  25.  
  26. /**
  27. * 手动创建DruidDataSource,通过DataSourceProperties 读取配置
  28. * 参数格式
  29. * spring.datasource.url=jdbc:mysql://localhost:3306/charles_blog
  30. * spring.datasource.username=root
  31. * spring.datasource.password=root
  32. * spring.datasource.driver-class-name=com.mysql.jdbc.Driver
  33. *
  34. * @return DataSource
  35. * @throws SQLException
  36. */
  37. @Bean(name = "slaveDataSource")
  38. @Qualifier("slaveDataSource")
  39. public DataSource slaveDataSource() throws SQLException {
  40. DruidDataSource dataSource = new DruidDataSource();
  41. dataSource.setUrl(properties.getUrl());
  42. dataSource.setDriverClassName(properties.getDriverClassName());
  43. dataSource.setUsername(properties.getUsername());
  44. dataSource.setPassword(properties.getPassword());
  45. dataSource.setInitialSize(5);
  46. dataSource.setMinIdle(5);
  47. dataSource.setMaxActive(100);
  48. dataSource.setMaxWait(60000);
  49. dataSource.setTimeBetweenEvictionRunsMillis(60000);
  50. dataSource.setMinEvictableIdleTimeMillis(300000);
  51. dataSource.setValidationQuery("SELECT 'x'");
  52. dataSource.setTestWhileIdle(true);
  53. dataSource.setTestOnBorrow(false);
  54. dataSource.setTestOnReturn(false);
  55. dataSource.setPoolPreparedStatements(true);
  56. dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
  57. dataSource.setFilters("stat,wall");
  58. return dataSource;
  59. }
  60.  
  61. /**
  62. * 构造多数据源连接池
  63. * Master 数据源连接池采用 HikariDataSource
  64. * Slave 数据源连接池采用 DruidDataSource
  65. * @param master
  66. * @param slave
  67. * @return
  68. */
  69. @Bean
  70. @Primary
  71. public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource master,
  72. @Qualifier("slaveDataSource") DataSource slave) {
  73. Map<Object, Object> targetDataSources = new HashMap<>();
  74. targetDataSources.put(DatabaseType.master, master);
  75. targetDataSources.put(DatabaseType.slave, slave);
  76.  
  77. DynamicDataSource dataSource = new DynamicDataSource();
  78. dataSource.setTargetDataSources(targetDataSources);// 该方法是AbstractRoutingDataSource的方法
  79. dataSource.setDefaultTargetDataSource(slave);// 默认的datasource设置为myTestDbDataSourcereturn dataSource;
  80. }
  81.  
  82. @Bean
  83. public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource myTestDbDataSource,
  84. @Qualifier("slaveDataSource") DataSource myTestDb2DataSource) throws Exception {
  85. SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
  86. fb.setDataSource(this.dataSource(myTestDbDataSource, myTestDb2DataSource));
  87. fb.setTypeAliasesPackage(env.getProperty("mybatis.type-aliases-package"));
  88. fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapper-locations")));
  89. return fb.getObject();
  90. }
  91. }

    上述代码块比较长,我们来解析一下:

    1. masterDataSource slaveDataSource 主要是用来创建数据源的,这里分别使用了 hikaridatasource druidDataSource 作为数据源
    1. DynamicDataSource 方法体中,我们主要是将两个数据源都放到 DynamicDataSource 中进行统一管理
    1. SqlSessionFactory 方法则是将所有数据源(DynamicDataSource )统一管理

    2.5 UserMapperTest

      接下来我们来简单观察一下 DataSource 的创建过程:

      首先我们可以看到我们的两个数据源以及构建好了,分别使用的是HikariDataSource 和 DruidDataSource,然后我们会将两个数据源放入到 targetDataSource 中,并且这里讲我们的 slave 作为默认数据源 defaultTargetDataSource

      

    然后到获取数据源这一块:

    主要是从 AbstractRoutingDataSource 这个类中的 determineTargetDataSource( ) 方法中进行判断,这里会调用到我们再 DynamicDataSource 中的方法, 去判断需要使用哪一个数据源。如果没有设置数据源,将采用默认数据源,就是我们刚才设置的DruidDataSource 数据源。

      在最后的代码运行结果中:

      我们可以看到确实是使用了我们设置的默认数据源。

3、读写分离

      在经历了千山万水后,终于来到我们的读写分离模块了,首先我们需要添加一些我们的配置信息:

  1. spring.datasource.read = get,select,count,list,query
  2. spring.datasource.write = add,create,update,delete,remove,insert

      这两个变量主要用于切面判断中,区分哪一些部分是需要使用 读数据源,哪些是需要使用写的。

    3.1 DynamicDataSource 修改

  1. public class DynamicDataSource extends AbstractRoutingDataSource {
  2.  
  3. static final Map<DatabaseType, List<String>> METHOD_TYPE_MAP = new HashMap<>();
  4.  
  5. @Nullable
  6. @Override
  7. protected Object determineCurrentLookupKey() {
  8. DatabaseType type = DatabaseContextHolder.getDatabaseType();
  9. logger.info("====================dataSource ==========" + type);
  10. return type;
  11. }
  12.  
  13. void setMethodType(DatabaseType type, String content) {
  14. List<String> list = Arrays.asList(content.split(","));
  15. METHOD_TYPE_MAP.put(type, list);
  16. }
  17. }

    在这里我们需要添加一个Map 进行记录一些读写的前缀信息。

    3.2 DataSourceConfig 修改

      在DataSourceConfig 中,我们再设置DynamicDataSource 的时候,将前缀信息设置进去。

  1. @Bean
  2. @Primary
  3. public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource master,
  4. @Qualifier("slaveDataSource") DataSource slave) {
  5. Map<Object, Object> targetDataSources = new HashMap<>();
  6. targetDataSources.put(DatabaseType.master, master);
  7. targetDataSources.put(DatabaseType.slave, slave);
  8.  
  9. DynamicDataSource dataSource = new DynamicDataSource();
  10. dataSource.setTargetDataSources(targetDataSources);// 该方法是AbstractRoutingDataSource的方法
  11. dataSource.setDefaultTargetDataSource(slave);// 默认的datasource设置为myTestDbDataSource
  12.  
  13. String read = env.getProperty("spring.datasource.read");
  14. dataSource.setMethodType(DatabaseType.slave, read);
  15.  
  16. String write = env.getProperty("spring.datasource.write");
  17. dataSource.setMethodType(DatabaseType.master, write);
  18.  
  19. return dataSource;
  20. }

    3.3 DataSourceAspect

      在配置好读写的方法前缀后,我们需要配置一个切面,监听在进入Mapper 方法前将数据源设置好:

      主要的操作点在于  DatabaseContextHolder.setDatabaseType(type); 结合我们上面多数据源的获取数据源方法,这里就是我们设置读或写数据源的关键了。

  1. @Aspect
  2. @Component
  3. @EnableAspectJAutoProxy(proxyTargetClass = true)
  4. public class DataSourceAspect {
  5. private static Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
  6.  
  7. @Pointcut("execution(* com.jaycekon.demo.mapper.*.*(..))")
  8. public void aspect() {
  9.  
  10. }
  11.  
  12. @Before("aspect()")
  13. public void before(JoinPoint point) {
  14. String className = point.getTarget().getClass().getName();
  15. String method = point.getSignature().getName();
  16. String args = StringUtils.join(point.getArgs(), ",");
  17. logger.info("className:{}, method:{}, args:{} ", className, method, args);
  18. try {
  19. for (DatabaseType type : DatabaseType.values()) {
  20. List<String> values = DynamicDataSource.METHOD_TYPE_MAP.get(type);
  21. for (String key : values) {
  22. if (method.startsWith(key)) {
  23. logger.info(">>{} 方法使用的数据源为:{}<<", method, key);
  24. DatabaseContextHolder.setDatabaseType(type);
  25. DatabaseType types = DatabaseContextHolder.getDatabaseType();
  26. logger.info(">>{}方法使用的数据源为:{}<<", method, types);
  27. }
  28. }
  29. }
  30. } catch (Exception e) {
  31. logger.error(e.getMessage(), e);
  32. }
  33. }
  34. }

    

   

    3.4 UserMapperTest

      方法启动后,先进入切面中,根据methodName 设置数据源类型。

      然后进入到determineTargetDataSource 方法中 获取到数据源:

        运行结果:

4、写在最后

  希望看完后觉得有帮助的朋友,帮博主到github 上面点个Start 或者 fork

  Spring-Blog 项目GitHub地址:https://github.com/jaycekon/Spring-Blog

  示例代码 Github 地址:https://github.com/jaycekon/SpringBoot

  我的博客即将搬运同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan

Spring-Blog:个人博客(一)-Mybatis 读写分离的更多相关文章

  1. Mybatis MapperScannerConfigurer 自动扫描 将Mapper接口生成代理注入到Spring - 大新博客 - 推酷 - 360安全浏览器 7.1

    Mybatis MapperScannerConfigurer 自动扫描 将Mapper接口生成代理注入到Spring - 大新博客 时间 2014-02-11 21:08:00  博客园-所有随笔区 ...

  2. [转]软件测试 Top 120 Blog (博客)

    [转]软件测试 Top 120 Blog (博客) 2015-06-08 转自:    软件测试 Top 120 Blog (博客) # Site Author Memo DevelopSense M ...

  3. 关于Spring Boot的博客集合

    掘金: 关于Spring Boot的博客集合 CSDN: Spring Boot教程 掘金: SpringBoot2 简书: Spring Boot 核心技术 天码营 Spring Data JPA: ...

  4. mybatis读写分离

    mybatis读写分离实现方式有很多种,当然如果没有太过复杂的处理,可以使用阿里云数据库自带的读写分离连接,那样会更加简洁.本文主要对mybatis实现读写分离.主要的实现方式有一下四种: 方案1 通 ...

  5. WordPress搭建Personal Blog 个人博客

    早就想搭建一个专属于自己的博客了,用来记录自己生活.学习的点点滴滴.之所以选WordPress,主要是因为它可以支持Latex,而且特别喜欢其简约的风格. WordPress有个the famous ...

  6. Spring定时任务解决博客缓存数据更新问题

    最近在做博客系统的时候,由于很多页面都有右边侧边栏,内容包括博客分类信息,归档日志,热门文章,标签列表等,为了不想每次访问页面都去查询数据库,因为本身这些东西相对来说是比较固定的,但是也有可能在网站后 ...

  7. Spring + Mybatis 读写分离

    项目背景:项目开发中数据库使用了读写分离,所有查询语句走从库,除此之外走主库. 实现思路是: 第一步,实现动态切换数据源:配置两个DataSource,配置两个SqlSessionFactory指向两 ...

  8. 搭建 springboot 2.0 mybatis 读写分离 配置区分不同环境

    最近公司打算使用springboot2.0, springboot支持HTTP/2,所以提前先搭建一下环境.网上很多都在springboot1.5实现的,所以还是有些差异的.接下来咱们一块看一下. 文 ...

  9. SpringBoot Mybatis 读写分离配置(山东数漫江湖)

    为什么需要读写分离 当项目越来越大和并发越来大的情况下,单个数据库服务器的压力肯定也是越来越大,最终演变成数据库成为性能的瓶颈,而且当数据越来越多时,查询也更加耗费时间,当然数据库数据过大时,可以采用 ...

随机推荐

  1. JMeter循环控制器循环次数使用变量控制注意事项

    1.进入循环控制器之前变量要有值: 2.BeanShell处理文件,读取行数,赋值给变量,要有相应的Sampler,不然脚本不会运行. 对于单个线程来说,假如设置了循环2次,线程启动后,运行结束,此时 ...

  2. HTML基础教程-元素

    HTML 元素 HTML 文档是由 HTML 元素定义的. HTML 元素 HTML 元素指的是从开始标签(start tag)到结束标签(end tag)的所有代码. 注释:开始标签常被称为开放标签 ...

  3. 【Uva10559】Blocks(区间DP)

    Description 题意:有一排数量为N的方块,每次可以把连续的相同颜色的区间消除,得到分数为区间长度的平方,然后左右两边连在一起,问最大分数为多少. \(1\leq N\leq200\) Sol ...

  4. 关于C++函数返回局部对象的详细分析

    以前一直挺好奇的,C++是怎么在函数内返回一个局部对象的.因为按照我之前的想法,函数返回一个基本类型的值是通过存放到ecx实现的(关于浮点不了解),但是局部对象又是比较大的,很明显不能使用寄存器作为通 ...

  5. boost::assign(标准容器填充库)

    boost::assign通过对"+="和","的重载非常方便的填充标准容器(std::vector,std::set,std::list,std::map), ...

  6. ArcGIS API for JavaScript 4.2学习笔记[24] 【IdentifyTask类】的使用(结合IdentifyParameters类)(第七章完结)

    好吧,我都要吐了. 接连三个例子都是类似的套路,使用某个查询参数类的实例,结合对应的Task类,对返回值进行取值.显示. 这个例子是Identify识别,使用了TileLayer这种图层,数据来自Se ...

  7. ArrayList中对象 排序

    public class Student implements Comparable { private String studentname; public int studentage; publ ...

  8. 通过 kms 激活 office 2016

    1.管理员模式打开cmd,并切换到office的安装路径 注:office2016默认安装在C:\Program Files\Microsoft Office\Office16,激活其他office自 ...

  9. display:inline-block引发的间隙思考

    一.导火线 没错,总有一类属性在助你轻松寻得捷径的同时,也可为你增添烦劳,比如本文的主谋display:inline-block.众前端们所诸知,其作用是将对象呈递为内联对象,但是对象的内容作为块对象 ...

  10. Python的类与类型

    1.经典类与新式类 在了解Python的类与类型前,需要对Python的经典类(classic classes)与新式类(new-style classes)有个简单的概念. 在Python 2.x及 ...