動態數據源讀寫分離,隨機或輪詢訪問從庫

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方法

 

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