SpringBoot实践之---mybatis+mysql读写分离(一写多读)+事务

原文地址:spring boot学习7之mybatis+mysql读写分离(一写多读)+事务

当业务的访问量(数据库的查询)非常大时,为了降低数据库的压力,希望有多个数据库进行负载均衡,避免所有的查询都集中在一台数据库,造成数据库压力过大。mysql支持一主多从,即在写库的数据库发生变动时,会同步到所有从库,只是同步过程中,会有一定的延迟(除非业务中出现,立即写立即读,否则稍微的延迟是可以接收的)。

       当数据库有主从之分了,那应用代码也应该读写分离了。那代码执行时,该如何决定选择哪个数据库呢。

方案一:

       就像配置多个数据源那样(见博文spring boot学习6之mybatis+PageHelper分页插件+jta多数据源事务整合),将dao都分别放到不通的包下,指明哪个包下dao接口或配置文件走哪个数据库,service层程序员决定走主库还是从库。

       缺点:相同的dao接口和配置文件要复制多份到不同包路径下,不易维护和扩展。

方案二:

       使用AbstractRoutingDataSource+aop+annotation在dao层决定数据源。

        缺点:不支持事务。因为事务在service层开启时,就必须拿到数据源了。

方案三:

      使用AbstractRoutingDataSource+aop+annotation在service层决定数据源,可以支持事务.

      缺点:类内部方法通过this.xx()方式相互调用时,aop不会进行拦截,需进行特殊处理。


     方案二和方案三的区别就是数据源的决定是方案dao还是service,所以本博文例子代码会都含有。

    源码下载,见github。


项目结构


pom.xml

[html]  view plain  copy
  1. <parent>  
  2.         <groupId>org.springframework.boot</groupId>  
  3.         <artifactId>spring-boot-starter-parent</artifactId>  
  4.         <version>1.5.2.RELEASE</version>  
  5.     </parent>     
  6.     
  7.     <dependencies>  
  8.         <dependency>  
  9.             <groupId>org.springframework.boot</groupId>  
  10.             <artifactId>spring-boot-starter-web</artifactId>  
  11.         </dependency>  
  12.           
  13.           <dependency>  
  14.              <groupId>org.mybatis.spring.boot</groupId>  
  15.              <artifactId>mybatis-spring-boot-starter</artifactId>  
  16.              <version>1.3.0</version>  
  17.         </dependency>   
  18.   
  19.         <dependency>  
  20.                 <groupId>mysql</groupId>  
  21.                 <artifactId>mysql-connector-java</artifactId>  
  22.         </dependency>  
  23.         <dependency>  
  24.             <groupId>com.alibaba</groupId>  
  25.             <artifactId>druid</artifactId>  
  26.             <version>1.0.29</version>  
  27.         </dependency>  
  28.           
  29.         <dependency>  
  30.             <groupId>org.aspectj</groupId>  
  31.             <artifactId>aspectjweaver</artifactId>  
  32.         </dependency>  
  33.         <dependency>  
  34.         <groupId>org.springframework</groupId>  
  35.         <artifactId>spring-beans</artifactId>  
  36.     </dependency>  
  37.           
  38.         <!-- 分页插件 -->  
  39.         <dependency>  
  40.             <groupId>com.github.pagehelper</groupId>  
  41.             <artifactId>pagehelper</artifactId>  
  42.             <version>4.1.6</version>  
  43.         </dependency>  
  44.   
  45.   
  46.         </dependencies>  

本例子的数据库,都是在本地的mysql中建立3个库,test,test_01,test_02,例子是为了测试代码的读写分离,而是mysqld

application.yml

