数据库读写分离

为什么数据库需要配置读写分离f

   在数据库层面实现读写分离,就是配置一个Master数据库,多个Slave数据库,Master负责保存数据和读取实时数据,Slave主要读取非实时性的数据。
   在数据库读写操作中,读的频率远远大于写,并且读取操作所占用的资源和耗时也更多,所以把读写分离,可有效的缓解数据库的压力。

实现的原理

   实现的原理主要分为两种:

        1.静态的选择Master/Slave数据库(通过单独配置读写库,在方法中已经写死使用读库或者写库)

        2.数据库选择是动态切换(这里写的是这种方式实现读写分离)

   动态获取连接的流程:

         ps:我们的ReadWriteDataSourceConfig需要继承于AbstractRoutingDataSource

        1.AbstractRoutingDataSource初始化会依赖注入配置文件中你所配置的DataSource, 并保存于Map中(结构为key-DataSource,其中key为配置文件中设置的,key是为了标识对应的数据库,后续动态获取时会从ThreadLocal中获取key)

        2.通过aop拦截注解DBReadOnly(自定义),readDataSource的key保存于ThreadLocal中。

        3.获取数据库链接时,AbstractRoutingDataSource会调用覆写的getConnetion,其中getConnetion方法会调用determineTargetDataSource方法,它会调用抽象方法determineCurrentLookupKey(目的就是为了获取对应保存数据库的Map的key),我们的ReadWriteDataSourceConfig需要覆写determineCurrentLookupKey方法,返回ThreadLocal中的key(如果Key不存在,会使用默认库,下面会详细介绍),从而AbstractRoutingDataSource会去获取对应的数据库连接,实现动态获取数据库连接。

 

实现的原理:

   动态切换实现的原理主要是使用了annotation, Spring aop 等技术,在应用获取数据库连接时动态给于Master/Slave的connection,要实现它我们需要继承AbstractRoutingDataSource。
   
   
   可以看到AbstractRoutingDataSource是继承于AbstractDataSource类的,AbstractDataSource是实现的DataSource的接口
/**
   * <p>Attempts to establish a connection with the data source that
   * this {@code DataSource} object represents.
   *
   * @return  a connection to the data source
   * @exception SQLException if a database access error occurs
   * @throws java.sql.SQLTimeoutException  when the driver has determined that the
   * timeout value specified by the {@code setLoginTimeout} method
   * has been exceeded and has at least tried to cancel the
   * current database connection attempt
   */
  Connection getConnection()  throws  SQLException;
    这是DataSource的抽象方法,最终获取数据库连接就是通过这个方法,最终的核心步骤就是去覆写getConnetion方法,让它去创建我们对应读写库的连接。
 
    我们的ReadWriteDataSourceConfig是继承于AbstractRoutingDataSource的,AbstractRoutingDataSource是去覆写了getConnetion的,但是在看getConnetion之前我们需要看另一段代码
     public  abstract  class  AbstractRoutingDataSource  extends  AbstractDataSource  implements  InitializingBean{
 
 
     private  Map<Object, Object> targetDataSources; //保存对应key的DataSource
 
     private  Object defaultTargetDataSource; //默认的使用的DataSource的Bean
 
     private  boolean  lenientFallback =  true ;
 
     private  DataSourceLookup dataSourceLookup =  new  JndiDataSourceLookup();
 
     private  Map<Object, DataSource> resolvedDataSources; //所有DateSource保存于这个Map中
 
     private  DataSource resolvedDefaultDataSource; //默认使用的DataSource,下面会详谈
 
 
     @Override
     public  void  afterPropertiesSet() {
         if  ( this .targetDataSources ==  null ) {
             throw  new  IllegalArgumentException( "Property 'targetDataSources' is required" );
         }
         this .resolvedDataSources =  new  HashMap<Object, DataSource>( this .targetDataSources.size());
         for  (Map.Entry<Object, Object> entry :  this .targetDataSources.entrySet()) { //这是
             Object lookupKey = resolveSpecifiedLookupKey(entry.getKey());
             DataSource dataSource = resolveSpecifiedDataSource(entry.getValue());
             this .resolvedDataSources.put(lookupKey, dataSource);
         }
         if  ( this .defaultTargetDataSource !=  null ) {
             this .resolvedDefaultDataSource = resolveSpecifiedDataSource( this .defaultTargetDataSource);
         }
     }
}

