封装读写分离数据源 - 969251639/study GitHub Wiki
近期使用Sharding-jdbc来做数据库主从下的读写分离,为了做到自动化,简单化,可扩展化,简单的做了以下封装
- 自定义主从配置文件
####配置规范####
#[]开头的表示环境,必须与工程环境一一匹配,匹配不到则抛异常
#---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]
- 解析上面自定义文件的文件
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)中
- 生成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;
}
}
这样上层应用可以无感知的使用数据库的读写分离