BUG
基于前一篇文章关于Sping多数据源实现,已经被我运用到实际项目中。但最近开始出现一些问题,服务刚启动,能看到数据源切换混乱的场景。由于项目中设计,服务启动会去从库查一些配置项数据,需要切换数据源,但经常数据查询失败,发现跑到主库去了,但随后又正常。
本着总想搞点大新闻的心态,开始了Debug之旅。
每次的坑,通常是我无意间挖的,这次也不例外。debug发现,一次操作,数据源被获取了两次。其中第一次是被分页插件PageHelper消耗了。看了下源码,是由于我干掉了一个配置。新项目这边有人说需要mysql、oracle多库同存的业务需求,我把PageHelper的方言配置,原本写死的 【dialect=mysql】给干掉了。
/**
* 设置属性值
*
* @param p 属性值
*/
public void setProperties(Properties p) {
//MyBatis3.2.0版本校验
try {
Class.forName("org.apache.ibatis.scripting.xmltags.SqlNode");//SqlNode是3.2.0之后新增的类
} catch (ClassNotFoundException e) {
throw new RuntimeException("您使用的MyBatis版本太低,MyBatis分页插件PageHelper支持MyBatis3.2.0及以上版本!");
}
//数据库方言
String dialect = p.getProperty("dialect");
if (dialect == null || dialect.length() == 0) {
autoDialect = true;
this.properties = p;
} else {
autoDialect = false;
sqlUtil = new SqlUtil(dialect);
sqlUtil.setProperties(p);
}
}
加载时,判断没有设置方言,则 autoDialect = true
/**
* Mybatis拦截器方法
*
* @param invocation 拦截器入参
* @return 返回执行结果
* @throws Throwable 抛出异常
*/
public Object intercept(Invocation invocation) throws Throwable {
if (autoDialect) {
initSqlUtil(invocation);
}
return sqlUtil.processPage(invocation);
}
/**
* 初始化sqlUtil
*
* @param invocation
*/
public synchronized void initSqlUtil(Invocation invocation) {
if (sqlUtil == null) {
String url = null;
try {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
MetaObject msObject = SystemMetaObject.forObject(ms);
DataSource dataSource = (DataSource) msObject.getValue("configuration.environment.dataSource");
url = dataSource.getConnection().getMetaData().getURL();
} catch (SQLException e) {
throw new RuntimeException("分页插件初始化异常:" + e.getMessage());
}
if (url == null || url.length() == 0) {
throw new RuntimeException("无法自动获取jdbcUrl,请在分页插件中配置dialect参数!");
}
String dialect = Dialect.fromJdbcUrl(url);
if (dialect == null) {
throw new RuntimeException("无法自动获取数据库类型,请通过dialect参数指定!");
}
sqlUtil = new SqlUtil(dialect);
sqlUtil.setProperties(properties);
properties = null;
autoDialect = false;
}
}
没有意外,然后就需要获取数据库连接,根据url判断方言。
回到前面的问题,为什么多获取了一次数据库连接,就导致后面数据源不正常,就得看第一版代码的坑爹之处:
标记存上下文:
public class DataRouteContext {
private static ThreadLocal<Deque<String>> route = new ThreadLocal<>();
public static String getRoute(){
Deque<String> deque = route.get();
if (deque == null || deque.size() == 0) {
return null;
}
return deque.pop();
}
Aspect:
@Aspect
@Component
@Order(1)
public class DataRouteAspect {
// @Around("execution(public * *(..)) && @annotation(dataRoute))")
@Around("@annotation(dataRoute)")
public Object setRouteName(ProceedingJoinPoint jp, DataRoute dataRoute) throws Throwable {
String routeKey = dataRoute.value();
DataRouteLogger.info("Aspect 数据路由设置为:"+routeKey);
if (StringUtils.isNotBlank(routeKey)) {
DataRouteContext.setRoute(routeKey);
}
return jp.proceed();
}
}
获取数据源:
@Override
public Connection getConnection(String username, String password) throws SQLException {
DataSource ds = null;
String routeName = DataRouteContext.getRoute();
if (routeName != null) {
DataRouteLogger.info("dataSource changed , current dataSource is:"+routeName);
ds = sourceMap.get(routeName);
} else {
DataRouteLogger.info("current dataSource is:defaultSource");
ds = this.defaultSource;
}
if (ds == null){
DataRouteLogger.error("dataSource is:" + routeName + " not found");
throw new IllegalArgumentException("dataSource is: " + routeName + "not found");
}
if(username == null || password == null) {
return ds.getConnection();
}
return ds.getConnection(username, password);
}
AOP将在需要切换数据源的方法前,往线程上下文队列里放一个数据源名称,然后获取数据源时,会根据上下文队列里取到的数据源名称,切换不同的数据源,取不到,则为默认数据源。
标记存放方式是队列,取是用pop(),返回并移除,存一次用一次,之前分页插件配置了方言,所以不会中间获取一次数据源,一切正常。当我删除了方言配置,中间获取了一次,就导致消耗掉了一次标记,到了正式使用的时候,就再拿不到对应数据源。
为什么之后又正常,那是因为分页插件,加载了一次方言后,就不再加载。所以之后获取数据源就正常了。
修复
要解决上面的问题,就需要解决数据源标记丢失的问题,所以修改了上下文队列获取标记的方法,将pop()改成peek()。返回数据,不移除。
public class DataRouteContext {
private static ThreadLocal<Deque<String>> route = new ThreadLocal<>();
public static String getRoute(){
Deque<String> deque = route.get();
if (deque == null || deque.size() == 0) {
return null;
}
return deque.peek();
}
然后修改了AOP逻辑,增加了reset动作
@Aspect
@Component
@Order(1)
public class DataRouteAspect {
// @Around("execution(public * *(..)) && @annotation(dataRoute))")
@Around("@annotation(dataRoute)")
public Object setRouteName(ProceedingJoinPoint jp, DataRoute dataRoute) throws Throwable {
String routeKey = dataRoute.value();
DataRouteLogger.info("Aspect 数据路由设置为:"+routeKey);
if (StringUtils.isNotBlank(routeKey)) {
DataRouteContext.setRoute(routeKey);
}
Object result = jp.proceed();
DataRouteContext.reset();
return result;
}
}
至此,之前的BUG就解决了。
另一个问题
分页插件多数据源配置,还需要新增一个参数
autoRuntimeDialect=true