[plain]  view plain  copy
  1. logging:  
  2.   config: classpath:logback.xml  
  3.   path: d:/logs  
  4. server:  
  5.   port: 80  
  6.   session-timeout: 60  
  7.   
  8.   
  9. mybatis:  
  10.      mapperLocations: classpath:/com/fei/springboot/dao/*.xml  
  11.      typeAliasesPackage: com.fei.springboot.dao      
  12.      mapperScanPackage: com.fei.springboot.dao  
  13.      configLocation: classpath:/mybatis-config.xml  
  14.   
  15. mysql:  
  16.     datasource:  
  17.         readSize: 2  #读库个数  
  18.         type: com.alibaba.druid.pool.DruidDataSource  
  19.         mapperLocations: classpath:/com/fei/springboot/dao/*.xml  
  20.         configLocation: classpath:/mybatis-config.xml  
  21.         write:  
  22.            url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8  
  23.            username: root  
  24.            password: root  
  25.            driver-class-name: com.mysql.jdbc.Driver  
  26.            minIdle: 5  
  27.            maxActive: 100  
  28.            initialSize: 10  
  29.            maxWait: 60000  
  30.            timeBetweenEvictionRunsMillis: 60000  
  31.            minEvictableIdleTimeMillis: 300000  
  32.            validationQuery: select 'x'  
  33.            testWhileIdle: true  
  34.            testOnBorrow: false  
  35.            testOnReturn: false  
  36.            poolPreparedStatements: true  
  37.            maxPoolPreparedStatementPerConnectionSize: 50  
  38.            removeAbandoned: true  
  39.            filters: stat  
  40.         read01:  
  41.            url: jdbc:mysql://127.0.0.1:3306/test_01?useUnicode=true&characterEncoding=utf-8  
  42.            username: root  
  43.            password: root  
  44.            driver-class-name: com.mysql.jdbc.Driver  
  45.            minIdle: 5  
  46.            maxActive: 100  
  47.            initialSize: 10  
  48.            maxWait: 60000  
  49.            timeBetweenEvictionRunsMillis: 60000  
  50.            minEvictableIdleTimeMillis: 300000  
  51.            validationQuery: select 'x'  
  52.            testWhileIdle: true  
  53.            testOnBorrow: false  
  54.            testOnReturn: false  
  55.            poolPreparedStatements: true  
  56.            maxPoolPreparedStatementPerConnectionSize: 50  
  57.            removeAbandoned: true  
  58.            filters: stat  
  59.         read02:  
  60.            url: jdbc:mysql://127.0.0.1:3306/test_02?useUnicode=true&characterEncoding=utf-8  
  61.            username: root  
  62.            password: root  
  63.            driver-class-name: com.mysql.jdbc.Driver  
  64.            minIdle: 5  
  65.            maxActive: 100  
  66.            initialSize: 10  
  67.            maxWait: 60000  
  68.            timeBetweenEvictionRunsMillis: 60000  
  69.            minEvictableIdleTimeMillis: 300000  
  70.            validationQuery: select 'x'  
  71.            testWhileIdle: true  
  72.            testOnBorrow: false  
  73.            testOnReturn: false  
  74.            poolPreparedStatements: true  
  75.            maxPoolPreparedStatementPerConnectionSize: 50  
  76.            removeAbandoned: true  
  77.            filters: stat    
mybatis-config.xml

[html]  view plain  copy
  1. <?xml version="1.0" encoding="UTF-8"?>    
  2. <!DOCTYPE configuration    
  3.      PUBLIC "-//mybatis.org//DTD Config 3.0//EN"    
  4.      "http://mybatis.org/dtd/mybatis-3-config.dtd">  
  5. <configuration>  
  6.     <settings>  
  7.         <!-- 使全局的映射器启用或禁用缓存。 -->  
  8.         <setting name="cacheEnabled" value="true" />  
  9.         <!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->  
  10.         <setting name="lazyLoadingEnabled" value="true" />  
  11.         <!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->          
  12.          <setting name="aggressiveLazyLoading" value="true"/>          
  13.          <!-- 是否允许单条sql 返回多个数据集  (取决于驱动的兼容性) default:true -->  
  14.         <setting name="multipleResultSetsEnabled" value="true" />  
  15.         <!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true -->  
  16.         <setting name="useColumnLabel" value="true" />  
  17.         <!-- 允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。  default:false  -->  
  18.         <setting name="useGeneratedKeys" value="false" />  
  19.         <!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不隐射 PARTIAL:部分  FULL:全部  -->  
  20.         <setting name="autoMappingBehavior" value="PARTIAL" />  
  21.         <!-- 这是默认的执行类型  (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句;BATCH: 执行器可以重复执行语句和批量更新)  -->  
  22.         <setting name="defaultExecutorType" value="SIMPLE" />  
  23.           
  24.         <setting name="defaultStatementTimeout" value="25" />  
  25.           
  26.         <setting name="defaultFetchSize" value="100" />  
  27.           
  28.         <setting name="safeRowBoundsEnabled" value="false" />  
  29.         <!-- 使用驼峰命名法转换字段。 -->  
  30.         <setting name="mapUnderscoreToCamelCase" value="true" />  
  31.         <!-- 设置本地缓存范围 session:就会有数据的共享  statement:语句范围 (这样就不会有数据的共享 ) defalut:session -->  
  32.         <setting name="localCacheScope" value="SESSION" />  
  33.         <!-- 默认为OTHER,为了解决oracle插入null报错的问题要设置为NULL -->  
  34.         <setting name="jdbcTypeForNull" value="NULL" />  
  35.         <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString" />  
  36.     </settings>  
  37.       
  38. </configuration>  

读取配置多个数据源

DataSourceConfiguration.java

[java]  view plain  copy
  1. package com.fei.springboot.config.dbconfig;  
  2.   
  3. import javax.sql.DataSource;  
  4.   
  5. import org.slf4j.Logger;  
  6. import org.slf4j.LoggerFactory;  
  7. import org.springframework.beans.factory.annotation.Value;  
  8. import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;  
  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.   
  16. import com.alibaba.druid.support.http.StatViewServlet;  
  17. import com.alibaba.druid.support.http.WebStatFilter;  
  18.   
  19. /** 
  20.  * 数据库源配置 
  21.  * @author Jfei 
  22.  * 
  23.  */  
  24. @Configuration  
  25. public class DataSourceConfiguration {  
  26.   
  27.     private static Logger log = LoggerFactory.getLogger(DataSourceConfiguration.class);  
  28.       
  29.     @Value("${mysql.datasource.type}")  
  30.     private Class<? extends DataSource> dataSourceType;  
  31.       
  32.     /** 
  33.      * 写库 数据源配置 
  34.      * @return 
  35.      */  
  36.     @Bean(name = "writeDataSource")  
  37.     @Primary  
  38.     @ConfigurationProperties(prefix = "mysql.datasource.write")  
  39.     public DataSource writeDataSource() {  
  40.         log.info("-------------------- writeDataSource init ---------------------");  
  41.         return DataSourceBuilder.create().type(dataSourceType).build();  
  42.     }  
  43.       
  44.     /** 
  45.      * 有多少个从库就要配置多少个 
  46.      * @return 
  47.      */  
  48.     @Bean(name = "readDataSource01")  
  49.     @ConfigurationProperties(prefix = "mysql.datasource.read01")  
  50.     public DataSource readDataSourceOne() {  
  51.         log.info("-------------------- read01 DataSourceOne init ---------------------");  
  52.         return DataSourceBuilder.create().type(dataSourceType).build();  
  53.     }  
  54.   
  55.     @Bean(name = "readDataSource02")  
  56.     @ConfigurationProperties(prefix = "mysql.datasource.read02")  
  57.     public DataSource readDataSourceTwo() {  
  58.         log.info("-------------------- read02 DataSourceTwo init ---------------------");  
  59.         return DataSourceBuilder.create().type(dataSourceType).build();  
  60.     }  
  61.       
  62.       
  63. }  