1.AbstractRoutingDataSource中的成员变量,和初始化所做的事情(转换并保存Map<key, DataSource>,可设置key不存在时候的默认DataSourcere)

 

      AbstractRoutingDataSource中有targetDataSources,AbstractRoutingDataSource初始化会依赖注入配置文件中你所配置的DataSource, 并保存于Map中(结构为key-DataSource,其中key为配置文件中设置的,key是为了标识对应的数据库,后续动态获取时会从ThreadLocal中获取key),最终你所需要的数据库连接是通过你给于的key去获取,覆写determineCurrentLookupKey方法给它。

 

     可以看到AbstractRoutingDataSource是实现了InitializingBean这个接口的,它的作用就是在初始化AbstractRoutingDataSource时去执行afterPropertiesSet方法(其中初始化顺序为:构造器>afterPropertiesSet>init)
     afterPropertiesSet所做事情就是把targetDataSources(依赖注入)的值转换为DataSource对象保存到resolvedDataSources中,其中key值为DataSource唯一标识。
     可以看到有一个变量是resolvedDefaultDataSource,这就是一个默认使用的库了,没有key对应,当拿不到Key是使用resolvedDefaultDataSource,AbstractRoutingDataSource会获取resolvedDefaultDataSource对应的数据库连接,就是下图中的代码(其中lenientFallback 默认值为true, lenientFallback 宽大处理,当lenientFallback为true时,即使key对应的dataSource不存在,也会使用默认的数据库resolvedDefaultDataSource)
Object lookupKey = determineCurrentLookupKey(); //需要ReadWriteDataSourceConfig覆写的方法
DataSource dataSource =  this .resolvedDataSources.get(lookupKey);
if  (dataSource ==  null  && ( this .lenientFallback || lookupKey ==  null )) {
     dataSource =  this .resolvedDefaultDataSource;
}
2.getConnection获取对应数据库的连接
@Override
public  Connection getConnection()  throws  SQLException {
     return  determineTargetDataSource().getConnection();
}
 可以看到AbstractRoutingDataSource实现了getConnection方法,其中调用了determineTargetDataSource().getConnetion(),下面是determineTargetDataSource方法的实现。
/**
  * Retrieve the current target DataSource. Determines the
  * {@link #determineCurrentLookupKey() current lookup key}, performs
  * a lookup in the {@link #setTargetDataSources targetDataSources} map,
  * falls back to the specified
  * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
  * @see #determineCurrentLookupKey()
  */
protected  DataSource determineTargetDataSource() {
     Assert.notNull( this .resolvedDataSources,  "DataSource router not initialized" );
     Object lookupKey = determineCurrentLookupKey();
     DataSource dataSource =  this .resolvedDataSources.get(lookupKey);
     if  (dataSource ==  null  && ( this .lenientFallback || lookupKey ==  null )) {
         dataSource =  this .resolvedDefaultDataSource;
     }
     if  (dataSource ==  null ) {
         throw  new  IllegalStateException( "Cannot determine target DataSource for lookup key ["  + lookupKey +  "]" );
     }
     return  dataSource;
}
这个方法就是为了动态获取数据库,其中determineCurrentLookupKey就是动态获取key,这是一个抽象方法,需要我们的应用程序去实现,返回它对应数据库的key,然后从resolvedDataSources获取对应的DataSource,然后获取对应connetion(假如DataSource使用的是DruidDataSource,getConnetion的最终实现是在DruidDataSource.class中)
/**
  * Determine the current lookup key. This will typically be
  * implemented to check a thread-bound transaction context.
  * <p>Allows for arbitrary keys. The returned key needs
  * to match the stored lookup key type, as resolved by the
  * {@link #resolveSpecifiedLookupKey} method.
  */
protected  abstract  Object determineCurrentLookupKey();
3.总结
所以,要实现读写分离,只需继承AbstractRoutinDataSource,覆写determineCurrentLookupKey方法,在配置中配置数据源和对应的key即可实现。

