springboot 多數據源和事務配置(基於mysql + druid數據源)

MybatisConfiguration (mybatis配置)導入驅動包和druid,略....

 

定義多個數據源,本項目只有一讀一寫(可以根據需要配置一寫多讀)

DruidDataBaseConfiguration.java

public class DruidDataBaseConfiguration {
    /**
     * 主庫, 一般只用於寫數據。 通過配置自動注入
     * @return DataSource
     */
    @Bean(name = "writeDataSource")
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 讀庫。通過配置自動注入
     * @return
     */
    @Bean(name = "readDataSource")
    @ConfigurationProperties("spring.datasource.druid.slave")
    public DataSource slaveDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 讀庫列表(目前本項目只有一個讀庫)
     * @return
     */
    @Bean(name = "readDataSources")
    public List<DataSource> readDataSources() {
        List<DataSource> dataSources = new ArrayList<>();
        dataSources.add(slaveDataSource());
        return dataSources;
    }
}

application.properties

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
datasource.readSize=1
#主庫配置
spring.datasource.druid.master.username=root
spring.datasource.druid.master.password=123456
spring.datasource.druid.master.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.master.url=jdbc:mysql:///master...
spring.datasource.druid.master.initialSize=5
spring.datasource.druid.master.minIdle=5
spring.datasource.druid.master.maxActive=20

#讀庫配置
spring.datasource.druid.slave.username=root
spring.datasource.druid.slave.password=123456
spring.datasource.druid.slave.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.slave.url=jdbc:mysql:///slave...
spring.datasource.druid.slave.initialSize=5
spring.datasource.druid.slave.minIdle=5
spring.datasource.druid.slave.maxActive=20

DataSourceType.java(枚舉)

DataSourceContextHolder.java(數據源負載切換)

MyAbstractRoutingDataSource.java (讀數據源負責均衡配置)

MybatisConfiguration.java (mybatis配置)

WriteDataSource.java (註解類,強制使用主數據源讀取則使用該註解)

DataSourceAspect.java (數據源切換)

public enum DataSourceType {
    slave("read", "從庫"),
    master("write", "主庫");

    private String type;

    private String name;

    public String getType() {
        return type;
    }
    public void setType(String type) {
        this.type = type;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    DataSourceType(String type, String name) {
        this.type = type;
        this.name = name;
    }
}

 

public class DataSourceContextHolder {
    private static final ThreadLocal<String> local = new ThreadLocal<>();

    public static ThreadLocal<String> getLocal() {
        return local;
    }

    /**
     * 讀可能是多個庫
     */
    public static void read() {
        local.set(DataSourceType.slave.getType());
    }

    /**
     * 寫只有一個庫
     */
    public static void write() {
        local.set(DataSourceType.master.getType());
    }

    public static String getJdbcType() {
        return local.get();
    }
}

 

public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {
    private final int dataSourceNumber;
    private AtomicInteger count = new AtomicInteger(0);

    public MyAbstractRoutingDataSource(int dataSourceNumber) {
        this.dataSourceNumber = dataSourceNumber;
    }

    @Override
    protected Object determineCurrentLookupKey() {
        String typeKey = DataSourceContextHolder.getJdbcType();
        if (DataSourceType.master.getType().equals(typeKey)) {
            return DataSourceType.master.getType();
        }
        // 讀 簡單負載均衡
        int number = count.getAndAdd(1);
        int lookupKey = number % dataSourceNumber;
        return new Integer(lookupKey);
    }

}
@Configuration
@Import({DruidDataBaseConfiguration.class})
@ConditionalOnClass({EnableTransactionManagement.class})
@MapperScan(basePackages = {"com.xxx.xxx.dao"})
public class MybatisConfiguration {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private Map<String, String> pagehelper = new LinkedHashMap<>();

    @Value("${spring.datasource.type}")
    private Class<? extends DataSource> dataSourceType;
    @Value("${datasource.readSize}")
    private String dataSourceSize;

    @Resource(name = "writeDataSource")
    private DataSource dataSource;
    @Resource(name = "readDataSources")
    private List<DataSource> readDataSources;

