以前说过,若是一个数据库中要存储的数据量总体比较小,可是其中一个表存储的数据比较多,好比日志表,这时候就要考虑分表存储了;可是若是一个数据库总体存储的容量就比较大,该怎么办呢?这时候就须要考虑分库了,就是创建多个数据库保存数据。这里以答案为例,就算调查对象不是不少,可是参与调查的人数很是多,那么须要保存的数据量就会很是大,怎样将答案以一种规则保存到不一样的数据库中就是如今须要考虑的问题(查询分库的问题未解决,先存档)。mysql
分库分为水平分库和竖直分库两种类型。spring
数据库之间是同构的,可是数据的存储范围不一样。好比以后我将使用水平分库的方法保存答案到不一样的数据库中。两个数据库中都有答案表,并且字段和约束等彻底相同,二者的差别只是保存的数据不一样,这样的分库方法就是水平分库。sql
数据库和数据库之间的结构不相同,好比一个数据库存放一个模块的功能,每一个模块的独立性比较强。并且量比较大。数据库
由于以前已经配置过数据源了,因此这里只须要直接继承上一个数据源而且修改url地址便可express
1 <!-- 配置数据源(主库) --> 2 <bean id="dateSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> 3 <property name="driverClass" value="${jdbc.driverclass}"></property> 4 <property name="jdbcUrl" value="${jdbc.url}"></property> 5 <property name="user" value="${jdbc.username}"></property> 6 <property name="password" value="${jdbc.password}"></property> 7 8 <!-- 配置c3p0自身的参数 --> 9 <property name="maxPoolSize" value="${c3p0.pool.maxsize}"></property> 10 <property name="minPoolSize" value="${c3p0.pool.minsize}"></property> 11 <property name="initialPoolSize" value="${c3p0.pool.initsieze}"></property> 12 <property name="acquireIncrement" value="${c3p0.pool.increment}"></property> 13 </bean> 14 <!-- (从库) 为了实现分库的功能,必须针对每一个数据库配置一个数据源 这里使用了包的继承的特殊属性使用parent属性对dataSource进行了继承 --> 15 <bean id="dataSource1" class="com.mchange.v2.c3p0.ComboPooledDataSource" 16 parent="dateSource"> 17 <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/lsn_surveypark1"></property> 18 </bean>
数据源路由器将会根据策略决定使用的数据源。dom
1 <!-- 配置数据源路由器 --> 2 <bean id="dataSource_router" class="com.kdyzm.datasource.SurveyparkDatasourceRouter"> 3 <property name="targetDataSources"> 4 <map> 5 <!-- 若是id是偶数,保存到主库中 --> 6 <entry key="even" value-ref="dateSource"></entry> 7 <!-- 若是id是奇数,保存到从库中 --> 8 <entry key="odd" value-ref="dataSource1"></entry> 9 </map> 10 </property> 11 <!-- 若是不知足上述规则,则直接使用默认的数据源 --> 12 <property name="defaultTargetDataSource" ref="dateSource"></property> 13 </bean>
这里的策略封装到了一个类中SurveyparkDatasourceRouter,该类必须继承org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource抽象类并重写determineCurrentLookupKey方法肯定策略。ide
自定义的方法就是继承org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource类并重写抽象方法。测试
1 package com.kdyzm.datasource; 2 3 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; 4 5 import com.kdyzm.domain.Survey; 6 7 /** 8 * 自定义数据源路由器 9 * 有一个默认的实现类,该类是以传播属性来路由数据的。 10 * @author kdyzm 11 * 12 */ 13 public class SurveyparkDatasourceRouter extends AbstractRoutingDataSource{ 14 15 /** 16 * 该方法实际上肯定了一个数据向哪里存放的策略 17 * 在这里使用id的就属性来肯定 18 * 若是答案id是偶数,就想lsn_surveypark数据库中的answer表(主表)中存放 19 * 若是答案的id是奇数,就向lsn_surveypark1数据库中的answer表(从表)中存放 20 */ 21 @Override 22 protected Object determineCurrentLookupKey() { 23 SurveyToken surveyToken=SurveyToken.getSurveyToken(); 24 if(surveyToken!=null){ 25 Survey survey=surveyToken.getSurvey(); 26 int surveyId=survey.getSurveyId(); 27 System.out.println("Survey对象不为空,值为:"+surveyId); 28 /** 29 * 在这里必须解除绑定 30 * 若是不在这里解除绑定的话就会将log日志写入到lsn_surveypark1数据库中。 31 * 因为lsn_surveypark1数据库中没有log表,因此必定会报错 32 */ 33 SurveyToken.unbind(); 34 return (surveyId%2)==0?"even":"odd"; //若是是偶数返回even字符串,若是是奇数返回odd字符串 35 } 36 System.out.println("survey对象为空"); 37 return null; 38 } 39 40 }
以上重写的方法中决定了路由数据源的策略:若是调查ID是偶数,就保存到主库lsn_surveypark的answer表中,若是是奇数,就保存到从库lsn_surveypark1中的answer表中。ui
接下来就是解决怎么拿到Survey对象的问题,上面的黄色背景部分的代码是关键。this
注意,若是Survey对象为空,就使用默认的数据源:主库,这是由以前的配置文件中的配置决定的。
何时决定数据源?这个问题不太肯定,应该是在进入Service方法以前,也就是说开启事务的时候(在分库查询的时候这种猜想被推翻了)。那么只要在进入Service方法以前将Survey对象传递给路由数据源中的相关方法就好了。
保存问题的时机是SurveyAction调用保存答案方法的时候。这时候就须要将数据保存到某个地方而后等待在determineCurrentLookupKey方法中获取该值就能够了。可是保存到哪里呢?保存到文件中是一种方式,可是这种方式很是烂~一般这种状况下都是讲对象绑定到ThreadLocal,而后在determineCurrentLookupKey方法中从ThreadLocal中拿出来便可。
我在这里建立一个新类SurveyToken,实现设置Survey对象、获取Survey对象的、将Survey对象绑定到ThreadLocal(其实是当前线程)和将Survey对象从ThreadLocal解除绑定的方法,固然前二者是非静态方法,后二者是静态方法。
1 package com.kdyzm.datasource; 2 3 import com.kdyzm.domain.Survey; 4 5 /** 6 * 令牌类 7 * 封装了一些比较重要的属性 8 * @author kdyzm 9 * 10 */ 11 public class SurveyToken { 12 private Survey survey; //绑定的对象的值,若是只是绑定surveyId也能够,可是为了之后的方便起见,使用该对象更划算 13 private static ThreadLocal<SurveyToken> t=new ThreadLocal<SurveyToken>(); 14 public Survey getSurvey() { 15 return survey; 16 } 17 public void setSurvey(Survey survey) { 18 this.survey = survey; 19 } 20 /** 21 * 绑定当前线程和SurveyToken对象之间的关系 22 */ 23 public static void bind(SurveyToken surveyToken){ 24 t.set(surveyToken); 25 } 26 27 /** 28 * 解除当前线程和SurveyToken对象之间的关系 29 */ 30 public static void unbind(){ 31 t.remove(); 32 } 33 34 /** 35 * 获取SurveyToken对象的方法 36 */ 37 public static SurveyToken getSurveyToken(){ 38 return t.get(); 39 } 40 }
1 private void writeAnswersToDB(List<Answer> answers) { 2 SurveyToken surveyToken=new SurveyToken(); 3 Survey survey=this.surveyService.getModelById(getSurveyId()); 4 surveyToken.setSurvey(survey); 5 SurveyToken.bind(surveyToken); 6 this.answerService.saveAllAnswers(answers); 7 }
由于answerService类中的saveAllAnswers方法带有事务,因此在调用该方法以前会调用determineCurrentLookupKey方法决定数据源。
若是只是通过了以上几个步骤,测试必定是失败的。
应该会报出"在lsn_surveypark1数据库中没法找到log表"诸如此类的异常信息。
分析缘由:lsn_surveypark1数据库原本就是从数据库,里面只有一张answer表,原本就没有log表,说明程序选取的数据源有问题。要知道,只要事务没结束,determineCurrentLookupKey方法就不会有机会被再次调用,即便中间可能会再次调用其它Service中的方法也没用,由于事务的传播性为"REQUIRED",这样就致使了其调用的全部方法都自动开启了事务,固然"保存日志"的动做也是"其它Service"中的方法,固然也就不会从新访问determineCurrentLookupKey方法,数据源也就会一直是lsn_surveypark1,所以就报出了上述的那个错误。因此就找到了问题的关键:事务通知和日志通知的开启顺序致使了该错误的发生,咱们须要让事务通知在后,日志通知在前,因此在配置AOP的时候就须要改变order属性,使得日志通知的order值小于事务通知的order值,这样就会先开启日志通知,再开启事务通知了,这样作的结果就是一旦保存答案完成以后,保存答案的事务就会结束;日志通知就会为了保存日志再次访问determineCurrentLookupKey方法,固然这时候必须保证Survey对象已经解除了绑定,不然仍然会使用以前肯定的数据源,因此解除绑定的时机也很重要,若是在Action中解除绑定,即便颠倒了事务通知和日志通知的启动顺序也没有什么做用,最好的方法就是在determineCurrentLookupKey方法中拿到Survey对象以后直接解除,这样就可以保证一次事务结束以后下一次事务开启的时候访问determineCurrentLookupKey的时候Survey对象已经解除绑定了。配置事务通知和日志通知的顺序方法以下:
1 <aop:config> 2 <!-- 日志切入点 --> 3 <aop:pointcut 4 expression="(execution(* *..*Service.save*(..)) 5 or execution(* *..*Service.update*(..)) 6 or execution(* *..*Service.delete*(..)) 7 or execution(* *..*Service.batch*(..)) 8 or execution(* *..*Service.create*(..)) 9 or execution(* *..*Service.new*(..))) and !bean(logService)" 10 id="loggerPointcut" /> 11 <aop:pointcut expression="execution(* *..*Service.*(..))" 12 id="txPointcut" /> 13 <!-- 必须配置order属性,使用该属性能够改变配置的通知的加载顺序,order值越大,优先级越高 必须让事务的通知放到后面,让日志的通知先执行,这样才能在执行完成日志的通知后事务确保可以结束。 14 order值越小,优先级越高 为了解决事务没有结束的问题,必须同时修改解除绑定的时间 --> 15 <aop:advisor advice-ref="cacheAdvice" 16 pointcut="execution(* com.kdyzm.service.SurveyService.*(..)) or 17 execution(* com.kdyzm.service.PageService.*(..)) or 18 execution(* com.kdyzm.service.QuestionService.*(..)) or 19 execution(* com.kdyzm.service.AnswerService.*(..))" order="0" /> 20 <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut" 21 order="2" /> 22 <aop:aspect id="loggerAspect" ref="logger" order="1"> 23 <aop:around method="record" pointcut-ref="loggerPointcut" /> 24 </aop:aspect> 25 </aop:config>
注意不要忘了在determineCurrentLookupKey方法中拿到Survey对象以后直接解除绑定,若是在Action中解除绑定的话,就算颠倒日志通知和事务通知的启动顺序也是没有任何做用的。