记一场由于错误使用final声明方法和CGLib引发的问题

前言:

近期发现某系统在业务高峰时,访问量回突然下降,开发测试并抓包后发现是调用获取城市列表功能时发生异常所致。下面是发现和解决问题的大概步骤:

1:分析代码,发现出问题的接口查询的所有数据均已缓存到redis中,然后查系统日志,发现在这个时间点有大量的redis访问超时。

2:问题比较清晰,大概率是redis调用出现问题,初期怀疑可能为redis连接数不足导致。

3:观测redis连接数的确偏小,改大后测试,问题没有解决。

4:继续分析线上各类指标,发现在此时生产cpu等指标会上升。

5:在排除掉其他环境配置可能引起的问题后。最终判断,代码层面可能有问题,仔细调查后发现某同事将调用redis的封装接口getAll()错误的使用了final声明,而我们系统的代理方式为CGLib。(下面会分析具体原因)

6:将final去掉:,发版测试后问题解决。

正文:

一:出问题部分的代码如下,该接口封装的比较深,平时很难发现,这也是我们在工作用会遇到的一个问题,接口或者基础包封装的比较多的话开发业务逻辑会比较方便,但是一旦封装的接口不够好或者架构有问题就会产生一些隐藏的bug

//以下代码为伪代码:为防侵权,删除了部分实现,仅留有相关逻辑部分
public final List<T> getAll() { // 取得全表缓存
		List<T> allPoList = null;
		try {
			allPoList = "查询redis“//此处注意
		} catch (Exception e) {
		//此处格外注意,此处allCacheKey赋值处在构造函数中,这里的赋值失效,allCacheKey为空
			allPoList = Optional.ofNullable(jedisUtil.smembers(jedisPool, allCacheKey));
		}
		if (CollectionUtils.isEmpty(allPoList)) {
			return refreshAll();//此处格外注意
		} else {
			return allPoList;
		}
	}
	
protected List<T> refreshAll() {
	delCacheByKey(allCacheKey);//此处删除所有key对应的缓存
	selectDB();//查询数据库所有该缓存表的数据
	jedisUtil.set();//将所有数据缓存到缓存中
	return allPoList;
	});
    }

值得注意的是,该方法用final声明,由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理由于使用final方法不能直接调用代理类,所以调用到了被代理类,而allCacheKey赋值给了代理类,所以每次调用getAll()时都会走如下流程:查询redis->数据为空->删除redis缓存->查库并返回查询数据结果给调用方->把数据更新到redis,由于数据库和redis配置都比较高,在平时常态访问量下一般不会暴露出问题,但是当流量比较大的情况下频繁刷新redis,自然扛不住。

二:对于在构造函数中赋值,代理类中为空的情况我也做了分析,分析过程为手动模拟CGLib实现方式

public static void main(String[] args) {
        DBQueryProxy dbQueryProxy = new DBQueryProxy();
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\");
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(DBQuery.class);
        enhancer.setCallback(dbQueryProxy);
        DBQuery dbQuery = (DBQuery) enhancer.create();
        System.out.println(dbQuery.getElement("Hello"));
        System.out.println();
        System.out.println(dbQuery.getAllElements());
        System.out.println(dbQuery.sayHello());
        System.out.println(dbQuery.a);
    }

生成的代理类中构造函数如下:可以看到赋值时被代理类中的值为空

public DBQuery$$EnhancerByCGLIB$$6dcb339a() {
        CGLIB$BIND_CALLBACKS(this);
    }
  private static final void CGLIB$BIND_CALLBACKS(Object var0) {
        DBQuery$$EnhancerByCGLIB$$6dcb339a var1 = (DBQuery$$EnhancerByCGLIB$$6dcb339a)var0;
        if (!var1.CGLIB$BOUND) {
            var1.CGLIB$BOUND = true;
            Object var10000 = CGLIB$THREAD_CALLBACKS.get();
            if (var10000 == null) {
                var10000 = CGLIB$STATIC_CALLBACKS;
                if (CGLIB$STATIC_CALLBACKS == null) {
                    return;
                }
            }

            var1.CGLIB$CALLBACK_0 = (MethodInterceptor)((Callback[])var10000)[0];
        }

    }

另外:关于代理方式上,一般有两种jdk和cglib,在jdk1.6以前,由于cglib是基于fastclass机制,可以直接到用到方法无需像jdk的反射式调用。要快一些,但是jdk1.7和1.8时一直在对动态代理进行优化,所以性能和速度已经不是选择动态代理方式的标准了,仅以需要为主。

猜你喜欢

转载自blog.csdn.net/houkun_dobest/article/details/85335800