1. 程式人生 > >資料庫讀寫分離與事務糾纏的那點坑

資料庫讀寫分離與事務糾纏的那點坑

本篇文章討論在資料庫讀寫分離時使用事務的那些坑:

1. 在讀寫分離時會不會造成事務主從切換錯誤

一個執行緒在Serivcie時Select時選擇的是從庫,DynamicDataSourceHolder中ThreadLocal對應執行緒儲存的是slave,然後呼叫Manager時進入事務,事務使用預設的transacatinManager關聯的dataSource,而此時會不會獲取到的是slave?

2. 事務隔離級別和傳播特性會不會影響資料連線池死鎖

一個執行緒在Service層Select資料會從資料庫獲取一個Connection,通常來講,後續DB的操作在同一線執行緒會複用這個DB Connection,但是從Service進入Manager的事務後,Get Seq獲取全域性唯一標識,所以Get Seq一般都會開啟新的事物從DB Pool裡重新獲取一個新連線進行操作,但是問題是如果兩個事務關聯的datasource是同一個,即DB Pool是同一個,那麼如果DB Pool已經為空,是否會造成死鎖?

為了減輕資料庫的壓力,一般會進行資料庫的讀寫分離,實現方法一是通過分析sql語句是insert/select/update/delete中的哪一種,從而對應選擇主從,二是通過攔截方法名稱的方式來決定主從的,如:save*()、insert*() 形式的方法使用master庫,select()開頭的使用slave庫。

通常在方法上標上自定義標籤來選擇主從。

1

2

@DataSource("slave")

int queryForCount(OrderQueryCondition queryCondition);

或者通過攔截器動態選擇主從。

1

2

3

4

5

6

7

8

<property name="methodType">

<map key-type="java.lang.String">

<!-- read -->

<entry key="master" value="find,get,select,count,list,query,stat,show,mine,all,rank,fetch"/>

<!-- write -->

<entry

 key="slave" value="save,insert,add,create,update,delete,remove,gain"/>

</map>

</property>

讀寫動態庫配置   

1

2

3

4

5

6

7

8

9

10

<bean id="fwmarketDataSource" class="com.jd.fwmarket.datasource.DynamicDataSource" lazy-init="true">

<property name="targetDataSources">

<map key-type="java.lang.String">

<entry key="master" value-ref="masterDB"/>

<entry key="slave" value-ref="slaveDB"/>

</map>

</property>

<!-- 設定預設的資料來源,這裡預設走寫庫 -->

<property name="defaultTargetDataSource" ref="masterDB"/>

</bean>

DynamicDataSource:

定義動態資料來源,實現通過整合Spring提供的AbstractRoutingDataSource,只需要實現determineCurrentLookupKey方法即可,由於DynamicDataSource是單例的,執行緒不安全的,所以採用ThreadLocal保證執行緒安全,由DynamicDataSourceHolder完成。

1

2

3

4

5

6

7

public class DynamicDataSource extends AbstractRoutingDataSource {

@Override

protected Object determineCurrentLookupKey() {

// 使用DynamicDataSourceHolder保證執行緒安全,並且得到當前執行緒中的資料來源key

return DynamicDataSourceHolder.getDataSourceKey();

}

}

DynamicDataSourceHolder類:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

public class DynamicDataSourceHolder {

// 寫庫對應的資料來源key

private static final String MASTER= "master";

// 讀庫對應的資料來源key

private static final String SLAVE = "slave";

// 使用ThreadLocal記錄當前執行緒的資料來源key

private static final ThreadLocal<String> holder = new ThreadLocal<String>();

public static void putDataSourceKey(String key) {

holder.set(key);

}

public static String getDataSourceKey() {

return holder.get();

}

public static void markDBMaster(){

putDataSourceKey(MASTER);

}

public static void markDBSlave(){

putDataSourceKey(SLAVE);

}

public static void markClear(){

putDataSourceKey(null);

}

}

動態設定資料來源可以通過Spring AOP來實現,而AOP切面的方式也有很多種。

Spring AOP的原理:Spring AOP採用動態代理實現,在Spring容器中的bean會被代理物件代替,代理物件里加入了增強邏輯,當呼叫代理物件的方法時,目標物件的方法就會被攔截。

事務切面和讀/寫庫選擇切面

1

2

3

4

5

6

7

8

9

<bean id="dataSourceAspect" class="com.jd.fwmarket.service.datasource.DataSourceAspect"/>

<aop:config>

<aop:pointcut id="txPointcut" expression="execution(* com.jd.fwmarket.dao..*Impl.*(..))"/>

<!-- 將切面應用到自定義的切面處理器上,-9999保證該切面優先順序最高執行 -->

<aop:aspect ref="dataSourceAspect" order="-9999">

<aop:before method="before" pointcut-ref="txPointcut"/>

<aop:after method="after" pointcut-ref="txPointcut"/>

</aop:aspect>

</aop:config>

Java邏輯:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

