Hibernate动态数据源切换

使用目的

出于在审计厅项目建设的需求,我们在项目建设工程先是使用了单一的数据库,经过三个月的代码编写,完成了单机的项目部署,在经过两台loadRunner进行2k的并发访问时,发现数据库的写日志缓冲区已经爆满,导致系统宕机。后来在老师的决策下将数据库分库存储,不同地区的数据利用切分工具进行数据的切分,然后使用ETL、dts配合自己写的脚本完成数据的迁移和各种角色、存储过程、权限的设置。

数据是分开存放了,那如何在保证单机系统的可运行的情况下,使用多数据源呢。经过调研,我们发现hibernate+spring可以使用多数据源,可以在使用中动态切换数据源。

数据源切换的步骤

我们的项目中用到了17个私有数据库实例,117个模式。1个公共数据库实例。

enter description here
enter description here

如上图所示,我们系统的数据源的切换过程大概是这种模型。刚开始所有用户的登录数据都是由公共数据源进行统一管理的,后面的审计数据的查询,自由上传的辅助数据都是存放在用户的私有数据源中的。

动态数据源切换的实现

编写动态数据源类

利用springAbstractRoutingDataSource来实现动态数据源的获取。AbstractRoutingDataSource,它的一个作用就是可以根据用户发起的不同请求去转换不同的数据源,比如根据用户的不同地区语言选择不同的数据库


import java.sql.SQLException;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

public class DynamicDataSource extends AbstractRoutingDataSource {

    // static Logger log = Logger.getLogger("DynamicDataSource");

    @Override
    protected Object determineCurrentLookupKey() {
        
        return DbContextHolder.getDbType();
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        
        return false;
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        
        return null;
    }
}

补充:dataSource.properties文件内容

DRIVER_NAME    =    xx.xx.xxx.xxxx

DATABASE_PASSWORD =  xxxxx
DATABASE_USER     =  xxxx
#私有数据源
DATABASE_URL_DEFAULT=jdbc:xx://localhost
#共有数据源
DATABASE_URL_COMMON=jdbc:xx://localhost

利用Druid实现私有数据源和公共数据源

    <bean id="dataSource_common" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="driverClassName" value="${DRIVER_NAME}" />
        <property name="url" value="${DATABASE_URL_DEFAULT}" />
        <property name="username" value="${DATABASE_USER}" />
        <property name="password" value="${DATABASE_PASSWORD}" />
        <property name="filters" value="stat" />
        <property name="maxActive" value="20" />
        <property name="initialSize" value="1" />
        <property name="maxWait" value="60000" />
        <property name="minIdle" value="1" />
        <property name="timeBetweenEvictionRunsMillis" value="3000" />
        <property name="minEvictableIdleTimeMillis" value="300000" />
        <property name="validationQuery" value="SELECT 'x'" />
        <property name="testWhileIdle" value="true" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
    </bean>


    <bean id="dataSource_default" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="driverClassName" value="${DRIVER_NAME}" />
        <property name="url" value="${DATABASE_URL_COMMON}" />
        <property name="username" value="${DATABASE_USER}" />
        <property name="password" value="${DATABASE_PASSWORD}" />
        <property name="filters" value="stat" />
        <property name="maxActive" value="20" />
        <property name="initialSize" value="1" />
        <property name="connectionProperties" value="druid.stat.slowSqlMillis=5000" />
        <property name="maxWait" value="60000" />
        <property name="minIdle" value="1" />
        <property name="timeBetweenEvictionRunsMillis" value="3000" />
        <property name="minEvictableIdleTimeMillis" value="300000" />
        <property name="validationQuery" value="SELECT 'x'" />
        <property name="testWhileIdle" value="true" />
        <property name="testOnBorrow" value="false" />
        <property name="testOnReturn" value="false" />
        <property name="poolPreparedStatements" value="true" />
        <property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
    </bean>

声明动态数据源springBean

map中可以存放多个数据源,在使用的时候指定key就可以获取对应的数据源了.

<bean id="dynamicDataSource" class="dao.DynamicDataSource">
    <property name="targetDataSources">
        <map key-type="java.lang.String">
            <!--私有数据源-->
            <entry key="default" value-ref="dataSource_default" />
            <!--公共数据源-->
            <entry key="common" value-ref="dataSource_common" />
        </map>
    </property>
    <!--默认的数据源-->
    <property name="defaultTargetDataSource" ref="dataSource_default" />