数据库的sqlSessionFactorys、roundRobinDataSouceProxy、sqlSessionTemplate、annotationDrivenTransactionManager的设置

MybatisConfiguration.java

[java]  view plain  copy
  1. package com.fei.springboot.config.dbconfig;  
  2.   
  3. import java.io.IOException;  
  4. import java.util.HashMap;  
  5. import java.util.Map;  
  6. import java.util.Properties;  
  7. import java.util.concurrent.atomic.AtomicInteger;  
  8.   
  9. import javax.sql.DataSource;  
  10.   
  11. import org.apache.ibatis.plugin.Interceptor;  
  12. import org.apache.ibatis.session.SqlSessionFactory;  
  13. import org.mybatis.spring.SqlSessionFactoryBean;  
  14. import org.mybatis.spring.SqlSessionTemplate;  
  15. import org.mybatis.spring.annotation.MapperScan;  
  16. import org.slf4j.Logger;  
  17. import org.slf4j.LoggerFactory;  
  18. import org.springframework.beans.factory.annotation.Autowired;  
  19. import org.springframework.beans.factory.annotation.Qualifier;  
  20. import org.springframework.beans.factory.annotation.Value;  
  21. import org.springframework.boot.autoconfigure.AutoConfigureAfter;  
  22. import org.springframework.context.annotation.Bean;  
  23. import org.springframework.context.annotation.Configuration;  
  24. import org.springframework.core.io.DefaultResourceLoader;  
  25. import org.springframework.core.io.Resource;  
  26. import org.springframework.core.io.support.PathMatchingResourcePatternResolver;  
  27. import org.springframework.jdbc.datasource.DataSourceTransactionManager;  
  28. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;  
  29. import org.springframework.transaction.PlatformTransactionManager;  
  30.   
  31. import com.fei.springboot.util.SpringContextUtil;  
  32. import com.github.pagehelper.PageHelper;  
  33.   
  34. @Configuration  
  35. @AutoConfigureAfter(DataSourceConfiguration.class)  
  36. @MapperScan(basePackages="com.fei.springboot.dao")  
  37. public class MybatisConfiguration {  
  38.   
  39.     private static Logger log = LoggerFactory.getLogger(MybatisConfiguration.class);  
  40.       
  41.     @Value("${mysql.datasource.readSize}")  
  42.     private String readDataSourceSize;  
  43.   
  44.     //XxxMapper.xml文件所在路径  
  45.       @Value("${mysql.datasource.mapperLocations}")  
  46.       private String mapperLocations;  
  47.   
  48.      //  加载全局的配置文件  
  49.       @Value("${mysql.datasource.configLocation}")  
  50.       private String configLocation;  
  51.         
  52.     @Autowired  
  53.     @Qualifier("writeDataSource")  
  54.     private DataSource writeDataSource;  
  55.     @Autowired  
  56.     @Qualifier("readDataSource01")  
  57.     private DataSource readDataSource01;  
  58.     @Autowired  
  59.     @Qualifier("readDataSource02")  
  60.     private DataSource readDataSource02;  
  61.       
  62.       
  63.     @Bean(name="sqlSessionFactory")  
  64.     public SqlSessionFactory sqlSessionFactorys() throws Exception {  
  65.         log.info("--------------------  sqlSessionFactory init ---------------------");  
  66.         try {  
  67.             SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();  
  68.        //     sessionFactoryBean.setDataSource(roundRobinDataSouce);  
  69.             sessionFactoryBean.setDataSource(roundRobinDataSouceProxy());  
  70.               
  71.             // 读取配置   
  72.             sessionFactoryBean.setTypeAliasesPackage("com.fei.springboot.domain");  
  73.               
  74.             //设置mapper.xml文件所在位置   
  75.             Resource[] resources = new PathMatchingResourcePatternResolver().getResources(mapperLocations);  
  76.             sessionFactoryBean.setMapperLocations(resources);  
  77.          //设置mybatis-config.xml配置文件位置  
  78.             sessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));  
  79.   
  80.             //添加分页插件、打印sql插件  
  81.             Interceptor[] plugins = new Interceptor[]{pageHelper(),new SqlPrintInterceptor()};  
  82.             sessionFactoryBean.setPlugins(plugins);  
  83.               
  84.             return sessionFactoryBean.getObject();  
  85.         } catch (IOException e) {  
  86.             log.error("mybatis resolver mapper*xml is error",e);  
  87.             return null;  
  88.         } catch (Exception e) {  
  89.             log.error("mybatis sqlSessionFactoryBean create error",e);  
  90.             return null;  
  91.         }  
  92.     }  
  93.   
  94.     /** 
  95.      * 分页插件 
  96.      * @return 
  97.      */  
  98.     @Bean  
  99.     public PageHelper pageHelper() {  
  100.         PageHelper pageHelper = new PageHelper();  
  101.         Properties p = new Properties();  
  102.         p.setProperty("offsetAsPageNum""true");  
  103.         p.setProperty("rowBoundsWithCount""true");  
  104.         p.setProperty("reasonable""true");  
  105.         p.setProperty("returnPageInfo""check");  
  106.         p.setProperty("params""count=countSql");  
  107.         pageHelper.setProperties(p);  
  108.         return pageHelper;  
  109.     }  
  110.     /** 
  111.      * 把所有数据库都放在路由中 
  112.      * @return 
  113.      */  
  114.     @Bean(name="roundRobinDataSouceProxy")  
  115.     public AbstractRoutingDataSource roundRobinDataSouceProxy() {  
  116.           
  117.         Map<Object, Object> targetDataSources = new HashMap<Object, Object>();  
  118.         //把所有数据库都放在targetDataSources中,注意key值要和determineCurrentLookupKey()中代码写的一至,  
  119.         //否则切换数据源时找不到正确的数据源  
  120.         targetDataSources.put(DataSourceType.write.getType(), writeDataSource);  
  121.         targetDataSources.put(DataSourceType.read.getType()+"1", readDataSource01);  
  122.         targetDataSources.put(DataSourceType.read.getType()+"2", readDataSource02);  
  123.       
  124.         final int readSize = Integer.parseInt(readDataSourceSize);  
  125.    //     MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(readSize);  
  126.           
  127.         //路由类,寻找对应的数据源  
  128.         AbstractRoutingDataSource proxy = new AbstractRoutingDataSource(){  
  129.              private AtomicInteger count = new AtomicInteger(0);  
  130.             /** 
  131.              * 这是AbstractRoutingDataSource类中的一个抽象方法, 
  132.              * 而它的返回值是你所要用的数据源dataSource的key值,有了这个key值, 
  133.              * targetDataSources就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。 
  134.              */  
  135.             @Override  
  136.             protected Object determineCurrentLookupKey() {  
  137.                 String typeKey = DataSourceContextHolder.getReadOrWrite();  
  138.                   
  139.                 if(typeKey == null){  
  140.                 //  System.err.println("使用数据库write.............");  
  141.                 //    return DataSourceType.write.getType();  
  142.                     throw new NullPointerException("数据库路由时,决定使用哪个数据库源类型不能为空...");  
  143.                 }  
  144.                   
  145.                 if (typeKey.equals(DataSourceType.write.getType())){  
  146.                     System.err.println("使用数据库write.............");  
  147.                     return DataSourceType.write.getType();  
  148.                 }  
  149.                       
  150.                 //读库, 简单负载均衡  
  151.                 int number = count.getAndAdd(1);  
  152.                 int lookupKey = number % readSize;  
  153.                 System.err.println("使用数据库read-"+(lookupKey+1));  
  154.                 return DataSourceType.read.getType()+(lookupKey+1);  
  155.             }  
  156.         };  
  157.           
  158.         proxy.setDefaultTargetDataSource(writeDataSource);//默认库  
  159.         proxy.setTargetDataSources(targetDataSources);  
  160.         return proxy;  
  161.     }  
  162.   
  163.   
  164.     @Bean  
  165.     public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {  
  166.         return new SqlSessionTemplate(sqlSessionFactory);  
  167.     }  
  168.       
  169.     //事务管理  
  170.     @Bean  
  171.     public PlatformTransactionManager annotationDrivenTransactionManager() {  
  172.         return new DataSourceTransactionManager((DataSource)SpringContextUtil.getBean("roundRobinDataSouceProxy"));  
  173.     }  
  174.       
  175. }  

   重点是roundRobinDataSouceProxy()方法,它把所有的数据库源交给AbstractRoutingDataSource类,并由它的determineCurrentLookupKey()进行决定数据源的选择,其中读库进行了简单的负载均衡(轮询)。

