使用spring的动态路由实现数据库负载均衡
系统中存在的多台服务器是“地位相当”的,不过,同一时间他们都处于活动(Active)状态,处于负载均衡等因素考虑,数据访问请求需要在这几台数据库服务器之间进行合理分配, 这个时候,通过统一的一个DataSource来屏蔽这种请求分配的需求,从而屏蔽数据访问类与具体DataSource的耦合;
系统中存在的多台数据库服务器现在地位可能相当也可能不相当,但数据访问类在系统启动时间无法明确到底应该使用哪一个数据源进行数据访问,而必须在系统运行期间通过某种条件来判定到底应该使用哪一个数据源,这个时候,我们也得使用这种“合纵连横”的方式 向数据访问类暴露一个统一的DataSource ,由该DataSource来解除数据访问类与具体数据源之间的过紧耦合;更多场景需要读者根据具体的应用来判定,不过,并非所有的应用要做这样的处理,如果能够保持简单,那尽量保持简单.要实现这种“合纵连横”的多数据源管理方式,总的指导原则就是 实现一个自定义的DataSource,让该DataSource来管理系统中存在的多个与具体数据库挂钩的数据源, 数据访问类只跟这个自定义的DataSource打交道即可 。在spring2.0.1发布之前,各个项目中可能存在多种针对这种情况下的多数据源管理方式, 不过,spring2.0.1发布之后,引入了AbstractRoutingDataSource,使用该类可以实现普遍意义上的多数据源管理功能。
假设我们有三台数据库用来实现负载均衡,所有的数据访问请求最终需要平均的分配到这三台数据库服务器之上,那么,我们可以通过继承AbstractRoutingDataSource来快速实现一个满足这样场景的原型(Prototype):
- public class PrototypeLoadBalanceDataSource extends AbstractRoutingDataSource {
- private Lock lock = new ReentrantLock();
- private int counter = 0;
- private int dataSourceNumber = 3;
- @Override
- protected Object determineCurrentLookupKey() {
- lock.lock();
- try{
- counter++;
- int lookupKey = counter % getDataSourceNumber();
- return new Integer(lookupKey);
- }finally{
- lock.unlock();
- }
- }
- // ...
- }
- <bean id="dataSourc1" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
- <property name="url" value=".."/>
- <property name="driverClassName" value=".."/>
- <property name="username" value=".."/>
- <property name="password" value=".."/>
- <!-- other property settings -->
- </bean>
- <bean id="dataSource2" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
- <property name="url" value=".."/>
- <property name="driverClassName" value=".."/>
- <property name="username" value=".."/>
- <property name="password" value=".."/>
- <!-- other property settings -->
- </bean>
- <bean id="dataSource3" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
- <property name="url" value=".."/>
- <property name="driverClassName" value=".."/>
- <property name="username" value=".."/>
- <property name="password" value=".."/>
- <!-- other property settings -->
- </bean>
- <util:map id="dataSources">
- <entry key="0" value-ref="dataSource1"/>
- <entry key="1" value-ref="dataSource2"/>
- <entry key="2" value-ref="dataSource3"/>
- </util:map>
- <bean id="dataSourceLookup" class="org.springframework.jdbc.datasource.lookup.MapDataSourceLookup">
- <constructor-arg>
- <ref bean="dataSources"/>
- </constructor-arg>
- </bean>
- <bean id="dataSource" class="..PrototypeLoadBalanceDataSource">
- <property name="defaultTargetDataSource" ref="dataSourc1"/>
- <property name="targetDataSources" ref="dataSources"/>
- <property name="dataSourceLookup" ref=""/>
- </bean>
- <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
- <property name="dataSource" ref="dataSource"/>
- </bean>
- <bean id="someDao" class="...">
- <property name=""jdbcTemplate"" ref=""jdbcTemplate""/>
- <!-- other property settings -->
- </bean>
Spring2.0.1以后的版本已经支持配置多数据源,并且可以在运行的时候动态加载不同的数据源。通过继承AbstractRoutingDataSource就可以实现多数据源的动态转换。目前做的项目就是需要访问2个数据源,每个数据源的表结构都是相同的,所以要求数据源的变动对于编码人员来说是透明,也就是说同样SQL语句在不同的环境下操作的数据库是不一样的。具体的流程如下:
1.建立一个获得和设置上下文的类
- package com.lvye.base.dao.impl.jdbc;
- /**
- *连接哪个数据源的环境变量
- */
- public class JdbcContextHolder {
- private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
- public static void setJdbcType(String jdbcType) {
- contextHolder.set(jdbcType);
- }
- public static void setSlave(){
- setJdbcType("slave");
- }
- public static void setMaster(){
- clearJdbcType();
- }
- public static String getJdbcType(){
- return (String) contextHolder.get();
- }
- public static void clearJdbcType() {
- contextHolder.remove();
- }
- }
- package com.lvye.base.dao.impl.jdbc;
- import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
- public class DynamicDataSource extends AbstractRoutingDataSource{
- /*(non-Javadoc)
- *@see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()
- *@author wenc
- */
- @Override
- protected Object determineCurrentLookupKey() {
- return JdbcContextHolder.getJdbcType();
- }
- }
3.编写spring的配置文件配置数据源
- <beans>
- <bean id="master" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
- <property name="driverClass">
- <value>com.mysql.jdbc.Driver</value>
- </property>
- <property name="jdbcUrl">
- <value>jdbc:mysql://192.168.18.143:3306/wenhq?useUnicode=true&characterEncoding=utf-8</value>
- </property>
- <property name="user">
- <value>root</value>
- </property>
- <property name="password">
- <value></value>
- </property>
- </bean>
- <bean id="slave" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
- <property name="driverClass">
- <value>com.mysql.jdbc.Driver</value>
- </property>
- <property name="jdbcUrl">
- <value>jdbc:mysql://192.168.18.144:3306/ wenhq?useUnicode=true&characterEncoding=utf-8</value>
- </property>
- <property name="user">
- <value>root</value>
- </property>
- <property name="password">
- <value></value>
- </property>
- </bean>
- <bean id="mySqlDataSource" class="com.lvye.base.dao.impl.jdbc.DynamicDataSource">
- <property name="targetDataSources">
- <map>
- <entry key="slave" value-ref="slave"/>
- </map>
- </property>
- <property name="defaultTargetDataSource" ref="master"/>
- </bean>
- <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
- <property name="dataSource" ref="mySqlDataSource" />
- </bean>
- <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
- <property name="dataSource" ref="mySqlDataSource" />
- </bean>
- </beans>
4.多数据库连接配置完毕,简单测试
- public void testSave() throws Exception{
- jdbcContextHolder.setSlave();//设置从数据源
- Test test = new Test();
- test.setTest("www.wenhq.com.cn");
- mydao.save(test);//使用dao保存实体
- jdbcContextHolder.setMaster();//设置主数据源
- mydao.save(test);//使用dao保存实体到另一个库中
- }
- public void execute(String sql) {
- JdbcContextHolder.setMaster();
- log.debug("execute-sql:" + sql);
- jdbcTemplate.execute(sql);
- }
- public List findObject(String queryString, Class clazz) {
- JdbcContextHolder.setSlave();
- log.debug("findObject-sql:" + queryString);
- List list = jdbcTemplate.queryForList(queryString);
- try {
- list = StringBase.convertList(list, clazz);// 将List转化为List<clazz>
- } catch (Exception e) {
- log.error("List convert List<Object> error:" + e);
- }
- AbstractRoutingDataSourcereturn list;
- }
1. 前提
好长时间不写博客了,应该吐槽,写点什么东西了!最近在研究数据库读写分离,分表分库的一些东西。其实这个问题好早之前就想好,只是以前使用hibernate,难点是不好判断什么样的sql走读库,什么样的sql走主库?用正则匹配开头或许可以,/^select 没想出什么好的解决方法,mybatis就不一样了,mappedstatement有commandtype属性,象select,update,delete等类型,为实现读写分离打下来良好的基础。
2. 解决方法
LazyConnectionProxy + RoutingDataSource + Plugin
在SqlSessionTemplate,创建DefaultSqlSession的时候,使用connection proxy的代理,这时并没有真正的获取connection,因为我们不知道是要取读还是写的数据源。待到StatementHandler的prepare()使用connection创建PreparedStatement的时候再根据mappedstatement的commandType去路由获取真实的connection。
RoutingDataSource支持一主一从,或者一主多从并采用round robin的方式简单负载均衡,预留接口路由和负载均衡策略可自定义。
不支持事务,适合auto commit为true的场景。表述能力
applicationContext-common.xml
- <?xml version="1.0" encoding="UTF-8"?>
- <beans xmlns="http://www.springframework.org/schema/beans"
- xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
- http://www.springframework.org/schema/aop
- http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
- http://www.springframework.org/schema/tx
- http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
- http://www.springframework.org/schema/context
- http://www.springframework.org/schema/context/spring-context-3.0.xsd">
- <!-- 导入属性配置文件 -->
- <context:property-placeholder location="classpath*:*.properties" />
- <bean id="abstractDataSource" abstract="true"
- class="com.mchange.v2.c3p0.ComboPooledDataSource"
- destroy-method="close">
- <property name="driverClass" value="com.mysql.jdbc.Driver" />
- <property name="user" value="root" />
- <property name="password" value="" />
- </bean>
- <bean id="readDS" parent="abstractDataSource">
- <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test" />
- </bean>
- <bean id="writeDS" parent="abstractDataSource">
- <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test" />
- </bean>
- <!--简单的一个master和一个slaver 读写分离的数据源 -->
- <bean id="routingDS" class="com.test.rwmybatis.RoutingDataSource">
- <property name="targetDataSources">
- <map key-type="java.lang.String">
- <entry key="read" value-ref="readDS"></entry>
- <entry key="write" value-ref="writeDS"></entry>
- </map>
- </property>
- <property name="defaultTargetDataSource" ref="writeDS"></property>
- </bean>
- <!-- 适用于一个master和多个slaver的场景,并用roundrobin做负载均衡 -->
- <bean id="roundRobinDs" class="com.test.rwmybatis.RoundRobinRWRoutingDataSource">
- <property name="writeDataSource" ref="writeDS"></property>
- <property name="readDataSoures">
- <list>
- <ref bean="readDS"/>
- <ref bean="readDS"/>
- <ref bean="readDS"/>
- </list>
- </property>
- <property name="readKey" value="READ"></property>
- <property name="writeKey" value="WRITE"></property>
- </bean>
- <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
- <property name="dataSource" ref="routingDS" />
- <property name="configLocation" value="classpath:mybatis-config.xml" />
- <!-- mapper和resultmap配置路径 -->
- <property name="mapperLocations">
- <list>
- <value>classpath:com/test/rwmybatis/mapper/**/*-Mapper.xml
- </value>
- </list>
- </property>
- </bean>
- <bean id="sqlSessionTemplate" class="com.test.rwmybatis.RWSqlSessionTemplate">
- <constructor-arg ref="sqlSessionFactory" />
- </bean>
- <!-- 通过扫描的模式,扫描目录下所有的mapper, 根据对应的mapper.xml为其生成代理类-->
- <bean id="mapper" class="com.test.rwmybatis.RWMapperScannerConfigurer">
- <property name="basePackage" value="com.test.rwmybatis.mapper" />
- <property name="sqlSessionTemplate" ref="sqlSessionTemplate"></property>
- </bean>
- <!-- <bean id="monitor" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"></bean> -->
- <!-- <aop:config> -->
- <!-- <aop:pointcut expression="execution(* com.taofang.smc.persistence..*.*(..))" id="my_pc"/> -->
- <!-- <aop:advisor advice-ref="monitor" pointcut-ref="my_pc"/> -->
- <!-- </aop:config> -->
- </beans>