    /**
     * 動態數據源
     *
     * @return
     */
    @Bean(name = "roundRobinDataSourceProxy")
    @Primary
    public AbstractRoutingDataSource roundRobinDataSourceProxy() {
        int dsSize = Integer.parseInt(StringUtils.isBlank(dataSourceSize) ? "1" : dataSourceSize);
        MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(dsSize);
        Map<Object, Object> targetDataSources = new HashMap<>();
        // 寫
        targetDataSources.put(DataSourceType.master.getType(), dataSource);
        //多個讀數據庫時
        for (int i = 0; i < dsSize; i++) {
            targetDataSources.put(i, readDataSources.get(i));
        }
        proxy.setDefaultTargetDataSource(dataSource);
        proxy.setTargetDataSources(targetDataSources);
        return proxy;
    }

    /**
     * @param dataSource
     * @return
     * @throws Exception
     */
    @Bean(name = "dynamicSqlSessionFactory")
    @Primary
    public SqlSessionFactory setSqlSessionFactory(@Qualifier("roundRobinDataSourceProxy") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setTypeAliasesPackage("com.xxx.xxx.dao");
        sqlSessionFactoryBean.getObject().getConfiguration().setMapUnderscoreToCamelCase(true);
        PageInterceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        properties.putAll(pagehelper);
        interceptor.setProperties(properties);
        sqlSessionFactoryBean.getObject().getConfiguration().addInterceptor(interceptor);
        return sqlSessionFactoryBean.getObject();
    }

    @Bean(name = "dynamicSessionTemplate")
    @Primary
    public SqlSessionTemplate setSqlSessionTemplate(@Qualifier("dynamicSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

定義一個主數據源註解,用於一些場景下強制查詢主庫數據(主從庫有延遲,某些場景只能查詢主庫數據)

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface WriteDataSource {
}

定義AOP,用於數據源切換

/**
 * 默認攔截dao層方法,切換數據源
 * dao層方法命名:
 * 以get,query,select,find開頭,切換到讀庫,否則切換到寫庫
 * 當方法添加@WriteDataSource,強制切換到寫庫
 */
@Aspect
@Component
public class DataSourceAspect {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @Around("execution(* com.xxx.xxx.dao..*.*(..))")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature ms = (MethodSignature) pjp.getSignature();
        Method method = ms.getMethod();
        boolean write = false;
        Annotation[] annotations = method.getDeclaredAnnotations();
        if (annotations != null) {
            for (int i = 0; i < annotations.length; i++) {
                write = annotations[i].annotationType().equals(WriteDataSource.class);
                if (write) {
                    DataSourceContextHolder.write();
                    log.debug("dataSource切換到:write");
                    return pjp.proceed();
                }
            }
        }
        String methodName = method.getName();
        if (methodName.startsWith("select")
                || methodName.startsWith("get")
                || methodName.startsWith("query")
                || methodName.startsWith("find")) {
            DataSourceContextHolder.read();
            log.debug("dataSource切換到:Read");
        } else {
            DataSourceContextHolder.write();
            log.debug("dataSource切換到:write");
        }
        return pjp.proceed();
    }
}

事務配置

MultiDataSourceTransactionManager.java

DataSourceTransactionManagerConfiguration.java

public class MultiDataSourceTransactionManager extends DataSourceTransactionManager {

    private static final long serialVersionUID = 8478667649867892934L;

    public MultiDataSourceTransactionManager(DataSource dataSource) {
        super(dataSource);
    }
    /**
      * 事務切換到寫庫, 讀庫不需要事務
      */
    @Override
    protected Object doGetTransaction() {
        DataSourceContextHolder.write();
        return super.doGetTransaction();
    }
}
@Configuration  
@EnableTransactionManagement  
public class DataSourceTransactionManagerConfiguration{
	 private final Logger logger = LoggerFactory.getLogger(this.getClass());
	/** 
     * 自定義事務 
     * MyBatis自動參與到spring事務管理中,無需額外配置,只要org.mybatis.spring.SqlSessionFactoryBean引用的數據源與DataSourceTransactionManager引用的數據源一致即可,否則事務管理會不起作用。 
     * @return 
     */  
    @Resource(name = "roundRobinDataSouceProxy")
    private DataSource dataSource;  
    
    @Bean(name = "transactionManager")  
    public DataSourceTransactionManager transactionManager() {
    	logger.info("-------------------- transactionManager init ---------------------");
    	return new MultiDataSourceTransactionManager(dataSource);
    } 
    
    @Bean(name = "transactionTemplate")
    public TransactionTemplate transactionTemplate(){
        logger.info("-------------------- transactionTemplate init ---------------------");
        return new TransactionTemplate(transactionManager());
    }
}

注意:事務的數據源必須與執行方法的數據源一致,否則事務不起作用。

 

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