使用Spring AOP切面解决数据库读写分离

来源:互联网 发布:逆战磁暴矩阵怎么用 编辑:程序博客网 时间:2024/05/21 06:40

为了减轻数据库的压力,一般会使用数据库主从(master/slave)的方式,但是这种方式会给应用程序带来一定的麻烦,比如说,应用程序如何做到把数据写到master库,而读取数据的时候,从slave库读取。如果应用程序判断失误,把数据写入到slave库,会给系统造成致命的打击。

解决读写分离的方案很多,常用的有SQL解析、动态设置数据源。SQL解析主要是通过分析sql语句是insert/select/update/delete中的哪一种,从而对应选择主从。而动态设置数据源,则是通过拦截方法名称的方式来决定主从的,例如:save*(),insert*() 形式的方法使用master库,select()开头的,使用slave库。蛮多公司会使用在方法上标上自定义的@Master、@Slave之类的标签来选择主从,也有公司直接就调用setxxMaster,setxxSlave之类的代码进行主从选择。

下面我主要介绍一下基于Spring AOP动态设置数据源这种方式。注意这篇文章是基于自己项目的实际情况的,不是通用的方案,请知晓。

原理图


 

Spring AOP的切面主要的职责是拦截Mybatis的Mapper接口,通过判断Mapper接口中的方法名称来决定主从。

 

Spring AOP 切面配置

 

[html] view plain copy
  1. <aop:config expose-proxy="true">  
  2.   
  3. <aop:pointcut id="txPointcut" expression="execution(* com.test..persistence..*.*(..))" />  
  4.   
  5. <aop:aspect ref="readWriteInterceptor" order="1">  
  6.   
  7. <aop:around pointcut-ref="txPointcut" method="readOrWriteDB"/>  
  8.   
  9. </aop:aspect>  
  10.   
  11. </aop:config>  
  12.   
  13.    
  14.   
  15. <bean id="readWriteInterceptor" class="com.test.ReadWriteInterceptor">  
  16.   
  17.    <property name="readMethodList">  
  18.   
  19.      <list>  
  20.   
  21.        <value>query*</value>  
  22.   
  23.        <value>use*</value>  
  24.   
  25.        <value>get*</value>  
  26.   
  27.        <value>count*</value>  
  28.   
  29.        <value>find*</value>  
  30.   
  31.        <value>list*</value>  
  32.   
  33.        <value>search*</value>  
  34.   
  35.     </list>  
  36.   
  37.   </property>  
  38.   
  39. <property name="writeMethodList">  
  40.   
  41.     <list>  
  42.   
  43.         <value>save*</value>  
  44.   
  45.         <value>add*</value>  
  46.   
  47.         <value>create*</value>  
  48.   
  49.         <value>insert*</value>  
  50.   
  51.         <value>update*</value>  
  52.   
  53.         <value>merge*</value>  
  54.   
  55.         <value>del*</value>  
  56.   
  57.         <value>remove*</value>  
  58.   
  59.         <value>put*</value>  
  60.   
  61.         <value>write*</value>  
  62.   
  63.    </list>  
  64.   
  65. </property>  
  66.   
  67. </bean>  


把所有Mybatis接口类都放置在persistence下。配置的切面类是ReadWriteInterceptor。这样当Mapper接口的方法被调用时,会先调用这个切面类的readOrWriteDB方法。在这里需要注意<aop:aspect>中的order="1" 配置,主要是为了解决切面于切面之间的优先级问题,因为整个系统中不太可能只有一个切面类。

 

Spring AOP 切面类实现

[java] view plain copy
  1. public class ReadWriteInterceptor {  
  2.    private static final String DB_SERVICE = "dbService";  
  3.    private List<String> readMethodList = new ArrayList<String>();  
  4.    private List<String> writeMethodList = new ArrayList<String>();  
[java] view plain copy
  1.    public Object readOrWriteDB(ProceedingJoinPoint pjp) throws Throwable {  
  2.         String methodName = pjp.getSignature().getName();  
  3.         if (isChooseReadDB(methodName)) {  
  4.             //选择slave数据源  
  5.         } else if (isChooseWriteDB(methodName)) {  
  6.            //选择master数据源  
  7.         } else {  
  8.           //选择master数据源  
  9.         }  
  10.        return pjp.proceed();  
  11. }  
  12.   
  13.  private boolean isChooseWriteDB(String methodName) {  
  14.      for (String mappedName : this.writeMethodList) {  
  15.          if (isMatch(methodName, mappedName)) {  
  16.              return true;  
  17.          }  
  18.      }  
  19.     return false;  
  20. }  
  21.   
  22.  private boolean isChooseReadDB(String methodName) {  
  23.     for (String mappedName : this.readMethodList) {  
  24.        if (isMatch(methodName, mappedName)) {  
  25.            return true;  
  26.        }  
  27.     }  
  28.     return false;  
  29. }  
  30.   
  31.  private boolean isMatch(String methodName, String mappedName) {  
  32.     return PatternMatchUtils.simpleMatch(mappedName, methodName);  
  33. }  
  34.   
  35.  public List<String> getReadMethodList() {  
  36.     return readMethodList;  
  37.  }  
  38.   
  39.  public void setReadMethodList(List<String> readMethodList) {  
  40.    this.readMethodList = readMethodList;  
  41. }  
  42.   
  43.  public List<String> getWriteMethodList() {  
  44.     return writeMethodList;  
  45.  }  
  46.   
  47.  public void setWriteMethodList(List<String> writeMethodList) {  
  48.     this.writeMethodList = writeMethodList;  
  49. }  

 

覆盖DynamicDataSource类中的getConnection方法

ReadWriteInterceptor中的readOrWriteDB方法只是决定选择主还是从,我们还必须覆盖数据源的getConnection方法,以便获取正确的connection。一般来说,是一主多从,即一个master库,多个slave库的,所以还得解决多个slave库之间负载均衡、故障转移以及失败重连接等问题。

1、负载均衡问题,slave不多,系统并发读不高的话,直接使用随机数访问也是可以的。就是根据slave的台数,然后产生随机数,随机的访问slave。

2、故障转移,如果发现connection获取不到了,则把它从slave列表中移除,等其回复后,再加入到slave列表中

3、失败重连,第一次连接失败后,可以多尝试几次,如尝试10次。

处理业务方法中的@Transactional注解

我参与的这个项目,大部分业务代码是不需要事务的,只有极个别情况需要。那么按照上面提到的方案,如果不对业务方法中@Transactional注解进行特殊处理的话,主从的选择会出现问题。大家都知道,如果使用了Spring的事务,那么在同一个业务方法内,只会调用一次数据源的getConnection方法,如果该业务方法内,调用的mapper接口刚好以select开头的,就会选择slave库,那么接下来调用以insert开头的mapper接口方法时,会把数据写入到slave库。如何解决这个问题呢?必须在进入标有@Transactional注解的业务方法前,指定选择master主库。可以通过覆盖DataSourceTransactionManager中的doBegin方法,如下:

[java] view plain copy
  1. public class MyTransactionManager extendsDataSourceTransactionManager{  
  2.   
  3. @Override  
  4.   
  5. protected void doBegin(Object transaction, TransactionDefinitiondefinition) {  
  6.   
  7. //选择master数据库  
  8.   
  9. super.doBegin(transaction, definition);  
  10.   
  11. }  
  12.   
  13. }  

这样既可以避免,把数据写入到从库的问题。

 

总结

本人的解决方案是基于项目实际的,不一定合适你,我只是展示了解决方案而已。当然你可以选择开源的框架,像阿里的Cobar360Atlas

0 0
原创粉丝点击