为什么是基于Spring的呢,因为实现方案基于Spring的事务以及AbstractRoutingDataSource(spring中的一个基础类,可以在其中放多个数据源,然后根据一些规则来确定当前需要使用哪个数据,既可以进行读写分离,也可以用来做分库分表)

我们只需要实现

determineCurrentLookupKey()

每次生成jdbc connection时,都会先调用该方法来甄选出实际需要使用的datasource,由于这个方法并没有参数,因此,最佳的方式是基于ThreadLocal变量

@Component
@Primary
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {@Resource(name = "masterDs")private DataSource masterDs;@Resource(name = "slaveDs")private DataSource slaveDs;@Overridepublic void afterPropertiesSet() {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put("slaveDs", slaveDs);targetDataSources.put("masterDs", masterDs);this.setTargetDataSources(targetDataSources);this.setDefaultTargetDataSource(masterDs);super.afterPropertiesSet();}@Overrideprotected Object determineCurrentLookupKey() {return DynamicDataSourceHolder.contextHolder.get();}/*** 持有当前线程所有数据源的程序*/public static class DynamicDataSourceHolder {public static final ThreadLocal<String> contextHolder = new ThreadLocal<>();public static void setWrite() {contextHolder.set("masterDs");}public static void setRead() {contextHolder.set("slaveDs");}public static void remove() {contextHolder.remove();}}
}

DynamicRoutingDataSource 仅仅是一个DataSource实现,更高层的类主要用它来创建连接,如getConnection(),getConnection会先寻找实际的datasource,在创建连接

@Overridepublic Connection getConnection() throws SQLException {return determineTargetDataSource().getConnection();}

以上代码,使用了ThreadLocal contextHolder来存取当前需要的datasource的loopkey
contextHolder上的值可能没有被初始化,此时contextHolder.get() 等于 null,此时会使用默认的datasource,我们设置的默认值对应的是主库

  /*** 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;}

下一步,是在sql请求前,先对contextHolder赋值,手动赋值工作量很大,并且容易出错,也没有办法规范

实现方案是基于Aop,根据方法名,或者方法注解来区分

  • 根据方法名的好处比较明显,只要工程的manager\dao层方法名称规范就行,不用到处添加注解,缺点是不精准
  • 根据注解来区分,缺点是需要在很多个类或接口上加注解,优点是精准

如果有特殊的业务,可以两种情况都使用,如以下场景:

  • 一个大的service,先后调用多个manager层\dao层sql,先insert、后query,并且想直接查主库,也就是写后立即查,由于mysql主从同步可能会有延迟,写后立即查可能会读到老数据,写后立即查的情况比较复杂,如果不是事务的话,实现其实比较复杂,如何在非事务场景下,让两个顺序执行的请求,保持同一个connection,需单独调研,根据实际情况进行修改

我们将设置contextHolder的地方加在了dao层,出于以下考量:

  • 透过manager层,直接调用dao时,无风险
  • 当前工程采用的是手动在manager层开启事务,开启事务lookupKey一定为null,采用默认的masterDs,没有问题,事务开启链接后,dao层的每个被调用的方法,会使用事务中的链接(由Spring Transaction控制)
  • 如果在manager层开启事务,manager层的方法名可能不规范,dao层是最接近sql请求的地方,规范更容易遵循

当然,这不是最佳实践,如果在manager层做这个事,也是可以的,看具体的情况,要求是,名称或注解表意为query的方法,里面不能做任何更新操作,因为manager层已经确定了它会查从库,以下方法是会执行失败的


public class Manager1{......public List getSome(params){dao1.getRecord1(params);dao2.updateLog(params);}}

因为dao2是一个更新请求,使用从库进行更新,肯定是会失败的
(使用Spring Boot,或Spring时,需确保开启AspectJ,Spring Boot开启方式 @EnableAspectJAutoProxy(proxyTargetClass = true)

aop示例

 @Aspect
@Order(-10)
@Component
public class DataSourceAspect {public static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);private static final String[] DefaultSlaveMethodStart= new String[]{"query", "find", "get", "select", "count", "list"};/*** 切入点,所有的mapper pcakge下面的类的方法*/@Pointcut(value = "execution(* com.xx.xx.dao.mapper..*.*(..))")@SuppressWarnings("all")public void pointCutTransaction() {}/*** 根据方法名称,判断是读还是写* @param jp*/@Before("pointCutTransaction()")public void doBefore(JoinPoint jp) {String methodName = jp.getSignature().getName();if (isReadReq(methodName)) {DynamicRoutingDataSource.DynamicDataSourceHolder.setRead();} else {DynamicRoutingDataSource.DynamicDataSourceHolder.setWrite();}}/*** 方法结束 finally 时执行* @param jp*/@After("pointCutTransaction()")public void after(JoinPoint jp) {DynamicRoutingDataSource.DynamicDataSourceHolder.remove();}/*** 根据方法名,判断是否为读请求** @param methodName* @return*/private boolean isReadReq(String methodName) {for (String start : DefaultSlaveMethodStart) {if (methodName.startsWith(start)) {return true;}}return false;}
}