配置与实现:

1.xml中配置多数据源

我们默认为使用写库,当标记了自定义注解DBReadOnly时使用读库(为了方便这里三个库都连的是同一个数据库)

  折叠源码
<bean id = "mysql_master"  parent= "dataSource" >
     <property name= "url"  value= "${test.jdbc.url}"  />
     <property name= "username"  value= "${test.jdbc.username}"  />
     <property name= "password"  value= "${test.jdbc.password}"  />
</bean>
 
<bean id = "mysql_slave1"  parent= "dataSource" >
     <property name= "url"  value= "${test.jdbc.url}"  />
     <property name= "username"  value= "${test.jdbc.username}"  />
     <property name= "password"  value= "${test.jdbc.password}"  />
</bean>
 
<bean id = "mysql_slave2"  parent= "dataSource" >
     <property name= "url"  value= "${test.jdbc.url}"  />
     <property name= "username"  value= "${test.jdbc.username}"  />
     <property name= "password"  value= "${test.jdbc.password}"  />
</bean>
<bean id= "dataSource"  class = "com.alibaba.druid.pool.DruidDataSource"
     init-method= "init"  destroy-method= "close" >
     <property name= "filters"  value= "mergeStat"  />
     <property name= "maxActive" >
         <value> 30 </value>
     </property>
     <property name= "initialSize" >
         <value> 5 </value>
     </property>
     <property name= "maxWait" >
         <value> 60000 </value>
     </property>
     <property name= "minIdle" >
         <value> 1 </value>
     </property>
     <property name= "timeBetweenEvictionRunsMillis" >
         <value> 600000 </value>
     </property>
     <property name= "minEvictableIdleTimeMillis" >
         <value> 300000 </value>
     </property>
     <property name= "validationQuery" >
         <value>SELECT  1 </value>
     </property>
     <property name= "testWhileIdle" >
         <value> true </value>
     </property>
     <property name= "testOnBorrow" >
         <value> false </value>
     </property>
     <property name= "testOnReturn" >
         <value> false </value>
     </property>
     <property name= "connectionProperties"  value= "druid.stat.slowSqlMillis=200"  />
     <property name= "poolPreparedStatements" >
         <value> false </value>
     </property>
     <property name= "removeAbandoned"  value= "false"  />
     <property name= "useGlobalDataSourceStat"  value= "false"  />
</bean>
 
<bean id = "mysqlDataSource"  class "cn.test.test.datasource.ReadWriteDataSourceConfig" >
     <property name = "defaultTargetDataSource"  ref= "mysql_master" />
     <property name= "targetDataSources"  >
         <map>
             <entry key= "read_mysql_slave1"   value-ref= "mysql_slave1" />
             <entry key= "read_mysql_slave2"   value-ref= "mysql_slave2" />
         </map>
     </property>
</bean>
     
<bean id= "sqlSessionFactory"  class = "org.mybatis.spring.SqlSessionFactoryBean" >
     <property name= "dataSource"  ref= "mysqlDataSource"  />
     <property name= "typeAliasesPackage"  value= "cn.test.test.bean"  />
     <property name= "mapperLocations"
         value= "classpath:cn/test/test/dao/mysql/**/*.xml"  />
     <property name= "configLocation"  value= "classpath:mybatis-config.xml" />
</bean>
 
<bean  class = "org.mybatis.spring.mapper.MapperScannerConfigurer" >
     <property name= "sqlSessionFactoryBeanName"  value= "sqlSessionFactory"  />
     <property name= "basePackage"  value= "cn.test.test.dao.mysql"  />
</bean>
 
<bean id= "transactionManager"
     class = "org.springframework.jdbc.datasource.DataSourceTransactionManager" >
     <property name= "dataSource"  ref= "mysqlDataSource"  />
     <qualifier value= "mysql"  />
</bean>
 
<tx:annotation-driven transaction-manager= "transactionManager" ></tx:annotation-driven>
其中ReadWriteDataSourceConfig就是我们去继承于AbstractRoutingDataSource的类,上述将2个slave通过key-value依赖注入于targetDataSources属性中
2.配置类实现
  折叠源码
