封装读写分离数据源 - 969251639/study GitHub Wiki

近期使用Sharding-jdbc来做数据库主从下的读写分离,为了做到自动化,简单化,可扩展化,简单的做了以下封装

  1. 自定义主从配置文件
####配置规范####
#[]开头的表示环境,必须与工程环境一一匹配,匹配不到则抛异常
#---xxx---表示具体的配置项,目前有三个子项
#1. globalConfig:表示公共配置,下面所有的数据源都会生效,如果下面的数据源配置配了相同的配置项则覆盖公共配置的配置项
#2. masterX:表示主库的配置项,X表示主库的个数,从1开始配起,已序号递增,如果有多个的话
#3. masterX.slaveX:表示从库的配置项,X表示从库的个数,masterX表示是同步那个主库,从1开始配起,已序号递增,如果有多个的话

[dev]
---globalConfig---
	#初始化连接数量,最大最小连接数
	initialSize=5
	maxActive=10
	minIdle=3
	#获取连接等待超时的时间
	maxWait=600000
	#超过时间限制是否回收
	removeAbandoned=true
	#超过时间限制多长
	removeAbandonedTimeout=180
	#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
	timeBetweenEvictionRunsMillis=600000
	#配置一个连接在池中最小生存的时间,单位是毫秒
	minEvictableIdleTimeMillis=300000
	#用来检测连接是否有效的sql,要求是一个查询语句
	validationQuery=SELECT 1 FROM DUAL
	#申请连接的时候检测
	testWhileIdle=true
	#申请连接时执行validationQuery检测连接是否有效,配置为true会降低性能
	testOnBorrow=false
	#归还连接时执行validationQuery检测连接是否有效,配置为true会降低性能
	testOnReturn=false
	#打开PSCache,并且指定每个连接上PSCache的大小
	poolPreparedStatements=true
	maxPoolPreparedStatementPerConnectionSize=50
	#属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:
	#监控统计用的filter:stat 日志用的filter:log4j 防御SQL注入的filter:wall
	filters=stat
---master1---
	driverClassName=com.mysql.jdbc.Driver
	url=jdbc:mysql://xxx.xxx.xxx.xxx:3306/aicare_saas?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
	username=root
	password=xxx
---master1.slave1---
	driverClassName=com.mysql.jdbc.Driver
	url=jdbc:mysql://xxx.xxx.xxx.xxx:3306/aicare_saas?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
	username=root
	password=xxx
---master1.slave2---
	driverClassName=com.mysql.jdbc.Driver
	url=jdbc:mysql://xxx.xxx.xxx.xxx:3306/aicare_saas?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
	username=root
	password=xxx
[test]

[pro]

  1. 解析上面自定义文件的文件
public Map<String, DataSource> getDataSources() throws Exception {
    	Map<String, DataSource> dataSourceMap = new HashMap<>();
    	Map<String, Map<String, String>> map = analyzeDatabase();
    	Map<String, String> globalConfigMap = map.get(GLOBAL_CONFIG_KEY);
    	for(Entry<String, Map<String, String>> entry : map.entrySet()) {
    		String key = entry.getKey();
    		if(GLOBAL_CONFIG_KEY.equals(key)) {//跳过key为globalConfig,这个只是公共配置,非数据库配置,跳过
    			continue;
    		}
    		Map<String, String> value = entry.getValue();
    		Properties properties = System.getProperties();
    		for(Entry<String, String> e : globalConfigMap.entrySet()) {//加载公共配置项
    			properties.setProperty(e.getKey(), e.getValue());
    		}
    		for(Entry<String, String> e : value.entrySet()) {//加载私有配置项
    			properties.setProperty(e.getKey(), e.getValue());
    		}
    		DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);//生成druid连接池
    		dataSourceMap.put(key, dataSource);
    	}
    	return dataSourceMap;
    }
    
    private Map<String, Map<String, String>> analyzeDatabase() throws IOException {
		URL u = DataConfig.class.getResource("/");
		
		if(u == null) {
			return null;
		}
		String protocol = u.getProtocol();
		URL url = null;
		if ("file".equals(protocol)) {
			url = PropertiesUtils.class.getResource("/" + JDBC_CONFIG_FILE_NAME);
		}else {
			url = PropertiesUtils.class.getClassLoader().getResource("BOOT-INF/classes/" + JDBC_CONFIG_FILE_NAME);
		}
		InputStreamReader isr = new InputStreamReader(url.openStream());
		BufferedReader br = new BufferedReader(isr);
		String str = null;  
		try {
			Map<String, Map<String, String>> map = new HashMap<>(); 
	        while((str = br.readLine()) != null) {  
	        	if(analyzeJdbcConfig(str, map)) {
	        		break;
	        	}
	        }
	        return map;
		}finally {
			if(br != null) {
				br.close();
			}
			if(isr != null) {
				isr.close();
			}
		}
	}
    
    private boolean analyzeJdbcConfig(String content, Map<String, Map<String, String>> map) {
    	if(StringUtils.isNotBlank(content)) {
    		content = content.trim();
    		if(!content.startsWith("#")) {//以#号开头的表示注释,忽略这一行
    			//以[]开头的表示环境,读取以当前环境相关的配置
    			if(content.startsWith("[") && content.endsWith("]")) {
    				if(isCurEnv) {//如果切到下一个环境时返回true,表示读取完成
    					return true;
    				}
    				content = content.substring(1, content.length() - 1);
    				if(env.equals(content)) {//看是否匹配环境
    					isCurEnv = true;
    					return false;
    				}
    			}else {
    				if(isCurEnv) {
    					if(content.startsWith("---") && content.endsWith("---")) {
    						key = content.replaceAll("---", "");
    						map.put(key, new HashMap<String, String>());
    					}else {
    						Map<String, String> m = map.get(key);
							int index = content.indexOf("=");
							String key = content.substring(0, index);
							String value = content.substring(index + 1);
							m.put(key, value);
    					}
    				}
    			}
    		}
    	}
    	return false;
    }

