spring boot学习7之mybatis+mysql读写分离(一写多读)+事务

深入理解Spring Boot结合MyBatis调用MySQL,并实现主从复制读写分离 MyBatis作为一款灵活的持久层框架,与Spring Boot结合使用,可以为开发者提供高效、简洁的数据库访问层。本文将详细讲解如何在Spring Boot项目中结合MyBatis调用MySQL,并实现主从复制的读写分离。我们将以电商交易系统为案例,通过实际代码示例展示如何进行配置和实现。 阅读详情

        当业务的访问量(数据库的查询)非常大时,为了降低数据库的压力,希望有多个数据库进行负载均衡,避免所有的查询都集中在一台数据库,造成数据库压力过大。mysql支持一主多从,即在写库的数据库发生变动时,会同步到所有从库,只是同步过程中,会有一定的延迟(除非业务中出现,立即写立即读,否则稍微的延迟是可以接收的)。

       当数据库有主从之分了,那应用代码也应该读写分离了。那代码执行时,该如何决定选择哪个数据库呢。

方案一:

       就像配置多个数据源那样(见博文spring boot学习6之mybatis+PageHelper分页插件+jta多数据源事务整合),将dao都分别放到不通的包下,指明哪个包下dao接口或配置文件走哪个数据库,service层程序员决定走主库还是从库。

       缺点:相同的dao接口和配置文件要复制多份到不同包路径下,不易维护和扩展。

方案二:

       使用AbstractRoutingDataSource+aop+annotation在dao层决定数据源。

        缺点:不支持事务。因为事务在service层开启时,就必须拿到数据源了。

方案三:

      使用AbstractRoutingDataSource+aop+annotation在service层决定数据源,可以支持事务.

      缺点:类内部方法通过this.xx()方式相互调用时,aop不会进行拦截,需进行特殊处理。


     方案二和方案三的区别就是数据源的决定是方案dao还是service,所以本博文例子代码会都含有。

    源码下载,见github。


项目结构


pom.xml

<parent>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-parent</artifactId>
	    <version>1.5.2.RELEASE</version>
	</parent>   
  
	<dependencies>
	    <dependency>
	        <groupId>org.springframework.boot</groupId>
	        <artifactId>spring-boot-starter-web</artifactId>
	    </dependency>
	    
		  <dependency>
   			 <groupId>org.mybatis.spring.boot</groupId>
    		 <artifactId>mybatis-spring-boot-starter</artifactId>
   			 <version>1.3.0</version>
		</dependency> 

		<dependency>
    			<groupId>mysql</groupId>
    			<artifactId>mysql-connector-java</artifactId>
		</dependency>
		<dependency>
    		<groupId>com.alibaba</groupId>
    		<artifactId>druid</artifactId>
    		<version>1.0.29</version>
		</dependency>
		
		<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>
        <dependency>
	    <groupId>org.springframework</groupId>
	    <artifactId>spring-beans</artifactId>
	</dependency>
		
		<!-- 分页插件 -->
		<dependency>
    		<groupId>com.github.pagehelper</groupId>
    		<artifactId>pagehelper</artifactId>
    		<version>4.1.6</version>
		</dependency>


		</dependencies>

本例子的数据库,都是在本地的mysql中建立3个库,test,test_01,test_02,例子是为了测试代码的读写分离,而是mysqld

application.yml

logging:
  config: classpath:logback.xml
  path: d:/logs
server:
  port: 80
  session-timeout: 60


