一.为什么要进行读写分离呢?

  因为数据库的“写操作”操作是比较耗时的(写上万条条数据到Mysql可能要1分钟分钟)。但是数据库的“读操作”却比“写操作”耗时要少的多(从Mysql读几万条数据条数据可能只要十秒钟)。

所以读写分离解决的是,数据库的“写操作”影响了查询的效率问题。

如下图所示:

读写分离: 大多数站点的数据库读操作比写操作更加密集,而且查询条件相对复杂,数据库的大部分性能消耗在查询操作上了。为保证数据库数据的一致性,我们要求所有对于数据库的更新操作都是针对主数据库的,但是读操作是可以针对从数据库来进行。

如下图所示:

以下进行一个代码层面的自动切换数据源进行读写分离的例子。

 第一。首先搭建一个SSM框架的web工程。省略。

jdb.properties配置如下:

  1. #主数据库连接
  2. jdbc_url_m=jdbc:mysql://localhost:3306/mama-bike?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
  3. #两个从数据库连接
  4. jdbc_url_s_1=jdbc:mysql://localhost:3307/mama-bike?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
  5. jdbc_url_s_2=jdbc:mysql://localhost:3308/mama-bike?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull
  6. jdbc_username=root
  7. jdbc_password=root

web.xml配置省略

第二。spring-cfg.xml文件中配置一个主数据源,两个从数据源,具体配置如下:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context"
  5. xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop"
  6. xsi:schemaLocation="http://www.springframework.org/schema/beans
  7. http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
  8. http://www.springframework.org/schema/context
  9. http://www.springframework.org/schema/context/spring-context-4.0.xsd
  10. http://www.springframework.org/schema/tx
  11. http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
  12.  
  13. <!--扫描注解生成bean-->
  14. <context:annotation-config/>
  15. <!--包扫描-->
  16. <context:component-scan base-package="com.coder520"/>
  17.  
  18. <context:property-placeholder location="classpath:jdbc.properties"/>
  19.  
  20. <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  21. <property name="dataSource" ref="dataSource"/>
  22. <property name="mapperLocations" value="classpath:com/coder520/**/**.xml"/>
  23. </bean>
  24.  
  25. <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
  26. <property name="basePackage" value="com.coder520.*.dao"/>
  27. <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
  28. </bean>
  29.  
  30. <!--声明事务管理 采用注解方式-->
  31. <tx:annotation-driven transaction-manager="transactionManager"/>
  32. <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  33. <property name="dataSource" ref="dataSource"/>
  34. </bean>
  35.  
  36. <!--开启切面代理-->
  37. <aop:aspectj-autoproxy/>
  38.  
  39. <!--切换数据源切面-->
  40. <bean id="switchDataSourceAspect" class="com.coder520.common.DataSourceAspect"/>
  41.  
  42. <!--切面配置-->
  43. <aop:config>
  44. <aop:aspect ref="switchDataSourceAspect">
  45. <aop:pointcut id="tx" expression="execution(* com.coder520.*.service.*.*(..))"/>
  46. <aop:before method="before" pointcut-ref="tx"/>
  47. </aop:aspect>
  48. </aop:config>
  49.  
  50. <!--主数据库设置-->
  51. <bean id="masterdataSource" class="com.alibaba.druid.pool.DruidDataSource"
  52. destroy-method="close" init-method="init">
  53. <property name="url" value="${jdbc_url_m}"/>
  54. <property name="username" value="${jdbc_username}"/>
  55. <property name="password" value="${jdbc_password}"/>
  56. </bean>
  57. <!--从数据库设置-->
  58. <bean id="slavedataSource_1" class="com.alibaba.druid.pool.DruidDataSource"
  59. destroy-method="close" init-method="init">
  60. <property name="url" value="${jdbc_url_s_1}"/>
  61. <property name="username" value="${jdbc_username}"/>
  62. <property name="password" value="${jdbc_password}"/>
  63. </bean>
  64. <!--从数据库设置-->
  65. <bean id="slavedataSource_2" class="com.alibaba.druid.pool.DruidDataSource"
  66. destroy-method="close" init-method="init">
  67. <property name="url" value="${jdbc_url_s_2}"/>
  68. <property name="username" value="${jdbc_username}"/>
  69. <property name="password" value="${jdbc_password}"/>
  70. </bean>
  71. <bean id="dataSource" class="com.coder520.common.DynamicDataSource">
  72. <property name="targetDataSources">
  73. <map>
  74. <entry key="master" value-ref="masterdataSource"/>
  75. <entry key="slave_1" value-ref="slavedataSource_1"/>
  76. <entry key="slave_2" value-ref="slavedataSource_2"/>
  77. </map>
  78. </property>
  79. <!--默认数据源为主数据库-->
  80. <property name="defaultTargetDataSource" ref="masterdataSource"/>
  81. </bean>
  82.  
  83. </beans>

