spring配置动态数据源-读写分离和多数据源

读书使人进步      2022-02-11     658

关键词:

  在现在互联网系统中,随着用户量的增长,单数据源通常无法满足系统的负载要求。因此为了解决用户量增长带来的压力,在数据库层面会采用读写分离技术和数据库拆分等技术。读写分离就是就是一个Master数据库,多个Slave数据库,Master数据库负责数据的写操作,slave库负责数据读操作,通过slave库来降低Master库的负载。因为在实际的应用中,数据库都是读多写少(读取数据的频率高,更新数据的频率相对较少),而读取数据通常耗时比较长,占用数据库服务器的CPU较多,从而影响用户体验。我们通常的做法就是把查询从主库中抽取出来,采用多个从库,使用负载均衡,减轻每个从库的查询压力。同时随着业务的增长,会对数据库进行拆分,根据业务将业务相关的数据库表拆分到不同的数据库中。不管是读写分离还是数据库拆分都是解决数据库压力的主要方式之一。本篇文章主要讲解Spring如何配置读写分离和多数据源手段。

1.读写分离  

  具体到开发中,如何方便的实现读写分离呢?目前常用的有两种方式:

  1. 第一种方式是最常用的方式,就是定义2个数据库连接,一个是MasterDataSource,另一个是SlaveDataSource。对数据库进行操作时,先根据需求获取dataSource,然后通过dataSource对数据库进行操作。这种方式配置简单,但是缺乏灵活新。
  2. 第二种方式动态数据源切换,就是在程序运行时,把数据源动态织入到程序中,从而选择读取主库还是从库。主要使用的技术是:annotation,Spring AOP ,反射。下面会详细的介绍实现方式。 

  在介绍实现方式之前,先准备一些必要的知识,spring的AbstractRoutingDataSource类。AbstractRoutingDataSource这个类是spring2.0以后增加的,我们先来看下AbstractRoutingDataSource的定义:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {} 

  AbstractRoutingDataSource继承了AbstractDataSource并实现了InitializingBean,因此AbstractRoutingDataSource会在系统启动时自动初始化实例。

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
	private Map<Object, Object> targetDataSources;
	private Object defaultTargetDataSource;
	private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();
	private Map<Object, DataSource> resolvedDataSources;
	private DataSource resolvedDefaultDataSource;
        ...
}

  AbstractRoutingDataSource继承了AbstractDataSource ,而AbstractDataSource 又是DataSource 的子类。DataSource 是javax.sql 的数据源接口,定义如下:

public interface DataSource  extends CommonDataSource,Wrapper {
  Connection getConnection() throws SQLException;
  Connection getConnection(String username, String password)
    throws SQLException;
}

  DataSource接口定义了2个方法,都是获取数据库连接。我们在看下AbstractRoutingDataSource如何实现了DataSource接口:

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

public Connection getConnection(String username, String password) throws SQLException {
	return determineTargetDataSource().getConnection(username, password);
}

  很显然就是调用自己的determineTargetDataSource() 方法获取到connection。determineTargetDataSource方法定义如下:

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

  我们最关心的还是下面2句话: 

Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);

  determineCurrentLookupKey方法返回lookupKey,resolvedDataSources方法就是根据lookupKey从Map中获得数据源。resolvedDataSources 和determineCurrentLookupKey定义如下:

private Map<Object, DataSource> resolvedDataSources;
protected abstract Object determineCurrentLookupKey()

  看到以上定义,我们是不是有点思路了,resolvedDataSources是Map类型,我们可以把MasterDataSource和SlaveDataSource存到Map中。通过写一个类DynamicDataSource继承AbstractRoutingDataSource,实现其determineCurrentLookupKey() 方法,该方法返回Map的key,master或slave。 

public class DynamicDataSource extends AbstractRoutingDataSource{
    @Override
    protected Object determineCurrentLookupKey() {
        return DatabaseContextHolder.getCustomerType(); 
    }
}

  定义DatabaseContextHolder

public class DatabaseContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();   
    public static void setCustomerType(String customerType) {  
        contextHolder.set(customerType);  
    }  
    public static String getCustomerType() {  
        return contextHolder.get();  
    }  
    public static void clearCustomerType() {  
        contextHolder.remove();  
    }  
}

  从DynamicDataSource 的定义看出,他返回的是DynamicDataSourceHolder.getDataSouce()值,我们需要在程序运行时调用DynamicDataSourceHolder.putDataSource()方法,对其赋值。下面是我们实现的核心部分,也就是AOP部分,DataSourceAspect定义如下:

