Spring配置动态数据源-读写分离和多数据源
在现在互联网系统中,随着用户量的增长,单数据源通常无法满足系统的负载要求。因此为了解决用户量增长带来的压力,在数据库层面会采用读写分离技术和数据库拆分等技术。读写分离就是就是一个Master数据库,多个Slave数据库,Master数据库负责数据的写操作,slave库负责数据读操作,通过slave库来降低Master库的负载。因为在实际的应用中,数据库都是读多写少(读取数据的频率高,更新数据的频率相对较少),而读取数据通常耗时比较长,占用数据库服务器的CPU较多,从而影响用户体验。我们通常的做法就是把查询从主库中抽取出来,采用多个从库,使用负载均衡,减轻每个从库的查询压力。同时随着业务的增长,会对数据库进行拆分,根据业务将业务相关的数据库表拆分到不同的数据库中。不管是读写分离还是数据库拆分都是解决数据库压力的主要方式之一。本篇文章主要讲解Spring如何配置读写分离和多数据源手段。
1.读写分离
具体到开发中,如何方便的实现读写分离呢?目前常用的有两种方式:
- 第一种方式是最常用的方式,就是定义2个数据库连接,一个是MasterDataSource,另一个是SlaveDataSource。对数据库进行操作时,先根据需求获取dataSource,然后通过dataSource对数据库进行操作。这种方式配置简单,但是缺乏灵活新。
- 第二种方式动态数据源切换,就是在程序运行时,把数据源动态织入到程序中,从而选择读取主库还是从库。主要使用的技术是: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配置动态数据源-读写分离和多数据源的更多相关文章
- 基于spring的aop实现读写分离与事务配置
项目开发中经常会遇到读写分离等多数据源配置的需求,在Java项目中可以通过Spring AOP来实现多数据源的切换. 一.Spring事务开启流程 Spring中通常通过@Transactional来 ...
- Mybatis多数据源读写分离(注解实现)
#### Mybatis多数据源读写分离(注解实现) ------ 首先需要建立两个库进行测试,我这里使用的是master_test和slave_test两个库,两张库都有一张同样的表(偷懒,喜喜), ...
- Mysql主从配置,实现读写分离
大型网站为了软解大量的并发访问,除了在网站实现分布式负载均衡,远远不够.到了数据业务层.数据访问层,如果还是传统的数据结构,或者只是单单靠一台服务器扛,如此多的数据库连接操作,数据库必然会崩溃,数据丢 ...
- 黄聪:Mysql主从配置,实现读写分离
大型网站为了软解大量的并发访问,除了在网站实现分布式负载均衡,远远不够.到了数据业务层.数据访问层,如果还是传统的数据结构,或者只是单单靠一台服务器扛,如此多的数据库连接操作,数据库必然会崩溃,数据丢 ...
- 使用Spring配置动态数据源实现读写分离
最近搭建的一个项目需要实现数据源的读写分离,在这里将代码进行分享,以供参考.关键词:DataSource .AbstractRoutingDataSource.AOP 首先是配置数据源 <!-- ...
- 阿里P7教你如何使用 Spring 配置动态数据源实现读写分离
最近搭建的一个项目需要实现数据源的读写分离,在这里将代码进行分享,以供参考. 关键词:DataSource .AbstractRoutingDataSource.AOP 首先是配置数据源 <!- ...
- spring项目配置双数据源读写分离
我们最早做新项目的时候一直想做数据库的读写分离与主从同步,由于一些原因一直没有去做这个事情,这次我们需要配置双数据源的起因是因为我们做了一个新项目用了另一个数据库,需要把这个数据库的数据显示到原来的后 ...
- 【Spring】Spring如何实现多数据源读写分离?这是我看过最详细的一篇!!
写在前面 很多小伙伴私聊我说:最近他们公司的业务涉及到多个数据源的问题,问我Spring如何实现多数据源的问题.回答这个问题之前,首先需要弄懂什么是多数据源:多数据源就是在同一个项目中,会连接两个甚至 ...
- spring mongodb 复制集配置(实现读写分离)
注:mongodb当前版本是3.4.3 spring连接mongodb复制集的字符串格式: mongodb://[username:password@]host1[:port1][,host2[: ...
随机推荐
- Jmeter5 实现多机集群压测(局域网组成多机集群)
想要模拟高并发用户访问的场景,用Jmeter5实现的话,单靠一台PC机,资源是不够的,包括单机的内存.使用端口数量等,所以最好是通过多台PC机组成几个集群来对服务器进行压测. 本文目录: 1.软硬件配 ...
- python字符串与列表的相互转换
学习内容: 1.字符串转列表 2.列表转字符串 1. 字符串转列表 s ='hello python !'li = s.split(' ') #注意:引号内有空格print (li)输出:['hell ...
- BZOJ4912 : [Sdoi2017]天才黑客
建立新图,原图中每条边在新图中是点,点权为$w_i$,边权为两个字符串的LCP. 对字典树进行DFS,将每个点周围一圈边对应的字符串按DFS序从小到大排序. 根据后缀数组利用height数组求LCP的 ...
- keepalived+mysql backup服务器可ping通过vip但telnet vip+3306失败问题
环境: OS:CentOS 7_X64 数据库:mysql-5.7 MASTER:192.168.119.23 BACKUP:192.168.119.24 VIP:192.168.119.138 ke ...
- 4989: [Usaco2017 Feb]Why Did the Cow Cross the Road
题面:4989: [Usaco2017 Feb]Why Did the Cow Cross the Road 连接 http://www.lydsy.com/JudgeOnline/problem.p ...
- 12、Bootstrap中文文档(其它插件分享)
给大家介绍一个前端框架让你从此写起前端代码与之先前相比如有神助般的效果拉就是Bootstrap. 本片导航: Bootstrap的下载 css样式的使用 JavaScript 效果的引用 其他前端插件 ...
- 从注册表清理 IE10,IE11 用户代理字符串(UserAgent)中的垃圾信息
某一天,我发现我的 IE User Agent 字符串里面竟然含有刷机大师.百度浏览器等许多垃圾,国货流氓见怪不怪了. 微软自家的.NET CLR也占据了一大片,看着也不爽. 决定清理一下,但是却没找 ...
- RestTemplate之GET和POST调用和异步回调
get方式 String url = "http://hostname:port/v1.0/data/data"; HttpHeaders headers = new HttpHe ...
- spring boot 中添加mongodb支持
1.添加maven依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactI ...
- windows下Graphviz安装及入门教程
下载安装配置环境变量 intall 配置环境变量 验证 基本绘图入门 graph digraph 一个复杂的例子 和python交互 发现好的工具,如同发现新大陆.有时,我们会好奇,论文中.各种专业的 ...