说明
通过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>