spring-mvc.xml配置如下:

  

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:context="http://www.springframework.org/schema/context"
  4. xmlns:mvc="http://www.springframework.org/schema/mvc"
  5. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  6. xmlns:aop="http://www.springframework.org/schema/aop"
  7. xsi:schemaLocation="http://www.springframework.org/schema/beans
  8. http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
  9. http://www.springframework.org/schema/context
  10. http://www.springframework.org/schema/context/spring-context.xsd
  11. http://www.springframework.org/schema/aop
  12. http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
  13. http://www.springframework.org/schema/mvc
  14. http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">
  15.  
  16. <!--开启切面编程自动代理-->
  17.  
  18. <mvc:annotation-driven>
  19. <mvc:message-converters>
  20. <bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
  21. <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
  22. <property name="supportedMediaTypes">
  23. <list>
  24. <value>text/html;charset=UTF-8</value>
  25. <value>application/json;charset=UTF-8</value>
  26. </list>
  27. </property>
  28. </bean>
  29. </mvc:message-converters>
  30. </mvc:annotation-driven>
  31. <!--包扫描-->
  32. <context:component-scan base-package="com.coder520.*.controller">
  33. </context:component-scan>
  34.  
  35. <!--开启注解扫描-->
  36. <mvc:annotation-driven/>
  37. <!--处理静态资源-->
  38. <mvc:default-servlet-handler/>
  39.  
  40. <bean id="velocityConfigurer" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
  41. <property name="resourceLoaderPath" value="/WEB-INF/views"/>
  42. <property name="velocityProperties">
  43. <props>
  44. <prop key="input.encoding">utf-8</prop>
  45. <prop key="output.encoding">utf-8</prop>
  46. <prop key="file.resource.loader.cache">false</prop>
  47. <prop key="file.resource.loader.modificationCheckInterval">1</prop>
  48. <prop key="velocimacro.library.autoreload">false</prop>
  49. </props>
  50. </property>
  51. </bean>
  52. <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
  53. <property name="exceptionMappings">
  54. <props>
  55. <prop key="org.apache.shiro.authz.UnauthorizedException">403</prop>
  56. </props>
  57. </property>
  58. </bean>
  59. <bean class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
  60. <property name="suffix" value=".vm"/>
  61. <property name="contentType" value="text/html;charset=utf-8"/>
  62. <property name="dateToolAttribute" value="date"/><!--日期函数名称-->
  63. </bean>
  64.  
  65. </beans>

Spring提供了一个AbstractRoutingDataSource这个类来帮我们切换数据源。故名思意,Routing,是路由的意思,可以帮我们切换到我们想切换到的数据库。因此我们需要自己创建一个类来继承它。

我们再进入看一下AbstractRoutingDataSource源码是如何实现。