mybatis:
     mapperLocations: classpath:/com/fei/springboot/dao/*.xml
     typeAliasesPackage: com.fei.springboot.dao    
     mapperScanPackage: com.fei.springboot.dao
     configLocation: classpath:/mybatis-config.xml

mysql:
    datasource:
        readSize: 2  #读库个数
        type: com.alibaba.druid.pool.DruidDataSource
        mapperLocations: classpath:/com/fei/springboot/dao/*.xml
        configLocation: classpath:/mybatis-config.xml
        write:
           url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8
           username: root
           password: root
           driver-class-name: com.mysql.jdbc.Driver
           minIdle: 5
           maxActive: 100
           initialSize: 10
           maxWait: 60000
           timeBetweenEvictionRunsMillis: 60000
           minEvictableIdleTimeMillis: 300000
           validationQuery: select 'x'
           testWhileIdle: true
           testOnBorrow: false
           testOnReturn: false
           poolPreparedStatements: true
           maxPoolPreparedStatementPerConnectionSize: 50
           removeAbandoned: true
           filters: stat
        read01:
           url: jdbc:mysql://127.0.0.1:3306/test_01?useUnicode=true&characterEncoding=utf-8
           username: root
           password: root
           driver-class-name: com.mysql.jdbc.Driver
           minIdle: 5
           maxActive: 100
           initialSize: 10
           maxWait: 60000
           timeBetweenEvictionRunsMillis: 60000
           minEvictableIdleTimeMillis: 300000
           validationQuery: select 'x'
           testWhileIdle: true
           testOnBorrow: false
           testOnReturn: false
           poolPreparedStatements: true
           maxPoolPreparedStatementPerConnectionSize: 50
           removeAbandoned: true
           filters: stat
        read02:
           url: jdbc:mysql://127.0.0.1:3306/test_02?useUnicode=true&characterEncoding=utf-8
           username: root
           password: root
           driver-class-name: com.mysql.jdbc.Driver
           minIdle: 5
           maxActive: 100
           initialSize: 10
           maxWait: 60000
           timeBetweenEvictionRunsMillis: 60000
           minEvictableIdleTimeMillis: 300000
           validationQuery: select 'x'
           testWhileIdle: true
           testOnBorrow: false
           testOnReturn: false
           poolPreparedStatements: true
           maxPoolPreparedStatementPerConnectionSize: 50
           removeAbandoned: true
           filters: stat  
mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE configuration  
     PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
     "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<settings>
	    <!-- 使全局的映射器启用或禁用缓存。 -->
		<setting name="cacheEnabled" value="true" />
		<!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
		<setting name="lazyLoadingEnabled" value="true" />
		<!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->        
         <setting name="aggressiveLazyLoading" value="true"/>        
         <!-- 是否允许单条sql 返回多个数据集  (取决于驱动的兼容性) default:true -->
		<setting name="multipleResultSetsEnabled" value="true" />
		<!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true -->
		<setting name="useColumnLabel" value="true" />
		<!-- 允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。  default:false  -->
		<setting name="useGeneratedKeys" value="false" />
		<!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不隐射 PARTIAL:部分  FULL:全部  -->
		<setting name="autoMappingBehavior" value="PARTIAL" />
		<!-- 这是默认的执行类型  (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句;BATCH: 执行器可以重复执行语句和批量更新)  -->
		<setting name="defaultExecutorType" value="SIMPLE" />
		
		<setting name="defaultStatementTimeout" value="25" />
		
		<setting name="defaultFetchSize" value="100" />
		
		<setting name="safeRowBoundsEnabled" value="false" />
		<!-- 使用驼峰命名法转换字段。 -->
		<setting name="mapUnderscoreToCamelCase" value="true" />
		<!-- 设置本地缓存范围 session:就会有数据的共享  statement:语句范围 (这样就不会有数据的共享 ) defalut:session -->
		<setting name="localCacheScope" value="SESSION" />
		<!-- 默认为OTHER,为了解决oracle插入null报错的问题要设置为NULL -->
		<setting name="jdbcTypeForNull" value="NULL" />
		<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString" />
	</settings>
	
</configuration>

读取配置多个数据源

DataSourceConfiguration.java

package com.fei.springboot.config.dbconfig;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;

/**
 * 数据库源配置
 * @author Jfei
 *
 */
@Configuration
public class DataSourceConfiguration {

	private static Logger log = LoggerFactory.getLogger(DataSourceConfiguration.class);
	
	@Value("${mysql.datasource.type}")
	private Class<? extends DataSource> dataSourceType;
    
	/**
	 * 写库 数据源配置
	 * @return
	 */
	@Bean(name = "writeDataSource")
    @Primary
    @ConfigurationProperties(prefix = "mysql.datasource.write")
    public DataSource writeDataSource() {
        log.info("-------------------- writeDataSource init ---------------------");
        return DataSourceBuilder.create().type(dataSourceType).build();
    }
	
	/**
     * 有多少个从库就要配置多少个
     * @return
     */
    @Bean(name = "readDataSource01")
    @ConfigurationProperties(prefix = "mysql.datasource.read01")
    public DataSource readDataSourceOne() {
        log.info("-------------------- read01 DataSourceOne init ---------------------");
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    @Bean(name = "readDataSource02")
    @ConfigurationProperties(prefix = "mysql.datasource.read02")
    public DataSource readDataSourceTwo() {
        log.info("-------------------- read02 DataSourceTwo init ---------------------");
        return DataSourceBuilder.create().type(dataSourceType).build();
    }
    
    
}


数据库的sqlSessionFactorys、roundRobinDataSouceProxy、sqlSessionTemplate、annotationDrivenTransactionManager的设置

MybatisConfiguration.java

package com.fei.springboot.config.dbconfig;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;

import javax.sql.DataSource;

import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.PlatformTransactionManager;

import com.fei.springboot.util.SpringContextUtil;
import com.github.pagehelper.PageHelper;

@Configuration
@AutoConfigureAfter(DataSourceConfiguration.class)
@MapperScan(basePackages="com.fei.springboot.dao")
public class MybatisConfiguration {

	private static Logger log = LoggerFactory.getLogger(MybatisConfiguration.class);
	
	@Value("${mysql.datasource.readSize}")
    private String readDataSourceSize;

	//XxxMapper.xml文件所在路径
	  @Value("${mysql.datasource.mapperLocations}")
      private String mapperLocations;

     //  加载全局的配置文件
      @Value("${mysql.datasource.configLocation}")
      private String configLocation;
      