上面的代码,根据方法名称前缀,反向判断哪些方法应该使用从库,凡是不匹配的方法,都走主库
方法结束后,必须清空contextHolder,否则他可能发生混乱,如这里是manager层 call dao层,dao层退出执行后,不清空contextHolder,则manager层开启事务时,会直接使用dao的值,如果这个请求是query,分配给从库了,那么manager层开启事务时就用的是从库了,结果可想而知

如此就完成了读写分离

spring 事务

当前使用的是编程声明式事务

    @Overridepublic <T> T execute(TransactionCallback<T> action) throws TransactionException {if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);}else {TransactionStatus status = this.transactionManager.getTransaction(this);T result;try {result = action.doInTransaction(status);}catch (RuntimeException ex) {// Transactional code threw application exception -> rollbackrollbackOnException(status, ex);throw ex;}catch (Error err) {// Transactional code threw error -> rollbackrollbackOnException(status, err);throw err;}catch (Throwable ex) {// Transactional code threw unexpected exception -> rollbackrollbackOnException(status, ex);throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");}this.transactionManager.commit(status);return result;}}

来发起事务,execute方法中

    this.transactionManager.getTransaction(this)

将获取事务需要的链接,其内部会读取ThreadLocal变量,判断是否有事务连接,如果已有,或允许嵌套事务,则会重复利用当前事务链接
如果当前请求已经被包含到了事务中,则根据策略,判断是否新开一个事务或不使用事务,它们的方法基本相同:通过创建一个新的连接来处理,并将当前已被打开的事务先挂起,等待当前操作执行结束后,再恢复外部事务,继续执行
TransactionSynchronizationManager类记录了这些ThreadLocal变量,允许将事务bind到其resources中,DatasourceTransactionManager则会触发这些操作

@Overrideprotected Object doSuspend(Object transaction) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;txObject.setConnectionHolder(null);return TransactionSynchronizationManager.unbindResource(this.dataSource);  //挂起事务的基本原理:将外部事务放到新建事务(可能是非事务)的suspendedResources上来进行挂起}@Overrideprotected void doResume(Object transaction, Object suspendedResources) {TransactionSynchronizationManager.bindResource(this.dataSource, suspendedResources);  //将suspendedResources重新绑定到threadLocal变量}

转载于:https://www.cnblogs.com/windliu/p/8920467.html

基于Spring读写分离相关推荐

  1. mysql读写分离插件_基于mybatis读写分离插件

    我相信很多朋友都尝试写过读写分离插件,或者项目中用到过.首先读写分离的职责应该属于数据访问层而不是业务层,其次读写分离不应该侵入我们代码层中.因此在 service-dao-orm- 数据库驱动调用链 ...

  2. 基于mybatis读写分离插件

    我相信很多朋友都尝试写过读写分离插件,或者项目中用到过.首先读写分离的职责应该属于数据访问层而不是业务层,其次读写分离不应该侵入我们代码层中.因此在 service-dao-orm- 数据库驱动调用链 ...

  3. 基于mysql-proxy读写分离

    一.可以使用rpm包安装,需要安装epel软件包库,或者直接从官网下载rpm包: 二.安装配置mysql-proxy: 2.1 下载所需要的版本 这里的系统平台为rhel6.4 32位系统,因此就以m ...

  4. [Spring] - 读写分离

    使用Spring可以做到在应用层中实现数据库的读写分离. 参考文档: http://blog.csdn.net/lifuxiangcaohui/article/details/7280202 思路是使 ...

  5. 50、mysql基于mysql-proxy读写分离实战

    一.主从配置 192.168.130.61 master 192.168.130.62 slave 192.168.130.63 proxy master配置 [mysqld] socket=/tmp ...

  6. mysql读写分离_MySQL基于amoeba读写分离实验

    =========================================== 主从复制只是一个同步数据的方式 读写分离:只在主的上面写,只在从的上面读 读写分离方案:[1]基于程序代码内部 ...

  7. Mysql基于Amoeba_读写分离搭架

    一.Amoeba简介 Amoeba是一个以MySQL为底层数据存储,并对应用提供MySQL协议接口的proxy.它集中地响应应用的请求,依据用户事先设置的规则,将SQL请求发送到特定的数据库上执行.基 ...

  8. 基于pgpool-II读写分离+postgresql10主从从流复制高可用部署方案

    一.环境准备 1.准备3台centos服务器: pgpool-II版本:pgpool-II-pg10-4.2.0-1pgdg postgresql版本:postgresql10 Server01:10 ...

  9. amoeba mysql读写分离_mysql数据库-基于amoeba读写分离

    环境: 主机A( huangzp2):172.16.115.157 主机B( huangzp3):172.16.115.100 主机C( huangzp4):172.16.115.87 说明: 依赖于 ...

最新文章

  1. 【工具篇】利用DBExportDoc V1.0 For MySQL自动生成数据库表结构文档(转
  2. 一步一步学Ruby(七):数学表达式
  3. mysql需要下载调试_Mysql安装和调试
  4. python 去空_Python 内存分配时的小秘密
  5. java rc4_nodejs 和 java 进行 rc4 加密得到的结果不一样
  6. 如何使用CPU来加速你的Linux命令
  7. [初级]Java命令学习系列(六)——jinfo
  8. 记录一次OOM排查经历
  9. java安卓如何实现定义接口
  10. Facebook怎样开发软件:工程师驱动的文化
  11. 计算机添加usb网络打印机,USB路由器设置网络打印机方法
  12. Vue2 + ElementUI登录界面模板
  13. App渠道安装来源识别与匹配
  14. 学习Axure RP原型设计
  15. bzoj4816 [Sdoi2017]数字表格(反演)
  16. vim 安装YouCompleteMe 自动补全插件
  17. 用python绘制熊猫图案,在python中绘制熊猫系列的CDF
  18. Windows定期删除过期文件
  19. C#XML序列化特性全中文教程
  20. 版权领域的发展趋势对版权保护有哪些重要意义?

热门文章

  1. 每天一道LeetCode-----实现LFU置换算法
  2. 网络虚拟化技术(一): linux网络虚拟化,网络虚拟化技术(一): Linux网络虚拟化...
  3. 内存拷贝函数的深入思考
  4. HDU 2050 折线分割平面
  5. java swing刷新_Swing界面刷新问题(转)
  6. 11.考虑用排序的vector替代关联容器
  7. 关于android的外文论文,关于android的外文文献.doc
  8. 视频播放器的界面设计并实现播放器
  9. NDK建立多个共享库
  10. java 2wei shuzu_JavaScript 2维数组(JavaScript 2 dimension array)