以前說過,若是一個數據庫中要存儲的數據量總體比較小,可是其中一個表存儲的數據比較多,好比日誌表,這時候就要考慮分表存儲了;可是若是一個數據庫總體存儲的容量就比較大,該怎麼辦呢?這時候就須要考慮分庫了,就是創建多個數據庫保存數據。這裏以答案爲例,就算調查對象不是不少,可是參與調查的人數很是多,那麼須要保存的數據量就會很是大,怎樣將答案以一種規則保存到不一樣的數據庫中就是如今須要考慮的問題(查詢分庫的問題未解決,先存檔)。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中解除綁定的話,就算顛倒日誌通知和事務通知的啓動順序也是沒有任何做用的。