	@Autowired
	@Qualifier("writeDataSource")
	private DataSource writeDataSource;
	@Autowired
	@Qualifier("readDataSource01")
	private DataSource readDataSource01;
	@Autowired
	@Qualifier("readDataSource02")
	private DataSource readDataSource02;
	
	
    @Bean(name="sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactorys() throws Exception {
        log.info("--------------------  sqlSessionFactory init ---------------------");
        try {
            SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
       //     sessionFactoryBean.setDataSource(roundRobinDataSouce);
            sessionFactoryBean.setDataSource(roundRobinDataSouceProxy());
            
            // 读取配置 
            sessionFactoryBean.setTypeAliasesPackage("com.fei.springboot.domain");
            
            //设置mapper.xml文件所在位置 
            Resource[] resources = new PathMatchingResourcePatternResolver().getResources(mapperLocations);
            sessionFactoryBean.setMapperLocations(resources);
         //设置mybatis-config.xml配置文件位置
            sessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));

            //添加分页插件、打印sql插件
            Interceptor[] plugins = new Interceptor[]{pageHelper(),new SqlPrintInterceptor()};
            sessionFactoryBean.setPlugins(plugins);
            
            return sessionFactoryBean.getObject();
        } catch (IOException e) {
            log.error("mybatis resolver mapper*xml is error",e);
            return null;
        } catch (Exception e) {
            log.error("mybatis sqlSessionFactoryBean create error",e);
            return null;
        }
    }

    /**
     * 分页插件
     * @return
     */
    @Bean
    public PageHelper pageHelper() {
        PageHelper pageHelper = new PageHelper();
        Properties p = new Properties();
        p.setProperty("offsetAsPageNum", "true");
        p.setProperty("rowBoundsWithCount", "true");
        p.setProperty("reasonable", "true");
        p.setProperty("returnPageInfo", "check");
        p.setProperty("params", "count=countSql");
        pageHelper.setProperties(p);
        return pageHelper;
    }
    /**
     * 把所有数据库都放在路由中
     * @return
     */
    @Bean(name="roundRobinDataSouceProxy")
    public AbstractRoutingDataSource roundRobinDataSouceProxy() {
    	
    	Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        //把所有数据库都放在targetDataSources中,注意key值要和determineCurrentLookupKey()中代码写的一至,
        //否则切换数据源时找不到正确的数据源
        targetDataSources.put(DataSourceType.write.getType(), writeDataSource);
        targetDataSources.put(DataSourceType.read.getType()+"1", readDataSource01);
        targetDataSources.put(DataSourceType.read.getType()+"2", readDataSource02);
    
        final int readSize = Integer.parseInt(readDataSourceSize);
   //     MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(readSize);
        
        //路由类,寻找对应的数据源
        AbstractRoutingDataSource proxy = new AbstractRoutingDataSource(){
        	 private AtomicInteger count = new AtomicInteger(0);
        	/**
             * 这是AbstractRoutingDataSource类中的一个抽象方法,
             * 而它的返回值是你所要用的数据源dataSource的key值,有了这个key值,
             * targetDataSources就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。
             */
        	@Override
        	protected Object determineCurrentLookupKey() {
        		String typeKey = DataSourceContextHolder.getReadOrWrite();
        		
        		if(typeKey == null){
        		//	System.err.println("使用数据库write.............");
                //    return DataSourceType.write.getType();
        			throw new NullPointerException("数据库路由时,决定使用哪个数据库源类型不能为空...");
        		}
        		
                if (typeKey.equals(DataSourceType.write.getType())){
                	System.err.println("使用数据库write.............");
                    return DataSourceType.write.getType();
                }
                	
                //读库, 简单负载均衡
                int number = count.getAndAdd(1);
                int lookupKey = number % readSize;
                System.err.println("使用数据库read-"+(lookupKey+1));
                return DataSourceType.read.getType()+(lookupKey+1);
        	}
        };
        
        proxy.setDefaultTargetDataSource(writeDataSource);//默认库
        proxy.setTargetDataSources(targetDataSources);
        return proxy;
    }


    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    	return new SqlSessionTemplate(sqlSessionFactory);
    }
    
    //事务管理
    @Bean
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return new DataSourceTransactionManager((DataSource)SpringContextUtil.getBean("roundRobinDataSouceProxy"));
    }
    
}

   重点是roundRobinDataSouceProxy()方法,它把所有的数据库源交给AbstractRoutingDataSource类,并由它的determineCurrentLookupKey()进行决定数据源的选择,其中读库进行了简单的负载均衡(轮询)。

DataSourceType.java

package com.fei.springboot.config.dbconfig;

public enum DataSourceType {

	read("read", "从库"),
	write("write", "主库");
	
    private String type;
    
    private String name;

    DataSourceType(String type, String name) {
        this.type = type;
        this.name = name;
    }

	public String getType() {
		return type;
	}

	public void setType(String type) {
		this.type = type;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}
    
}
DataSourceContextHolder.java