DataSourceType.java

[java]  view plain  copy
  1. package com.fei.springboot.config.dbconfig;  
  2.   
  3. public enum DataSourceType {  
  4.   
  5.     read("read""从库"),  
  6.     write("write""主库");  
  7.       
  8.     private String type;  
  9.       
  10.     private String name;  
  11.   
  12.     DataSourceType(String type, String name) {  
  13.         this.type = type;  
  14.         this.name = name;  
  15.     }  
  16.   
  17.     public String getType() {  
  18.         return type;  
  19.     }  
  20.   
  21.     public void setType(String type) {  
  22.         this.type = type;  
  23.     }  
  24.   
  25.     public String getName() {  
  26.         return name;  
  27.     }  
  28.   
  29.     public void setName(String name) {  
  30.         this.name = name;  
  31.     }  
  32.       
  33. }  
DataSourceContextHolder.java

[java]  view plain  copy
  1. package com.fei.springboot.config.dbconfig;  
  2.   
  3. import org.slf4j.Logger;  
  4. import org.slf4j.LoggerFactory;  
  5.   
  6. /** 
  7.  * 本地线程,数据源上下文 
  8.  * @author Jfei 
  9.  * 
  10.  */  
  11. public class DataSourceContextHolder {  
  12.   
  13.     private static Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);  
  14.       
  15.     //线程本地环境  
  16.     private static final ThreadLocal<String> local = new ThreadLocal<String>();  
  17.   
  18.     public static ThreadLocal<String> getLocal() {  
  19.         return local;  
  20.     }  
  21.   
  22.     /** 
  23.      * 读库 
  24.      */  
  25.     public static void setRead() {  
  26.         local.set(DataSourceType.read.getType());  
  27.         log.info("数据库切换到读库...");  
  28.     }  
  29.   
  30.     /** 
  31.      * 写库 
  32.      */  
  33.     public static void setWrite() {  
  34.         local.set(DataSourceType.write.getType());  
  35.         log.info("数据库切换到写库...");  
  36.     }  
  37.   
  38.     public static String getReadOrWrite() {  
  39.         return local.get();  
  40.     }  
  41.       
  42.     public static void clear(){  
  43.         local.remove();  
  44.     }  
  45. }  

