spring讀寫分離 - 事務註解篇


思路參照 spring讀寫分離 - 事務配置篇(轉) ,不過是基於@Transactional判斷,所以每個需要事務的方法上都必須添加上這個註解,這裏直接貼出代碼:

配置文件:

多數據源配置:

  <bean id="dataSource"
    class="com.lmiky.platform.database.datasource.DynamicDataSource">
    <property name="readDataSources">
      <list>
        <ref bean="readDataSource1" />
        <ref bean="readDataSource2" />
      </list>
    </property>
    <property name="writeDataSource" ref="writeDataSource" />
  </bean>

數據源攔截器:

  <bean id="dateSourceAspect"
    class="com.lmiky.platform.database.datasource.DateSourceAspect" />
  <aop:config expose-proxy="true">
    <aop:aspect ref="dateSourceAspect" order="0">
      <aop:pointcut id="dateSourcePointcut"
        expression="execution(* com.lmiky..service.impl..*.*(..))"/>
      <aop:around pointcut-ref="dateSourcePointcut" method="determineReadOrWriteDB" />
    </aop:aspect>
  </aop:config>

要保證讓這個攔截在事務的攔截器之前,否則如果spring先攔截事務的話,就不會起效了。可以用order設的值來排序,或者把這個配置的代碼放在跟事務配置的代碼同一個頁面,並且放在事務配置代碼的前面,spring是按代碼順序來執行的。

其他的跟單個數據源配置一樣。

java代碼:


數據源攔截器
package com.lmiky.platform.database.datasource;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.transaction.annotation.Transactional;

import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 數據源切片
 *
 * @author lmiky
 * @date 2015年9月7日 下午3:25:54
 */
public class DateSourceAspect {
    /**
     * 緩存
     */
    private static ConcurrentHashMap<String, Boolean> methodIsReadCache = new ConcurrentHashMap<>();

    /**
     * 決策是否只讀
     *
     * @param pjp 織入點
     * @return 方法執行結果
     * @throws Throwable
     * @author lmiky
     * @date 2015年9月7日 下午3:45:27
     */
    public Object determineReadOrWriteDB(ProceedingJoinPoint pjp) throws Throwable {
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        Object target = pjp.getTarget();
        String cacheKey = target.getClass().getName() + "." + method.getName();
        Boolean isReadCacheValue = methodIsReadCache.get(cacheKey);
        if (isReadCacheValue == null) {
            // 重新獲取方法,否則傳遞的是接口的方法信息
            Method realMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());
            isReadCacheValue = isChoiceReadDB(realMethod);
            methodIsReadCache.put(cacheKey, isReadCacheValue);
        }
        if (isReadCacheValue) {
            DynamicDataSourceHolder.markRead();
        } else {
            DynamicDataSourceHolder.markWrite();
        }
        try {
            return pjp.proceed();
        } finally {
            DynamicDataSourceHolder.reset();
        }
    }

    /**
     * 判斷是否只讀方法
     *
     * @param method 執行方法
     * @return 當前方法是否只讀
     * @author lmiky
     * @date 2015年9月7日 下午3:45:10
     */
    private boolean isChoiceReadDB(Method method) {
        Transactional transactionalAnno = AnnotationUtils.findAnnotation(method, Transactional.class);
        if (transactionalAnno == null) {
            return true;
        }
        // 如果之前選擇了寫庫,則現在還選擇寫庫
        if (DynamicDataSourceHolder.isChoiceWrite()) {
            return false;
        }
        if (transactionalAnno.readOnly()) {
            return true;
        }
        return false;
    }
}
數據源選擇:

package com.lmiky.platform.database.datasource;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 動態數據源
 *
 * @author lmiky
 * @date 2015年9月7日 下午2:01:31
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    private Object writeDataSource;
    private List<Object> readDataSources;
    private int readDataSourceSize = 0;

    private AtomicInteger readIndex = new AtomicInteger(0);

    /**
     * 數據源鍵名
     */
    private static final String DATASOURCE_KEY_WRITE = "write";
    private static final String DATASOURCE_KEY_READ = "read";

    /* (non-Javadoc)
     * @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#afterPropertiesSet()
     */
    @Override
    public void afterPropertiesSet() {
        if (this.writeDataSource == null) {
            throw new IllegalArgumentException("Property 'writeDataSource' is required");
        }
        setDefaultTargetDataSource(writeDataSource);
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DATASOURCE_KEY_WRITE, writeDataSource);
        if (this.readDataSources == null) {
            readDataSourceSize = 0;
        } else {
            for(int i=0; i<readDataSources.size(); i++) {
                targetDataSources.put(DATASOURCE_KEY_READ + i, readDataSources.get(i));
            }
            readDataSourceSize = readDataSources.size();
        }
        setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    /*
     * (non-Javadoc)
     * @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()
     */
    @Override
    protected Object determineCurrentLookupKey() {
        if(DynamicDataSourceHolder.isChoiceNone() || DynamicDataSourceHolder.isChoiceWrite() || readDataSourceSize == 0) {
            return DATASOURCE_KEY_WRITE;
        }
        int index = readIndex.incrementAndGet() % readDataSourceSize;
        return DATASOURCE_KEY_READ + index;
    }

    /**
     * @return the writeDataSource
     */
    public Object getWriteDataSource() {
        return writeDataSource;
    }

    /**
     * @param writeDataSource the writeDataSource to set
     */
    public void setWriteDataSource(Object writeDataSource) {
        this.writeDataSource = writeDataSource;
    }

    /**
     * @return the readDataSources
     */
    public List<Object> getReadDataSources() {
        return readDataSources;
    }

    /**
     * @param readDataSources the readDataSources to set
     */
    public void setReadDataSources(List<Object> readDataSources) {
        this.readDataSources = readDataSources;
    }


}

package com.lmiky.platform.database.datasource;

/**
 * 數據源管理器
 *
 * @author lmiky
 * @date 2015年9月7日 下午2:02:23
 */
public class DynamicDataSourceHolder {
    private static enum DataSourceType {
        write, read;
    }

    public static final ThreadLocal<DataSourceType> holder = new ThreadLocal<>();

    /**
     * 數據源名稱
     */
    public static final String DATASOURCE_WRITE = "write";
    public static final String DATASOURCE_READ = "read";

    
    /**
     * 標記爲寫數據源
     * @author lmiky
     * @date 2015年9月9日 下午8:57:43
     */
    public static void markWrite() {
        holder.set(DataSourceType.write);
    }
    
    /**
     * 標記爲讀數據源
     * @author lmiky
     * @date 2015年9月9日 下午8:57:43
     */
    public static void markRead() {
        holder.set(DataSourceType.read);
    }
    
    /**
     * 重置
     * @author lmiky
     * @date 2015年9月9日 下午8:58:01
     */
    public static void reset() {
        holder.set(null);
    }
    
    /**
     * 是否還未設置數據源
     * @author lmiky
     * @date 2015年9月9日 下午8:58:09
     * @return
     */
    public static boolean isChoiceNone() {
        return null == holder.get(); 
    }
    
    /**
     * 當前是否選擇了寫數據源
     * @author lmiky
     * @date 2015年9月9日 下午8:58:19
     * @return
     */
    public static boolean isChoiceWrite() {
        return DataSourceType.write == holder.get();
    }
    
    /**
     * 當前是否選擇了讀數據源
     * @author lmiky
     * @date 2015年9月9日 下午8:58:29
     * @return
     */
    public static boolean isChoiceRead() {
        return DataSourceType.read == holder.get();
    }
}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章