里面的方法到底干嘛用的,都在源码里面写明注释,并且标记执行顺序。如下://

  1. // Source code recreated from a .class file by IntelliJ IDEA
  2. // (powered by Fernflower decompiler)
  3. //
  4.  
  5. package org.springframework.jdbc.datasource.lookup;
  6.  
  7. import java.sql.Connection;
  8. import java.sql.SQLException;
  9. import java.util.HashMap;
  10. import java.util.Iterator;
  11. import java.util.Map;
  12. import java.util.Map.Entry;
  13. import javax.sql.DataSource;
  14. import org.springframework.beans.factory.InitializingBean;
  15. import org.springframework.jdbc.datasource.AbstractDataSource;
  16. import org.springframework.util.Assert;
  17.  
  18. public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
      //装载spring-cfg.xml中配置的那三个数据源。
  19. private Map<Object, Object> targetDataSources;
      //默认数据源
  20. private Object defaultTargetDataSource;
      //出错回滚
  21. private boolean lenientFallback = true;
      //Map中各个数据源对应的key
  22. private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
      //装载Map<Object,Object> targetDataSources,即一个MAP装载一个旧MAP
  23. private Map<Object, DataSource> resolvedDataSources;
      //这属性是为了得到defaultTargetDataSource,
  24. private DataSource resolvedDefaultDataSource;
  25.  
  26. public AbstractRoutingDataSource() {
  27. }

  28.    //1.装载spring-cfg.xml中配置的那三个数据源
  29. public void setTargetDataSources(Map<Object, Object> targetDataSources) {
  30. this.targetDataSources = targetDataSources;
  31. }
  32.    //1.设置默认数据源
  33. public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
  34. this.defaultTargetDataSource = defaultTargetDataSource;
  35. }
  36.   
  37. public void setLenientFallback(boolean lenientFallback) {
  38. this.lenientFallback = lenientFallback;
  39. }
  40. public void setDataSourceLookup(DataSourceLookup dataSourceLookup) {
  41. this.dataSourceLookup = (DataSourceLookup)(dataSourceLookup != null?dataSourceLookup:new JndiDataSourceLookup());
  42. }

  43.   // 2.根据spring-cfg.xml中配置targetDataSources可以在afterPropertiesSet方法中对targetDataSources进行解析,获取真正的datasources
  44. public void afterPropertiesSet() {
  45. if(this.targetDataSources == null) {
  46. throw new IllegalArgumentException("Property 'targetDataSources' is required");
  47. } else {
           //新建一个跟MAP targetDataSource一样的MAP
  48. this.resolvedDataSources = new HashMap(this.targetDataSources.size());
            //遍历MAP
  49. Iterator var1 = this.targetDataSources.entrySet().iterator();
  50.         //判断MAP中是否还有数据源
  51. while(var1.hasNext()) {
              //获取数据源Entry
  52. Entry<Object, Object> entry = (Entry)var1.next();
              //设置每一个数据源Entry对应的key
  53. Object lookupKey = this.resolveSpecifiedLookupKey(entry.getKey());
              //设置数据源Entry对应的value,即数据源
  54. DataSource dataSource = this.resolveSpecifiedDataSource(entry.getValue());
              //放入到新建的MAP中
  55. this.resolvedDataSources.put(lookupKey, dataSource);
  56. }
  57.         //设置默认数据源
  58. if(this.defaultTargetDataSource != null) {
  59. this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);
  60. }
  61.  
  62. }
  63. }
  64.  
  65. protected Object resolveSpecifiedLookupKey(Object lookupKey) {
  66. return lookupKey;
  67. }
  68.  
  69. protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
  70. if(dataSource instanceof DataSource) {
  71. return (DataSource)dataSource;
  72. } else if(dataSource instanceof String) {
  73. return this.dataSourceLookup.getDataSource((String)dataSource);
  74. } else {
  75. throw new IllegalArgumentException("Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
  76. }
  77. }
  78.  
  79. public Connection getConnection() throws SQLException {
  80. return this.determineTargetDataSource().getConnection();
  81. }
  82.  
  83. public Connection getConnection(String username, String password) throws SQLException {
  84. return this.determineTargetDataSource().getConnection(username, password);
  85. }
  86.  
  87. public <T> T unwrap(Class<T> iface) throws SQLException {
  88. return iface.isInstance(this)?this:this.determineTargetDataSource().unwrap(iface);
  89. }
  90.  
  91. public boolean isWrapperFor(Class<?> iface) throws SQLException {
  92. return iface.isInstance(this) || this.determineTargetDataSource().isWrapperFor(iface);
  93. }

  94.   //3.最关键的一个方法。此方法决定选择哪一个数据源
  95. protected DataSource determineTargetDataSource() {
  96. Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        //决定选择数据源的key,即传进来的那个数据源
  97. Object lookupKey = this.determineCurrentLookupKey();
        //获取相应的数据源
  98. DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
        //如果为空,就用默认的那个数据源
  99. if(dataSource == null && (this.lenientFallback || lookupKey == null)) {
  100. dataSource = this.resolvedDefaultDataSource;
  101. }
  102.     //如果默认数据源还是为空,证明没配置默认数据源,就会抛异常
  103. if(dataSource == null) {
  104. throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
  105. } else {
  106. return dataSource;
  107. }
  108. }
  109.   //这是最重要的方法,要我们实现改方法的。
  110. protected abstract Object determineCurrentLookupKey();
  111. }
  1. 因此实现该determineCurrentLookupKey()方法:首先自己创建的类要继承AbstractRoutingDataSource
  1.  
  2. 如下代码
  1. package com.coder520.common;
  2.  
  3. import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
  4.  
  5. /**
    * Created by cong on 2018/3/14.
    */
    public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
    return DynamicDataSourceHolder.getDataSource();
    }
    }
  1. DynamicDataSourceHolder.getDataSource()是获取数据源。但是呢,spring中的数据源是唯一,每一个用户过来都是共用这个数据源的。我们知道高并发的情况下,多个用户共享一个资源,这是有线程问题的,这样获取数据源是不安全的。
  1. 因此我们要用到并发编程问题呢,我们要用到并发编程里面的一个类ThreadLocal这个类,这个类用来ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过getset方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。
    ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。
  2.  
  3. 那么我们在两个从库中进行读操作如何公平的分配来读操作呢?我们自然想到要有轮询的思维。通过一个计时器来自增求模运算。这个计时器的只从-1开始,这样得到的结果就只有01了,根据0 1来分配两个从库进行读操作。
    注意这个计时器如果用Inter类型的话,必然会出现线程安全问题的,因为这是共享的数据类型。因此我们可以用并发编程里面的AtomicInterger原子属性的类。解决线程安全问题。我们知道Integer是有范围的,我们不能让
    这个计数器一直自增,这样下去会去问题的。因此还需要来一个计数器重置。
  1. DynamicDataSourceHolder类代码如下:
  1. package com.coder520.common;
  2.  
  3. import java.util.concurrent.atomic.AtomicInteger;
  4.  
  5. /**
  6. * Created by cong on 2018/3/14.
  7. */
  8. public class DynamicDataSourceHolder {
  9.  
  10. //绑定本地线程
  11. public static final ThreadLocal<String> holder = new ThreadLocal<>();
  12.  
  13. //计数器
  14. private static AtomicInteger counter = new AtomicInteger(-1);
  15.  
  16. //写库对应的数据源Key
  17. private static final String MASTER = "master";
  18.  
  19. //从库对应的数据源key
  20. private static final String SLAVE_1 = "slave_1";
  21. private static final String SLAVE_2 = "slave_2";
  22.  
  23. //设置数据源,判断传进来的主库还是从库的类型
  24. public static void setDataSource(DataSourceType dataSourceType){
  25. if (dataSourceType == DataSourceType.MASTER){
  26. System.out.println("-----MASTER------");
  27. holder.set(MASTER);
  28. }else if (dataSourceType == DataSourceType.SLAVE){
  29. holder.set(roundRobinSlaveKey());
  30. }
  31. }
  32. //获取数据源
  33. public static String getDataSource(){
  34. return holder.get();
  35. }
  36.  
  37. //轮询选择哪一个从数据库去读操作
  38. private static String roundRobinSlaveKey() {
  39. //计数器模运算
  40. Integer index = counter.getAndIncrement() % 2;
  41. //计数器重置
  42. if (counter.get()>9999){
  43. counter.set(-1);
  44. }
  45. //轮询判断
  46. if (index == 0){
  47. System.out.println("----SLAVE_1-----");
  48. return SLAVE_1;
  49. }else {
  50. System.out.println("----SLAVE_2-----");
  51. return SLAVE_2;
  52. }
  53.  
  54. }
  55.  
  56. }