analyzeDatabase方法解析文件,生成一个map
首先找打配置文件,按行读取,交给analyzeJdbcConfig方法进行处理
analyzeJdbcConfig方法分以下几种情况:

  • 遇到已 # 开头的则表示注释,忽略该行的解析
  • 判断是不是 [ 开头, ] 结尾,是的话读取 [ ] 中的内容,并跟当前环境做判断,是不是当前环境的配置,如果是则将isCurEnv置为true,当读取到下一个 [ ] 时表示已经有一个环境匹配了,直接返回ture,跳出解析
  • 解析 --- 开头 --- 结尾的属性,取出中间的内容做为key,把下面的配置作为映射表来当做value,直到遇到下一个 --- ---

最后生成的Map如下:

{globalConfig:{initialSize: 5, maxActive: 10, ...}, master1: {driverClassName: xxx, url: xxx, ...}, master1.slave1: {driverClassName: xxx, url: xxx, ...}, ...}

通过analyzeDatabase方法生成map之后就有了具体的数据库的配置,那么接下来就生成Druid数据库连接池,放到另外一个map(dataSourceMap)中

  1. 生成SqlSessionFactory,注入到Spring
    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactoryBean() {
        try {
        	Map<String, DataSource> dataSourceMap = getDataSources();
        	// 通过ShardingSlaveDataSourceFactory继续创建ShardingDataSource
        	ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        	int i = 0;
        	for(Entry<String, DataSource> entry : dataSourceMap.entrySet()) {
        		String key = entry.getKey();
        		if(key.contains(MASTER_KEY) && !key.contains(SLAVE_KEY)) {//配置主库
        			// 构建读写分离配置
                	MasterSlaveRuleConfiguration masterSlaveRuleConfig = new MasterSlaveRuleConfiguration();
                	masterSlaveRuleConfig.setName("ds_" + i);
                	masterSlaveRuleConfig.setMasterDataSourceName(key);
                	for(Entry<String, DataSource> e : dataSourceMap.entrySet()) {//找出该主库下的所有从库
                		String slaveKey = e.getKey();
                		if(slaveKey.contains(key + "." + SLAVE_KEY)) {//判断是否是slave还是master
                			masterSlaveRuleConfig.getSlaveDataSourceNames().add(slaveKey);//添加到该主库下做读写分离
                		}
                	}
masterSlaveRuleConfig.setLoadBalanceAlgorithmType(MasterSlaveLoadBalanceAlgorithmType.ROUND_ROBIN);//轮询算法做负载均衡
        			shardingRuleConfig.getMasterSlaveRuleConfigs().add(masterSlaveRuleConfig);//将配置好的主从规则添加到sharding-jdbc管理
        			i++;
        		}
        	}
        	DataSource dataSource = null;
        	for(MasterSlaveRuleConfiguration s : shardingRuleConfig.getMasterSlaveRuleConfigs()) {
        		if(s.getSlaveDataSourceNames().isEmpty()) {//如果只有一个master,没有slave,暂不支持多主
        			dataSource = dataSourceMap.get(s.getMasterDataSourceName());
        			break;
        		}
        	}
        	if(dataSource == null) { //多个datasource
        		dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new HashMap<>(0), null);
        	}

            SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
            bean.setDataSource(dataSource);//注入数据源
            ...
            return bean.getObject();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

这样上层应用可以无感知的使用数据库的读写分离