写库、读库的注解

[java]  view plain  copy
  1. package com.fei.springboot.annotation;  
  2.   
  3. import java.lang.annotation.Documented;  
  4. import java.lang.annotation.ElementType;  
  5. import java.lang.annotation.Inherited;  
  6. import java.lang.annotation.Retention;  
  7. import java.lang.annotation.RetentionPolicy;  
  8. import java.lang.annotation.Target;  
  9.   
  10. @Target({ElementType.METHOD, ElementType.TYPE})  
  11. @Retention(RetentionPolicy.RUNTIME)  
  12. @Inherited  
  13. @Documented  
  14. public @interface ReadDataSource {  
  15.   
  16. }  
[java]  view plain  copy
  1. package com.fei.springboot.annotation;  
  2.   
  3. import java.lang.annotation.Documented;  
  4. import java.lang.annotation.ElementType;  
  5. import java.lang.annotation.Inherited;  
  6. import java.lang.annotation.Retention;  
  7. import java.lang.annotation.RetentionPolicy;  
  8. import java.lang.annotation.Target;  
  9.   
  10. @Target({ElementType.METHOD, ElementType.TYPE})  
  11. @Retention(RetentionPolicy.RUNTIME)  
  12. @Inherited  
  13. @Documented  
  14. public @interface WriteDataSource {  
  15.   
  16. }  

UserMapper.java

[java]  view plain  copy
  1. package com.fei.springboot.dao;  
  2.   
  3. import java.util.List;  
  4.   
  5. import org.apache.ibatis.annotations.Insert;  
  6. import org.apache.ibatis.annotations.Mapper;  
  7. import org.apache.ibatis.annotations.Param;  
  8. import org.apache.ibatis.annotations.Select;  
  9.   
  10. import com.fei.springboot.domain.User;  
  11.   
  12. @Mapper  
  13. public interface UserMapper {  
  14.   
  15.     @Insert("insert sys_user(id,user_name) values(#{id},#{userName})")  
  16.     void insert(User u);  
  17.       
  18.     @Select("select id,user_name from sys_user where id=#{id} ")  
  19.     User findById(@Param("id")String id);  
  20.       
  21.     //注:方法名和要UserMapper.xml中的id一致  
  22.     List<User> query(@Param("userName")String userName);  
  23.       
  24. }  

如果想在dao进行数据源的决定,在aop的拦截路径写明是dao

