package com.taobao.tddl.group.dbselector;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import javax.sql.DataSource;
import com.taobao.tddl.group.config.GroupConfigManager;
import com.taobao.tddl.group.config.GroupExtraConfig;
import com.taobao.tddl.group.exception.NoMoreDataSourceException;
import com.taobao.tddl.group.jdbc.DataSourceWrapper;
import com.taobao.tddl.group.utils.WeightRandom;
import com.taobao.tddl.monitor.utils.NagiosUtils;
import com.taobao.tddl.common.utils.logger.Logger;
import com.taobao.tddl.common.utils.logger.LoggerFactory;
/**
* 对等数据库管理器 可以是读对等:如多个读库,每个库的数据完全相同。对等读取 可以是写对等:如日志库,每个库数据不同,一条数据写入哪个库都可以。对等写入
* 支持动态推送权重,动态加减库
*
* @author linxuan
* @author yangzhu
*/
// 因为当配置信息变动时每次都会重新生成一个新的EquityDbManager实例,
// 所以原有的与"动态改变"相关的代码在新的EquityDbManager实现中已全部删除
public class EquityDbManager extends AbstractDBSelector {
private static final Logger logger = LoggerFactory.getLogger(EquityDbManager.class);
private Map<String /* dsKey */, DataSourceHolder> dataSourceMap;
private WeightRandom weightRandom;
public EquityDbManager(Map<String, DataSourceWrapper> dataSourceWrapperMap, Map<String, Integer> weightMap){
this.dataSourceMap = new HashMap<String, DataSourceHolder>(dataSourceWrapperMap.size());
for (Map.Entry<String, DataSourceWrapper> e : dataSourceWrapperMap.entrySet()) {
this.dataSourceMap.put(e.getKey(), new DataSourceHolder(e.getValue()));
}
this.weightRandom = new WeightRandom(weightMap);
}
public EquityDbManager(Map<String, DataSourceWrapper> dataSourceWrapperMap, Map<String, Integer> weightMap,
GroupExtraConfig groupExtraConfig){
super.groupExtraConfig = groupExtraConfig;
this.dataSourceMap = new HashMap<String, DataSourceHolder>(dataSourceWrapperMap.size());
for (Map.Entry<String, DataSourceWrapper> e : dataSourceWrapperMap.entrySet()) {
this.dataSourceMap.put(e.getKey(), new DataSourceHolder(e.getValue()));
}
this.weightRandom = new WeightRandom(weightMap);
}
private static String selectAliveKey(WeightRandom weightRandom, List<String> excludeKeys) {
if (null == excludeKeys) {
excludeKeys = new ArrayList<String>();
}
return weightRandom.select(excludeKeys);
}
/**
* @return 根据权重,随机返回一个DataSource
*/
public DataSource select() {
String key = selectAliveKey(weightRandom, null);
if (null != key) {
return this.get(key);
} else {
return null;
}
}
/**
* 返回指定dsKey对应的数据源。若对应数据源的当前权重为0,则返回null
*
* @param dsKey 内部和每一个物理DataSource对应的key, 在初始化dbSelector时指定
* @return 返回dsKey对应的数据源
*/
public DataSourceWrapper get(String dsKey) {
DataSourceHolder holder = dataSourceMap.get(dsKey);
Integer weigthValue = this.weightRandom.getWeightConfig().get(dsKey);
if (weigthValue == null || weigthValue.equals(0)) {
return null;
}
return holder == null ? null : holder.dsw;
}
// TODO 考虑接口是否缩小为只返回DataSource[]
public Map<String, DataSource> getDataSources() {
Map<String, DataSource> dsMap = new HashMap<String, DataSource>(dataSourceMap.size());
for (Map.Entry<String, DataSourceHolder> e : dataSourceMap.entrySet()) {
dsMap.put(e.getKey(), e.getValue().dsw);
}
return dsMap;
}
public Map<String, Integer> getWeights() {
return weightRandom.getWeightConfig();
}
/**
* 在所管理的数据库上重试执行一个回调操作。失败了根据权重选下一个库重试 以根据权重选择到的DataSource,和用户传入的自用参数args,
* 重试调用DataSourceTryer的tryOnDataSource方法
*
* @param failedDataSources 已知的失败DS及其异常
* @param args 透传到DataSourceTryer的tryOnDataSource方法中
* @return null表示执行成功。否则表示重试次内执行失败,返回SQLException列表
*/
protected <T> T tryExecuteInternal(Map<DataSource, SQLException> failedDataSources, DataSourceTryer<T> tryer,
int times, Object... args) throws SQLException {
// 如果不支持重试,把times设为1就可以了
if (!this.isSupportRetry) {
times = 1;
}
WeightRandom wr = this.weightRandom;
List<SQLException> exceptions = new ArrayList<SQLException>(0);
List<String> excludeKeys = new ArrayList<String>(0);
if (failedDataSources != null) {
exceptions.addAll(failedDataSources.values());
times = times - failedDataSources.size(); // 扣除已经失败掉的重试次数
for (SQLException e : failedDataSources.values()) {
if (!exceptionSorter.isExceptionFatal(e)) {
// 有一个异常(其实是最后加入的异常,因map无法知道顺序,只能遍历)不是数据库不可用异常,则抛出
// 是不是应该在发现非数据库fatal之后就立刻抛出,而不是放到failedDataSources这个map里?(guangxia)
return tryer.onSQLException(exceptions, exceptionSorter, args);
}
}
}
for (int i = 0; i < times; i++) {
String name = selectAliveKey(wr, excludeKeys);
if (name == null) {
// 为了扩展
exceptions.add(new NoMoreDataSourceException("tryTime:" + i + ", excludeKeys:" + excludeKeys
+ ", weightConfig:" + wr.getWeightConfig()));
break;
}
DataSourceHolder dsHolder = dataSourceMap.get(name);
if (dsHolder == null) {
// 不应该出现的。初始化逻辑应该保证空的数据源(null)不会被加入dataSourceMap
throw new IllegalStateException("Can't find DataSource for name:" + name);
}
if (failedDataSources != null && failedDataSources.containsKey(dsHolder.dsw)) {
excludeKeys.add(name);
i--; // 这次不算重试次数
continue;
}
// TODO 有必要每次都检查DataSource的状态吗 检查一下数据源,如果是NA或往一个只读的库中写记录都要重试下一个数据源
if (!GroupConfigManager.isDataSourceAvailable(dsHolder.dsw, this.readable)) {
excludeKeys.add(name);
i--; // 这次不算重试次数
continue;
}
try {
if (dsHolder.isNotAvailable) {
boolean toTry = System.currentTimeMillis() - dsHolder.lastRetryTime > retryBadDbInterval;
if (toTry && dsHolder.lock.tryLock()) {
try {
T t = tryer.tryOnDataSource(dsHolder.dsw, args); // 同一个时间只会有一个线程继续使用这个数据源。
dsHolder.isNotAvailable = false; // 用一个线程重试,执行成功则标记为可用,自动恢复
return t;
} finally {
dsHolder.lastRetryTime = System.currentTimeMillis();
dsHolder.lock.unlock();
}
} else {
excludeKeys.add(name); // 其他线程跳过已经标记为notAvailable的数据源
i--; // 这次不算重试次数
continue;
}
} else {
return tryer.tryOnDataSource(dsHolder.dsw, args); // 有一次成功直接返回
}
} catch (SQLException e) {
exceptions.add(e);
boolean isFatal = exceptionSorter.isExceptionFatal(e);
if (isFatal) {
NagiosUtils.addNagiosLog(NagiosUtils.KEY_DB_NOT_AVAILABLE + "|" + name, e.getMessage());
dsHolder.isNotAvailable = true;
}
if (!isFatal || failedDataSources == null) {
// throw e; //如果不是数据库不可用异常,或者不要求重试,直接抛出
break;
}
logger.warn(new StringBuilder().append(i + 1)
.append("th try locate on [")
.append(name)
.append("] failed:")
.append(e.getMessage())
.toString()); // 这里不打异常栈了,全部重试失败才由调用者打
excludeKeys.add(name);
}
}
return tryer.onSQLException(exceptions, exceptionSorter, args);
}
private final Random random = new Random();
/**
* <pre>
* 分流:随机返回权重串里包含值为dataSourceIndex的i的数据源
* 如果权重串没有定义i/I,则dataSourceIndex等于几,就路由到group中的第几个的数据源
* 一个db可以同时配置多个i;不同的db可以配置相同的i,
* 例如权重串= db0:rwi0i2, db1:ri1, db2:ri1, db3:ri2
* a. 用户指定dataSourceIndex=0,路由到db0;(只有db0有i0)
* b. 用户指定dataSourceIndex=1,随机路由到db1和db2;(db1和db2都有i1)
* c. 用户指定dataSourceIndex=2,随机路由到db0和db3;(db0和db3都有i2)
* d. 如果没有配置i,例如db0:rw, db1:r;指定dataSourceIndex=1则路由到db1
* </pre>
*/
protected DataSourceHolder findDataSourceWrapperByIndex(int dataSourceIndex) {
List<DataSourceHolder> holders = new ArrayList<DataSourceHolder>();
for (DataSourceHolder dsh : dataSourceMap.values()) {
if (dsh.dsw.isMatchDataSourceIndex(dataSourceIndex)) holders.add(dsh);
}
if (!holders.isEmpty()) {
return holders.get(random.nextInt(holders.size()));
} else {
return null;
}
}
}