SSH动态数据源切换,事务场景下使用AOP

上周写代码遇到了切换数据源的问题,在同一个方法中向两个不同数据源做一些操作,但是这个方法使用了事务,所以网上一般动态切换数据源的方法就失效了。框架是spirngmvc+hibernate,数据库是oracle,连接池druid。

一般情况下,操作数据都是在DAO层进行处理。一种办法是使用多个DataSource 然后创建多个SessionFactory,在使用Dao层的时候通过不同的SessionFactory进行处理,不过这样的入侵性比较明显,一般的情况下我们都是使用继承HibernateSupportDao进行封装了的处理,如果多个SessionFactor这样处理就是比较的麻烦了,修改的地方估计也很多。最后一点,也就是使用AbstractRoutingDataSource的实现类通过AOP或者手动处理实现动态的使用我们的数据源,这样入侵性较低,且很好地满足了使用的需求。比如我们希望对于读写分离或者其他的数据进行同步的业务场景。

下面来看看图片
image
  • 单数据源的场景(一般的Web项目工程这样配置进行处理,就已经比较能够满足我们的业务需求)。
  • 多数据源多SessionFactory这样的场景,估计作为刚刚开始想象想处理在使用框架的情况下处理业务,配置多个SessionFactory,然后在Dao层中对于特定的请求,通过特定的SessionFactory即可处理实现这样的业务需求,不过这样的处理带来了很多的不便之处,所有很多情况下我们宁愿直接使用封装的JDBC编程,或者使用Mybatis处理这样的业务场景。
  • 使用AbstractRoutingDataSource 的实现类,进行灵活的切换,可以通过AOP或者手动编程设置当前的DataSource,不用修改我们编写的对于继承HibernateSupportDao的实现类的修改,这样的编写方式比较好,至于其中的实现原理,让我细细道来。我们想看看如何去应用,实现原理慢慢的说!
  • 编写AbstractRoutingDataSource的实现类,DataSourceContextHolder就是提供给我们动态选择数据源的工具类,我们这里编写一个根据当前线程来选择数据源,然后通过AOP拦截特定的注解,设置当前的数据源信息,也可以手动的设置当前的数据源,在编程的类中。
/**
 *类名:DynamicDataSource.java
 *功能:动态数据源类
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    /*
     * 该方法必须要重写  方法是为了根据数据库标示符取得当前的数据库
     */
    @Override
    protected Object determineCurrentLookupKey() {
        DataSourceType type = DataSourceContextHolder.getType();
        return type;
    }
 
    public void setDataSourceLookup(DataSourceLookup dataSourceLookup) {
        super.setDataSourceLookup(dataSourceLookup);
    }
 
    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
    }
     
    public void setTargetDataSources(Map targetDataSources) {
        super.setTargetDataSources(targetDataSources);
    }
}
  • 设置动态选择的Datasource,这里的Set方法可以留给AOP调用,或者留给我们的具体的Dao层或者Service层中手动调用,在执行SQL语句之前。
/**
 * 获得和设置上下文环境的类,主要负责改变上下文数据源的名称,根据当前线程来选择具体的数据源
 */
public class DataSourceContextHolder {
   /**获取当前线程*/
    private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal();
   /**提供给AOP去设置当前线程数据源的信息*/
    public static void setType(DataSourceType type) {
        contextHolder.set(type);
    }
   /**提供给AbstractRoutingDataSource的实现类,通过key选择数据源*/
    public static DataSourceType getType() {
        return contextHolder.get();
    }
  /**使用默认的数据源*/
    public static void clear() {
        contextHolder.remove();
    }
}
  • 设置拦截数据源的注解,可以设置在具体的类上,或者在具体的方法上,DataSourceType是当前数据源的一个别名用于标识我们的数据源的信息(此处是一个枚举类)。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicSwitchDataSource {
    DataSourceType type();
}
public enum DataSourceType {
    DATASOURCE_RDP("dataSource_rdp"),
    DATASOURCE_OCM("dataSource_ocm");
 
    private String type;
 
    DataSourceType(String type) {
        this.type = type;
    }
 
    public String type() {
        return type;
    }
}
  • AOP拦截类的实现,通过拦截上面的注解,在其执行之前处理设置当前执行SQL的数据源的信息,DataSourceContextHolder.setType(),这里的数据源信息从我们设置的注解上面获取信息,如果没有设置就是用默认的数据源的信息。

@Component
@Aspect
@Order(1)
public class DataSourceAspect {
    private static final Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);
 
    //@within在类上设置
    //@annotation在方法上进行设置
    @Pointcut("@within(org.jeecgframework.core.annotation.DynamicSwitchDataSource)||@annotation(org.jeecgframework.core.annotation.DynamicSwitchDataSource)")
    public void pointcut() {
    }
 
    @Before("pointcut()")
    public void doBefore(JoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        DynamicSwitchDataSource annotationClass = method.getAnnotation(DynamicSwitchDataSource.class);//获取方法上的注解
        if (annotationClass == null) {
            annotationClass = joinPoint.getTarget().getClass().getAnnotation(DynamicSwitchDataSource.class);//获取类上面的注解
            if (annotationClass == null) return;
        }
        //获取注解上的数据源的值的信息
        DataSourceType dataSourceKey = annotationClass.type();
        if (dataSourceKey != null) {
            //给当前的执行SQL的操作设置特殊的数据源的信息
            DataSourceContextHolder.setType(dataSourceKey);
        }
        logger.info("AOP动态切换数据源,className" + joinPoint.getTarget().getClass().getName() + "methodName" + method.getName() + ";dataSourceKey:" + dataSourceKey == "" ? "默认数据源" : dataSourceKey.type());
    }
 
    @After("pointcut()")
    public void after(JoinPoint point) {
        //清理掉当前设置的数据源,让默认的数据源不受影响
        DataSourceContextHolder.clear();
    }
}
  • 配置数据源在Spring 核心容器中配置