@Aspect
@Order(1)
@Component
public class DataSourceAspect {
    @Before(value = "execution(* com.netease.nsip.DynamicDataSource.dao..*.insert*(..))"
            + "||execution(* com.netease.nsip.DynamicDataSource.dao..*.add*(..))"
            + "||@org.springframework.transaction.annotation.Transactional * *(..)")
    public Object before(ProceedingJoinPoint joinPoint) throws Throwable {
        DatabaseContextHolder.setCustomerType("master");
        Object object = joinPoint.proceed();
        DatabaseContextHolder.setCustomerType("slave");
        return object;
    }
}

  为了方便测试,我定义了2个数据库,Master库和Slave库,两个库中person表结构一致,但数据不同,properties文件配置如下:

#common
db-driver=com.mysql.jdbc.Driver

#master 
master-url=jdbc:mysql://127.0.0.1:3306/master?serverTimezone=UTC
master-user=root
master-password=root

#salve
slave-url=jdbc:mysql://127.0.0.1:3306/slave?serverTimezone=UTC
slave-user=root
slave-password=root

  Spring中的xml定义如下:

<!-- 配置数据源公共参数 -->
	<bean name="baseDataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName">
			<value>${db-driver}</value>
		</property>
	</bean>

	<!-- 配置主数据源 -->
	<bean name="masterDataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="url">
			<value>${master-url}</value>
		</property>
		<property name="username">
			<value>${master-user}</value>
		</property>
		<property name="password">
			<value>${master-password}</value>
		</property>
	</bean>

	<!--配置从数据源 -->
	<bean name="slavueDataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="url">
			<value>${slave-url}</value>
		</property>
		<property name="username">
			<value>${slave-user}</value>
		</property>
		<property name="password">
			<value>${slave-password}</value>
		</property>
	</bean>

	<bean id="dataSource"
		class="com.netease.nsip.DynamicDataSource.commom.DynamicDataSource">
		<property name="targetDataSources">
			<map key-type="java.lang.String">
				<entry key="master" value-ref="masterDataSource" />
				<entry key="slave" value-ref="slavueDataSource" />
			</map>
		</property>
		<property name="defaultTargetDataSource" ref="slavueDataSource" />
	</bean>

	<!-- 配置SqlSessionFactoryBean -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="configLocation" value="classpath:SqlMapConfig.xml" />
		<property name="dataSource" ref="dataSource" />
	</bean>

	<!-- 持久层访问模板化的工具,线程安全,构建sqlSessionFactory -->
	<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg index="0" ref="sqlSessionFactory" />
	</bean>

	<!-- 事务管理器 -->
	<bean id="txManager"
		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>

	<tx:annotation-driven transaction-manager="txManager"
		proxy-target-class="true" order="200" />

	<!-- 回滚方式 -->
	<tx:advice id="txAdvice" transaction-manager="txManager">
		<tx:attributes>
			<tx:method name="*" rollback-for="Throwable" />
		</tx:attributes>
	</tx:advice>

	<!-- 定义@Transactional的注解走事务管理器 -->
	<aop:config>
		<aop:pointcut id="transactionPointcutType"
			expression="@within(org.springframework.transaction.annotation.Transactional)" />
		<aop:pointcut id="transactionPointcutMethod"
			expression="@annotation(org.springframework.transaction.annotation.Transactional)" />
		<aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointcutType" />
		<aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointcutMethod" />
	</aop:config>

  到目前读写分离已经配置好了,所有的以insert和add开头的dao层,以及带有Transaction注解的会走主库,其他的数据库操作走从库。当然也可以修改切入点表达式让update和delete方法走主库。上述方法是基于AOP的读写分离配置,下面使用实例结合注解讲述多数据源的配置。

2.多数据源配置

  上面的实例使用AOP来配置读写分离,接下来将结合Spring注解配置多数据源,该方法也可以用于配置读写分离。先看下annotation的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Profile {
    String value();
}

  定义MultiDataSourceAspect ,在MultiDataSourceAspect根据注解获取数据源.

