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

  在现在互联网系统中,随着用户量的增长,单数据源通常无法满足系统的负载要求。因此为了解决用户量增长带来的压力,在数据库层面会采用读写分离技术和数据库拆分等技术。读写分离就是就是一个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层进行。

时间: 2024-10-29 19:09:48

Spring配置动态数据源-读写分离和多数据源的相关文章

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

需求:系统中要实现切换数据库(业务数据库和his数据库) 网上很多资料上有提到AbstractRoutingDataSource,大致是这么说的 在Spring 2.0.1中引入了AbstractRoutingDataSource, 该类充当了DataSource的路由中介, 能有在运行时, 根据某种key值来动态切换到真正的DataSource上. Spring动态配置多数据源,即在大型应用中对数据进行切分,并且采用多个数据库实例进行管理,这样可以有效提高系统的水平伸缩性.而这样的方案就会不同

Spring+MyBatis实现数据库读写分离方案

方案1通过MyBatis配置文件创建读写分离两个DataSource,每个SqlSessionFactoryBean对象的mapperLocations属性制定两个读写数据源的配置文件.将所有读的操作配置在读文件中,所有写的操作配置在写文件中. 优点:实现简单缺点:维护麻烦,需要对原有的xml文件进行重新修改,不支持多读,不易扩展实现方式 <bean id="abstractDataSource" abstract="true" class="com

Mybatis多数据源读写分离(注解实现)

#### Mybatis多数据源读写分离(注解实现) ------ 首先需要建立两个库进行测试,我这里使用的是master_test和slave_test两个库,两张库都有一张同样的表(偷懒,喜喜),表结构 表名 t_user | 字段名 | 类型 | 备注 | | :------: | :------: | :------: | | id | int | 主键自增ID | | name | varchar | 名称 | ![file](https://img2018.cnblogs.com/b

Mysql主从配置,实现读写分离

大型网站为了软解大量的并发访问,除了在网站实现分布式负载均衡,远远不够.到了数据业务层.数据访问层,如果还是传统的数据结构,或者只是单单靠一台服务器扛,如此多的数据库连接操作,数据库必然会崩溃,数据丢失的话,后果更是 不堪设想.这时候,我们会考虑如何减少数据库的联接,一方面采用优秀的代码框架,进行代码的优化,采用优秀的数据缓存技术如:memcached,如果资金丰厚的话,必然会想到假设服务器群,来分担主数据库的压力.Ok切入今天微博主题,利用MySQL主从配置,实现读写分离,减轻数据库压力.这种

MySQL主从配置及实现读写分离

大型网站为了软解大量的并发访问,除了在网站实现分布式负载均衡,远远不够.到了数据业务层.数据访问层,如果还是传统的数据结构,或者只是单单靠一台服务器扛,如此多的数据库连接操作,数据库必然会崩溃,数据丢失的话,后果更是 不堪设想.这时候,我们会考虑如何减少数据库的联接,一方面采用优秀的代码框架,进行代码的优化,采用优秀的数据缓存技术如:memcached,如果资金丰厚的话,必然会想到假设服务器群,来分担主数据库的压力.Ok切入今天主题,利用MySQL数据库主从配置,实现读写分离,减轻数据库压力.这

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

注:mongodb当前版本是3.4.3 spring连接mongodb复制集的字符串格式: mongodb://[username:[email protected]]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]] mongodb:// 前缀,代表这是一个Connection String username:[email protected] 如果启用了用户认证,需要指定用户密码 hostX:por

1.Spring实现数据库的读写分离

现在大型的电子商务系统,在数据库层面大都采用读写分离技术,就是一个Master数据库,多个Slave数据库.Master库负责数据更新和实时数据查询,Slave库当然负责非实时数据查询.因为在实际的应用中,数据库都是读多写少(读取数据的频率高,更新数据的频率相对较少),而读取数据通常耗时比较长,占用数据库服务器的CPU较多,从而影响用户体验.我们通常的做法就是把查询从主库中抽取出来,采用多个从库,使用负载均衡,减轻每个从库的查询压力. 采用读写分离技术的目标:有效减轻Master库的压力,又可以

mysql配置主从复制,读写分离(附amoeba源码包)

mysql5.5基于表锁定 读取和写入互阻塞,为了解决这个问题便有了主从复制,读写分离的群集设置.三次认证1.主从同步认证2.amoeba访问数据库认证3.客户端访问amoeba认证 写入和读取:客户端写入数据写入到的是mysql主服务器中从服务器同步主服务器读取是读取从服务器上的内容从而实现读写分离 本实验使用mysql5.5附mysql5.5源码包以及安装脚本链接:https://pan.baidu.com/s/1kA80VX67fXOBVChUt72__g 密码:e42o jdk文件使用的

laravel实现数据库读写分离配置或者多读写分离配置

config\database.php里 读写分离:'mysql' => array( 'read' => array( 'host' => '192.168.1.1', ), 'write' => array( 'host' => '196.168.1.2' ), 'driver' => 'mysql', 'database' => 'database', 'username' => 'root', 'password' => '', 'charse