记一次性能优化

记一次性能优化

文章发布于 2021-01-13 22:29:54,最后更新于 2021-01-13 23:10:42

静态代理和动态代理

u=3311768838,3416265630&fm=26&gp=0

前言:

关于性能优化最近看了很多文章和博客,其中最多的话题就是,JVM性能调优、和查询优化。关于JVM性能调优,我觉得自己要学的还挺多,之后在做一个总结吧,今天就复盘一下自己关于代码里面查询优化的一些想法和实现吧,当然个人想法仅供参考。

问题抛出:

之前在做开发的时候,遇到一些网页查询很慢,有时候点了查询结果出来甚至需要10s左右,我就好奇为什么会这么就,就去查看了接口,发现这个接口涉及的查询数据比较多,大概要查询4次然后做个汇总给到前端。

image20210112212645994.png

问题解析:

很简单,一个主线程查询4次,假设有A、B、C、D四个查询,每个查询2s,执行完就是8s。这就是典型的串行执行。如果说A、B、C、D有明显的依赖关系,B要A的结果才能查询,这种情况下这样的设计是没有问题的。但是如果A、B、C、D是四个独立的查询,这样设计就存在不合理性。我们完全可以将串行改成并行。

解决办法:

启用多线程,和懒加载。他们直接相互隔离,不存在依赖关系,四个查询四个线程。在最后需要汇总的时候在获取数据。

设计方案:

既然是查询,我们开了多个线程,所以可以使用线程池,线程的实现我们采用Future创建线程,这样的好处就是在线程执行完后我们可以获取到返回值。

优化前 (total是查询消耗的时间)

@Override
public BaseResponse doHandleBusi(PostscriptSelectRequest request) throws Exception {
    long l = System.currentTimeMillis();
    PostscriptSelectResponse response = new PostscriptSelectResponse();
    response.setTotal(postscriptOperationService.select(request).size());
    PageHelper.offsetPage(request.getOffset(), request.getLimit());
    List<PostscriptOperation> operationList = postscriptOperationService.select(request);
    response.setRows(operationList);
    response.buildSuccess();
    long m = System.currentTimeMillis();
    long l1 = m - l;
    response.setTotal((int) l1);
    return response;
}

优化后

public BaseResponse doHandleBusi(PostscriptSelectRequest request) throws Exception {
        long l = System.currentTimeMillis();
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        PageHelper.offsetPage(request.getOffset(), request.getLimit());
        Future<List<PostscriptOperation>> future1 = executorService.submit(new Task1(request));
        List<PostscriptOperation> postscriptOperations = futureGetProxy(future1, List.class);
       //延迟加载,此时postscriptOperations对象还未真正获得
        Future<Result> future2 = executorService.submit(new Task2(request));
        Result a = futureGetProxy(future2, Result.class);
       //延迟加载,此时a对象还未真正获得
        PostscriptSelectResponse response = new PostscriptSelectResponse();
  		//到这里真正使用了对象所以加载结果,两个线程同时开始并行查询的。
        response.setRows(postscriptOperations);
     	response.setTotal(a.getTotal());
        response.buildSuccess();
        long m = System.currentTimeMillis();
        long l1 = m - l;
        response.setTotal((int) l1);
        return response;
    }
    private <T> T futureGetProxy(Future<T> future, Class clazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(clazz);
        return (T) enhancer.create(clazz, new FutureLazyLoader(future));
    }
    /**
     * 延迟加载类
     * @param <T>
     */
    class FutureLazyLoader<T> implements LazyLoader {
        private Future<T> future;
        public FutureLazyLoader(Future<T> future) {
            this.future = future;
        }
        @Override
        public Object loadObject() throws Exception {
            return future.get();
        }
    }
    class Task1 implements Callable<List<PostscriptOperation>> {
        private PostscriptSelectRequest request;
        public Task1( PostscriptSelectRequest request) {
            this.request = request;
        }
        @Override
        public List<PostscriptOperation> call() throws Exception {
            return postscriptOperationService.select(request);
        }
    }
    class Task2 implements Callable<Result> {
        private PostscriptSelectRequest request;
        public Task2( PostscriptSelectRequest request) {
            this.request = request;
        }
        @Override
        public Result call() throws Exception {
            Result result=new Result();
            result.setNum(postscriptOperationService.select(request).size());
            return result;
        }
    }

效果展示:

遗憾的是,之前的那个耗时10s的查询接口因为要改的东西太多了就没有换,换了另一个相似的项目做了优化看了一下效果。