</bean>

配置Hibernate的SessionFactory

这里是在spring的配置文件中配置Hibernate的SessionFactorty,这里使用的dataSource已经是动态的数据源了。

    <!-- 配置会话工厂 -->
    <bean id="sessionFactory"
        class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
        <!-- 设置数据源 -->
        <property name="dataSource" ref="dynamicDataSource" />
        <!-- 接管了hibernate对象映射文件 -->
        <property name="mappingResources">
            <list>
                <value>xxx/xxx.hbm.xml</value>
            </list>
        </property>

        <!--指定hibernate的属性值 -->
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.xxx</prop>
                <!-- <prop key="hibernate.hbm2ddl.auto">update</prop> -->
                <prop key="hibernate.show_sql">false</prop>
                <prop key="hibernate.format_sql">true</prop>
                <!-- -->
                <prop key="javax.persistence.validation.mode">none</prop>
                <prop key="connection.driver_class">xx.xxx.xx.xx</prop>
            </props>
        </property>
    </bean>

测试数据源是否可以连接成功

这里使用的是基于注解的测试类,测试的运行结果如下图所示。

import dao.DynamicDataSource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.sql.SQLException;

/**
 * Created by frank on 16-5-30.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class NIuTest {
    @Autowired
    ApplicationContext context;
    @Test
    public void testContext() throws SQLException {
        DynamicDataSource ds = (DynamicDataSource) context.getBean("dynamicDataSource");
        System.out.println("loginTimeOutValue:"+ds.getLoginTimeout());
    }

}
enter description here
enter description here

创建线程私有的数据源上下文

创建一个数据源上下文持有者,该类使用了ThreadLocal能够保证线程私有,使得不同地区的用户访问时,不会出现数据源冲突。

public class DbContextHolder {
    private static final ThreadLocal contextHolder = new ThreadLocal();
    public static final String DATA_SOURCE_DEFAULT = "default";
    public static final String DATA_SOURCE_COMMON = "common";
    

    public static void setDbType(String dbType) {
        contextHolder.set(dbType);
    }

    public static String getDbType() {
        return (String) contextHolder.get();
    }

    public static void clearDbType() {
        contextHolder.remove();
    }

}

在程序运行时,如何切换数据源

service类中调用Hibernate进行数据操作之前,如果需要切换数据源到私有的数据源,可以调用下面的方法.然后springDynamicDataSource实例则会调用determineCurrentLookupKey方法进行数据源的切换。

DbContextHolder.setDbType(DbContextHolder.DATA_SOURCE_COMMON);

一个更为实际的例子如下,如果我们想要获取某条用户输入的sql语句可以得到多少条数据,那么我们需将数据源切换到用户私有的数据源。

    public long getAllMessageCount_Common(String sql)
            throws HibernateException, NullPointerException, SQLException {
        DbContextHolder.setDbType(DbContextHolder.DATA_SOURCE_COMMON);
        StringBuilder hql = null;
        hql = new StringBuilder("SELECT count(*) from (");
        hql.append(sql);
        hql.append(")");
        // logger.info("得到的sql语句:" + hql);
        long result = basicDao.executeQueryCountSQL_throwEX(hql.toString());
        logger.info("后台得到的数据count:" + result);
        return result;
    }

从上面可以看出,我们使用的是DbContextHolder.setDbType()方法来动态的进行数据源的切换。

总结

数据源的动态切换就整理到这里,项目已经做了很久了,今天在整理项目中使用到的知识时,想到这块自己可以整理一下,才写此文作为记录吧。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,638评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,778评论 6 342
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,870评论 25 707
  • 今天是2017年5月20日,是年轻人最有激情的一天。是对心爱的人表白日,可以想象:郑州市民政局结婚登记大厅的场面是...
    新呐喊阅读 1,010评论 7 9
  • 谁不说咱家乡好?每个人都对自己的故乡有一种特殊的情感。尤其是当离乡多年后回到故土,激动彷徨同时存在,近乡情更怯。即...
    凯里木阅读 250评论 0 0