public class MultiDataSourceAspect {
    public void before(JoinPoint joinPoint) throws Throwable {
        Object target = joinPoint.getTarget();  
        String method = joinPoint.getSignature().getName();  
        Class<?>[] classz = target.getClass().getInterfaces();  
  
        Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).
                getMethod().getParameterTypes();  
        try {  
            Method m = classz[0].getMethod(method, parameterTypes);  
            if (m != null&&m.isAnnotationPresent(Profile.class)) {  
                Profile data = m  .getAnnotation(Profile.class);  
                DatabaseContextHolder.setCustomerType(data.value());  
            }        
        } catch (Exception e) {  
        }
    }
}  

  同样为了测试,数据源properties文件如下:

#common
db-driver=com.mysql.jdbc.Driver

#master 
account-url=jdbc:mysql://127.0.0.1:3306/master?serverTimezone=UTC
account-user=root
account-password=root

#salve
goods-url=jdbc:mysql://127.0.0.1:3306/slave?serverTimezone=UTC
goods-user=root
goods-password=root 

  Spring的XML文件定义如下:

<!-- 配置数据源公共参数 -->
	<bean name="baseDataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName">
			<value>${db-driver}</value>
		</property>
	</bean>

	<!-- 配置主数据源 -->
	<bean name="accountDataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="url">
			<value>${account-url}</value>
		</property>
		<property name="username">
			<value>${account-user}</value>
		</property>
		<property name="password">
			<value>${account-password}</value>
		</property>
	</bean>

	<!--配置从数据源 -->
	<bean name="goodsDataSource"
		class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="url">
			<value>${goods-url}</value>
		</property>
		<property name="username">
			<value>${goods-user}</value>
		</property>
		<property name="password">
			<value>${goods-password}</value>
		</property>
	</bean>

	<bean id="dataSource"
		class="com.netease.nsip.DynamicDataSource.commom.MultiDataSource">
		<property name="targetDataSources">
			<map key-type="java.lang.String">
				<entry key="goods" value-ref="goodsDataSource" />
				<entry key="account" value-ref="accountDataSource" />
			</map>
		</property>
	</bean>

	<!-- 配置SqlSessionFactoryBean -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="configLocation" value="classpath:multiSqlMapConfig.xml" />
		<property name="dataSource" ref="dataSource" />
	</bean>

	<!-- 持久层访问模板化的工具,线程安全,构建sqlSessionFactory -->
	<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg index="0" ref="sqlSessionFactory" />
	</bean>


	<!-- 配置AOP -->
	<bean id="multiAspect"
		class="com.netease.nsip.DynamicDataSource.commom.MultiDataSourceAspect" />
	<aop:config>
		<aop:aspect id="datasourceAspect" ref="multiAspect">
			<aop:pointcut
				expression="execution(* com.netease.nsip.DynamicDataSource.dao..*.insert*(..))"
				id="tx" />
			<aop:before pointcut-ref="tx" method="before" />
		</aop:aspect>
	</aop:config>

  dao层接口定义如下:

public interface IAccountDao {
    @Profile("account")
    public boolean insert(Accounts accounts);
}

public interface IGoodsDao {
    @Profile("goods")
    public boolean insert(Goods goods);  
}

  Spring配置多数据源的主要方式如上所示,在实例中为了方便数据源的选择都在dao进行。而在日常开发的过程中事务通常在Service层,而事务又和数据源绑定,所以为了在Service层使用事务可以将数据源的选择在service层进行。


spring配置双数据源并读写分离

...AbstractRoutingSource类还有方法名称和切入点去控制使用哪个数据源  1.首先在配置文件配置多个数据源并且交给继承自springAbstractRoutingSourc 查看详情

spring主从数据库的配置和动态数据源切换原理

原文:https://www.liaoxuefeng.com/article/00151054582348974482c20f7d8431ead5bc32b30354705000 在大型应用程序中,配置主从数据库并使用读写分离是常见的设计模式。在Spring应用程序中,要实现读写分离,最好不要对现有代码进行改动,而是在... 查看详情

springboot+mybatis实现数据库读写分离

本文不包含数据库主从配置。实现思路:在项目中配置多数据源,通过代码控制访问哪一个数据源。spring-jdbc为我们提供了AbstractRoutingDataSource,DataSource的抽象实现,基于查找键,返回不通不同的数据源。编写我们自己的动态数... 查看详情

spring读写分离-事务配置篇(转)(代码片段)

转自:http://jinnianshilongnian.iteye.com/blog/1720618如何配置mysql数据库的主从?单机配置mysql主从:http://my.oschina.net/god/blog/496 常见的解决数据库读写分离有两种方案1、应用层http://neoremind.net/2011/06/spring实现数据库读写分 查看详情