DataSourceType是一个枚举类型,这些这样写是让代码美观一些。

DataSourceType枚举类型代码如下:

  1. package com.coder520.common;
  2.  
  3. /**
  4. */
  5. public enum DataSourceType {
  6.  
  7. MASTER,SLAVE;
  8.  
  9. }

到这里已经万事具备了,到了关键一步了,那么我们什么时候切换数据源呢?我怎么切换数据源呢?

我们要切换数据源的时候我们手动去控制它,我们希望在业务层打一个注解,比如现在我们需要读库了,业务层的方法都是读库了,我们只要打一个注解就把它搞定,例如@DataSource(DataSourceType.SLAVE),

然后让DynamicDataSourceHolder这个类自动帮我们切换一下,用它setDataSource(DataSourceType dataSourceType)方法将数据源设置成SLAVE.这样读操作就走读库了。

那么问题来了,我们想达到这个效果,那改怎么办呢?那么首先我们要定义一个注解。

那么又有疑问了,为什么我们不在每一个查询的方法里面调用DynamicDataSourceHolder.setDataSource(DataSourceType dataSourceType)方法设置一下不就行了吗?

这样做也可以,但是这样做岂不是很蛋疼?因为这样做代码就不够优雅了,要重复写很多代码。每一个查询方法里面都这样写,岂不是烦死?

