侯体宗的博客
  • 首页
  • Hyperf版
  • beego仿版
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

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

数据库  /  管理员 发布于 6年前   129

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的切面主要的职责是拦截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。

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


  • 上一条:
    Hibernate识别数据库特有字段实例详解
    下一条:
    详解Spring Hibernate连接oracle数据库的配置
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • 分库分表的目的、优缺点及具体实现方式介绍(0个评论)
    • DevDB - 在 VS 代码中直接访问数据库(0个评论)
    • 在ubuntu系统中实现mysql数据存储目录迁移流程步骤(0个评论)
    • 在mysql中使用存储过程批量新增测试数据流程步骤(0个评论)
    • php+mysql数据库批量根据条件快速更新、连表更新sql实现(0个评论)
    • 近期文章
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(0个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 欧盟关于强迫劳动的规定的官方举报渠道及官方举报网站(0个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf文件功能(0个评论)
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • 在go + gin中gorm实现指定搜索/区间搜索分页列表功能接口实例(0个评论)
    • 在go语言中实现IP/CIDR的ip和netmask互转及IP段形式互转及ip是否存在IP/CIDR(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2017-06
    • 2017-08
    • 2017-09
    • 2017-10
    • 2017-11
    • 2018-01
    • 2018-05
    • 2018-10
    • 2018-11
    • 2020-02
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2020-07
    • 2020-08
    • 2020-09
    • 2021-02
    • 2021-04
    • 2021-07
    • 2021-08
    • 2021-11
    • 2021-12
    • 2022-02
    • 2022-03
    • 2022-05
    • 2022-06
    • 2022-07
    • 2022-08
    • 2022-09
    • 2022-10
    • 2022-11
    • 2022-12
    • 2023-01
    • 2023-03
    • 2023-04
    • 2023-05
    • 2023-07
    • 2023-08
    • 2023-10
    • 2023-11
    • 2023-12
    • 2024-01
    • 2024-03
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客