數據庫讀寫分離與事務糾纏的那點坑

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

一個線程在Serivcie時Select時選擇的是從庫,DynamicDataSourceHolder中ThreadLocal對應線程存儲的是slave,而後調用Manager時進入事務,事務使用默認的transacatinManager關聯的dataSource,而此時會不會獲取到的是slave?javascript

2. 事務隔離級別和傳播特性會不會影響數據鏈接池死鎖

一個線程在Service層Select數據會從數據庫獲取一個Connection,一般來說,後續DB的操做在同一線線程會複用這個DB Connection,可是從Service進入Manager的事務後,Get Seq獲取全局惟一標識,因此Get Seq通常都會開啓新的事物從DB Pool裏從新獲取一個新鏈接進行操做,可是問題是若是兩個事務關聯的datasource是同一個,即DB Pool是同一個,那麼若是DB Pool已經爲空,是否會形成死鎖?html

爲了減輕數據庫的壓力,通常會進行數據庫的讀寫分離,實現方法一是經過分析sql語句是insert/select/update/delete中的哪種,從而對應選擇主從,二是經過攔截方法名稱的方式來決定主從的,如:savejava

()、insert
() 形式的方法使用master庫,select()開頭的使用slave庫。

一般在方法上標上自定義標籤來選擇主從。mysql

@DataSource("slave")
int queryForCount(OrderQueryCondition queryCondition);
複製代碼

或者經過攔截器動態選擇主從。面試

<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>
複製代碼

讀寫動態庫配置

<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完成。spring

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        // 使用DynamicDataSourceHolder保證線程安全,而且獲得當前線程中的數據源key
        return DynamicDataSourceHolder.getDataSourceKey();
    }
}
複製代碼
DynamicDataSourceHolder類:
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切面的方式也有不少種。sql

Spring AOP的原理:Spring AOP採用動態代理實現,在Spring容器中的bean會被代理對象代替,代理對象里加入了加強邏輯,當調用代理對象的方法時,目標對象的方法就會被攔截。數據庫

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

<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邏輯:
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建立代理
<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邏輯:
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的方式。express

事務管理器

Spring中一般經過@Transactional來聲明使用事務。若是@Transactional不指定事務管理器,使用缺省。注意若是Spring容器中定義了兩個事務管理器,@Transactional標註是不支持區分使用哪一個事務管理器的,Spring 3.0以後的版本Transactional增長了個string類型的value屬性來特殊指定加以區分。安全

@Transactional
public int insertEntryCreateId(UrpMenu urpMenu) {
    urpMenu.setMId(this.sequenceUtil.get(SequenceConstants.MARKET_URP_MENU));
    return super.insertEntryCreateId(urpMenu);
}
複製代碼

同時進行XML配置

<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事務隔離級別和傳播特性 www.cnblogs.com/zhishan/p/3…

SQL四類隔離級別

事務的實現是基於數據庫的存儲引擎。不一樣的存儲引擎對事務的支持程度不同。Mysql中支持事務的存儲引擎有InnoDB和NDB。InnoDB是mysql默認的存儲引擎,默認的隔離級別是RR(Repeatable Read)。

事務的隔離性是經過鎖實現,而事務的原子性、一致性和持久性則是經過事務日誌實現。

(推薦閱讀:數據庫事務與MySQL事務總結 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放到事務裏處理。

總結

分析的不是很深,有不少地方還不是特別瞭解,歡迎吐槽相互學習,尤爲是說錯了的地方,必定請幫忙指正,以避免誤人子弟。

在此我向你們推薦一個架構學習交流羣。交流學習羣號:833145934 裏面資深架構師會分享一些整理好的錄製視頻錄像和BATJ面試題:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多。

做者:張鬆然

京東商城,商家研發部架構師

文章出處:www.linkedkeeper.com/1043.html

相關文章
相關標籤/搜索