DataSourceAopInDao.java

[java]  view plain  copy
  1. package com.fei.springboot.aop;  
  2.   
  3. import org.aspectj.lang.annotation.Aspect;  
  4. import org.aspectj.lang.annotation.Before;  
  5. import org.slf4j.Logger;  
  6. import org.slf4j.LoggerFactory;  
  7. import org.springframework.stereotype.Component;  
  8.   
  9. import com.fei.springboot.config.dbconfig.DataSourceContextHolder;  
  10. import com.fei.springboot.config.dbconfig.DataSourceType;  
  11.   
  12. /** 
  13.  * 在dao层决定数据源(注:如果用这方式,service层不能使用事务,否则出问题,因为打开事务打开时,就会觉得数据库源了) 
  14.  * @author Jfei 
  15.  * 
  16.  */  
  17. //@Aspect  
  18. //@Component  
  19. public class DataSourceAopInDao {  
  20.   
  21.     private static Logger log = LoggerFactory.getLogger(DataSourceAopInDao.class);  
  22.       
  23.     @Before("execution(* com.fei.springboot.dao..*.find*(..)) "  
  24.             + " or execution(* com.fei.springboot.dao..*.get*(..)) "  
  25.             + " or execution(* com.fei.springboot.dao..*.query*(..))")  
  26.     public void setReadDataSourceType() {  
  27.         DataSourceContextHolder.setRead();  
  28.     }  
  29.   
  30.     @Before("execution(* com.fei.springboot.dao..*.insert*(..)) "  
  31.             + " or execution(* com.fei.springboot.dao..*.update*(..))"  
  32.             + " or execution(* com.fei.springboot.dao..*.add*(..))")  
  33.     public void setWriteDataSourceType() {  
  34.         DataSourceContextHolder.setWrite();  
  35.     }  
  36.       
  37.       
  38. /*    @Before("execution(* com.fei.springboot.dao..*.*(..)) " 
  39.             + " and @annotation(com.fei.springboot.annotation.ReadDataSource) ") 
  40.     public void setReadDataSourceType() { 
  41.         //如果已经开启写事务了,那之后的所有读都从写库读 
  42.         if(!DataSourceType.write.getType().equals(DataSourceContextHolder.getReadOrWrite())){ 
  43.             DataSourceContextHolder.setRead(); 
  44.         } 
  45.          
  46.     } 
  47.      
  48.     @Before("execution(* com.fei.springboot.dao..*.*(..)) " 
  49.             + " and @annotation(com.fei.springboot.annotation.WriteDataSource) ") 
  50.     public void setWriteDataSourceType() { 
  51.         DataSourceContextHolder.setWrite(); 
  52.     }*/  
  53. }  


如果是service,必须实现Ordered,并且优先级优于事务的开启。

DataSourceAopInService.java

[java]  view plain  copy
  1. package com.fei.springboot.aop;  
  2.   
  3. import org.aspectj.lang.annotation.Aspect;  
  4. import org.aspectj.lang.annotation.Before;  
  5. import org.slf4j.Logger;  
  6. import org.slf4j.LoggerFactory;  
  7. import org.springframework.context.annotation.EnableAspectJAutoProxy;  
  8. import org.springframework.core.PriorityOrdered;  
  9. import org.springframework.stereotype.Component;  
  10. import org.springframework.transaction.annotation.EnableTransactionManagement;  
  11.   
  12. import com.fei.springboot.config.dbconfig.DataSourceContextHolder;  
  13. import com.fei.springboot.config.dbconfig.DataSourceType;  
  14.   
  15. /** 
  16.  * 在service层觉得数据源 
  17.  *  
  18.  * 必须在事务AOP之前执行,所以实现Ordered,order的值越小,越先执行 
  19.  * 如果一旦开始切换到写库,则之后的读都会走写库 
  20.  *  
  21.  * @author Jfei 
  22.  * 
  23.  */  
  24. @Aspect  
  25. @EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)  
  26. @Component  
  27. public class DataSourceAopInService implements PriorityOrdered{  
  28.   
  29. private static Logger log = LoggerFactory.getLogger(DataSourceAopInService.class);  
  30.       
  31. /*  @Before("execution(* com.fei.springboot.service..*.find*(..)) " 
  32.             + " or execution(* com.fei.springboot.service..*.get*(..)) " 
  33.             + " or execution(* com.fei.springboot.service..*.query*(..))") 
  34.     public void setReadDataSourceType() { 
  35.         //如果已经开启写事务了,那之后的所有读都从写库读 
  36.         if(!DataSourceType.write.getType().equals(DataSourceContextHolder.getReadOrWrite())){ 
  37.             DataSourceContextHolder.setRead(); 
  38.         } 
  39.          
  40.     } 
  41.  
  42.     @Before("execution(* com.fei.springboot.service..*.insert*(..)) " 
  43.             + " or execution(* com.fei.springboot.service..*.update*(..))" 
  44.             + " or execution(* com.fei.springboot.service..*.add*(..))") 
  45.     public void setWriteDataSourceType() { 
  46.         DataSourceContextHolder.setWrite(); 
  47.     }*/  
  48.       
  49.   
  50.     @Before("execution(* com.fei.springboot.service..*.*(..)) "  
  51.             + " and @annotation(com.fei.springboot.annotation.ReadDataSource) ")  
  52.     public void setReadDataSourceType() {  
  53.         //如果已经开启写事务了,那之后的所有读都从写库读  
  54.         if(!DataSourceType.write.getType().equals(DataSourceContextHolder.getReadOrWrite())){  
  55.             DataSourceContextHolder.setRead();  
  56.         }  
  57.           
  58.     }  
  59.       
  60.     @Before("execution(* com.fei.springboot.service..*.*(..)) "  
  61.             + " and @annotation(com.fei.springboot.annotation.WriteDataSource) ")  
  62.     public void setWriteDataSourceType() {  
  63.         DataSourceContextHolder.setWrite();  
  64.     }  
  65.       
  66.     @Override  
  67.     public int getOrder() {  
  68.         /** 
  69.          * 值越小,越优先执行 
  70.          * 要优于事务的执行 
  71.          * 在启动类中加上了@EnableTransactionManagement(order = 10)  
  72.          */  
  73.         return 1;  
  74.     }  
  75.   
  76. }  



