天天看点

动态数据源读写分离,随机或轮询访问从库

1.首先是创建执行策略枚举类

/**
 * ${DESCRIPTION}
 * 动态数据源执行策略
 * @author syliu
 * @create 2020-02-04 上午 10:23
 **/
public enum DataSourceStrategy {
	RANDOM("random","随机策略"),
	TRAINING("training","轮询策略")
	;
	
	DataSourceStrategy( String name,String desc) {
		this.name = name;
		this.desc = desc;
	}
	
	private final String name;
	
	private final String desc;
	
	public String getName ( ) {
		return name;
	}
	
	public String getDesc ( ) {
		return desc;
	}
}
           

2.编写AbstractRoutingDataSource的实现类,DynamicDataSource来动态获取数据源

/**
 * Spring boot提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,
 * 这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,
 * 在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。
 *
 * @author syliu
 * @create 2020-02-04 上午 10:23
 **/
public class DynamicDataSource  extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}
           

3.DynamicDataSourceContextHolder,保存及获取数据源,包括全部数据源和单独的从库数据源,方便获取和计算

/**
 * DynamicDataSourceContextHolder,保存及获取数据源
 *
 * @author syliu
 * @create 2020-02-04 上午 10:22
 **/
public class DynamicDataSourceContextHolder {
    /**
     * 存放当前线程使用的数据源类型信息
     */
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    /**
     * 存放所有数据源id
     */
    public static List<String> dataSourceIds = new ArrayList<String>();
    
    /**
     * 存放其他从库数据源的数据
     */
    public static List<String> customDataSourceIds = new ArrayList<String>();
    
    /**
     * 设置数据源
     * @param dataSourceType
     */
    public static void setDataSourceType(String dataSourceType) {
        contextHolder.set(dataSourceType);
    }
    
    /**
     * 获取数据源
     * @return
     */
    public static String getDataSourceType() {
        return contextHolder.get();
    }
    
    /**
     * 清除数据源
     */
    public static void clearDataSourceType() {
        contextHolder.remove();
    }
    
    /**
     * 判断当前数据源是否存在
     * @param dataSourceId
     * @return
     */
    public static boolean isContainsDataSource(String dataSourceId) {
        return dataSourceIds.contains(dataSourceId);
    }
}
           

4.DynamicDataSourceRegister实现数据源注册,实现EnvironmentAware接口,从而获取application.yml配置文件中数据源的配置信息,实现ImportBeanDefinitionRegistrar,从而注册DynamicDataSource

/**
 * DynamicDataSourceRegister实现数据源注册,实现EnvironmentAware接口,
 * 从而获取application.yml配置文件中数据源的配置信息
 * 实现ImportBeanDefinitionRegistrar,从而注册DynamicDataSource
 *
 * @author syliu
 * @create 2020-02-04 上午 9:07
 **/
public class DynamicDataSourceRegister implements ImportBeanDefinitionRegistrar, EnvironmentAware {
    private Logger logger = LoggerFactory.getLogger (DynamicDataSourceRegister.class );
    
    /**
     * 指定默认数据源
     */
    private static final String DATASOURCE_TYPE_DEFAULT = "com.alibaba.druid.pool.DruidDataSource";
    /**
     * 默认数据源
     */
    private DataSource defaultDataSource;
    /**
     * 用户自定义数据源
     */
    private Map<String, DataSource> customDataSources = new HashMap<>();
    
    @Override
    public void setEnvironment(Environment environment) {
        //初始化默认数据源
        initDefaultDataSource(environment);
        //初始化从库数据源
        initCustomDataSources(environment);
    }
    
    private void initDefaultDataSource(Environment env) {
        // 读取主数据源
        Map<String, Object> dsMap = new HashMap<>();
        dsMap.put("driver-class-name", env.getProperty("spring.datasource.druid.driver-class-name"));
        dsMap.put("url", env.getProperty("spring.datasource.druid.url"));
        dsMap.put("username", env.getProperty("spring.datasource.druid.username"));
        dsMap.put("password", env.getProperty("spring.datasource.druid.password"));
        defaultDataSource = buildDataSource(dsMap);
    }
    
    
    private void initCustomDataSources(Environment env) {
        // 读取配置文件获取更多数据源
        String dataSourcesPre = env.getProperty("custom.datasource.names");
        for (String dataSourcesPrefix : dataSourcesPre.split(",")) {
            // 多个数据源
            Map<String, Object> dsMap = new HashMap<>(4);
            dsMap.put("driver-class-name", env.getProperty("custom.datasource." + dataSourcesPrefix + ".driver-class-name"));
            dsMap.put("url", env.getProperty("custom.datasource." + dataSourcesPrefix + ".url"));
            dsMap.put("username", env.getProperty("custom.datasource." + dataSourcesPrefix + ".username"));
            dsMap.put("password", env.getProperty("custom.datasource." + dataSourcesPrefix + ".password"));
            DataSource ds = buildDataSource(dsMap);
            customDataSources.put(dataSourcesPrefix, ds);
        }
    }
    
