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

  1. 基于spring的aop实现读写分离与事务配置

    项目开发中经常会遇到读写分离等多数据源配置的需求,在Java项目中可以通过Spring AOP来实现多数据源的切换. 一.Spring事务开启流程 Spring中通常通过@Transactional来 ...

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

    #### Mybatis多数据源读写分离(注解实现) ------ 首先需要建立两个库进行测试,我这里使用的是master_test和slave_test两个库,两张库都有一张同样的表(偷懒,喜喜), ...

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

    大型网站为了软解大量的并发访问,除了在网站实现分布式负载均衡,远远不够.到了数据业务层.数据访问层,如果还是传统的数据结构,或者只是单单靠一台服务器扛,如此多的数据库连接操作,数据库必然会崩溃,数据丢 ...

  4. 黄聪:Mysql主从配置,实现读写分离

    大型网站为了软解大量的并发访问,除了在网站实现分布式负载均衡,远远不够.到了数据业务层.数据访问层,如果还是传统的数据结构,或者只是单单靠一台服务器扛,如此多的数据库连接操作,数据库必然会崩溃,数据丢 ...

  5. 使用Spring配置动态数据源实现读写分离

    最近搭建的一个项目需要实现数据源的读写分离,在这里将代码进行分享,以供参考.关键词:DataSource .AbstractRoutingDataSource.AOP 首先是配置数据源 <!-- ...

  6. 阿里P7教你如何使用 Spring 配置动态数据源实现读写分离

    最近搭建的一个项目需要实现数据源的读写分离,在这里将代码进行分享,以供参考. 关键词:DataSource .AbstractRoutingDataSource.AOP 首先是配置数据源 <!- ...

  7. spring项目配置双数据源读写分离

    我们最早做新项目的时候一直想做数据库的读写分离与主从同步,由于一些原因一直没有去做这个事情,这次我们需要配置双数据源的起因是因为我们做了一个新项目用了另一个数据库,需要把这个数据库的数据显示到原来的后 ...

  8. 【Spring】Spring如何实现多数据源读写分离?这是我看过最详细的一篇!!

    写在前面 很多小伙伴私聊我说:最近他们公司的业务涉及到多个数据源的问题,问我Spring如何实现多数据源的问题.回答这个问题之前,首先需要弄懂什么是多数据源:多数据源就是在同一个项目中,会连接两个甚至 ...

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

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

随机推荐

  1. 如何去掉linux配置文件的注释行和空行

    1.使用grep -v "^#"  来去掉注释行,其中:-v  就是取相反的   ^# 表示以#开头的行 eg. grep -v "^#" /etc/vsftp ...

  2. Linux搭建 SVN 服务器

    安装 Subversion Subversion 是一个版本控制系统,相对于的 RCS . CVS ,采用了分支管理系统,它的设计目标就是取代 CVS . yum install -y subvers ...

  3. [蓝点ZigBee] Zstack 之点亮LED灯 ZigBee/CC2530 视频资料

    LED点灯实验,主要是依据Zstack 现有程序修改LED相关驱动,适配到我们自己的开发板上,我们开发板共有4个LED灯,其中一个是电源指示灯,剩余3个都是GPIO控制的LED灯,有LED 灯连接方式 ...

  4. 潭州课堂25班:Ph201805201 django 项目 第八课 注册功能分析,图片验证码视图设计 (课堂笔记)

    1,用户名     是否已注册 2,手机号 是否被注册 3,图形验证码 4,短信验证码, 5,验证成功后,向后台提交数据:用户名,密码,手机号,短信验证 要写这五个视图, 获取图形验证码,请求方式:g ...

  5. C++学习笔记48:链表的基本操作

    //链表的基本操作 //生成链表,插入结点,查找结点,删除结点,遍历链表,清空链表 //链表类模板 //LinkedList.h #ifndef LINKEDLIST_H #define LINKED ...

  6. day8网络编程,面向对象1

    一.只是回顾 1.导入模块的顺序,首先从当前目录下找,再从环境变量里面找,使用"sys.path.insert(0,'需要导入的环境变量')"加入需要导入文件的环境变量; 2.如果 ...

  7. 深入理解JVM(7)——类加载器

    一.类和类加载器 a)        类加载器的作用:将class文件加载到JVM的方法区,并且在方法区中创建一个java.lang.Class对象作为外界访问这个类的接口. b)        类和 ...

  8. visual studio 设计第一个WinForm小程序

    WinForm小程序之消息框 首先打开visual studio 软件,然后[文件]-[新建]-[项目]-[Visual C#]-[Windows],选择Windows窗体应用程序,根据自己的需要修改 ...

  9. python之编程风格

    第一:语句和语法 #   表示注释掉的内容 \    续行 print("yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\ yyyyyyyyyyyyyyyyyyyyy ...

  10. JDBC(14)—对DAO进行改进修改

    结构: DAO2_7< T >(接口)->DAOTestImpl< T >(实现类)->CustomerDAO(继承的子类)->CustomerDAOTest ...