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 readDataSources() {
        List 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 local = new ThreadLocal<>();

    public static ThreadLocal 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 pagehelper = new LinkedHashMap<>();

    @Value("${spring.datasource.type}")
    private Class dataSourceType;
    @Value("${datasource.readSize}")
    private String dataSourceSize;

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

    /**
     * 动态数据源
     *
     * @return
     */
    @Bean(name = "roundRobinDataSourceProxy")
    @Primary
    public AbstractRoutingDataSource roundRobinDataSourceProxy() {
        int dsSize = Integer.parseInt(StringUtils.isBlank(dataSourceSize) ? "1" : dataSourceSize);
        MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource(dsSize);
        Map 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());
    }
}

注意:事务的数据源必须与执行方法的数据源一致,否则事务不起作用。

 

你可能感兴趣的:(springboot,Java)