问题表现:偶发性的出现登录异常,或者是其他功能出现系统异常的情况
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: java.lang.ArithmeticException
### The error may exist in jnpf/base/mapper/SysconfigMapper.java (best guess)
### The error may involve jnpf.base.mapper.SysconfigMapper.selectList
### The error occurred while executing a query
### Cause: java.lang.ArithmeticException
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:96)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:441)
问题分析:
查看某个系统日志发现是在查询数据库操作时出现了一个ArithmeticException,初步确定不是自己编写的代码中抛出的这个异常,之后根据异常抛出的堆栈信息定位到jnpf-commom-database动态数据库切换时,做均衡策略时会对当前数据库链接集合size进行取模运算,根据异常日志前后信息判断,就是在取模运算时分母为0导致的异常。
解决方案
1、负载均衡策略中的size是在项目启动,以及第一次数据库请求时,保存的数据库链接信息。分为两部分,第一部分是dataSourceMap全部数据库链接信息是以租户+主从+唯一键的方式保存再map中,第二部分则是groupDataSourcesMap分组数据库链接信息是以租户+主从方式存在map中,所以数据结构是groupDataSourcesMap中也就是一个租户会对应多个数据库链接,以租户进行分组管理
/**
* 新数据源添加到分组
*
* @param ds 新数据源的名字
* @param dataSource 新数据源
*/
private void addGroupDataSource(String ds, DataSource dataSource) {
if (ds.contains(UNDERLINE)) {
String group = ds.split(UNDERLINE)[0];
GroupDataSource groupDataSource = groupDataSources.get(group);
if (groupDataSource == null) {
try {
groupDataSource = new GroupDataSource(group, strategy.getDeclaredConstructor().newInstance());
groupDataSources.put(group, groupDataSource);
} catch (Exception e) {
throw new RuntimeException("dynamic-datasource - add the datasource named " + ds + " error", e);
}
}
groupDataSource.addDatasource(ds, dataSource);
}
}
2、通过异常一日分析,在出现ArithmeticException之前,数据库链接池因为多次请求链接失败,将groupDataSourcesMap中的dataSourceMap中的数据库链接信息移除了,所以这个groupDataSourcesMap下的dataSourceMap.size()为0。
public class LoadBalanceDynamicDataSourceStrategy implements DynamicDataSourceStrategy {
/**
* 负载均衡计数器
*/
private final AtomicInteger index = new AtomicInteger(0);
@Override
public String determineKey(List<String> dsNames) {
return dsNames.get(Math.abs(index.getAndAdd(1) % dsNames.size()));
}
}
3、通过分析dataSourceMap.add()方法出现的代码,再通过debug验证添加数据库链接的时间。得到是每次在进行数据库请求时,首先判断groupDataSources中是否有当前链接,如果有则不重新添加。但是在上述数据结构中,只是将groupDataSourcesMap下的dataSourceMap数据移除,groupDataSourcesMap实际上还存在,所以在进行添加分组链接数据的前置判断中,会默认groupDataSourcesMap是有数据的,也就是默认groupDataSourcesMap下的dataSourceMap不为0.所以就会一直出现算数异常。程序始终在报错,只有通过重启来完整的重新加载groupDataSourcesMap数据才能解决
public synchronized void removeDataSource(String ds) {
if (!StringUtils.hasText(ds)) {
throw new RuntimeException("remove parameter could not be empty");
}
if (primary.equals(ds)) {
throw new RuntimeException("could not remove primary datasource");
}
if (dataSourceMap.containsKey(ds)) {
DataSource dataSource = dataSourceMap.remove(ds);
closeDataSource(ds, dataSource);
if (ds.contains(UNDERLINE)) {
String group = ds.split(UNDERLINE)[0];
if (groupDataSources.containsKey(group)) {
DataSource oldDataSource = groupDataSources.get(group).removeDatasource(ds);
if (oldDataSource == null) {
log.warn("fail for remove datasource from group. dataSource: {} ,group: {}", ds, group);
}
}
}
log.info("dynamic-datasource - remove the database named [{}] success", ds);
} else {
log.warn("dynamic-datasource - could not find a database named [{}]", ds);
}
}
4、具体的代码修改点:第一在请求链接出现算数异常时进行捕获处理,移除dataSourceMap出现异常的链接,并判断如果dataSourceMap移除问题数据后size为0,则将这个dataSourceMap所属的groupDataSourcesMap也同步移除掉,保证在下一次请求时必须让完整的加载groupDataSourcesMap数据。
public static void removeAllAssignDataSource() {
if (isTenantAssignDataSource()) {
String tenantId = DataSourceContextHolder.getDatasourceId();
String dbKey = tenantId + StrPool.DASHED + DdConstants.MASTER;
TenantVO tenantVO = null;
try {
tenantVO = TenantDataSourceUtil.getTenantInfo(tenantId);
} catch (LoginException e) {
log.error("获取缓存租户库列表失败:" + tenantId, e);
}
if (tenantVO != null) {
List<TenantLinkModel> linkList = tenantVO.getLinkList();
if (linkList != null) {
// 添加数据源信息到redis中
String mKey = dbKey + UNDERLINE;
String sKey = tenantId + StrPool.DASHED + DdConstants.SLAVE + UNDERLINE;
for (TenantLinkModel model : linkList) {
DbLinkEntity dbLinkEntity = model.toDbLinkEntity();
String key;
if ("0".equals(String.valueOf(model.getConfigType()))) {
key = mKey + dbLinkEntity.getId();
} else {
key = sKey + dbLinkEntity.getId();
}
try {
DataSource dataSource = DynamicDataSourceUtil.dynamicRoutingDataSource.getDataSources().get(key);
if (dataSource instanceof ItemDataSource && ((ItemDataSource) dataSource).getRealDataSource() instanceof DruidDataSource) {
//Druid数据源如果正在获取数据源 有概率连接创建线程无法停止
((DruidDataSource) ((ItemDataSource) dataSource).getRealDataSource()).setBreakAfterAcquireFailure(true);
}
DynamicDataSourceUtil.dynamicRoutingDataSource.removeDataSource(key);
if (key.contains(UNDERLINE)) {
String group = key.split(UNDERLINE)[0];
if (DynamicDataSourceUtil.dynamicRoutingDataSource.getGroupDataSources().containsKey(group)) {
Map<String, DataSource> dataSourceMap =
DynamicDataSourceUtil.dynamicRoutingDataSource.getGroupDataSources().get(group).getDataSourceMap();
if (dataSourceMap.size() == 0) {
DynamicDataSourceUtil.dynamicRoutingDataSource.getGroupDataSources().remove(group);
}
log.warn("clear datasource from group. dataSource: {} ,group: {}", key, group);
}
}
} catch (Exception e) {
}
}
}
} else {
log.error("获取缓存租户库列表失败: {}", tenantId);
}
}
}