<!-- 配置数据源rdp -->
    <bean id="dataSource_rdp" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${rdp.url}"/>
        <property name="username" value="${rdp.username}"/>
        <property name="password" value="${rdp.password}"/>
        <!-- 初始化连接大小 -->
        <property name="initialSize" value="0"/>
        <!-- 连接池最大使用连接数量 -->
        <property name="maxActive" value="50"/>
        <!-- 连接池最小空闲 -->
        <property name="minIdle" value="5"/>
        <!-- 获取连接最大等待时间 -->
        <property name="maxWait" value="60000"/>
        <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
        <property name="timeBetweenEvictionRunsMillis" value="60000"/>
        <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
        <property name="minEvictableIdleTimeMillis" value="300000"/>
        <!--<property name="validationQuery" value="${validationQuery.sql}" />-->
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>
        <property name="testWhileIdle" value="true"/>
        <!-- 开启Druid的监控统计功能 -->
        <property name="filters" value="stat"/>
        <!-- 打开removeAbandoned功能 -->
        <property name="removeAbandoned" value="true"/>
        <!-- 1800秒,也就是30分钟 -->
        <property name="removeAbandonedTimeout" value="3600"/>
        <!-- 关闭abanded连接时输出错误日志 -->
        <property name="logAbandoned" value="true"/>
        <!-- Oracle连接是获取字段注释 -->
        <property name="connectProperties">
            <props>
                <prop key="remarksReporting">true</prop>
            </props>
        </property>
    </bean>
 
    <!-- 配置数据源ocm -->
    <bean id="dataSource_ocm" class="com.alibaba.druid.pool.DruidDataSource"
        ...数据源2
    </bean>
<!-- 数据源集合 -->
    <bean id="dataSource" class="org.jeecgframework.core.extend.datasource.DynamicDataSource">
        <property name="targetDataSources">
            <map key-type="org.jeecgframework.core.extend.datasource.DataSourceType">
                <entry key="DATASOURCE_RDP" value-ref="dataSource_rdp"/>
                <entry key="DATASOURCE_OCM" value-ref="dataSource_ocm"/>
            </map>
        </property>
        <property name="defaultTargetDataSource" ref="dataSource_rdp"/>
    </bean>
 
    <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="entityInterceptor" ref="hiberAspect"/>
        <property name="hibernateProperties">
            <props>
                <!--<prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop> -->
                <prop key="hibernate.dialect">org.hibernate.dialect.OracleDialect</prop>
                <!--<prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>-->
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.format_sql">true</prop>
                <prop key="hibernate.temp.use_jdbc_metadata_defaults">false</prop>
            </props>
        </property>
  • 配置之前我们实现的数据源选择的中间层AbstractRoutingDataSource的实现类,这里的key就是数据源信息的别名,通过这个key可以选择到数据源的信息。DynamicDataSource就是上面写的数据源选择器的实现类。
  • SessionFactory的配置还是照旧,使用以前的配置,只不过当前选择的数据源是datasource,也就是数据源选择的中间层DynamicDataSource,因为当前的中间层中实现了DataSource这个接口,所以可以看做为DataSource的是实现类啦,所以配置不会出现问题。
简单的使用AOP进行测试一下,这里测试的结果时不同的,所以是生效的,使用了不同的数据源,但是底层的实现没有进行任何的修改处理。(Transactional是事务注解)
  • 看下AOP配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"...  >
    <!--设置expose-proxy属性为true,将代理暴露出来-->
    <aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>
    <context:component-scan base-package="org.jeecgframework.core.aop"/>
</beans>
@Service("cgFormFieldService")
@Slf4j
@Transactional
public class CgFormFieldServiceImpl implements CgFormFieldServiceI{
    @DynamicSwitchDataSource(type = DataSourceType.DATASOURCE_RDP)
    public void deleteCgForm(CgFormHeadEntity cgFormHead) {
        this.delete(cgFormHead);
        String sql = getTableUtil().dropTableSQL(cgFormHead.getTableName());
        //执行ocm中sql
        ((CgFormFieldServiceImpl) AopContext.currentProxy()).executeOcmSql(sql);
    }
   @DynamicSwitchDataSource(type = DataSourceType.DATASOURCE_OCM)
   @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void executeOcmSql(String sql) {
        this.executeSql(sql);
    }
}
事务环境的隔离性

如果在一个方法A中已经开启事务,在这个方法A中调用另一个事务方法B,如果不作其他配置,那么方法B会沿用方法A的事务环境,而不会开启一个新的事务。如果不开启一个新的事务,当然也不会进行一系列的改变直至数据源的切换。这也证明他们同处于一个事务环境,因为hibernate的session适合transaction绑定的。由于事务环境的隔离性,所以下面一个方法中必须设置为REQUIRES_NEW。

在Service层,A方法调用B方法的时候,用了((Service)AopContext.currentProxy()).B() 作用:
原来在springAOP的用法中,只有代理的类才会被切入,我们在controller层调用service的方法的时候,是可以被切入的,但是如果我们在service层 A方法中,调用B方法,切点切的是B方法,那么这时候是不会切入的,解决办法就是如上所示,在A方法中使用((Service)AopContext.currentProxy()).B() 来调用B方法,这样一来,就能切入了!

总结

​ 以上就是所有关于动态切换数据源的内容,相关知识主要涉及到AOP,事务管理。因此只要掌握相关知识,想来处理起来不算困难。需要注意的点:AbstractRoutingDataSource 的使用、带参枚举类、事物的隔离性、AOP代理等,希望能够帮到大家。

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