因此我们自定义一个注解,代码如下:

  1. package com.coder520.common;
  2.  
  3. import java.lang.annotation.ElementType;
  4. import java.lang.annotation.Retention;
  5. import java.lang.annotation.RetentionPolicy;
  6. import java.lang.annotation.Target;
  7.  
  8. /**
  9. * Created by cong on 2018/3/14.
  10. */
  11.  
  12. //运行时影响程序注解
  13. @Retention(RetentionPolicy.RUNTIME)
    //这个注解作用于所有方法
  14. @Target({ElementType.METHOD})
  15. public @interface DataSource {
  16. //打了这个注解,如果没设置值,我们就默认用MASTER主库
  17. DataSourceType value() default DataSourceType.MASTER;
  18.  
  19. }

那么我们到这里就OK了吗?并不是的,我们只是打了个注解,还没进行数据源的切换呢。然后做呢?

这时我们就要用切面编程AOP方法来执行所有的切面,我们切哪个方法呢?我们切所有的业务层,service层的方法,然后获取到它的注解,看一下注解标记的是MASTER,还是SLAVE

然后调用DynamicDataSourceHolder.setDataSource(DataSourceType dataSourceType)方法设置一下就行了。这是正是切面编程大显身手的时候,切面编程让我们一段代码让我们给每一个方法执行一段业务逻辑,

减少我们的代码量。

我们都是AOP有前置通知,后置通知,环绕通知,我们在这里一定要用前置通知,因为进入方法前就一定先要切换数据源,方法执行完了,再切换数据源还有个屁用。