UserService.java

[java]  view plain  copy
  1. package com.fei.springboot.service;  
  2.   
  3. import org.springframework.aop.framework.AopContext;  
  4. import org.springframework.beans.factory.annotation.Autowired;  
  5. import org.springframework.context.annotation.EnableAspectJAutoProxy;  
  6. import org.springframework.stereotype.Service;  
  7. import org.springframework.transaction.annotation.Isolation;  
  8. import org.springframework.transaction.annotation.Propagation;  
  9. import org.springframework.transaction.annotation.Transactional;  
  10.   
  11. import com.fei.springboot.annotation.ReadDataSource;  
  12. import com.fei.springboot.annotation.WriteDataSource;  
  13. import com.fei.springboot.dao.UserMapper;  
  14. import com.fei.springboot.domain.User;  
  15. import com.fei.springboot.util.SpringContextUtil;  
  16. import com.github.pagehelper.Page;  
  17. import com.github.pagehelper.PageHelper;  
  18. import com.github.pagehelper.PageInfo;  
  19. /** 
  20.  * 如果需要事务,自行在方法上添加@Transactional 
  21.  * 如果方法有内部有数据库操作,则必须指定@WriteDataSource还是@ReadDataSource 
  22.  *  
  23.  * 注:AOP ,内部方法之间互相调用时,如果是this.xxx()这形式,不会触发AOP拦截,可能会 
  24.  * 导致无法决定数据库是走写库还是读库 
  25.  * 方法: 
  26.  * 为了触发AOP的拦截,调用内部方法时,需要特殊处理下,看方法getService() 
  27.  *  
  28.  * @author Jfei 
  29.  * 
  30.  */  
  31. @Service  
  32. public class UserService {  
  33.   
  34.     @Autowired  
  35.     private UserMapper userMapper;  
  36.       
  37.     @WriteDataSource  
  38.     @Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,readOnly=false)  
  39.     public void insertUser(User u){  
  40.         this.userMapper.insert(u);  
  41.       
  42.         //如果类上面没有@Transactional,方法上也没有,哪怕throw new RuntimeException,数据库也会成功插入数据  
  43.     //  throw new RuntimeException("测试插入事务");  
  44.     }  
  45.     /** 
  46.      * 写事务里面调用读 
  47.      * @param u 
  48.      */  
  49.     public void wirteAndRead(User u){  
  50.         getService().insertUser(u);//这里走写库,那后面的读也都要走写库  
  51.         //这是刚刚插入的  
  52.         User uu = getService().findById(u.getId());  
  53.         System.out.println("==读写混合测试中的读(刚刚插入的)====id="+u.getId()+",  user_name=" + uu.getUserName());  
  54.         //为了测试,3个库中id=1的user_name是不一样的  
  55.         User uuu = getService().findById("1");  
  56.         System.out.println("==读写混合测试中的读====id=1,  user_name=" + uuu.getUserName());  
  57.           
  58.     }  
  59.       
  60.     public void readAndWirte(User u){  
  61.         //为了测试,3个库中id=1的user_name是不一样的  
  62.         User uu = getService(). findById("1");  
  63.         System.out.println("==读写混合测试中的读====id=1,user_name=" + uu.getUserName());  
  64.         getService().insertUser(u);  
  65.           
  66.     }  
  67.       
  68.     @ReadDataSource  
  69.     public User findById(String id){  
  70.         User u = this.userMapper.findById(id);  
  71.         return u;  
  72.     }  
  73.       
  74.       
  75.     @ReadDataSource  
  76.     public PageInfo<User> queryPage(String userName,int pageNum,int pageSize){  
  77.         Page<User> page = PageHelper.startPage(pageNum, pageSize);  
  78.         //PageHelper会自动拦截到下面这查询sql  
  79.         this.userMapper.query(userName);  
  80.         return page.toPageInfo();  
  81.     }  
  82.       
  83.     private UserService getService(){  
  84.         // 采取这种方式的话,  
  85.         //@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)  
  86.         //必须设置为true  
  87.     /*  if(AopContext.currentProxy() != null){ 
  88.             return (UserService)AopContext.currentProxy(); 
  89.         }else{ 
  90.             return this; 
  91.         } 
  92.         */  
  93.         return SpringContextUtil.getBean(this.getClass());  
  94.     }  
  95.       
  96. }  


   开头的方案三中也说到了,如果aop拦截在service层,但是aop不会拦截类内部的调用,至于原因,看博文 http://www.oschina.net/translate/comparative_analysis_between_spring_aop_and_aspectj
  所以UserService中增加了getService()方法进行处理。


   写个controller进行简单测试

