欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring AOP切面解决数据库读写分离实例详解

程序员文章站 2022-10-10 20:53:26
spring aop切面解决数据库读写分离实例详解 为了减轻数据库的压力,一般会使用数据库主从(master/slave)的方式,但是这种方式会给应用程序带来一定的麻烦,...

spring aop切面解决数据库读写分离实例详解

为了减轻数据库的压力,一般会使用数据库主从(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切面解决数据库读写分离实例详解
 

spring aop的切面主要的职责是拦截mybatis的mapper接口,通过判断mapper接口中的方法名称来决定主从。

 spring aop 切面配置

<aop:config expose-proxy="true"> 
 
<aop:pointcut id="txpointcut" expression="execution(* com.test..persistence..*.*(..))" /> 
 
<aop:aspect ref="readwriteinterceptor" order="1"> 
 
<aop:around pointcut-ref="txpointcut" method="readorwritedb"/> 
 
</aop:aspect> 
 
</aop:config> 
 
  
 
<bean id="readwriteinterceptor" class="com.test.readwriteinterceptor"> 
 
  <property name="readmethodlist"> 
 
   <list> 
 
    <value>query*</value> 
 
    <value>use*</value> 
 
    <value>get*</value> 
 
    <value>count*</value> 
 
    <value>find*</value> 
 
    <value>list*</value> 
 
    <value>search*</value> 
 
  </list> 
 
 </property> 
 
<property name="writemethodlist"> 
 
  <list> 
 
    <value>save*</value> 
 
    <value>add*</value> 
 
    <value>create*</value> 
 
    <value>insert*</value> 
 
    <value>update*</value> 
 
    <value>merge*</value> 
 
    <value>del*</value> 
 
    <value>remove*</value> 
 
    <value>put*</value> 
 
    <value>write*</value> 
 
  </list> 
 
</property> 
 
</bean> 

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

spring aop 切面类实现

public class readwriteinterceptor { 
  private static final string db_service = "dbservice"; 
  private list<string> readmethodlist = new arraylist<string>(); 
  private list<string> writemethodlist = new arraylist<string>(); 
  public object readorwritedb(proceedingjoinpoint pjp) throws throwable { 
    string methodname = pjp.getsignature().getname(); 
    if (ischoosereaddb(methodname)) { 
      //选择slave数据源 
    } else if (ischoosewritedb(methodname)) { 
      //选择master数据源 
    } else { 
     //选择master数据源 
    } 
    return pjp.proceed(); 
} 
 
 private boolean ischoosewritedb(string methodname) { 
   for (string mappedname : this.writemethodlist) { 
     if (ismatch(methodname, mappedname)) { 
       return true; 
     } 
   } 
  return false; 
} 
 
 private boolean ischoosereaddb(string methodname) { 
  for (string mappedname : this.readmethodlist) { 
    if (ismatch(methodname, mappedname)) { 
      return true; 
    } 
  } 
  return false; 
} 
 
 private boolean ismatch(string methodname, string mappedname) { 
  return patternmatchutils.simplematch(mappedname, methodname); 
} 
 
 public list<string> getreadmethodlist() { 
  return readmethodlist; 
 } 
 
 public void setreadmethodlist(list<string> readmethodlist) { 
  this.readmethodlist = readmethodlist; 
} 
 
 public list<string> getwritemethodlist() { 
  return writemethodlist; 
 } 
 
 public void setwritemethodlist(list<string> writemethodlist) { 
  this.writemethodlist = writemethodlist; 
} 
 

覆盖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方法,如下:

public class mytransactionmanager extendsdatasourcetransactionmanager{ 
 
@override 
 
protected void dobegin(object transaction, transactiondefinitiondefinition) { 
 
//选择master数据库 
 
super.dobegin(transaction, definition); 
 
} 
 
} 

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

总结

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

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!