    @Override
    public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        //添加默认数据源
        targetDataSources.put("dataSource", this.defaultDataSource);
        DynamicDataSourceContextHolder.dataSourceIds.add("dataSource");
        //添加其他数据源
        targetDataSources.putAll(customDataSources);
        for (String key : customDataSources.keySet()) {
            DynamicDataSourceContextHolder.dataSourceIds.add(key);
            DynamicDataSourceContextHolder.customDataSourceIds.add ( key );
        }
        
        //创建DynamicDataSource
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(DynamicDataSource.class);
        beanDefinition.setSynthetic(true);
        MutablePropertyValues mpv = beanDefinition.getPropertyValues();
        mpv.addPropertyValue("defaultTargetDataSource", defaultDataSource);
        mpv.addPropertyValue("targetDataSources", targetDataSources);
        //注册 - BeanDefinitionRegistry
        beanDefinitionRegistry.registerBeanDefinition("dataSource", beanDefinition);
        logger.info("动态数据源注册");
    }
    
    public DataSource buildDataSource(Map<String, Object> dataSourceMap) {
        try {
            Object type = dataSourceMap.get("type");
            if (type == null) {
                // 默认DataSource
                type = DATASOURCE_TYPE_DEFAULT;
            }
            Class<? extends DataSource> dataSourceType;
            dataSourceType = (Class<? extends DataSource>) Class.forName((String) type);
            String driverClassName = dataSourceMap.get("driver-class-name").toString();
            String url = dataSourceMap.get("url").toString();
            String username = dataSourceMap.get("username").toString();
            String password = dataSourceMap.get("password").toString();
            // 自定义DataSource配置
            DataSourceBuilder factory = DataSourceBuilder.create().driverClassName(driverClassName).url(url)
                    .username(username).password(password).type(dataSourceType);
            return factory.build();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
}
           

5.自定义注解,用来标注哪些执行切面的时候切换从数据源

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    /**
     * 执行策略
     * @return
     */
    DataSourceStrategy strategy () default DataSourceStrategy.RANDOM;
}
           

6.自定义切面,用来切换数据源

/**
 * 自定义切面,处理数据源
 *
 * @author syliu
 * @create 2020-02-04 上午 10:28
 **/
@Aspect
@Order(-10)//保证该AOP在@Transactional之前执行
@Component
public class DynamicDataSourceAspect {
    private Logger logger = LoggerFactory.getLogger (DynamicDataSourceAspect.class );
    
    private static final AtomicInteger dataSourceIndex=new AtomicInteger ( 0 );
    
    /**
     * 改变数据源
     * @param joinPoint
     * @param targetDataSource
     */
    @Before("@annotation(targetDataSource)")
    public void changeDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) {
        List < String > dataSourceIds = DynamicDataSourceContextHolder.customDataSourceIds;
        //执行策略
        DataSourceStrategy strategy = targetDataSource.strategy ( );
        String dataSourceId=getDbId(dataSourceIds,strategy);
        if (!DynamicDataSourceContextHolder.isContainsDataSource(dataSourceId)) {
            //joinPoint.getSignature() :获取连接点的方法签名对象
            logger.error("数据源 " + dataSourceId + " 不存在使用默认的数据源 -> " + joinPoint.getSignature());
        } else {
            logger.info ("使用数据源:" + dataSourceId);
            DynamicDataSourceContextHolder.setDataSourceType(dataSourceId);
        }
    }
    
    @After("@annotation(targetDataSource)")
    public void clearDataSource(JoinPoint joinPoint, TargetDataSource targetDataSource) {
        logger.info("清除数据源 " + targetDataSource.value() + " !");
        DynamicDataSourceContextHolder.clearDataSourceType();
    }
    
    /**
     * 数据源策略
     * @param dataSourceIds
     * @param strategy
     * @return
     */
    private String getDbId(List<String> dataSourceIds,DataSourceStrategy strategy){
        if ( dataSourceIds.size ()>0 ){
            int length = dataSourceIds.size ();
            //随机策略
            if ( DataSourceStrategy.RANDOM==strategy ){
                int index=(int)(Math.random()*length);
                String dataSource = dataSourceIds.get (index);
                logger.info ( "执行随机策略,返回数据源:{}" ,dataSource);
                return dataSource;
            }else if (  DataSourceStrategy.TRAINING==strategy){
                    int index = dataSourceIndex.get ( );
                    if ( index==0 ){
                        dataSourceIndex.addAndGet ( length-1 );
                    }else {
                        dataSourceIndex.decrementAndGet ();
                    }
                    String dataSource = dataSourceIds.get (index);
                    logger.info ( "执行轮讯策略,返回数据源:{}" ,dataSource);
                   return dataSource;
            }
        }
        return "";
        
    };
}
           

7.注册多数据源数据库,需要在启动类开启

Import ({ DynamicDataSourceRegister.class}) //注册多源数据库
           

8.配置文件

动态数据源读写分离,随机或轮询访问从库

9.使用

@TargetDataSource ( strategy=DataSourceStrategy.TRAINING )
           

执行策略有轮询和随机,主要是减轻一些压力,而不需要用到mysql中间件,所以自己简单写了下,如果互为主从数据库的所有service都可以使用它来进行读写压力分离,如果是只有从库的情况下,只能在所有查询service上进行使用,service方法中必须全为select 查询的mysql方法

继续阅读