UserController.java

[java]  view plain  copy
  1. package com.fei.springboot.controller;  
  2.   
  3. import org.springframework.beans.factory.annotation.Autowired;  
  4. import org.springframework.stereotype.Controller;  
  5. import org.springframework.web.bind.annotation.PathVariable;  
  6. import org.springframework.web.bind.annotation.RequestMapping;  
  7. import org.springframework.web.bind.annotation.ResponseBody;  
  8.   
  9. import com.fei.springboot.domain.User;  
  10. import com.fei.springboot.service.UserService;  
  11. import com.github.pagehelper.PageInfo;  
  12.   
  13. @Controller  
  14. @RequestMapping("/user")  
  15. public class UserController {  
  16.   
  17.       
  18.     @Autowired  
  19.     private UserService userService;  
  20.       
  21.     @RequestMapping("/hello")  
  22.     @ResponseBody  
  23.     public String hello(){  
  24.         return "hello";  
  25.     }  
  26.     /** 
  27.      * 测试插入 
  28.      * @return 
  29.      */  
  30.     @RequestMapping("/add")  
  31.     @ResponseBody  
  32.     public String add(String id,String userName){  
  33.         User u = new User();  
  34.         u.setId(id);  
  35.         u.setUserName(userName);  
  36.         this.userService.insertUser(u);  
  37.         return u.getId()+"    " + u.getUserName();  
  38.     }  
  39.     /** 
  40.      * 测试读 
  41.      * @param id 
  42.      * @return 
  43.      */  
  44.     @RequestMapping("/get/{id}")  
  45.     @ResponseBody  
  46.     public String findById(@PathVariable("id") String id){  
  47.         User u = this.userService.findById(id);  
  48.         return u.getId()+"    " + u.getUserName();  
  49.     }  
  50.     /** 
  51.      * 测试写然后读 
  52.      * @param id 
  53.      * @param userName 
  54.      * @return 
  55.      */  
  56.     @RequestMapping("/addAndRead")  
  57.     @ResponseBody  
  58.     public String addAndRead(String id,String userName){  
  59.         User u = new User();  
  60.         u.setId(id);  
  61.         u.setUserName(userName);  
  62.         this.userService.wirteAndRead(u);  
  63.         return u.getId()+"    " + u.getUserName();  
  64.     }  
  65.       
  66.     /** 
  67.      * 测试读然后写 
  68.      * @param id 
  69.      * @param userName 
  70.      * @return 
  71.      */  
  72.     @RequestMapping("/readAndAdd")  
  73.     @ResponseBody  
  74.     public String readAndWrite(String id,String userName){  
  75.         User u = new User();  
  76.         u.setId(id);  
  77.         u.setUserName(userName);  
  78.         this.userService.readAndWirte(u);  
  79.         return u.getId()+"    " + u.getUserName();  
  80.     }  
  81.       
  82.     /** 
  83.      * 测试分页插件 
  84.      * @return 
  85.      */  
  86.     @RequestMapping("/queryPage")  
  87.     @ResponseBody  
  88.     public String queryPage(){  
  89.         PageInfo<User> page = this.userService.queryPage("tes"12);  
  90.         StringBuilder sb = new StringBuilder();  
  91.         sb.append("<br/>总页数=" + page.getPages());  
  92.         sb.append("<br/>总记录数=" + page.getTotal()) ;  
  93.         for(User u : page.getList()){  
  94.             sb.append("<br/>" + u.getId() + "      " + u.getUserName());  
  95.         }  
  96.         System.out.println("分页查询....\n" + sb.toString());  
  97.         return sb.toString();  
  98.     }  
  99.       
  100. }  

http://127.0.0.1/user/get/1  时,获取到的结果要么是库test_01要么是test_02中来的,其他方法也可以测试均符合预期结果。这里就不一一贴结果了。其他的类源码也不一一贴出来了。想看的可以到github上看或下载。

   完整源码


猜你喜欢

转载自blog.csdn.net/luckykapok918/article/details/79651067