因为10s的那个接口分页查询使用了公共包里的Pagenation类,遗憾的是这个类是封装好的,因为查询使用的是Hibernate。

优化前:

total是查询时间 (单位是毫秒,因为数据量小加上接口本来查询就只有两个所以还挺快)

image20210113103457714.png

优化后:(从834毫秒优化到了4毫秒,如果数据量放大几百倍 ,或者接口查询更加复杂的话,可能效果会更好)

image20210113103750572.png

补充说明:

其实对于延迟加载,刚开始我的理解还是不够透彻的,对于上面的项目怎西分析其实使用了延迟加载并没有对性能有提升。通过我后面的分析其实完全可以不使用延迟就,单单使用多线程就能载达到上面效果。

上代码

public static void main(String[] args) throws ExecutionException, InterruptedException {
    long l = System.currentTimeMillis();
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    Future<String> future1 = executorService.submit(new Callable<String>(){
        @Override
        public String call() throws Exception {
            Thread.sleep(5000);
            return "你好1";
        }
    });
    Future<String> future2 = executorService.submit(new Callable<String>(){
        @Override
        public String call() throws Exception {
            Thread.sleep(5000);
            return "你好2";
        }
    });
    String s = future1.get();
    String m = future2.get();
    long q = System.currentTimeMillis();
    long l1 = q - l;
    System.out.println(l1);
}

我们知道使用Future来创建的线程最大的好处就是可以获得线程的返回值,但是如果调用Future的get方法来获取返回值会导致线程阻塞,如上代码,如果改成如下

public static void main(String[] args) throws ExecutionException, InterruptedException {
    long l = System.currentTimeMillis();
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    Future<String> future1 = executorService.submit(new Callable<String>(){
        @Override
        public String call() throws Exception {
            Thread.sleep(5000);
            return "你好1";
        }
    });
     String s = future1.get();
    Future<String> future2 = executorService.submit(new Callable<String>(){
        @Override
        public String call() throws Exception {
            Thread.sleep(5000);
            return "你好2";
        }
    });
    String m = future2.get();
    long q = System.currentTimeMillis();
    long l1 = q - l;
    System.out.println(l1);
}

性能就会有所改变,因为在get的时候Future线程会阻塞,导致另一个线程未启动。

当然这就又回到了问题的开始,就是关于延迟加载,像这种情况我们可以使用动态代理,代理Future类,然后对Future类做一个延迟加载的实现,就可以避免get的时候阻塞线程的问题,只有当数据真正被使用的时候再会读取数据。

静态代理和动态代理

提到代理,不得不说Proxy代理模式。他是一种结构型设计模式,主要解决的问题是:在直接访问对象时带来的问题。

代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。

JAVA中的代理分为三种角色:

  1. 代理类(ProxySubject)
  2. 委托类(RealSubject)
  3. 接口(Subject,定义了RealSubject和Proxy的公用接口,这样在任何使用RealSubject的地方都可以使用Proxy)
    image20210113095543717.png

为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。

Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。

静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。

一\静态代理

接口(Subject)

interface HelloService {
    void sayHello();
}

委托类

class HelloServiceImpl implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("Hello World!");
    }
}

代理类

class HelloServiceProxy implements HelloService {
    private HelloService helloService;
    
    public HelloServiceProxy(HelloService helloService) {
        this.helloService = helloService;
    }
    
    @Override
    public void sayHello() {
        System.out.println("Before say hello...");
        helloService.sayHello();
        System.out.println("After say hello...");
    }
}

测试

public class HelloServiceProxyTest {
    
    public static void main(String[] args) {
        HelloService helloService = new HelloServiceImpl();
        HelloServiceProxy proxy = new HelloServiceProxy(helloService);
        proxy.sayHello();
    }
}

输出结果

Before say hello...
Hello World!
After say hello...
优点

代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合)

缺点
  1. 代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度
  2. 代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了。如上的代码是只为UserManager类的访问提供了代理,但是如果还要为其他类如Department类提供代理的话,就需要我们再次添加代理Department的代理类。
解决办法

根据上面发现,每个代理类只能为一个接口服务,这样程序开发中必然会产生许多的代理类。

所以我们就会想办法可以通过一个代理类完成全部的代理功能,那么我们就需要用动态代理

二、 JDK动态代理

Java中的动态代理依靠反射来实现,代理类和委托类不需要实现同一个接口。委托类需要实现接口,否则无法创建动态代理。代理类在JVM运行时动态生成,而不是编译期就能确定。