DataSourceAspect切面类的代码如下:

  1. package com.coder520.common;
  2.  
  3. import org.aspectj.lang.JoinPoint;
  4. import org.aspectj.lang.reflect.MethodSignature;
  5.  
  6. import java.lang.reflect.Method;
  7.  
  8. /***/
  9. public class DataSourceAspect {
  10.  
  11. public void before(JoinPoint point) throws NoSuchMethodException {
  12. //获取切点
  13. Object target = point.getTarget();
  14. //获取方法的名字
  15. String method = point.getSignature().getName();
  16. //获取字节码对象
  17. Class classz = target.getClass();
  18. //获取方法上的参数
  19. Class<?>[] parameterTypes = ((MethodSignature)point.getSignature()).getMethod().getParameterTypes();
  20. //获取方法
  21. Method m = classz.getMethod(method,parameterTypes);
  22. //判断方法是否存在,并且判断是否有DataSource这个注释。
  23. if (m != null && m.isAnnotationPresent(DataSource.class)){
  24. //获取注解
  25. DataSource dataSource = m.getAnnotation(DataSource.class);
  26. //设置数据源
  27. DynamicDataSourceHolder.setDataSource(dataSource.value());
  28. }
  29. }
  30.  
  31. }
  1. 注意:必须在spirng-cfg.xml中声明切面这个BEAN,并指定切哪里。
    如下:
  1. <!--开启切面代理-->
  2. <aop:aspectj-autoproxy/>
  3.  
  4. <!--切换数据源切面Bean-->
  5. <bean id="switchDataSourceAspect" class="com.coder520.common.DataSourceAspect"/>
  6.  
  7. <!--切面配置-->
  8. <aop:config>
  9. <aop:aspect ref="switchDataSourceAspect">
  10. <aop:pointcut id="tx" expression="execution(* com.coder520.*.service.*.*(..))"/>
  11. <aop:before method="before" pointcut-ref="tx"/>
  12. </aop:aspect>
  13. </aop:config>
到这里就完成了,接着就是测试了。
我们简单的进行CURD来测试一下:
如下 业务层代码:
  1. package com.coder520.user.service;
  2.  
  3. import com.coder520.common.DataSource;
  4. import com.coder520.common.DataSourceType;
  5. import com.coder520.common.DynamicDataSourceHolder;
  6. import com.coder520.user.dao.UserMapper;
  7. import com.coder520.user.entity.User;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.stereotype.Service;
  10. import org.springframework.transaction.annotation.Transactional;
  11.  
  12. import java.io.UnsupportedEncodingException;
  13. import java.security.NoSuchAlgorithmException;
  14.  
  15. @Service("userServiceImpl")
  16. public class UserServiceImpl implements UserService{
  17.  
  18. @Autowired
  19. private UserMapper userMapper;
  20.  
  21. /**
  22. *@Description 根据用户名查询用户
  23. */
  24. @DataSource(DataSourceType.SLAVE)
  25. @Override
  26. public User findUserByUserId(long id) {
  27. User user=null;
  28. try {
  29. user =userMapper.selectByPrimaryKey(id);
  30. }catch (Exception e){
  31. e.printStackTrace();
  32. throw e;
  33. }
  34. return user;
  35. }
  36.  
  37. @Override
  38. @Transactional
  39. public int insertUser() {
  40. User user = new User();
  41. user.setMobile("1234567");
  42. user.setNickname("laowang");
  43. User user1 = new User();
  44. user1.setId(2L);
  45. user1.setMobile("11111111");
  46. user1.setNickname("laowang2");
  47. userMapper.insertSelective(user);
  48. userMapper.insertSelective(user1);
  49. return 0;
  50. }
  51.  
  52. @Override
  53. public void createUser(User user) {
  54. userMapper.insertSelective(user);
  55. }
  56. }