mybatis-plus动态数据源读写分离+shardingjdbc分库分表

...择mybaitis-plus做读写分离,遇到分库分表的时候切换sharding数据源,也就是一般情况下使用的还是jdbc,有分表的时候才会用shardingJDBCdynamic-datasource-spring-boot-starter采用3.3以上的好像就切不过来数据源,具体没找到原因配置mybatis-plus... 查看详情

spring+mybatis实现主从数据库读写分离

Spring+Mybatis实现主从数据库读写分离采用配置+注解的方式。自定义@DataSource注解importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Ta 查看详情

mariadb集群配置(主从和多主)

mariadb集群配置(主从和多主)mariadb主从主从多用于网站架构,因为主从的同步机制是异步的,数据的同步有一定延迟,也就是说有可能会造成数据的丢失,但是性能比较好,因此网站大多数用的是主从架构的数据库,读写分离... 查看详情

spring读写分离-事务注解篇(代码片段)

...这个注解,这里直接贴出代码:配置文件:多数据源配置:<beanid="dataSource"class= 查看详情

springjpa读写分离

...分离。思想:在dataSource做路由,根据事务判断使用主从数据源。背景:spring+springdatajpa(hibernatejpa)首先是jpa配置,时间有限在原基础上该的,既有java配置也有xml配置,见谅。先声明EntityManagerXml代码  650)this.width=650;"class... 查看详情

java实现数据库的读写分离

...gDataSource类,重写determineCurrentLookupKey方法,实现动态切换数据源的功能;读写分离可以有效减轻写库的压力,又可以把查询数据的请求分发到不同读库;2、写数据库:当调用insert、update、delete及一些实时数据用到的库;3、读数... 查看详情

读写分离注解

...p;一:概述1.结构文档    2.思路  组装好各个数据源,然后通过注解选择使用读或者写的数据源,将其使用AbstractRoutingDataSource中的方法determineCurrentLookuoKey进行选择datasource的key。  然后,通过key,就找到了要使用的... 查看详情

mybatis+spring实现mysql读写分离

使用springAbstractRoutingDatasource实现多数据源publicclassDynamicDataSourceextendsAbstractRoutingDataSource{//写数据源privateObjectwriteDataSource;//读数据源privateObjectreadDataSource;@OverridepublicvoidafterPropert 查看详情

spring读写分离-事务注解篇(代码片段)

...这个注解,这里直接贴出代码:配置文件:多数据源配置:<beanid="dataSource"class="com.lmiky.platform.database.datasource.DynamicDataSource"><propertyname="readDataSources"><list><refbean="read... 查看详情

使用spring实现读写分离

1.  背景我们一般应用对数据库而言都是“读多写少”,也就说对数据库读取数据的压力比较大,有一个思路就是说采用数据库集群的方案,其中一个是主库,负责写入数据,我们称之为:写库;其它都是从库,负责读取... 查看详情

spring+mybatis项目实现动态切换数据源

...此之外走主库。最简单的办法其实就是建两个包,把之前数据源那一套配置copy一份,指向另外的包,但是这样扩展很有限,所有采用下面的办法。参考了两篇文章如下:http://blog.csdn.net/zl3450341/article/details/20150687http://www.blogjava.ne... 查看详情

springboot和mycat动态数据源项目整合

SpringBoot项目整合动态数据源(读写分离)1.配置多个数据源,根据业务需求访问不同的数据,指定对应的策略:增加,删除,修改操作访问对应数据,查询访问对应数据,不同数据库做好的数据一致性的处理。由于此方法相对易懂... 查看详情

springboot多数据源读写分离和主库数据源service层事务控制

 需求:系统中要实现切换数据库(业务数据库和his数据库)网上很多资料上有提到AbstractRoutingDataSource,大致是这么说的在Spring2.0.1中引入了AbstractRoutingDataSource,该类充当了DataSource的路由中介,能有在运行时,根据某种key值来动... 查看详情

mycat实现读写分离(代码片段)

...至少有两种方法:应用本身通过代码实现,例如基于动态数据源、AOP的原理来实现写操作时用主数据库,读操作时用从数据库。通过中间件的方式实现,例如通过Mycat,即中间件会分析对应的SQL,写操作时会连接主数据库,读操... 查看详情