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

我们只需要实现


  1. determineCurrentLookupKey()

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


  1. @Component
  2. @Primary
  3. public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
  4. @Resource(name = "masterDs")
  5. private DataSource masterDs;
  6. @Resource(name = "slaveDs")
  7. private DataSource slaveDs;
  8. @Override
  9. public void afterPropertiesSet() {
  10. Map<Object, Object> targetDataSources = new HashMap<>();
  11. targetDataSources.put("slaveDs", slaveDs);
  12. targetDataSources.put("masterDs", masterDs);
  13. this.setTargetDataSources(targetDataSources);
  14. this.setDefaultTargetDataSource(masterDs);
  15. super.afterPropertiesSet();
  16. }
  17. @Override
  18. protected Object determineCurrentLookupKey() {
  19. return DynamicDataSourceHolder.contextHolder.get();
  20. }
  21. /**
  22. * 持有当前线程所有数据源的程序
  23. */
  24. public static class DynamicDataSourceHolder {
  25. public static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
  26. public static void setWrite() {
  27. contextHolder.set("masterDs");
  28. }
  29. public static void setRead() {
  30. contextHolder.set("slaveDs");
  31. }
  32. public static void remove() {
  33. contextHolder.remove();
  34. }
  35. }
  36. }

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


  1. @Override
  2. public Connection getConnection() throws SQLException {
  3. return determineTargetDataSource().getConnection();
  4. }

以上代码,使用了ThreadLocal contextHolder来存取当前需要的datasource的loopkey

contextHolder上的值可能没有被初始化,此时contextHolder.get() 等于 null,此时会使用默认的datasource,我们设置的默认值对应的是主库


  1. /**
  2. * Retrieve the current target DataSource. Determines the
  3. * {@link #determineCurrentLookupKey() current lookup key}, performs
  4. * a lookup in the {@link #setTargetDataSources targetDataSources} map,
  5. * falls back to the specified
  6. * {@link #setDefaultTargetDataSource default target DataSource} if necessary.
  7. * @see #determineCurrentLookupKey()
  8. */
  9. protected DataSource determineTargetDataSource() {
  10. Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
  11. Object lookupKey = determineCurrentLookupKey();
  12. DataSource dataSource = this.resolvedDataSources.get(lookupKey);
  13. if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
  14. dataSource = this.resolvedDefaultDataSource;
  15. }
  16. if (dataSource == null) {
  17. throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
  18. }
  19. return dataSource;
  20. }

下一步,是在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层已经确定了它会查从库,以下方法是会执行失败的


  1. public class Manager1{
  2. ......
  3. public List getSome(params){
  4. dao1.getRecord1(params);
  5. dao2.updateLog(params);
  6. }
  7. }

因为dao2是一个更新请求,使用从库进行更新,肯定是会失败的

(使用Spring Boot,或Spring时,需确保开启AspectJ,Spring Boot开启方式 @EnableAspectJAutoProxy(proxyTargetClass = true)

aop示例

  1. @Aspect
  2. @Order(-10)
  3. @Component
  4. public class DataSourceAspect {
  5. public static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
  6. private static final String[] DefaultSlaveMethodStart
  7. = new String[]{"query", "find", "get", "select", "count", "list"};
  8. /**
  9. * 切入点,所有的mapper pcakge下面的类的方法
  10. */
  11. @Pointcut(value = "execution(* com.xx.xx.dao.mapper..*.*(..))")
  12. @SuppressWarnings("all")
  13. public void pointCutTransaction() {
  14. }
  15. /**
  16. * 根据方法名称,判断是读还是写
  17. * @param jp
  18. */
  19. @Before("pointCutTransaction()")
  20. public void doBefore(JoinPoint jp) {
  21. String methodName = jp.getSignature().getName();
  22. if (isReadReq(methodName)) {
  23. DynamicRoutingDataSource.DynamicDataSourceHolder.setRead();
  24. } else {
  25. DynamicRoutingDataSource.DynamicDataSourceHolder.setWrite();
  26. }
  27. }
  28. /**
  29. * 方法结束 finally 时执行
  30. * @param jp
  31. */
  32. @After("pointCutTransaction()")
  33. public void after(JoinPoint jp) {
  34. DynamicRoutingDataSource.DynamicDataSourceHolder.remove();
  35. }
  36. /**
  37. * 根据方法名,判断是否为读请求
  38. *
  39. * @param methodName
  40. * @return
  41. */
  42. private boolean isReadReq(String methodName) {
  43. for (String start : DefaultSlaveMethodStart) {
  44. if (methodName.startsWith(start)) {
  45. return true;
  46. }
  47. }
  48. return false;
  49. }
  50. }

上面的代码,根据方法名称前缀,反向判断哪些方法应该使用从库,凡是不匹配的方法,都走主库

方法结束后,必须清空contextHolder,否则他可能发生混乱,如这里是manager层 call dao层,dao层退出执行后,不清空contextHolder,则manager层开启事务时,会直接使用dao的值,如果这个请求是query,分配给从库了,那么manager层开启事务时就用的是从库了,结果可想而知

如此就完成了读写分离

spring 事务

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

  1. @Override
  2. public <T> T execute(TransactionCallback<T> action) throws TransactionException {
  3. if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
  4. return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
  5. }
  6. else {
  7. TransactionStatus status = this.transactionManager.getTransaction(this);
  8. T result;
  9. try {
  10. result = action.doInTransaction(status);
  11. }
  12. catch (RuntimeException ex) {
  13. // Transactional code threw application exception -> rollback
  14. rollbackOnException(status, ex);
  15. throw ex;
  16. }
  17. catch (Error err) {
  18. // Transactional code threw error -> rollback
  19. rollbackOnException(status, err);
  20. throw err;
  21. }
  22. catch (Throwable ex) {
  23. // Transactional code threw unexpected exception -> rollback
  24. rollbackOnException(status, ex);
  25. throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
  26. }
  27. this.transactionManager.commit(status);
  28. return result;
  29. }
  30. }