动态代理也叫做:JDK代理,接口代理

接口(Subject)

interface HelloService {
    void sayHello();
}

委托类

class HelloServiceImpl implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("Hello World!");
    }
}

动态代理类:

class HelloServiceDynamicProxy {

    private Object targertObject;
    public HelloServiceDynamicProxy(Object targertObject) {
        this.targertObject = targertObject;
    }

    public Object getProxyInstance() {
        return Proxy.newProxyInstance(targertObject.getClass().getClassLoader(), targertObject.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("Before say hello...");
                Object ret = method.invoke(helloService, args);
                System.out.println("After say hello...");
                return ret;
            }
        });
    }
}

测试类

public class HelloServieDynamicProxyTest {
    public static void main(String[] args){
        
        //第一种
        HelloServiceDynamicProxy proxy=new HelloServiceDynamicProxy();
        HelloService helloService=proxy.getProxyInstance(new helloService())
        helloService.sayHello();
        //第二种
      //  HelloService helloService = new HelloServiceImpl();
       // HelloService dynamicProxy = (HelloService) new HelloServiceDynamicProxy(helloService).getProxyInstance();
       // dynamicProxy.sayHello();
    }
}

结果

Before say hello...
Hello World!
After say hello...

三、 Cglib动态代理

上面的静态代理和动态代理模式都是要求目标对象是实现一个接口的目标对象,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候就可以使用以目标对象子类的方式类实现代理,这种方法就叫做:Cglib代理。

  • JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口,如果想代理没有实现接口的类,就可以使用Cglib实现.
  • Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展java类与实现java接口.它广泛的被许多AOP的框架使用,例如Spring AOP和synaop,为他们提供方法的interception(拦截)
  • Cglib包的底层是通过使用一个小而块的字节码处理框架ASM来转换字节码并生成新的类.不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉.

委托类:

public class PersonDao {
	public void update() {
		// TODO Auto-generated method stub
		System.out.println("修改个人信息");
	}
}

Cglib代理工厂:

public class ProxyFactory implements MethodInterceptor{
	private Object target;
	public ProxyFactory(Object target){
		this.target = target;
	}
	
	//给目标对象创建一个代理对象
	public Object getProxyInstance(){
		//工具类
		Enhancer en = new Enhancer();
		//设置父类
		en.setSuperclass(target.getClass());
		//设置回调函数
		en.setCallback(this);
		//创建子类代理对象
		return en.create();
	}

	@Override
	public Object intercept(Object obj, Method method, Object[] arg2,MethodProxy proxy) throws Throwable {
		// TODO Auto-generated method stub
		System.out.println("开始事务...");
		Object returnValue = method.invoke(target, arg2);
		System.out.println("提交事务...");
		return returnValue;
	}

 

测试类:


public class App {
	public static void main(String[] args){
		PersonDao target = new PersonDao();
		ProxyFactory proxy = new ProxyFactory(target);
		PersonDao personPproxy = (PersonDao)proxy.getProxyInstance();
		//PersonDao personPproxy = (PersonDao)new ProxyFactory(target).getProxyInstance();
		personPproxy.update();
	}

}

​ 注意:Cglib动态代理可以代理没有实现接口的对象,但是对象必须要有无参的构造函数,如果没有无参的构造函数吗,程序运行会报错!但是幸运的是在Spring4版本以后,Spring会自动生成无参的构造函数,解决了这一问题!

四、反射

什么是反射?

反射是Java 中提供的运行期获取对象信息的能力。先记住二个关键词:运行期、对象信息。

为什么Java 需要反射呢?需要在运行期获取对象信息呢?

如下代码:

Method method =anagle.getClass.getMethod("dance",null);
if(method!=null){
    method.invoke(method,null);
}

angela 对象你如果运行期不知道它是否有dance 方法, 可以调用getClass().getMethod("dance") 判断一下。

为什么不直接调用angela.dance() 方法?

要用 angela.dance() 方法,包里需要 import Angela 类,一定要有确定的Angela 对象,很多框架场景,是不知道目标对象的Class类型的,要动态获取对象的类类型。

小结:

反射就是对于任意一个对象,我们能够运行时访问它的方法和属性。(编译期,类型是确定的,很多时候在拿不到确定的对象的属性和值的时候,需要运行时动态调用方法或获取属性)

反射的API相关类:

image20210113092803772.png

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://mxyblogs.club/archives/记一次线上项目性能优化

Buy me a cup of coffee ☕.