Controller层代码:

  1. package com.coder520.user.controller;
  2.  
  3. import com.coder520.user.entity.User;
  4. import com.coder520.user.service.UserService;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.stereotype.Controller;
  7. import org.springframework.ui.Model;
  8. import org.springframework.web.bind.annotation.RequestMapping;
  9. import org.springframework.web.bind.annotation.ResponseBody;
  10. import org.springframework.web.servlet.ModelAndView;
  11.  
  12. import javax.servlet.http.HttpSession;
  13.  
  14. /***/
  15. @Controller
  16. @RequestMapping("user")
  17. public class UserController {
  18.  
  19. @Autowired
  20. private UserService userService;
  21.  
  22. /**
  23. *@Description 获取用户信息
  24. */
  25. @RequestMapping("/getuser")
  26. @ResponseBody
  27. public User getUser(){
  28. return userService.findUserByUserId(1);
  29. }
  30.  
  31. @RequestMapping("/setuser")
  32. @ResponseBody
  33. public int setUser(){
  34. return userService.insertUser();
  35. }
  36.  
  37. }

mybatis那部分的代码省略。

运行结果如下:

 
  1.  

可以看到两个SLVE是轮询切换的。

接着自己可以测试一下插入,修改数据源,是否切换到主库中。查看3个数据库是否同步了,这里就不演示了。

就算中途出错,事务会回滚的。这里不演示了,自己可以去试一下。

主从复制数据是异步完成的,这就导致主从数据库中的数据有一定的延迟,在读写分离的设计中必须要考虑这一点。

以博客为例,用户登录后发表了一篇文章,他需要马上看到自己的文章,但是对于其它用户来讲可以允许延迟一段时间(1分钟/5分钟/30分钟),不会造成什么问题。

这时对于当前用户就需要读主数据库,对于其他访问量更大的外部用户就可以读从数据库。

解决办法:

      适当放弃一致性:在一些实时性要求不高的场合,我们适当放弃一致性要求。这样就可以充分利用多种手段来提高系统吞吐量,例如页面缓存(cookie,session)、分布式数据缓存(redis)、数据库读写分离、查询数据搜索索引化。

总结:

  我的想法是要使用读写分离来实现系统吞吐量的提升就要从业务上想办法降低一致性的要求。

  对必须要有一致性的功能是无法进行读写分离的,可以采用多库不区分读写以及redis缓存等技术来实现。

  所以主从分离后,去从数据库读的话,可能还没同步过来。

下一篇用中间件来屏蔽掉这些复杂的操作来进行数据源切换

MySQL多数据源笔记2-Spring多数据源一主多从读写分离(手写)的更多相关文章

  1. spring+hibernate 配置多个数据源过程 以及 spring中数据源的配置方式

    spring+hibernate 配置多个数据源过程 以及 spring中数据源的配置方式[部分内容转载] 2018年03月27日 18:58:41 守望dfdfdf 阅读数:62更多 个人分类: 工 ...

  2. spring+mybatis利用interceptor(plugin)兑现数据库读写分离

    使用spring的动态路由实现数据库负载均衡 系统中存在的多台服务器是"地位相当"的,不过,同一时间他们都处于活动(Active)状态,处于负载均衡等因素考虑,数据访问请求需要在这 ...

  3. Mysql + keepalived 实现双主热备读写分离【转】

    Mysql + keepalived 实现双主热备读写分离 2013年6月16日frankwong发表评论阅读评论   架构图 系统:CentOS6.4_X86_64软件版本:Mysql-5.6.12 ...

  4. 各种数据库(oracle、mysql、sqlserver等)在Spring中数据源的配置和JDBC驱动包----转

    在开发基于数据库的应用系统时,需要在项目中进行数据源的配置来为数据 库的操作取得数据库连接.配置不同数据库的数据源的方法大体上都是相同的,不同的只是不同数据库的JDBC驱动类和连接URL以及相应的数据 ...

  5. spring boot sharding-jdbc实现分佈式读写分离和分库分表的实现

    分布式读写分离和分库分表采用sharding-jdbc实现. sharding-jdbc是当当网推出的一款读写分离实现插件,其他的还有mycat,或者纯粹的Aop代码控制实现. 接下面用spring ...

  6. spring mongodb 复制集配置(实现读写分离)

    注:mongodb当前版本是3.4.3   spring连接mongodb复制集的字符串格式: mongodb://[username:password@]host1[:port1][,host2[: ...

  7. MySQL自动化安装(双主多从读写分离)

    shell #!/bin/bash # Create by # version 1.0 # // # # check out lockfile whether or not exist IsInput ...

  8. mycat 1.6.6.1安装以及配置docker 安装mysql 5.7.24 双主多从读写分离主主切换

    mycat和mysql的高可用参考如下两个图 简介:应用程序仅需要连接HAproxy或者mycat,后端服务器的读写分离由mycat进行控制,后端服务器数据的同步由MySQL主从同步进行控制. 服务器 ...

  9. MySQL集群系列2:通过keepalived实现双主集群读写分离

    在上一节基础上,通过添加keepalived实现读写分离. 首先关闭防火墙 安装keepalived keepalived 2台机器都要安装 rpm .el6.x86_64/ 注意上面要替换成你的内核 ...