来发起事务,execute方法中

  1. this.transactionManager.getTransaction(this)

将获取事务需要的链接,其内部会读取ThreadLocal变量,判断是否有事务连接,如果已有,或允许嵌套事务,则会重复利用当前事务链接

如果当前请求已经被包含到了事务中,则根据策略,判断是否新开一个事务或不使用事务,它们的方法基本相同:通过创建一个新的连接来处理,并将当前已被打开的事务先挂起,等待当前操作执行结束后,再恢复外部事务,继续执行

TransactionSynchronizationManager类记录了这些ThreadLocal变量,允许将事务bind到其resources中,DatasourceTransactionManager则会触发这些操作


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

基于Spring读写分离的更多相关文章

  1. [Spring] - 读写分离

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

  2. spring读写分离(配置多数据源)[marked]

    我们今天的主角是AbstractRoutingDataSource,在Spring2.0.1发布之后,引入了AbstractRoutingDataSource,使用该类可以实现普遍意义上的多数据源管理 ...

  3. mysql主从之基于atlas读写分离

    一 mysql读写分离的概念 写在主库,主库一般只有一个,读可以分配在多个从库上,如果写压力不大的话,也能把读分配到主库上. 实现是基于atlas实现的,atlas是数据库的中间件,程序只需要连接at ...

  4. 基于Amoeba读写分离

    Amoeba 原理:amoeba相当于业务员,处理client的读写请求,并将读写请求分开处理.amoeba和master以及slave都有联系,如果是读的请求,amoeba就从slave读取信息反馈 ...

  5. spring读写分离

    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class ChooseData ...

  6. mysql基于Altas读写分离并实现高可用

    实验环境准备: master:192.168.200.111 slave1:192.168.200.112 slave2:192.168.200.113 Altas:192.168.200.114 c ...

  7. springboot读写分离--temp

    我最初的想法是: 读方法走读库,写方法走写库(一般是主库),保证在Spring提交事务之前确定数据源. 保证在Spring提交事务之前确定数据源,这个简单,利用AOP写个切换数据源的切面,让他的优先级 ...

  8. mysql 读写分离

    常见的读写分离方案:1)Amoeba读写分离2)MySQL-Proxy读写分离3)基于程序读写分离(效率很高,实施难度大,开发改代码) 2)原理 web 访问数据库,通过proxy4040端口作为转发 ...

  9. LAMP企业架构读写分离

    1.1  LAMP企业架构读写分离 LAMP+Discuz+Redis缓解了MYSQL的部分压力,但是如果访问量非常大,Redis缓存中第一次没有缓存数据,会导致MYSQL数据库压力增大,此时可以基于 ...

随机推荐

  1. 网络爬虫:利用selenium,pyquery库抓取并处理京东上的图片并存储到使用mongdb数据库进行存储

    一,环境的搭建已经简单的工具介绍 1.selenium,一个用于Web应用程序测试的工具.其特点是直接运行在浏览器中,就像真正的用户在操作一样.新版本selenium2集成了 Selenium 1.0 ...

  2. VUE3.0发布,自己搞个文档网站

    9月19日,尤大神发表了VUE3.0版本的演说,强大且震撼,这两天一直在找网站文档,可能还未被百度收录,未找到文档网站.后来在github上面找到了中文代码. 地址为:https://github.c ...

  3. Java Web学习(三)数据加密方式详解

    一.对称加密 定义:加密和解密使用相同密钥的算法. 常见的有DES.3DES.AES.PBE等加密算法,这几种算法安全性依次是逐渐增强的. DES加密 特点:简便.密钥长度比较短. import ja ...

  4. 深入解析Vue里函数的调用顺序介绍

    今天为大家分享一篇对vue里函数的调用顺序介绍,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下.如有不足之处,欢迎批评指正. method用来定义方法的,比如你@click=& ...

  5. Centos-退出抽取设备-eject

    eject 退出抽取设备,如光驱或磁带,如果设备已经挂载,则卸载设备 相关选项 -q 退出磁盘 -r 退出光盘 -d 显示默认设备

  6. 「面试」拿到B站的意向书

    此次B站服务端开发面试之旅可谓惊险,不过通过对大部分面试题套路的掌握,不出意外还是拿下了,下面我们来看看这些骚题是不是常见的不能再常见的了.这些面试题看了就能面上?当然不是,只是通过这些题让自己知道所 ...

  7. 【转载】C/走迷宫代码

    1 #include<iostream> 2 #include<windows.h> 3 #include"GotoXY.h" 4 #include < ...

  8. 030 01 Android 零基础入门 01 Java基础语法 03 Java运算符 10 条件运算符

    030 01 Android 零基础入门 01 Java基础语法 03 Java运算符 10 条件运算符 本文知识点:Java中的条件运算符 条件运算符是Java当中唯一一个三目运算符 什么是三目运算 ...

  9. 源码安装IVRE

    简介:IVRE(又名DRUNK)是一款开源的网络侦查框架工具,IVRE使用Nmap.Zmap进行主动网络探测.使用Bro.P0f等进行网络流量被动分析,探测结果存入数据库中,方便数据的查询.分类汇总统 ...

  10. matlab中的polyfit函数。

    来源:https://blog.csdn.net/zhaluo0051/article/details/77949170 :https://blog.csdn.net/g28_gwf/article/ ...