项目开发中读写的频率差距很大,所以实现读写分离:主库(master)中非实时读取的查询交给负载均衡的从库(slave),查询cpu的消耗和写入的io延时,保证DB系统的健壮性。
最终问题,分布式事务的线索。最后附源码[springmvc+spring+mybatis+MySQL]:
注:注释和部分代码省略。 ①:AbstractRoutingDataSource这个数据源路由类是spring2.0以后增加的,AbstractRoutingDataSource的定义: /* @since 2.0.1*/ public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {……} 通过实现determineCurrentLookupKey()抽象方法可以实现读写数据源的切换。 ②:AbstractRoutingDataSource继承了AbstractDataSource,而AbstractDataSource是DataSource的子类。DataSource是javax.sql的数据源接口,定义: public interface DataSource extends CommonDataSource,Wrapper { Connection getConnection() throws SQLException; Connection getConnection(String username, String password) throws SQLException; } DataSource数据源接口定义了2个方法,都是获取数据库连接的。 ③:AbstractRoutingDataSource这个数据源路由类对获取连接的实现: public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { private Map<Object, DataSource> resolvedDataSources; private Object defaultTargetDataSource; private Map<Object, Object> targetDataSources; @Override public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); } @Override public Connection getConnection(String username, String password) throws SQLException { return determineTargetDataSource().getConnection(username, password); } protected abstract Object determineCurrentLookupKey(); } 通过自己定义的determineTargetDataSource()方法获取到数据连接connection。 ④:determineTargetDataSource的定义: protected DataSource determineTargetDataSource() { Assert.notNull(this.resolvedDataSources, "DataSource router not initialized"); Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = this.resolvedDataSources.get(lookupKey); if (dataSource == null && (this.lenientFallback || lookupKey == null)) { dataSource = this.resolvedDefaultDataSource; } if (dataSource == null) { throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]"); } return dataSource; } determineCurrentLookupKey方法返回lookupKey,resolvedDataSources方法就是根据lookupKey从Map中获得数据源。从③看出resolvedDataSources集合属性和determineCurrentLookupKey()抽象方法定义。 ⑤:我们自己定义类继承路由类去实现determineCurrentLookupKey()方法,然后自己来切换字符串告诉this.resolvedDataSources.get(lookupKey);方法这个lookupKey是谁?(也就是这个字符所对应的数据源的key(请看mybatis-context.xml配置40行注入defaultTargetDataSource和targetDataSources的值,通过‘lookupKey’字符串切换,spring<span style="font-family: Arial, Helvetica, sans-serif;">注入的数据源对象))。</span>1:通过注解在dao方法上注解,此方法开启那个数据源:
package com.common.readwriteseparate; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DataSource { String value(); }2:自己定义类继承路由类去实现determineCurrentLookupKey()方法:
package com.common.readwriteseparate; import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; public class DynamicDataSource extends AbstractRoutingDataSource{ @Override protected Object determineCurrentLookupKey() { String dataSouceKey = DynamicDataSourceHolder.getDataSouce(); return dataSouceKey; } }多线程辅助类:
package com.common.readwriteseparate; public class DynamicDataSourceHolder { public static final ThreadLocal<String> holder = new ThreadLocal<String>(); public static void putDataSource(String name) { holder.set(name); } public static String getDataSouce() { return holder.get(); } }
在DynamicDataSource的定义知道,返回的是DynamicDataSourceHolder.getDataSouce()值,我们需要在程序运行时调用DynamicDataSourceHolder.putDataSource()方法,对dataSouceKey数据源key赋值。
package com.common.readwriteseparate; import java.lang.reflect.Method; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DataSourceAspect { //log private static final Logger LOG = LoggerFactory.getLogger(DataSourceAspect.class); public void before(JoinPoint point) { Object target = point.getTarget(); String method = point.getSignature().getName(); Class<?>[] classz = target.getClass().getInterfaces(); Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes(); try { Method methodRefRes = classz[0].getMethod(method, parameterTypes); if (methodRefRes != null && methodRefRes.isAnnotationPresent(DataSource.class)) { DataSource data = methodRefRes.getAnnotation(DataSource.class); DynamicDataSourceHolder.putDataSource(data.value()); LOG.info("\n************************************************\n" + "\t~~~DB:: " + data.value() + "\n************************************************"); } } catch (Exception e) { LOG.error("数据源失败切面获取异常:" + e.getMessage(), e); } } }5:先看看配置:
①spring的配置(spring-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:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd"> <!-- 使用annotation 自动注册bean,并检查@Required,@Autowired的属性已被注入 --> <context:component-scan base-package="com.user.service,com.user.dao,com.user.bean,com.user.xml" /> <!-- 引入属性配置文件 --> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="location" value="classpath:database.properties" /> </bean> <!--或 <context:property-placeholder location="classpath*:*.properties" /> --> </beans>②:mybatis-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:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd" default-lazy-init="true"> <description>MyBatis的数据库持久层配置/配置主-从数据源</description> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <!-- 自动扫描entity目录, 省掉Configuration.xml里的手工配置 --> <property name="mapperLocations" value="classpath*:com/user/xml/*.xml" /> <property name="configLocation" value="classpath:mybatis-config.xml"></property> </bean> <!-- 扫描dao --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.user.dao" /> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> </bean> <!-- 配置数据库注解aop --> <aop:aspectj-autoproxy /> <bean id="dataSourceAspect" class="com.common.readwriteseparate.DataSourceAspect" /> <aop:config> <aop:aspect id="c" ref="dataSourceAspect"> <aop:pointcut id="tx" expression="execution(* com.user.dao.*.*(..))" /> <aop:before pointcut-ref="tx" method="before" /> </aop:aspect> </aop:config> <!-- 主-从数据源路由 --> <bean id="dataSource" class="com.common.readwriteseparate.DynamicDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <!-- write --> <entry key="master" value-ref="masterDataSource"/> <!-- read --> <entry key="slave" value-ref="slaveDataSource"/> </map> </property> <property name="defaultTargetDataSource" ref="masterDataSource"/> </bean> </beans>③:mybatis-config.xml:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <typeAliases> <package name="com.user.bean"/> </typeAliases> </configuration>④:database.properties:
#mysql-Used to verify the effectiveness of the database connection validationQuery=SELECT 1 jdbc.initialSize=5 jdbc.maxActive=20 jdbc.maxWait=60000 jdbc.poolPreparedStatements=false jdbc.poolMaximumIdleConnections=0 jdbc.driverClassName=org.gjt.mm.mysql.Driver #1.tms business. 2.The db level optimization,data concurrency,desirable. master.jdbc.url=jdbc:mysql://your ip:3306/master?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull slave.jdbc.url=jdbc:mysql://your ip:3306/slave?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull jdbc.username=username jdbc.password=password⑤:数据源配置:datasource-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:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd" default-lazy-init="true"> <description>配置主-从数据源</description> <!-- 配置数据源-Master --> <bean name="masterDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${jdbc.driverClassName}" /> <property name="url" value="${master.jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <!-- 初始化连接大小 --> <property name="initialSize" value="0" /> <!-- 连接池最大使用连接数量 --> <property name="maxActive" value="20" /> <!-- 连接池最大空闲 error:maxIdle is deprecated --> <!-- <property name="maxIdle" value="20" /> --> <!-- 连接池最小空闲 --> <property name="minIdle" value="0" /> <!-- 获取连接最大等待时间 --> <property name="maxWait" value="60000" /> <!-- <property name="poolPreparedStatements" value="true" /> <property name="maxPoolPreparedStatementPerConnectionSize" value="33" /> --> <property name="validationQuery" value="${validationQuery}" /> <property name="testOnBorrow" value="false" /> <property name="testOnReturn" value="false" /> <property name="testWhileIdle" value="true" /> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="60000" /> <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="25200000" /> <!-- 打开removeAbandoned功能 --> <property name="removeAbandoned" value="true" /> <!-- 1800秒,也就是30分钟 --> <property name="removeAbandonedTimeout" value="1800" /> <!-- 关闭abanded连接时输出错误日志 --> <property name="logAbandoned" value="true" /> <!-- 监控数据库 --> <!-- <property name="filters" value="stat" /> --> <property name="filters" value="mergeStat" /> </bean> <!-- 配置数据源-Slave --> <bean name="slaveDataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${jdbc.driverClassName}" /> <property name="url" value="${slave.jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <!-- 初始化连接大小 --> <property name="initialSize" value="0" /> <!-- 连接池最大使用连接数量 --> <property name="maxActive" value="20" /> <!-- 连接池最大空闲 error:maxIdle is deprecated --> <!-- <property name="maxIdle" value="20" /> --> <!-- 连接池最小空闲 --> <property name="minIdle" value="0" /> <!-- 获取连接最大等待时间 --> <property name="maxWait" value="60000" /> <!-- <property name="poolPreparedStatements" value="true" /> <property name="maxPoolPreparedStatementPerConnectionSize" value="33" /> --> <property name="validationQuery" value="${validationQuery}" /> <property name="testOnBorrow" value="false" /> <property name="testOnReturn" value="false" /> <property name="testWhileIdle" value="true" /> <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> <property name="timeBetweenEvictionRunsMillis" value="60000" /> <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> <property name="minEvictableIdleTimeMillis" value="25200000" /> <!-- 打开removeAbandoned功能 --> <property name="removeAbandoned" value="true" /> <!-- 1800秒,也就是30分钟 --> <property name="removeAbandonedTimeout" value="1800" /> <!-- 关闭abanded连接时输出错误日志 --> <property name="logAbandoned" value="true" /> <!-- 监控数据库 --> <!-- <property name="filters" value="stat" /> --> <property name="filters" value="mergeStat" /> </bean> </beans>⑥transaction-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:mvc="http://www.springframework.org/schema/mvc" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd" default-lazy-init="true"> <description>配置事物</description> <!-- 注解方式配置事物 --> <tx:annotation-driven transaction-manager="transactionManager" /> <!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <aop:config> <aop:pointcut expression="(execution(* com.user.service.*.* (..)))" id="pointcut" /> <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut" /> </aop:config> <!-- 事务控制 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="load*" read-only="true" /> <tx:method name="get*" read-only="true" /> <tx:method name="create*" propagation="REQUIRED" rollback-for="java.lang.Exception" /> <tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Exception" /> <tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Exception" /> <tx:method name="insert*" propagation="REQUIRED" rollback-for="java.lang.Exception" /> <tx:method name="delete*" propagation="REQUIRED" rollback-for="java.lang.Exception" /> <tx:method name="schedule*" propagation="REQUIRED" rollback-for="java.lang.Exception" /> <tx:method name="do*" propagation="REQUIRED" rollback-for="java.lang.Exception" /> <!-- 一个事务涉及一个数据源不能在事务内部去切换数据源成功,所以对多数据源的方法暂不开启事务~分布式事务 --> <!-- <tx:method name="crud*" propagation="REQUIRED" rollback-for="java.lang.Exception" /> --> <!-- <tx:method name="*" /> --> </tx:attributes> </tx:advice> </beans>⑦:log4j.properties日志配置:
# Set root category priority to INFO and its only appender to CONSOLE. log4j.rootLogger=info, STDOUT, FILEOUT # CONSOLE is set to be a ConsoleAppender using a PatternLayout. log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender log4j.appender.STDOUT.encoding=UTF-8 log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout log4j.appender.STDOUT.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m %n # FILEOUT is set to be a File appender using a PatternLayout. log4j.appender.FILEOUT=org.apache.log4j.RollingFileAppender #tomcat1 #log4j.appender.FILEOUT.File=/home/logs/timespacexstar1/timespacexstar.log #tomcat2 log4j.appender.FILEOUT.File=/home/logs/mydemoweb/mydemoweb.log log4j.appender.FILEOUT.MaxFileSize=5MB log4j.appender.FILEOUT.MaxBackupIndex=10 log4j.appender.FILEOUT.encoding=UTF-8 log4j.appender.FILEOUT.layout=org.apache.log4j.PatternLayout log4j.appender.FILEOUT.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m %n # SQL log4j.logger.java.sql.ResultSet=debug log4j.logger.org.apache=debug log4j.logger.java.sql.Connection=debug log4j.logger.java.sql.Statement=debug log4j.logger.java.sql.PreparedStatement=debug6:代码实现:
①:dao层:
package com.user.dao; import com.common.readwriteseparate.DataSource; import com.user.bean.Member; public interface MemberMapper { @DataSource("master") int deleteByPrimaryKey(Integer id); @DataSource("master") int insert(Member record); @DataSource("master") int insertSelective(Member record); @DataSource("master") int updateByPrimaryKeySelective(Member record); @DataSource("master") int updateByPrimaryKey(Member record); @DataSource("slave") Member selectByPrimaryKey(Integer id); }②:业务层代码:
package com.user.service; import com.user.bean.Member; public interface MemberService { Member crudMember(Member member); }实现:
package com.user.service.impl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.interceptor.TransactionAspectSupport; import com.user.bean.Member; import com.user.dao.MemberMapper; import com.user.service.MemberService; @Service("memberService") public class MemberServiceImpl implements MemberService { //log private static final Logger LOG = LoggerFactory.getLogger(MemberServiceImpl.class); @Autowired private MemberMapper memeberDao; @Override public Member crudMember(Member member) { Member resMember = null; try { resMember = memeberDao.selectByPrimaryKey(member.getId()); LOG.info(member.getId() + "查询成功~"); memeberDao.insert(member); LOG.info(member.getUsername() + "注册成功~"); } catch (Exception e) { LOG.error("用户id:"+member.getId()+"保存和查询失败~~~" + e.getMessage(), e); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } return resMember; } }③:master数据库没有id为2的member注册,但slave中有一个用户,先查询从库用户,再插入主库这个member,不涉及业务仅对分库测试:
junit测试代码:
package com.user.test; import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.common.definition.DictionaryDefinition; import com.user.bean.Member; import com.user.service.MemberService; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath:spring-context.xml","classpath:datasource-context.xml", "classpath:mybatis-context.xml","classpath:transaction-context.xml"}) public class UserTest { //log private static final Logger LOG = LoggerFactory.getLogger(UserTest.class); @Autowired private MemberService memberService; /** * 主从分离测试,非支持事务 * */ @Test public void crudTraM(){ Member member = new Member(); member.setId(2); member.setUsername("Tony5"); member.setPassword("12345678"); member.setStatus(DictionaryDefinition.LOGIN_NORMAL); Member member1 = memberService.crudMember(member); if(member1 != null){ LOG.info("结果~~:" + member1.getUsername()); }else{ LOG.info("主从事物回滚测试失败数据为空"); } } }
效果:master插入前:
slave前后不变:
junit代码测试结果log:
2016-04-11 18:48:32 [ main:2195 ] - [ INFO ] ************************************************ ~~~DB:: slave ************************************************ 2016-04-11 18:48:32 [ main:2879 ] - [ INFO ] 2查询成功~ 2016-04-11 18:48:32 [ main:2880 ] - [ INFO ] ************************************************ ~~~DB:: master ************************************************ 2016-04-11 18:48:32 [ main:2977 ] - [ INFO ] Tony5注册成功~ 2016-04-11 18:48:32 [ main:2977 ] - [ INFO ] 结果~~:kekeai上边完成了在service层同一方法中数据源的切换,但这个方法必须不支持事务,不然切换失败!!!
A:spring事务的默然配置:
<tx:method name="*" />
默认意义:
①:事务传播设置是 REQUIRED
②:隔离级别是数据库 DEFAULT
③:事务是支持 读/写
④:事务超时默认是依赖于事务系统的,或者事务超时没有被支持。
⑤:任何 RuntimeException 将触发事务回滚,但是任何 checked Exception 将不触发事务回滚
<tx:method/> 有关事务自定义的设置:
源代码:Spring实现数据库读写分离
数据库sql:db_rw_separate.sql