随机推荐

  1. 阿里云Aliyun_server

    一.云概念 云主机是基于云计算平台的一种虚拟的主机服务器产品 云服务器(99.999%安全性,扩展性) 特点: 1.资源分配配置灵活,安全性能强. 2.优于VPS和独立服务器产品.通俗的理解: 云主机 ...

  2. curl模拟post和get请求

    function _post($url,$post_data){     $ch = curl_init();     curl_setopt($ch, CURLOPT_URL, $url);     ...

  3. 老男孩Python全栈开发(92天全)视频教程 自学笔记14

    day14课程内容: 深浅拷贝 #浅拷贝只能拷贝一层s=[1,'a','b']s1=s.copy()#浅拷贝print(s1)#[1, 'a', 'b']s[0]=2print(s1,s)#[1, ' ...

  4. qml 静态编译程序执行错误 无法定位程序输入点 CreateDXGIFactory2 于动态链接库 dxgi.dll 上

    重新编译 qt 静态库即可,或 删除该动态库. -no-feature-d3d12 解决方案请参考如下网址: https://forum.qt.io/topic/78380/entry-point-n ...

  5. HDU - 4135 Co-prime 容斥定理

    题意:给定区间和n,求区间中与n互素的数的个数, . 思路:利用容斥定理求得先求得区间与n互素的数的个数,设表示区间中与n互素的数的个数, 那么区间中与n互素的数的个数等于.详细分析见求指定区间内与n ...

  6. 简单的GIT上传

    简单的GIT上传 上传项目时先新建一个 文件夹 mkdir test 然后在切换到test文件夹中然后把github 中的项目拷贝下来 git glone url 然后git init 查看文件 然后 ...

  7. home目录迁移至新分区

    在用户home目录越来越大时,就可以考虑将home目录迁移至新的分区. 1.创建新分区. fidisk /dev/sda:用磁盘管理器打开磁盘 n:新建 +10g :设置分区为10G w :保存 保存 ...

  8. 质量管理:PDCA循环

    PDCA循环又叫质量环,是管理学中的一个通用模型,最早由休哈特于1930年构想,后来被美国质量管理专家戴明博士在1950年再度挖掘出来,并加以广泛宣传和运用于持续改善产品质量的过程.[1] 中文名 P ...

  9. 在U-boot中添加以太网驱动

    当定义CONFIG_CMD_NET和CONFIG_CMD_PING,编译之后执行ping命令,告警没有找到以太网. 因此,需要打开U-boot的网络功能, u-boot-sunxi-sunxi中没有找 ...

  10. VxWorks 符号表

    符号表初始化           符号表用于建立符号名称.类型和值之间的关系.其中,名称为null结尾的任意字符串:类型为标识各种符号的整数:值为一个字符指针.符号表主要用来作为目标模块加载的基础,但 ...