public class DataSourceAspect {

private static final String[] defaultSlaveMethodStart 

new String[]{"query""find""get""select""count""list"};

/**

* 在進入Dao方法之前執行

*

* @param point 切面物件

*/

public void before(JoinPoint point) {

String methodName = point.getSignature().getName();

boolean isSlave = isSlave(methodName);

if (isSlave) {

DynamicDataSourceHolder.markDBSlave();

else {

DynamicDataSourceHolder.markDBMaster();

}

}

public void after(){

DynamicDataSourceHolder.markClear();

}

}

使用BeanNameAutoProxyCreator建立代理

1

2

3

4

5

6

7

8

9

10

11

12

13

14

<bean id="MySqlDaoSourceInterceptor" class="com.jd.fwmarket.dao.aop.DaoSourceInterceptor">

<property name="dbType" value="mysql"/>

<property name="packageName" value="com.jd.fwmarket"/>

</bean>

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">

<property name="beanNames">

<value>*Mapper</value>

</property>

<property name="interceptorNames">

<list>

<value>MySqlDaoSourceInterceptor</value>

</list>

</property>

</bean>

Java邏輯:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

public class DaoSourceInterceptor implements MethodInterceptor {

public Object invoke(MethodInvocation invocation) throws Throwable {

dataSourceAspect(invocation);

Object result = invocation.proceed();

DataSourceHandler.putDataSource(null);

return result;

}

private void dataSourceAspect(MethodInvocation invocation) {

String method = invocation.getMethod().getName();

for (String key : ChooseDataSource.METHOD_TYPE_MAP.keySet()) {

for (String type : ChooseDataSource.METHOD_TYPE_MAP.get(key)) {

if (method.startsWith(type)) {

DataSourceHandler.putDataSource(key);

return;

}

}

}

}

}

Spring的事務處理為了與資料訪問解耦,它提供了一套處理資料資源的機制,而這個機制採用ThreadLocal的方式。

事務管理器

Spring中通常通過@Transactional來宣告使用事務。如果@Transactional不指定事務管理器,使用預設。注意如果Spring容器中定義了兩個事務管理器,@Transactional標註是不支援區分使用哪個事務管理器的,Spring 3.0之後的版本Transactional增加了個string型別的value屬性來特殊指定加以區分。

1

2

3

4

5

@Transactional

public int insertEntryCreateId(UrpMenu urpMenu) {

urpMenu.setMId(this.sequenceUtil.get(SequenceConstants.MARKET_URP_MENU));

return super.insertEntryCreateId(urpMenu);

}

同時進行XML配置

1

2

3

4

5

<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>

<bean id="transactionManager" 

class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

<property name="dataSource" ref="fwmarketDataSource"/>

</bean>

其中dataSource是在Spring配置檔案中定義的資料來源的物件例項。transaction-manager屬性儲存一個對在Spring配置檔案中定義的事務管理器bean的引用,如果沒有它,就會忽略@Transactional註釋,導致程式碼不會使用任何事務。proxy-target-class控制是基於介面的還是基於類的代理被建立,如果屬性值被設定為true,那麼基於類的代理將起作用,如果屬性值為false或者被省略,那麼標準的JDK基於介面的代理將起作用。

注意@Transactional建議在具體的類(或類的方法)上使用,不要使用在類所要實現的任何介面上。

(推薦閱讀:Spring事務隔離級別和傳播特性 http://www.cnblogs.com/zhishan/p/3195219.html)

SQL四類隔離級別

事務的實現是基於資料庫的儲存引擎。不同的儲存引擎對事務的支援程度不一樣。Mysql中支援事務的儲存引擎有InnoDB和NDB。InnoDB是mysql預設的儲存引擎,預設的隔離級別是RR(Repeatable Read)。

事務的隔離性是通過鎖實現,而事務的原子性、一致性和永續性則是通過事務日誌實現。

(推薦閱讀:資料庫事務與MySQL事務總結 https://zhuanlan.zhihu.com/p/29166694)

Q1 在讀寫分離時會不會造成事務主從切換錯誤

一個執行緒在Serivcie時Select時選擇的是從庫,DynamicDataSourceHolder中ThreadLocal對應執行緒儲存的是slave,然後呼叫Manager時進入事務,事務使用預設的transacatinManager關聯的dataSource,而此時會不會獲取到的是slave?

經驗證不會,但這是因為在AOP設定動態織出的時候,都要清空DynamicDataSourceHolder的ThreadLocal,如此避免了資料庫事務傳播行為影響的主從切換錯誤。如果Selelct DB從庫完成之後不清空ThreadLocal,那麼ThreadLocal跟執行緒繫結就會傳播到Transaction,造成事務操作從庫異常。而清空ThreadLocal之後,Spring的事務攔截先於動態資料來源的判斷,所以事務會切換成主庫,即使事務中再有查詢從庫的操作,也不會造成主庫事務異常。

Q2 事務隔離級別和傳播特性會不會影響資料連線池死鎖

一個執行緒在Service層Select資料會從資料庫獲取一個Connection,通常來講,後續DB的操作在同一線執行緒會複用這個DB Connection,但是從Service進入Manager的事務後,Get Seq獲取全域性唯一標識,所以Get Seq一般都會開啟新的事物從DB Pool裡重新獲取一個新連線進行操作,但是問題是如果兩個事務關聯的datasource是同一個,即DB Pool是同一個,那麼如果DB Pool已經為空,是否會造成死鎖?

經驗證會死鎖,所以在實踐過程中,如果有此實現,建議Get Seq不要使用與事務同一個連線池。或者採用事務隔離級別設定PROPAGATION_REQUIRES_NEW進行處理。最優的實踐是宎把Get SeqId放到事務裡處理。

架構視訊學習

分享一些架構學習的視訊資料,想要的可以自己去領取。

阿里P8架構師細分的架構體系:高效能+微服務+開源框架+架構築基

當然還有更多,這邊就不一一列舉了,你如果覺得你能全部吃下,也不擋著你要到更多。

歡迎加入Java高階架構學習交流群:939420159 本群提供免費的學習指導 架構資料 以及免費的解答 不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導 進群修改群備註:開發年限-地區-經驗 方便架構師解答問題 免費領取架構師全套視訊!!!!!!!!