public  class  ReadWriteDataSourceConfig  extends  AbstractRoutingDataSource {
 
     private  static  final Test Logger logger = TestLogger.getInstance(ReadWriteDataSourceConfig. class );
 
     /**
      * @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()
      */
     @Override
     protected  Object determineCurrentLookupKey() {
         return  HoldDataSourceKey.getRead();
     }
 
     enum  ReadKey {
         read_mysql_slave1, read_mysql_slave2;
     }
 
     static  class  HoldDataSourceKey {
         private  static  final  ThreadLocal<ReadKey> holder =  new  ThreadLocal<>();
 
         public  static  void  setRead() {
             ReadKey[] keys = ReadKey.values();
             holder.set(keys[ new  Random().nextInt(keys.length)]);
         }
 
         public  static  String getRead() {
             logger.infoLog( "所使用的dataSource`s key = {}" , String.valueOf(holder.get()));
             return  holder.get() ==  null  null  : holder.get().toString();
         }
 
         public  static  void  reset() {
             holder.set( null );
         }
 
     }
 
}
其中determineCurrentLookupKey方法就是AbstractRoutingDataSource中的抽象方法,目的是返回对应数据源的key,其中内部枚举类ReadKey的值要和xml中配置的key一一对应,HoldDataSourceKey类就时为了获取对应的key,如果是读库, 会随机分配一个slaveKey,现在基本操作都已完成,就差一个aop去拦截需要使用读库的方法了。
3.aop拦截访问读库请求
 

通过aop拦截标有注解DBReadOnly的操作

ps:@DBReadOnly最好是注在相关业务的外层service上,并处于同一事物中,不然可能会出现多次查询查询到不同的Slave,Slave之间也有延迟,导致数据不同步,或者数据不存在等等问题。

  折叠源码
@Target ({ ElementType.METHOD })
@Retention (RetentionPolicy.RUNTIME)
public  @interface  DBReadOnly {
}
  折叠源码
@Component
@Aspect
public  class  ReadWriteAspect  implements  Ordered {
     private  static  final  TestLogger  logger = TestLogger .getInstance(ReadWriteAspect. class );
 
     @Override
     public  int  getOrder() {
         return  1 ;
     }
 
     @Around ( "@annotation(cn.test.test.datasource.annotation.DBReadOnly)" )
     public  Object around(ProceedingJoinPoint pjp) {
         Object result =  null ;
         try  {
             HoldDataSourceKey.setRead();
             result = pjp.proceed();
             HoldDataSourceKey.reset();
         catch  (Throwable e) {
             logger.errorLog( "get datasource  key fail!" , e);
         finally  {
             HoldDataSourceKey.reset();
         }
         return  result;
     }
 
}
4.运行

在下面方法上中加@DBReadOnly,调用该方法

  折叠源码
     @DBReadOnly
     @Override
     public  UserPayWayConfig queryByUserId(Integer userId, Integer type) {
         return  userPayWayConfigMapper.queryByUserId(userId, type);
     }
 
 
 
 
2017 - 12 - 28  16 : 06 : 25.820  INFO  itpsgf1tHHPCkFLYmsHIbG ReadWriteDataSourceConfig 所使用的dataSource`s key =read_mysql_slave2
 
看到调用获取的key是read_mysql_slave2 ,获取的也是read_mysql_slave2 的DataSource,数据库的读写分离在这里就完成了。
 

缺陷与不足:

       最大的缺陷就是继承AbstractRoutingDataSource不能实现多数据库之间的读写分离,比如使用了sqlserver、mysql,它是无法做到同时使用读/写库的,因为获取Key的方法determineCurrentLookupKey是没有上下文的,无法确定该操作是操作sqlserver还是mysql,那么它就无法返回对应数据源的key了。

所以,要实现数据库的读写分离。。。还是去继承AbstractDataSource吧,实现原理和AbstractRoutingDataSource实现差不多,就是将单例的bean变为多例的class。

但是,如果只是使用单一数据库,继承这个类还是相对来说比较简单的。

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值