package com.fei.springboot.config.dbconfig;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 本地线程,数据源上下文
 * @author Jfei
 *
 */
public class DataSourceContextHolder {

	private static Logger log = LoggerFactory.getLogger(DataSourceContextHolder.class);
	
	//线程本地环境
	private static final ThreadLocal<String> local = new ThreadLocal<String>();

    public static ThreadLocal<String> getLocal() {
        return local;
    }

    /**
     * 读库
     */
    public static void setRead() {
        local.set(DataSourceType.read.getType());
        log.info("数据库切换到读库...");
    }

    /**
     * 写库
     */
    public static void setWrite() {
        local.set(DataSourceType.write.getType());
        log.info("数据库切换到写库...");
    }

    public static String getReadOrWrite() {
        return local.get();
    }
    
    public static void clear(){
    	local.remove();
    }
}

写库、读库的注解

package com.fei.springboot.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface ReadDataSource {

}
package com.fei.springboot.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface WriteDataSource {

}

UserMapper.java

package com.fei.springboot.dao;

import java.util.List;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

import com.fei.springboot.domain.User;

@Mapper
public interface UserMapper {

	@Insert("insert sys_user(id,user_name) values(#{id},#{userName})")
	void insert(User u);
	
	@Select("select id,user_name from sys_user where id=#{id} ")
	User findById(@Param("id")String id);
	
	//注:方法名和要UserMapper.xml中的id一致
	List<User> query(@Param("userName")String userName);
	
}

如果想在dao进行数据源的决定,在aop的拦截路径写明是dao

DataSourceAopInDao.java

package com.fei.springboot.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.fei.springboot.config.dbconfig.DataSourceContextHolder;
import com.fei.springboot.config.dbconfig.DataSourceType;

/**
 * 在dao层决定数据源(注:如果用这方式,service层不能使用事务,否则出问题,因为打开事务打开时,就会觉得数据库源了)
 * @author Jfei
 *
 */
//@Aspect
//@Component
public class DataSourceAopInDao {

	private static Logger log = LoggerFactory.getLogger(DataSourceAopInDao.class);
	
	@Before("execution(* com.fei.springboot.dao..*.find*(..)) "
			+ " or execution(* com.fei.springboot.dao..*.get*(..)) "
			+ " or execution(* com.fei.springboot.dao..*.query*(..))")
    public void setReadDataSourceType() {
        DataSourceContextHolder.setRead();
    }

    @Before("execution(* com.fei.springboot.dao..*.insert*(..)) "
    		+ " or execution(* com.fei.springboot.dao..*.update*(..))"
    		+ " or execution(* com.fei.springboot.dao..*.add*(..))")
    public void setWriteDataSourceType() {
        DataSourceContextHolder.setWrite();
    }
    
    
/*    @Before("execution(* com.fei.springboot.dao..*.*(..)) "
			+ " and @annotation(com.fei.springboot.annotation.ReadDataSource) ")
	public void setReadDataSourceType() {
		//如果已经开启写事务了,那之后的所有读都从写库读
		if(!DataSourceType.write.getType().equals(DataSourceContextHolder.getReadOrWrite())){
			DataSourceContextHolder.setRead();
		}
	    
	}
	
	@Before("execution(* com.fei.springboot.dao..*.*(..)) "
			+ " and @annotation(com.fei.springboot.annotation.WriteDataSource) ")
	public void setWriteDataSourceType() {
	    DataSourceContextHolder.setWrite();
	}*/
}


如果是service,必须实现Ordered,并且优先级优于事务的开启。

DataSourceAopInService.java

package com.fei.springboot.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.PriorityOrdered;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import com.fei.springboot.config.dbconfig.DataSourceContextHolder;
import com.fei.springboot.config.dbconfig.DataSourceType;

/**
 * 在service层觉得数据源
 * 
 * 必须在事务AOP之前执行,所以实现Ordered,order的值越小,越先执行
 * 如果一旦开始切换到写库,则之后的读都会走写库
 * 
 * @author Jfei
 *
 */
@Aspect
@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)
@Component
public class DataSourceAopInService implements PriorityOrdered{

private static Logger log = LoggerFactory.getLogger(DataSourceAopInService.class);
	
/*	@Before("execution(* com.fei.springboot.service..*.find*(..)) "
			+ " or execution(* com.fei.springboot.service..*.get*(..)) "
			+ " or execution(* com.fei.springboot.service..*.query*(..))")
    public void setReadDataSourceType() {
		//如果已经开启写事务了,那之后的所有读都从写库读
		if(!DataSourceType.write.getType().equals(DataSourceContextHolder.getReadOrWrite())){
			DataSourceContextHolder.setRead();
		}
        
    }

    @Before("execution(* com.fei.springboot.service..*.insert*(..)) "
    		+ " or execution(* com.fei.springboot.service..*.update*(..))"
    		+ " or execution(* com.fei.springboot.service..*.add*(..))")
    public void setWriteDataSourceType() {
        DataSourceContextHolder.setWrite();
    }*/
    

