說明
通過AOP的方式,根據當前操作的讀寫類型,自動切換數據源爲主庫還是從庫,配置和使用都很簡單,減少支持讀寫分離中間的引入,避免性能損失。
項目地址:https://gitee.com/laofeng/DynamicDatabaseSource
一、介紹
生產環境下,單個MySQL在小業務量下,支持讀寫是沒有問題的,但是隨着業務量的增加,至少此時需要做的就是將數據庫的讀寫進行分離,以便於支撐更高的流量。
目前有一些中間件可以做無感知的支持讀寫分離,如MyCAT,在後端配置好主庫和重庫,其會自動路由寫操作到主庫,讀操作到從庫,減少了開發人員的工作量,特別對於舊項目升級,非常的方便。但是引入了中間層,也就意味着處理的流程就變長了,出問題的機率也增加了,響應時間也會加長,其本身也可能隨着業務量的增加,成爲性能瓶頸。同了爲了避免單點,至少得兩臺服務器搭建高可用集羣,前面得再放一層LVS和HAPROXY等,這裏又是幾臺服務器的開銷,並且增加了運維成本和工作量。對於追到極致的程序員來說,還是想盡可能的再減少處理環節,提高工作效率。
DynamicDatabaseSource,通過在應用層支持主從的動態路由,其擴展自Spring動態數據源的支持,在主從環境數據環境中,支持自動根據操作的類型切換爲不同的數據源,如寫操作(Insert、Update、Delete)會自動切換爲主庫操作,讀和查操作,則會使用配置的從庫。
主要功能:
1、支持配置一主多從,寫走主庫,讀從多個從庫中隨機選擇一個數據源;
2、支持配置多主多從,寫操作從多個庫中隨機選擇一個,不過此時需要當前MySQL集羣支持多主,,讀從多個從庫中隨機選擇一個數據源;(注:多主目前不支持跨數據庫的事務)
3、支持通過方法名稱的前綴判斷是讀操作還是寫操作,如以delete、update、insert爲前綴的操作,則判斷爲寫操作,將其路由到主庫操作,如果是以select、query等爲前綴,則判斷爲讀操作,將其路徑到從庫進行操作;
4、支持通過註解的方式,指定當前操作是讀操作還是還寫操作,目前支持的註解:
@DataSourceMaster:指定當前操作爲寫操作
@DataSourceSlave:指定當前操作爲讀操作
@DataSource:通過Value的方式,指定DataOperateType的操作方式來判斷是寫操作還是讀操作,目前支持的類型爲:INSERT("insert"), UPDATE("update"), DELETE("delete"), SELECT("select"), GET("get"),QUERY("query")
5、支持多個分庫、每個分庫中多個分表的自動路由,使用場景用戶需要對抽象net.xiake6.orm.datasource.sharding.ShardingCondition進行實現,自定義數據路由到不同分庫、路由到不同分表的規則的實現,可以參看默認的實現示例類:
net.xiake6.orm.datasource.sharding.DefaultTableShardingCondition
net.xiake6.orm.datasource.sharding.DefaultDatabaseShardingCondition
6、包含有相應的單元測試,test/resources下面的test.sql爲用於測試的SQL語句,jdbc-sharding.properties爲測試數據源的配置,測試類在測試工程下,可以根據實際情況增減測試類。
二、使用說明-主從數據源配置(applicationContext-db-masterslave-context.xml)
需要在應用方增加類似如下的數據源配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:jee="http://www.springframework.org/schema/jee" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.3.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<context:property-placeholder location="classpath:jdbc.properties" ignore-unresolvable="true"/>
<!-- proxool連接池 -->
<bean id="dataSourceMaster" class="org.logicalcobwebs.proxool.ProxoolDataSource">
<property name="alias" value="${alias}" />
<property name="driver" value="${driver}" />
<property name="driverUrl" value="${driverUrl}" />
<property name="user" value="${db_user}" />
<property name="password" value="${db_password}" />
<property name="houseKeepingTestSql" value="${house-keeping-test-sql}" />
<property name="maximumConnectionCount" value="${maximum-connection-count}" />
<property name="minimumConnectionCount" value="${minimum-connection-count}" />
<property name="prototypeCount" value="${prototype-count}" />
<property name="simultaneousBuildThrottle" value="${simultaneous-build-throttle}" />
<property name="trace" value="${trace}" />
</bean>
<bean id="dataSourceSlave1" class="org.logicalcobwebs.proxool.ProxoolDataSource">
<property name="alias" value="${alias_slave1}" />
<property name="driver" value="${driver}" />
<property name="driverUrl" value="${driverUrl_slave1}" />
<property name="user" value="${db_user_slave1}" />
<property name="password" value="${db_password_slave1}" />
<property name="houseKeepingTestSql" value="${house-keeping-test-sql}" />
<property name="maximumConnectionCount" value="${maximum-connection-count}" />
<property name="minimumConnectionCount" value="${minimum-connection-count}" />
<property name="prototypeCount" value="${prototype-count}" />
<property name="simultaneousBuildThrottle" value="${simultaneous-build-throttle}" />
<property name="trace" value="${trace}" />
</bean>
<bean id="dataSourceSlave2" class="org.logicalcobwebs.proxool.ProxoolDataSource">
<property name="alias" value="${alias_slave2}" />
<property name="driver" value="${driver}" />
<property name="driverUrl" value="${driverUrl_slave2}" />
<property name="user" value="${db_user_slave2}" />
<property name="password" value="${db_password_slave2}" />
<property name="houseKeepingTestSql" value="${house-keeping-test-sql}" />
<property name="maximumConnectionCount" value="${maximum-connection-count}" />
<property name="minimumConnectionCount" value="${minimum-connection-count}" />
<property name="prototypeCount" value="${prototype-count}" />
<property name="simultaneousBuildThrottle" value="${simultaneous-build-throttle}" />
<property name="trace" value="${trace}" />
</bean>
<bean id="targetDataSources" class="java.util.HashMap">
<constructor-arg>
<map>
<!--
注:master數據源的key一定要以master開頭,slave數據源的key一定要以slave開頭。
master和slave都可以配置多個,框架會根據要執行的操作是數據修改還是查詢操作,
分別從master及slave中隨機獲取一個。
-->
<entry key="master" value-ref="dataSourceMaster" />
<entry key="slave1" value-ref="dataSourceSlave1"/>
<entry key="slave2" value-ref="dataSourceSlave2"/>
</map>
</constructor-arg>
</bean>
<bean id="dataSource" class="net.xiake6.orm.datasource.DynamicDataSource">
<property name="targetDataSources" ref="targetDataSources"/>
<property name="defaultTargetDataSource" ref="dataSourceMaster" />
</bean>
<!-- 對數據源進行事務管理 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 事務註解配置 -->
<tx:annotation-driven transaction-manager="transactionManager" />
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" rollback-for="***Exception"
propagation="REQUIRED" isolation="DEFAULT" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="interceptorPointCuts"
expression="execution(* net.xiake6.biz.service..*.*(..))" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="interceptorPointCuts" />
</aop:config>
<!-- 通過切面的方式控制在執行數據庫方法之前,切換主從 -->
<bean id="dataSourceAspect" class="net.xiake6.orm.datasource.DataSourceAspect" >
<property name="targetDataSources" ref="targetDataSources"/>
</bean>
<aop:config proxy-target-class="true">
<aop:aspect id="dataSourceAspect" ref="dataSourceAspect"
order="1">
<aop:pointcut id="tx"
expression="execution(* net.xiake6.orm.persistence.mapper.*.*(..)) " />
<aop:before pointcut-ref="tx" method="before" />
</aop:aspect>
</aop:config>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
<!--
指定用戶執行的Executor,默認爲SimpleExecutor.
SIMPLE表示SimpleExecutor,REUSE表示ResueExecutor,BATCH表示BatchExecutor,CLOSE表示CloseExecutor
-->
<constructor-arg index="1" value="REUSE" />
</bean>
<!-- mybatis文件配置,掃描所有mapper文件 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage"
value="net.xiake6.orm.persistence.mapper" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
<property name="sqlSessionTemplateBeanName" value="sqlSessionTemplate"></property>
</bean>
</beans>
該配置中的關鍵點: 1、配置多個數據源,並將其存在Map中
<bean id="targetDataSources" class="java.util.HashMap">
<constructor-arg>
<map>
<!--
注:master數據源的key一定要以master開頭,slave數據源的key一定要以slave開頭。
master和slave都可以配置多個,框架會根據要執行的操作是數據修改還是查詢操作,
分別從master及slave中隨機獲取一個。
-->
<entry key="master" value-ref="dataSourceMaster" />
<entry key="slave1" value-ref="dataSourceSlave1"/>
<entry key="slave2" value-ref="dataSourceSlave2"/>
</map>
</constructor-arg>
</bean>
2、指定數據源爲動態數據源net.xiake6.orm.datasource.DynamicDataSource:
<bean id="dataSource" class="net.xiake6.orm.datasource.DynamicDataSource">
<property name="targetDataSources" ref="targetDataSources"/>
<property name="defaultTargetDataSource" ref="dataSourceMaster" />
</bean>
3、通過切面的方式控制在執行數據庫方法之前,切換主從
<!-- 通過切面的方式控制在執行數據庫方法之前,切換主從 -->
<bean id="dataSourceAspect" class="net.xiake6.orm.datasource.DataSourceAspect" >
<property name="targetDataSources" ref="targetDataSources"/>
</bean>
三、使用說明-分庫分表數據源配置(applicationContext-db-sharding-context.xml)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
xmlns:jee="http://www.springframework.org/schema/jee" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.3.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.3.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<!-- 啓用aop -->
<aop:aspectj-autoproxy expose-proxy="true"></aop:aspectj-autoproxy>
<!-- 開啓註解配置,使Spring關注Annotation -->
<context:annotation-config />
<!-- 啓用aop -->
<aop:aspectj-autoproxy expose-proxy="true"></aop:aspectj-autoproxy>
<context:component-scan base-package="net.xiake6.orm.datasource">
</context:component-scan>
<!-- jdbc配置文件 -->
<context:property-placeholder location="classpath:jdbc-sharding.properties" ignore-unresolvable="true"/>
<bean id="logFilter" class="com.alibaba.druid.filter.logging.Slf4jLogFilter">
<property name="statementExecutableSqlLogEnable" value="true" />
</bean>
<!-- Druid連接池 -->
<bean id="dataSource_1" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${db_user_master_1}"></property>
<property name="password" value="${db_password_master_1}"></property>
<property name="url" value="${driverUrl_master_1}"></property>
<property name="driverClassName" value="${driver}"></property>
<!-- 初始化連接大小 -->
<property name="initialSize" value="${initialSize}"></property>
<!-- 連接池最大使用連接數量 -->
<property name="maxActive" value="${maxActive}"></property>
<!-- 連接池最小空閒 -->
<property name="minIdle" value="${minIdle}" />
<!-- 獲取連接最大等待時間 -->
<property name="maxWait" value="${maxWait}" />
<!-- 打開PSCache,並且指定每個連接上PSCache的大小 -->
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
<!-- 這裏配置提交方式,默認就是TRUE,可以不用配置 -->
<property name="defaultAutoCommit" value="true" />
<property name="validationQuery">
<value>${validationQuery}</value>
</property>
<!-- 這裏建議配置爲TRUE,防止取到的連接不可用 -->
<property name="testOnBorrow" value="${testOnBorrow}" />
<property name="testOnReturn" value="${testOnReturn}" />
<property name="testWhileIdle" value="${testWhileIdle}" />
<!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}" />
<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}" />
<!-- 打開removeAbandoned功能 -->
<property name="removeAbandoned" value="${removeAbandoned}" />
<!-- 移除被拋棄的鏈接的超時時間,單位爲秒 -->
<property name="removeAbandonedTimeout" value="${removeAbandonedTimeout}" />
<!-- 關閉abanded連接時輸出錯誤日誌 -->
<property name="logAbandoned" value="${logAbandoned}" />
<!-- 監控數據庫 -->
<!-- <property name="filters" value="stat" /> -->
<property name="filters" value="${druid-filter}" />
<property name="proxyFilters">
<list>
<ref bean="dynamicTableFilter"/>
<ref bean="logFilter" />
</list>
</property>
</bean>
<bean id="dataSource_2" class="com.alibaba.druid.pool.DruidDataSource">
<property name="username" value="${db_user_master_2}"></property>
<property name="password" value="${db_password_master_2}"></property>
<property name="url" value="${driverUrl_master_2}"></property>
<property name="driverClassName" value="${driver}"></property>
<!-- 初始化連接大小 -->
<property name="initialSize" value="${initialSize}"></property>
<!-- 連接池最大使用連接數量 -->
<property name="maxActive" value="${maxActive}"></property>
<!-- 連接池最小空閒 -->
<property name="minIdle" value="${minIdle}" />
<!-- 獲取連接最大等待時間 -->
<property name="maxWait" value="${maxWait}" />
<!-- 打開PSCache,並且指定每個連接上PSCache的大小 -->
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
<!-- 這裏配置提交方式,默認就是TRUE,可以不用配置 -->
<property name="defaultAutoCommit" value="true" />
<property name="validationQuery">
<value>${validationQuery}</value>
</property>
<!-- 這裏建議配置爲TRUE,防止取到的連接不可用 -->
<property name="testOnBorrow" value="${testOnBorrow}" />
<property name="testOnReturn" value="${testOnReturn}" />
<property name="testWhileIdle" value="${testWhileIdle}" />
<!-- 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}" />
<!-- 配置一個連接在池中最小生存的時間,單位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}" />
<!-- 打開removeAbandoned功能 -->
<property name="removeAbandoned" value="${removeAbandoned}" />
<!-- 移除被拋棄的鏈接的超時時間,單位爲秒 -->
<property name="removeAbandonedTimeout" value="${removeAbandonedTimeout}" />
<!-- 關閉abanded連接時輸出錯誤日誌 -->
<property name="logAbandoned" value="${logAbandoned}" />
<!-- 監控數據庫 -->
<!-- <property name="filters" value="stat" /> -->
<property name="filters" value="${druid-filter}" />
<property name="proxyFilters">
<list>
<ref bean="dynamicTableFilter"/>
<ref bean="logFilter" />
</list>
</property>
</bean>
<bean id="targetDataSources" class="java.util.HashMap">
<constructor-arg>
<map>
<!--
key一定是字符串+下劃線+DB序號,DB的序號從0開始,如有兩個DB,
則序號分別爲0和1,有四個DB,則序號分別爲O,1,2,3。
-->
<entry key="dataSource_0" value-ref="dataSource_1" />
<entry key="dataSource_1" value-ref="dataSource_2"/>
</map>
</constructor-arg>
</bean>
<!-- Sharding數據庫規則實現配置 -->
<bean id="databaseShardingCondition" class="net.xiake6.orm.datasource.sharding.DefaultDatabaseShardingCondition">
<property name="dbNums" value="2"/>
</bean>
<!-- Sharding表規則實現配置 -->
<bean id="tableShardingCondition" class="net.xiake6.orm.datasource.sharding.DefaultTableShardingCondition">
<!-- 建立Sharding表時,須按照實際表名+下劃線+序號,且表的序號要從0開始,如apps分成4個分表,則每個表分別爲:
apps_0、apps_1、apps_2、apps_3
-->
<property name="tableNums" value="4" />
</bean>
<bean id="shardingConfig" class="net.xiake6.orm.datasource.sharding.ShardingConfig">
<!-- 配置需要支持分表的表名 -->
<property name="shardingTables">
<set>
<value>apps</value>
</set>
</property>
<!-- 如果不需要配置多數據庫, databaseShardingCondition屬性可以不配置-->
<property name="databaseShardingCondition" ref="databaseShardingCondition" />
<property name="tableShardingCondition" ref="tableShardingCondition" />
</bean>
<bean id="dataSource" class="net.xiake6.orm.datasource.sharding.DynamicShardingDataSource">
<property name="targetDataSources" ref="targetDataSources"/>
<property name="defaultTargetDataSource" ref="dataSource_1" />
</bean>
<!-- 通過切面的方式控制在執行數據庫方法之前,切換多數據庫-->
<!-- expression一定要指向mapper所在的包,且確保mapper裏面的都是數據庫操作方法 -->
<!-- 如果不需要支持多數據庫,以下切面配置可以去掉 -->
<aop:config proxy-target-class="true">
<aop:aspect id="dataSourceAspect" ref="shardingDataSourceAspect"
order="1">
<aop:pointcut id="tx"
expression="execution(* net.xiake6.orm.persistence.mapper.*.*(..)) " />
<aop:before pointcut-ref="tx" method="before" />
</aop:aspect>
</aop:config>
<!-- 對數據源進行事務管理 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 事務註解配置 -->
<tx:annotation-driven transaction-manager="transactionManager" />
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" rollback-for="***Exception"
propagation="REQUIRED" isolation="DEFAULT" />
</tx:attributes>
</tx:advice>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
<!--
指定用戶執行的Executor,默認爲SimpleExecutor.
SIMPLE表示SimpleExecutor,REUSE表示ResueExecutor,BATCH表示BatchExecutor,CLOSE表示CloseExecutor
-->
<constructor-arg index="1" value="REUSE" />
</bean>
<!-- mybatis文件配置,掃描所有mapper文件 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage"
value="net.xiake6.orm.persistence.mapper" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property>
<property name="sqlSessionTemplateBeanName" value="sqlSessionTemplate"></property>
</bean>
</beans>
分庫分表的核心配置:
<!-- Sharding數據庫規則實現配置 -->
<bean id="databaseShardingCondition" class="net.xiake6.orm.datasource.sharding.DefaultDatabaseShardingCondition">
<property name="dbNums" value="2"/>
</bean>
<!-- Sharding表規則實現配置 -->
<bean id="tableShardingCondition" class="net.xiake6.orm.datasource.sharding.DefaultTableShardingCondition">
<!-- 建立Sharding表時,須按照實際表名+下劃線+序號,且表的序號要從0開始,如apps分成4個分表,則每個表分別爲:
apps_0、apps_1、apps_2、apps_3
-->
<property name="tableNums" value="4" />
</bean>
<bean id="shardingConfig" class="net.xiake6.orm.datasource.sharding.ShardingConfig">
<!-- 配置需要支持分表的表名 -->
<property name="shardingTables">
<set>
<value>apps</value>
</set>
</property>
<!-- 如果不需要配置多數據庫, databaseShardingCondition屬性可以不配置-->
<property name="databaseShardingCondition" ref="databaseShardingCondition" />
<property name="tableShardingCondition" ref="tableShardingCondition" />
</bean>
<bean id="dataSource" class="net.xiake6.orm.datasource.sharding.DynamicShardingDataSource">
<property name="targetDataSources" ref="targetDataSources"/>
<property name="defaultTargetDataSource" ref="dataSource_1" />
</bean>
<!-- 通過切面的方式控制在執行數據庫方法之前,切換多數據庫-->
<!-- expression一定要指向mapper所在的包,且確保mapper裏面的都是數據庫操作方法 -->
<!-- 如果不需要支持多數據庫,以下切面配置可以去掉 -->
<aop:config proxy-target-class="true">
<aop:aspect id="dataSourceAspect" ref="shardingDataSourceAspect"
order="1">
<aop:pointcut id="tx"
expression="execution(* net.xiake6.orm.persistence.mapper.*.*(..)) " />
<aop:before pointcut-ref="tx" method="before" />
</aop:aspect>
</aop:config>