spring事務配置見:https://blog.csdn.net/andyzhaojianhui/article/details/74357100?locationNum=9&fps=1
根據這篇文章做了一些修改以適用於springboot項目,可能還有一些未知問題,目前使用中尚未發現,歡迎指正,不勝感激
注意:我們約定
配置文件中的寫庫的連接信息spring.datasource開頭,例如spring.datasource.url=
spring.read.datasource.name這項來確定有多少個讀庫,多個讀庫以英文逗號分隔
例如spring.read.datasource.name=read1,read2
讀庫連接信息以spring. + 上面的name對應的讀庫名 + .datasource開頭,
例如
spring.read1.datasource.url=
spring.read2.datasource.url=
1.DataSourceConfiguration,實例化數據源,事務管理等
import com.github.pagehelper.PageInterceptor;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.tomcat.jdbc.pool.PoolProperties;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.interceptor.TransactionInterceptor;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* 初始化數據源、事務管理器等
*/
@Configuration
public class DataSourceConfiguration {
@Autowired
private Environment env;
@Bean
public DataSource dataSource() {
ReadWriteDataSource dataSource = new ReadWriteDataSource();
dataSource.setWriteDataSource(writeDataSource());
dataSource.setReadDataSourceMap(readDataSourceMap());
return dataSource;
}
@Autowired
private TransactionInterceptor txAdvice;
@Bean
public ReadWriteDataSourceProcessor readWriteDataSourceTransactionProcessor() {
ReadWriteDataSourceProcessor processor = new ReadWriteDataSourceProcessor();
processor.setForceChoiceReadWhenWrite(true);
processor.postProcessAfterInitialization(txAdvice.getTransactionAttributeSource(), null);
return processor;
}
//初始化寫數據源
public DataSource writeDataSource() {
PoolProperties properties = new PoolProperties();
properties.setUrl(env.getProperty("spring.datasource.url"));
properties.setUsername(env.getProperty("spring.datasource.username"));
properties.setPassword(env.getProperty("spring.datasource.password"));
properties.setDriverClassName(env.getProperty("spring.datasource.jdbc.driver"));
properties.setInitialSize(Integer.valueOf(env.getProperty("spring.datasource.tomcat.initial-size")));
properties.setMinIdle(Integer.valueOf(env.getProperty("spring.datasource.tomcat.min-idle")));
properties.setMaxActive(Integer.valueOf(env.getProperty("spring.datasource.tomcat.max-active")));
properties.setMaxIdle(Integer.valueOf(env.getProperty("spring.datasource.tomcat.max-idle")));
properties.setMaxWait(Integer.valueOf(env.getProperty("spring.datasource.tomcat.max-wait")));
properties.setValidationQuery(env.getProperty("spring.datasource.tomcat.validation-query"));
properties.setTestWhileIdle(Boolean.valueOf(env.getProperty("spring.datasource.tomcat.test-while-idle")));
properties.setTimeBetweenEvictionRunsMillis(Integer.valueOf(env.getProperty("spring.datasource.tomcat.time-between-eviction-runs-millis")));
org.apache.tomcat.jdbc.pool.DataSource dataSource = new org.apache.tomcat.jdbc.pool.DataSource(properties);
return dataSource;
}
//初始化讀數據源,這裏可以看出爲什麼上面要進行那樣的約定
public Map<String, DataSource> readDataSourceMap() {
String readCount = env.getProperty("spring.read.datasource.name");
if (!StringUtils.isEmpty(readCount)) {
String[] split = readCount.split(",");
if (split.length > 0) {
Map<String, DataSource> dMap = new HashMap<>(split.length);
for (String s : split) {
PoolProperties properties = new PoolProperties();
properties.setDriverClassName(env.getProperty("spring.datasource.jdbc.driver"));
properties.setInitialSize(Integer.valueOf(env.getProperty("spring.datasource.tomcat.initial-size")));
properties.setMinIdle(Integer.valueOf(env.getProperty("spring.datasource.tomcat.min-idle")));
properties.setMaxActive(Integer.valueOf(env.getProperty("spring.datasource.tomcat.max-active")));
properties.setMaxWait(Integer.valueOf(env.getProperty("spring.datasource.tomcat.max-wait")));
properties.setValidationQuery(env.getProperty("spring.datasource.tomcat.validation-query"));
properties.setTestWhileIdle(Boolean.valueOf(env.getProperty("spring.datasource.tomcat.test-while-idle")));
properties.setMaxIdle(Integer.valueOf(env.getProperty("spring.datasource.tomcat.max-idle")));
properties.setTimeBetweenEvictionRunsMillis(Integer.valueOf(env.getProperty("spring.datasource.tomcat.time-between-eviction-runs-millis")));
properties.setUrl(env.getProperty("spring." + s + ".datasource.url"));
properties.setUsername(env.getProperty("spring." + s + ".datasource.username"));
properties.setPassword(env.getProperty("spring." + s + ".datasource.password"));
dMap.put(s, new org.apache.tomcat.jdbc.pool.DataSource(properties));
}
return dMap;
}
}
return null;
}
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resolver.getResources(env.getProperty("mybatis.mapper-locations")));
//mybatis的分頁插件
sqlSessionFactoryBean.setPlugins(mybatisPlugins());
//自定義一些配置
sqlSessionFactoryBean.setConfiguration(myConfiguration());
return sqlSessionFactoryBean.getObject();
}
private Interceptor[] mybatisPlugins() {
PageInterceptor interceptor = new PageInterceptor();
Properties pageHelperProps = new Properties();
pageHelperProps.setProperty("helperDialect", "mysql");
pageHelperProps.setProperty("offsetAsPageNum", "true");
pageHelperProps.setProperty("pageSizeZero", "true");
pageHelperProps.setProperty("rowBoundsWithCount", "true");
interceptor.setProperties(pageHelperProps);
Interceptor[] plugins = {interceptor};
return plugins;
}
private org.apache.ibatis.session.Configuration myConfiguration() {
org.apache.ibatis.session.Configuration conf = new org.apache.ibatis.session.Configuration();
//是否啓用 數據中 a_column 自動映射 到 java類中駝峯命名的屬性。[默認:false]
conf.setMapUnderscoreToCamelCase(true);
return conf;
}
}
2.ReadWriteDataSource
借用本文開頭的文章鏈接中的ReadWriteDataSource類
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.util.CollectionUtils;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 讀/寫動態選擇數據庫實現
* 目前實現功能
* 一寫庫多讀庫選擇功能,請參考
* 默認按順序輪詢使用讀庫
* 默認選擇寫庫
* 已實現:一寫多讀、當寫時默認讀操作到寫庫、當寫時強制讀操作到讀庫
* 讀庫負載均衡、讀庫故障轉移
*/
public class ReadWriteDataSource extends AbstractDataSource implements InitializingBean {
private static final Logger log = LoggerFactory.getLogger(ReadWriteDataSource.class);
private DataSource writeDataSource;
private Map<String, DataSource> readDataSourceMap;
private String[] readDataSourceNames;
private DataSource[] readDataSources;
private int readDataSourceCount;
private AtomicInteger counter = new AtomicInteger(1);
/**
* 設置讀庫(name, DataSource)
*/
public void setReadDataSourceMap(Map<String, DataSource> dMap) {
this.readDataSourceMap = dMap;
}
/**
* 配置寫庫
*/
public void setWriteDataSource(DataSource dataSource) {
this.writeDataSource = dataSource;
}
private DataSource determineDataSource() {
if (ReadWriteDataSourceDecision.isChoiceWrite()) {
return writeDataSource;
}
if (ReadWriteDataSourceDecision.isChoiceNone()) {
return writeDataSource;
}
return determineReadDataSource();
}
private DataSource determineReadDataSource() {
//按照順序選擇讀庫
//算法改進
int index = counter.incrementAndGet() % readDataSourceCount;
if (index < 0) {
index = -index;
}
return readDataSources[index];
}
@Override
public Connection getConnection() throws SQLException {
return determineDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineDataSource().getConnection(username, password);
}
@Override
public void afterPropertiesSet() throws Exception {
if (writeDataSource == null) {
throw new IllegalArgumentException("property 'writeDataSource' is required");
}
if (CollectionUtils.isEmpty(readDataSourceMap)) {
throw new IllegalArgumentException("property 'readDataSourceMap' is required");
}
readDataSourceCount = readDataSourceMap.size();
readDataSources = new DataSource[readDataSourceCount];
readDataSourceNames = new String[readDataSourceCount];
int i = 0;
for (Entry<String, DataSource> e : readDataSourceMap.entrySet()) {
readDataSources[i] = e.getValue();
readDataSourceNames[i] = e.getKey();
i++;
}
}
}
3.ReadWriteDataSourceDecision
借用本文開頭的文章鏈接中的ReadWriteDataSourceDecision類
import org.springframework.context.annotation.Configuration;
/**
* 讀/寫動態數據庫 決策者
* 根據DataSourceType是write/read 來決定是使用讀/寫數據庫
* 通過ThreadLocal綁定實現選擇功能
*/
@Configuration
public class ReadWriteDataSourceDecision {
public enum DataSourceType {
write, read;
}
private static final ThreadLocal<DataSourceType> holder = new ThreadLocal<>();
public static void markWrite() {
holder.set(DataSourceType.write);
}
public static void markRead() {
holder.set(DataSourceType.read);
}
public static void reset() {
holder.set(null);
}
public static boolean isChoiceNone() {
return null == holder.get();
}
public static boolean isChoiceWrite() {
return DataSourceType.write == holder.get();
}
public static boolean isChoiceRead() {
return DataSourceType.read == holder.get();
}
}
4.ReadWriteDataSourceProcessor
借用本文開頭的文章鏈接中的ReadWriteDataSourceProcessor類
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.NestedRuntimeException;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
/**
* 此類實現了兩個職責(爲了減少類的數量將兩個功能合併到一起了):
* 讀/寫動態數據庫選擇處理器
* 通過AOP切面實現讀/寫選擇
* <p>
* ★★讀/寫動態數據庫選擇處理器★★
* 1、首先讀取<tx:advice>事務屬性配置
* <p>
* 2、對於所有讀方法設置 read-only="true" 表示讀取操作(以此來判斷是選擇讀還是寫庫),其他操作都是走寫庫
* 如<tx:method name="×××" read-only="true"/>
* <p>
* 3、 forceChoiceReadOnWrite用於確定在如果目前是寫(即開啓了事務),下一步如果是讀,
* 是直接參與到寫庫進行讀,還是強制從讀庫讀<br/>
* forceChoiceReadOnWrite:true 表示目前是寫,下一步如果是讀,強制參與到寫事務(即從寫庫讀)
* 這樣可以避免寫的時候從讀庫讀不到數據
* <p>
* 通過設置事務傳播行爲:SUPPORTS實現
* <p>
* forceChoiceReadOnWrite:false 表示不管當前事務是寫/讀,都強制從讀庫獲取數據
* 通過設置事務傳播行爲:NOT_SUPPORTS實現(連接是儘快釋放)
* 『此處藉助了 NOT_SUPPORTS會掛起之前的事務進行操作 然後再恢復之前事務完成的』
* 4、配置方式
* <bean id="readWriteDataSourceTransactionProcessor" class="cn.javass.common.datasource.ReadWriteDataSourceProcessor">
* <property name="forceChoiceReadWhenWrite" value="false"/>
* </bean>
* <p>
* 5、目前只適用於<tx:advice>情況
* 支持@Transactional註解事務
* <p>
* ★★通過AOP切面實現讀/寫庫選擇★★
* <p>
* 1、首先將當前方法 與 根據之前【讀/寫動態數據庫選擇處理器】 提取的讀庫方法 進行匹配
* <p>
* 2、如果匹配,說明是讀取數據:
* 2.1、如果forceChoiceReadOnWrite:true,即強制走讀庫
* 2.2、如果之前是寫操作且forceChoiceReadOnWrite:false,將從寫庫進行讀取
* 2.3、否則,到讀庫進行讀取數據
* <p>
* 3、如果不匹配,說明默認將使用寫庫進行操作
* <p>
* 4、配置方式
* <aop:aspect order="-2147483648" ref="readWriteDataSourceTransactionProcessor">
* <aop:around pointcut-ref="txPointcut" method="determineReadOrWriteDB"/>
* </aop:aspect>
* 4.1、此處order = Integer.MIN_VALUE 即最高的優先級(請參考http://jinnianshilongnian.iteye.com/blog/1423489)
* 4.2、切入點:txPointcut 和 實施事務的切入點一樣
* 4.3、determineReadOrWriteDB方法用於決策是走讀/寫庫的,請參考
* @see cn.javass.common.datasource.ReadWriteDataSourceDecision
* @see cn.javass.common.datasource.ReadWriteDataSource
*/
@Aspect
public class ReadWriteDataSourceProcessor implements BeanPostProcessor {
private static final Logger log = LoggerFactory.getLogger(ReadWriteDataSourceProcessor.class);
@Pointcut(TxAdviceInterceptor.AOP_POINTCUT_EXPRESSION)
public void txPointcut() {
}
private boolean forceChoiceReadWhenWrite = false;
private Map<String, Boolean> readMethodMap = new HashMap<>();
/**
* 當之前操作是寫的時候,是否強制從從庫讀 默認(false) 當之前操作是寫,默認強制從寫庫讀
*/
public void setForceChoiceReadWhenWrite(boolean forceChoiceReadWhenWrite) {
this.forceChoiceReadWhenWrite = forceChoiceReadWhenWrite;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (!(bean instanceof NameMatchTransactionAttributeSource)) {
return bean;
}
try {
NameMatchTransactionAttributeSource transactionAttributeSource = (NameMatchTransactionAttributeSource) bean;
Field nameMapField = ReflectionUtils.findField(NameMatchTransactionAttributeSource.class, "nameMap");
nameMapField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, TransactionAttribute> nameMap = (Map<String, TransactionAttribute>) nameMapField.get(transactionAttributeSource);
for (Entry<String, TransactionAttribute> entry : nameMap.entrySet()) {
RuleBasedTransactionAttribute attr = (RuleBasedTransactionAttribute) entry.getValue();
// 僅對read-only的處理
if (!attr.isReadOnly()) {
continue;
}
String methodName = entry.getKey();
Boolean isForceChoiceRead = Boolean.FALSE;
if (forceChoiceReadWhenWrite) {
// 不管之前操作是寫,默認強制從讀庫讀 (設置爲NOT_SUPPORTED即可)
// NOT_SUPPORTED會掛起之前的事務
attr.setPropagationBehavior(Propagation.NOT_SUPPORTED
.value());
isForceChoiceRead = Boolean.TRUE;
} else {
// 否則 設置爲SUPPORTS(這樣可以參與到寫事務)
attr.setPropagationBehavior(Propagation.SUPPORTS.value());
}
readMethodMap.put(methodName, isForceChoiceRead);
}
} catch (Exception e) {
throw new ReadWriteDataSourceTransactionException(
"process read/write transaction error", e);
}
return bean;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
private class ReadWriteDataSourceTransactionException extends NestedRuntimeException {
private static final long serialVersionUID = 7537763615924915804L;
public ReadWriteDataSourceTransactionException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* 確定選擇哪個數據源(讀庫還是寫庫)
*
* @param pjp
* @return
* @throws Throwable
*/
@Around("txPointcut()")
public Object determineReadOrWriteDB(ProceedingJoinPoint pjp)
throws Throwable {
if (isChoiceReadDB(pjp.getSignature().getName())) {
ReadWriteDataSourceDecision.markRead();
} else {
ReadWriteDataSourceDecision.markWrite();
}
try {
return pjp.proceed();
} finally {
ReadWriteDataSourceDecision.reset();
}
}
/**
* 根據方法名確定是否選擇 讀庫
*
* @param methodName 方法名
* @return
*/
private boolean isChoiceReadDB(String methodName) {
String bestNameMatch = null;
for (String mappedName : this.readMethodMap.keySet()) {
if (isMatch(methodName, mappedName)) {
bestNameMatch = mappedName;
break;
}
}
Boolean isForceChoiceRead = readMethodMap.get(bestNameMatch);
// 表示強制選擇 讀 庫
if (Objects.equals(isForceChoiceRead, Boolean.TRUE)) {
return true;
}
// 如果之前選擇了寫庫 現在還選擇 寫庫
if (ReadWriteDataSourceDecision.isChoiceWrite()) {
return false;
}
// 表示應該選擇讀庫
if (isForceChoiceRead != null) {
return true;
}
// 默認選擇 寫庫
return false;
}
protected boolean isMatch(String methodName, String mappedName) {
return PatternMatchUtils.simpleMatch(mappedName, methodName);
}
}
5.TxAdviceInterceptor
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.interceptor.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* 用於配置事務處理
* requiredTx對應的是寫事務,走寫庫
* readOnlyTx對應的是讀事務,走讀庫
* 一個事務既有寫又有讀,走寫庫
*/
@Configuration
public class TxAdviceInterceptor {
private static final int TX_METHOD_TIMEOUT = 3000;
public static final String AOP_POINTCUT_EXPRESSION = "execution (* com.iclassmate.abel.service.*.*(..))";
@Autowired
private PlatformTransactionManager transactionManager;
@Bean
public TransactionInterceptor txAdvice() {
NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();
/*只讀事務,不做更新操作*/
RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
readOnlyTx.setReadOnly(true);
readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_NOT_SUPPORTED);
/*當前存在事務就使用當前事務,當前不存在事務就創建一個新的事務*/
RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
requiredTx.setRollbackRules(
Collections.singletonList(new RollbackRuleAttribute(Exception.class)));
requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
requiredTx.setTimeout(TX_METHOD_TIMEOUT);
Map<String, TransactionAttribute> txMap = new HashMap<>();
txMap.put("save*", requiredTx);
txMap.put("add*", requiredTx);
txMap.put("create*", requiredTx);
txMap.put("insert*", requiredTx);
txMap.put("update*", requiredTx);
txMap.put("delete*", requiredTx);
txMap.put("merge*", requiredTx);
txMap.put("remove*", requiredTx);
txMap.put("put*", requiredTx);
txMap.put("drop*", requiredTx);
txMap.put("sync*",requiredTx);
txMap.put("get*", readOnlyTx);
txMap.put("query*", readOnlyTx);
txMap.put("count*", readOnlyTx);
txMap.put("exist*", readOnlyTx);
txMap.put("find*", readOnlyTx);
txMap.put("list*", readOnlyTx);
txMap.put("translate*", readOnlyTx);
txMap.put("select*", readOnlyTx);
txMap.put("*", requiredTx);
source.setNameMap(txMap);
TransactionInterceptor txAdvice = new TransactionInterceptor(transactionManager, source);
return txAdvice;
}
@Bean
public Advisor txAdviceAdvisor() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
return new DefaultPointcutAdvisor(pointcut, txAdvice());
}
}