	@Before("execution(* com.fei.springboot.service..*.*(..)) "
			+ " and @annotation(com.fei.springboot.annotation.ReadDataSource) ")
	public void setReadDataSourceType() {
		//如果已经开启写事务了,那之后的所有读都从写库读
		if(!DataSourceType.write.getType().equals(DataSourceContextHolder.getReadOrWrite())){
			DataSourceContextHolder.setRead();
		}
	    
	}
	
	@Before("execution(* com.fei.springboot.service..*.*(..)) "
			+ " and @annotation(com.fei.springboot.annotation.WriteDataSource) ")
	public void setWriteDataSourceType() {
	    DataSourceContextHolder.setWrite();
	}
    
	@Override
	public int getOrder() {
		/**
		 * 值越小,越优先执行
		 * 要优于事务的执行
		 * 在启动类中加上了@EnableTransactionManagement(order = 10) 
		 */
		return 1;
	}

}



UserService.java

package com.fei.springboot.service;

import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import com.fei.springboot.annotation.ReadDataSource;
import com.fei.springboot.annotation.WriteDataSource;
import com.fei.springboot.dao.UserMapper;
import com.fei.springboot.domain.User;
import com.fei.springboot.util.SpringContextUtil;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
/**
 * 如果需要事务,自行在方法上添加@Transactional
 * 如果方法有内部有数据库操作,则必须指定@WriteDataSource还是@ReadDataSource
 * 
 * 注:AOP ,内部方法之间互相调用时,如果是this.xxx()这形式,不会触发AOP拦截,可能会
 * 导致无法决定数据库是走写库还是读库
 * 方法:
 * 为了触发AOP的拦截,调用内部方法时,需要特殊处理下,看方法getService()
 * 
 * @author Jfei
 *
 */
@Service
public class UserService {

	@Autowired
	private UserMapper userMapper;
	
	@WriteDataSource
	@Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,readOnly=false)
	public void insertUser(User u){
		this.userMapper.insert(u);
	
		//如果类上面没有@Transactional,方法上也没有,哪怕throw new RuntimeException,数据库也会成功插入数据
	//	throw new RuntimeException("测试插入事务");
	}
	/**
	 * 写事务里面调用读
	 * @param u
	 */
	public void wirteAndRead(User u){
		getService().insertUser(u);//这里走写库,那后面的读也都要走写库
		//这是刚刚插入的
		User uu = getService().findById(u.getId());
		System.out.println("==读写混合测试中的读(刚刚插入的)====id="+u.getId()+",  user_name=" + uu.getUserName());
		//为了测试,3个库中id=1的user_name是不一样的
		User uuu = getService().findById("1");
		System.out.println("==读写混合测试中的读====id=1,  user_name=" + uuu.getUserName());
		
	}
	
	public void readAndWirte(User u){
		//为了测试,3个库中id=1的user_name是不一样的
		User uu = getService(). findById("1");
		System.out.println("==读写混合测试中的读====id=1,user_name=" + uu.getUserName());
		getService().insertUser(u);
		
	}
	
	@ReadDataSource
	public User findById(String id){
		User u = this.userMapper.findById(id);
		return u;
	}
	
	
	@ReadDataSource
	public PageInfo<User> queryPage(String userName,int pageNum,int pageSize){
		Page<User> page = PageHelper.startPage(pageNum, pageSize);
		//PageHelper会自动拦截到下面这查询sql
		this.userMapper.query(userName);
		return page.toPageInfo();
	}
	
	private UserService getService(){
		// 采取这种方式的话,
		//@EnableAspectJAutoProxy(exposeProxy=true,proxyTargetClass=true)
		//必须设置为true
	/*	if(AopContext.currentProxy() != null){
			return (UserService)AopContext.currentProxy();
		}else{
			return this;
		}
		*/
		return SpringContextUtil.getBean(this.getClass());
	}
	
}


   开头的方案三中也说到了,如果aop拦截在service层,但是aop不会拦截类内部的调用,至于原因,看博文 http://www.oschina.net/translate/comparative_analysis_between_spring_aop_and_aspectj
  所以UserService中增加了getService()方法进行处理。


   写个controller进行简单测试

UserController.java

package com.fei.springboot.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fei.springboot.domain.User;
import com.fei.springboot.service.UserService;
import com.github.pagehelper.PageInfo;

@Controller
@RequestMapping("/user")
public class UserController {

	
	@Autowired
	private UserService userService;
	
