使用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();
- }
- }
- // ...
- }
我們在介紹AbstractRoutingDataSource的時候說過,要繼承該類,通常只需要給出determineCurrentLookupKey()方法的邏輯即可。 下面是針對PrototypeLoadBalanceDataSource的配置:
- <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>
使用spring的動態路由實現數據庫讀寫分離
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();
- }
- }
2.建立動態數據源類,這個類必須繼承AbstractRoutingDataSource
- 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();
- }
- }
這個類實現了determineCurrentLookupKey方法,該方法返回一個Object,一般是返回字符串。該方法中直接使用了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>
在這個配置中可以看到首先配置兩個真實的數據庫連接,使用的msyql數據庫;master和slave是按照mysql配置的主從關係的數據庫,數據會自動實時同步mySqlDataSource會根據上下文選擇不同的數據源。在這個配置中第一個property屬性配置目標數據源,<entry key="slave" value-ref=" slave"/>中key的值必須要和JdbcContextHolder類中設置的參數值相同,如果有多個值,可以配置多個<entry>標籤。第二個property屬性配置默認的數據源,我們一般默認爲主數據庫。有些朋友喜歡使用hibernate,只需要把上面的jdbcTemplate替換爲hibernate的就可以了。
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保存實體到另一個庫中
- }
5.實現讀寫分離,上面的測試通過了,現在就簡單了我的程序是使用jdbc實現的保存數據,只是使用了c3p0的數據庫連接池而已。把所有訪問數據庫的方法包裝一下,統一調用。把執行更新的sql發送到主數據庫了
- public void execute(String sql) {
- JdbcContextHolder.setMaster();
- log.debug("execute-sql:" + sql);
- jdbcTemplate.execute(sql);
- }
把查詢的發送到從數據庫,需要注意的是像LAST_INSERT_ID這類的查詢需要特殊處理,必須發送到主數據庫,建議增加專門的方法,用於獲取自增長的主鍵。
- 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>