• Spring多数据源配置(2)[PageHelper插件下应用bug修复]


    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
    
  • 相关阅读:
    vim 高级使用技巧第二篇
    你所不知道的Html5那些事(一)
    linux设备驱动第三篇:如何实现一个简单的字符设备驱动
    Android Metro风格的Launcher开发系列第二篇
    「BZOJ3123」[SDOI2013]森林
    【模板】左偏树
    「luogu3157」[CQOI2011]动态逆序对
    「luogu3567」[POI2014]KUR-Couriers
    【模板】二逼平衡树
    「luogu3313」[SDOI2014]旅行
  • 原文地址:https://www.cnblogs.com/coderzl/p/7490495.html
Copyright © 2020-2023  润新知