	@RequestMapping("/hello")
	@ResponseBody
	public String hello(){
		return "hello";
	}
	/**
	 * 测试插入
	 * @return
	 */
	@RequestMapping("/add")
	@ResponseBody
	public String add(String id,String userName){
		User u = new User();
		u.setId(id);
		u.setUserName(userName);
		this.userService.insertUser(u);
		return u.getId()+"    " + u.getUserName();
	}
	/**
	 * 测试读
	 * @param id
	 * @return
	 */
	@RequestMapping("/get/{id}")
	@ResponseBody
	public String findById(@PathVariable("id") String id){
		User u = this.userService.findById(id);
		return u.getId()+"    " + u.getUserName();
	}
	/**
	 * 测试写然后读
	 * @param id
	 * @param userName
	 * @return
	 */
	@RequestMapping("/addAndRead")
	@ResponseBody
	public String addAndRead(String id,String userName){
		User u = new User();
		u.setId(id);
		u.setUserName(userName);
		this.userService.wirteAndRead(u);
		return u.getId()+"    " + u.getUserName();
	}
	
	/**
	 * 测试读然后写
	 * @param id
	 * @param userName
	 * @return
	 */
	@RequestMapping("/readAndAdd")
	@ResponseBody
	public String readAndWrite(String id,String userName){
		User u = new User();
		u.setId(id);
		u.setUserName(userName);
		this.userService.readAndWirte(u);
		return u.getId()+"    " + u.getUserName();
	}
	
	/**
	 * 测试分页插件
	 * @return
	 */
	@RequestMapping("/queryPage")
	@ResponseBody
	public String queryPage(){
		PageInfo<User> page = this.userService.queryPage("tes", 1, 2);
		StringBuilder sb = new StringBuilder();
		sb.append("<br/>总页数=" + page.getPages());
		sb.append("<br/>总记录数=" + page.getTotal()) ;
		for(User u : page.getList()){
			sb.append("<br/>" + u.getId() + "      " + u.getUserName());
		}
		System.out.println("分页查询....\n" + sb.toString());
		return sb.toString();
	}
	
}

http://127.0.0.1/user/get/1  时,获取到的结果要么是库test_01要么是test_02中来的,其他方法也可以测试均符合预期结果。这里就不一一贴结果了。其他的类源码也不一一贴出来了。想看的可以到github上看或下载。

   完整源码













Spring Boot + MyBatis + MySQL 实现读写分离,面试必知必会 很多人感叹“学习无用”,实际上之所以产生无用论,是因为自己想要的与自己所学的匹配不上,这也就意味着自己学得远远不够。无论是学习还是工作,都应该有主动性,所以如果拥有大厂梦,那么就要自己努力去实现它。最后祝愿各位身体健康,顺利拿到心仪的offer!由于文章的篇幅有限,所以这次的蚂蚁金服和京东面试题答案整理在了PDF文档里存中…(img-mCeRhp5c-1711720102094)] 阅读详情

相关推荐

Spring Boot + MyBatis + MySQL 实现读写分离!

<goals></goals>

2501_90252551的博客 1047

mysql事务+读写分离_MySQL 读写分离 (基于Amoeba)

一.MySQL读写分离主数据库处理事务性查询,从数据库处理select查询。数据库复制用来把事务性查询导致的变更同步到从数据库中。二.最为常见的读写分离有两种:1.基于程序代码实现在代码中根据select,insert进程路由分类;优点:性能好,因为在代码中实现,不需要额外的硬件设备;缺点:需要开发人员来实现,对代码改动比较大,不适合大型复杂应用;2.基于代理层实现MySQL-Proxy:隶属于M...

weixin_35761085的博客 433

Spring Boot + MyBatis + MySQL 实现读写分离,常见java面试题和答案

总而言之,面试官问来问去,问的那些Redis知识点也就这么多吧,复习的不够到位,知识点掌握不够熟练,所以面试才会卡壳。将这些Redis面试知识解析以及我整理的一些学习笔记分享出来给大家参考学习还有更多学习笔记面试资料也分享如下:摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**因此收集整理了一份《2024年最新Java开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

2201_75604580的博客 826

MySQL_12_ShardingJDBC实现读写分离与分布式事务

文章目录五、ShardingJDBC分布式事务5.1 分布式事务5.2 强一致性的XA事务5.2.1 全局事务5.2.2 2pc提交协议5.2.3 XA事务存在的问题5.3 实践:强一致性的XA事务六、尾声 五、ShardingJDBC分布式事务 5.1 分布式事务 事务包括两种,一个是本地事务,一个是分布式事务,而分布式事务又分为两种,强一致性的XA事务和最终一致性的柔性事务,官网 https://shardingsphere.apache.org/document/current/cn/feature

毛毛的专栏 2623

如何设计QR反激变换器

摘要:本文分析了基于GaN技术的PD适配器市场现状,重点探讨了准谐振反激变换器(QR Flyback)的工作原理及设计方法。文章首先介绍了反激变换器的三种工作模式(CCM、DCM、CRM/QR)及其特点,指出QR模式通过谷底开通技术能显著降低开关损耗。随后以65W适配器为例,详细阐述了变压器参数设计流程,包括匝比确定、原边电感计算和绕组匝数设计等关键步骤。文章强调QR反激变换器凭借高效率、小体积和低成本优势,已成为20W-65W PD适配器的主流拓扑方案,配合GaN技术进一步提升了功率密度和便携性。

m0_62868254的博客 1498

Ubuntu 22.04 安装 IsaacLab 2.1.0

本文介绍了IsaacLab的完整安装流程:1)创建Python3.10虚拟环境;2)安装PyTorch2.5.1(CUDA11.8);3)升级pip并安装IsaacSim4.5.0;4)克隆IsaacLab仓库;5)运行安装脚本(需接受EULA)。若遇cmake缺失错误,需执行sudo apt安装。安装完成后可通过Ant-v0训练任务验证,成功运行后将显示IsaacSim窗口和训练完成提示。注意:IsaacSim不同版本可共存于不同环境。

weixin_47880303的博客 2224

springboot+mysql+mybatis实现读写分离-基于程序(一招解决)

简单、快速的利用程序实现MySQL读写分离

zwjzone的博客 1336

条件风险价值CVaR内容介绍(MATLAB例程)

在对微电网优化调度过程中,由于新能源机组出力和负荷的不确定性,若采用确定性的优化调度方案会存在一定的风险。反之,若风电和光伏输出功率的实际值高于预测值时,则需要降低可控机组出力或者弃风弃光,这都会给系统带来运行费用增加的风险。下,损失超过VaR值时的条件均值[1]。VaR是指在一定的置信水平下,某一投资组合在未来某一时间段内的最大损失。为m维决定系统风险损失的随机向量f(y)为y的概率密度函数。式中,m为所有场景的数量,yk为第k个场景下随机变量的值,为第k个场景发生的概率。的最小值即为CVaR,对应的。

贾贾的博客 3060

mysql(32) : mybatis分区管理(按小时分区)

参考 : https://blog.51cto.com/lawsonabs/3061281 mysql 按小时分区_mysql怎么按小时分区_没有幻觉的博客-优快云博客无分区初始化分区及创建小时分区,定时删除旧分区注意 : 分区字段必须为主键字段,联合主键,字段类型为datetime...

Lxinccode的博客 650

[官方培训]22-UE资产优化 Epic 肖月 笔记

Nanite+Lumen目标:面向TA,美术,不影响视觉的前提下,优化运行和编辑效率引擎:UE5.1延迟管线平台:主流PC,Windows 10 with DX12。

Tcohneyn的博客 1576

【多模态融合】TransFusion学习笔记(1)

工作上主要还是以纯lidar的算法开发,部署以及系统架构设计为主。对于多模态融合(这里主要是只指Lidar和Camer的融合)这方面研究甚少。最近借助和朋友们讨论论文的契机接触了一下这方面的知识,起步是晚了一点,但好歹是开了个头。下面就借助TransFusion论文及其开源代码来串一串这方面的知识。这个很好理解,参考FPointNet网络图示。就是利用现有的2D检测器在图像上提取2D初始框,根据Camera成像原理,捕获2D框视锥内的点云,再利用点云分类模型(如:PointNet)做进一步分类。

4962

mysql 外键 与slave_sql_running_数据库读写分离Master-Slave

数据库读写分离Master-Slave一个平台或系统随着时间的推移和用户量的增多,数据库操作往往会变慢,这时我们需要一些有效的优化手段来提高数据库的执行速度;如SQL优化、表结构优化、索引优化、引擎优化和读写分离优化等手段。1、SQL优化(简单列几点):尽量避免用SELECT*;只查询一条记录时使用limit1;使用连接查询代替子查询;尽量使用一些能通过索引查询的关键字。2、表结构优化:尽量使用数...

weixin_32001433的博客 247

动力学约束下的运动规划算法——Kinodynamic RRT*算法

动力学约束下的运动规划算法——Kinodynamic RRT*算法

慕羽★的博客 6753

新人一看就懂:Spring Boot+Spring mvc+Mybatis的基础框架demo

Spring Boot的设计目的是来简化新Spring应用的初始搭建以及开发过程,大大减少了代码量,通过这篇文章你可以清楚的看到。这是一个基于Spring Boot的简单demo,希望读者可以通过这篇文章大概能看懂这一个简单的框架搭建。后续我会陆续更新,微服务架构(Spring Boot、Spring Cloud)、分布式架构(Dobbo+Zookeeper)、大数据架构以及源码解析等相关的文章,感兴趣的话可以关注一下。

程序员云帆哥的博客 2万+

汇川H5U PLC 实现直线插补、圆弧插补、直线圆弧混合连续插补

使用汇川H5UPLC实现直线插补,圆弧插补,直线圆弧混合路径插补,通过示教可以走出复杂的路径,仅供参考学习。

weixin_39832501的博客 4787

2023推免(夏令营+套实验室)面试总结(博文附问题汇总和参考答案)

2023推免(夏令营+套实验室)面试总结(博文附问题汇总和参考答案)

m0_51339444的博客 1808

「Spring Boot + MyBatis-Plus + MySQL 一主两从」读写分离实战教程

本文介绍了基于Spring Boot 3.3.12、MyBatis-Plus和MySQL实现一主两从读写分离架构的完整方案。文章详细讲解了Docker Compose配置MySQL主从复制环境,项目结构设计,以及关键依赖配置(包括dynamic-datasource多数据源组件)。主要内容包括:application.yml中多数据源的配置方法(主库master+两个从库slave1/slave2),MyBatis-Plus分页插件的集成,以及通过AOP切面动态切换数据源的实现。该方案覆盖了企业级应用中99

qq_39251691的博客 1533

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

druid mysql读写分离_Spring Boot + Mybatis + Druid 实现MySQL一主多从读写分离

Spring Boot 读写分离实现原理实现方式主要是通过扩展Spring AbstractRoutingDataSource这个类充当DataSource路由中介,在运行当中根据自定义KEY值动态切换正真的DataSource。具体实现创建测试库、表:create database `mytest_db`;CREATE TABLE `t_user` (`id` int(11) NOT NULL ...

weixin_35817939的博客 807

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

Spring Boot MyBatis 读写分离插件教程

Spring Boot MyBatis 读写分离插件教程 项目介绍 spring-boot-mybatis-rw 是一个基于 MyBatis 和 Spring Boot 的开箱即用读写分离插件。该插件旨在简化读写分离的配置和实现,支持一写多读的数据源代理,并提供 MyBatis 插件实现读写路由。 项目快速启动 1. 添加 Maven 依赖 在您的 pom.xml 文件中添加以下依赖: <d...

gitblog_00507的博客 762

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

Spring Boot+MyBatis+MySQL如何实现读写分离

​博客主页: 南来_北往系列专栏:Spring Boot实战读写分离是数据库架构中的一种优化策略,它将读操作(查询)和写操作(更新、插入、删除)分开处理,通常通过将读请求和写请求分别发送到不同的数据库服务器来实现。下面介绍实现读写分离的原因:总之,读写分离不仅能有效提升数据库性能,还能根据不同的业务需求,提供灵活的数据处理能力。然而,在实施时也需考虑数据同步延迟、一致性等问题,以确保系统整体的稳定性和可靠性。1、配置数据源:在application.properties或application.ym

2301_76419561的博客 1046

Spring Boot + MyBatis-Plus实现数据库读写分离

MyBatis-Plus是MyBatis的增强工具,提供了许多实用的功能,包括但不限于代码生成器、通用Mapper、分页插件等。在本文中,我们将专注于使用MyBatis-Plus实现数据库读写分离。通过本文的学习,我们了解了如何利用Spring Boot和MyBatis-Plus实现数据库读写分离。这一策略在提升系统性能和稳定性方面有着显著的效果。通过合理配置数据源、使用MyBatis-Plus框架以及编写自定义注解和AOP切面,我们成功地搭建了一个简单而完整的读写分离系统。

学IT,找陈寒 5535

在Spring Boot + MyBatis项目中实现MySQL读写分离

在Spring Boot和MyBatis项目中实现MySQL读写分离,主要有两种思路:一种是在应用层通过代码和配置手动控制,另一种是借助中间件自动路由。

悟空码字的博客 1018

Spring Boot 集成 MyBatis-Plus 动态数据源:基于 @DS 注解实现主从多数据源读写分离

本篇文章以 spring-boot-demo 仓库中的 [demo-multi-datasource-mybatis](https://link.gitcode.com/i/c8966683cc4ed5df0c7e65ad46f0504e) 模块为主线,完整演示了 Spring Boot 如何基于 MyBatis-Plus 官方生态的 `dynamic-datasource-spring-boot

gitblog_00723的博客 1015

mybatis mysql回滚_spring boot 使用 mybatis 开启事务回滚 的总结

1.前言以前没有使用mybatis,可以关闭自动提交,然后做sql操作,对操作进行catch捕获异常,如果没有异常则commit 提交 ,有异常则 rollback 回滚,新增的数据则删除 ,修改的数据则修改回去,删除的则新增,这就是事务操作。事务有四大特性(1)原子性:要么全部执行成功,要么不执行。(2)一致性:事务执行的结果,必须使数据库从一个一致性状态变到另一个一致性状态。(3)隔离性:并发...

weixin_36368715的博客 1967

springboot实现读写分离(基于Mybatis,mysql)

近日工作任务较轻,有空学习学习技术,遂来研究如果实现读写分离。这里用博客记录下过程,一方面可备日后查看,同时也能分享给大家(网上的资料真的大都是抄来抄去,,还不带格式的,看的真心难受)。 完整代码:https://github.com/FleyX/demo-project/tree/master/dxfl 1、背景   一个项目中数据库最基础同时也是最主流的是单机数据库,读写都在一个库中。当用户逐渐增多,单机数据库无法满足性能要求时,就会进行读写分离改造(适用于读多写少),写操作一个库,读操作多个库,

lisheng19870305的专栏 580
上一篇: spring之@Transactional事务传播性
下一篇: spring boot学习8之Servlet/Filter/Listener配置
夢_殤
博客等级 码龄15年 348粉丝 140原创
评论 21
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值