写点什么

小白也能看懂!清华学霸整理的 Java 知识点手册,让你从 0 到 1

发布于: 3 小时前
小白也能看懂!清华学霸整理的Java知识点手册,让你从0到1

今日分享开始啦,请大家多多指教~


今天给大家介绍一下 Java 缓存以及动态代理。


缓存可以让原本打开很慢的页面,变得能“秒开”,平时访问的 APP 与网站几乎都涉及缓存的运用。


代理模式是为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

本文就给大家分享一下如何理解缓存以及动态代理,以及它们的运用思路,希望对大家有所启发。

缓存

一、前言

缓存除了能加速数据的访问之外,还有什么作用呢?

另外,任何事物都有两面性,我们如何才能将缓存的优点发挥得淋漓尽致,同时避免它的弊端呢?

二、缓存能做什么?

正如前面所说,大家最普遍的理解就是当我们遇到某个页面打开很慢的时候,会想到引入缓存,这样页面打开就快了。其实快和慢是相对的,从技术角度来说,缓存之所以快是因为缓存是基于内存去建立的,而内存的读写速度比硬盘快很多倍,所以用内存来代替磁盘作为读写的介质自然能大大提高访问数据的速度。

这个过程大致是这样的,通过在内存中存储被访问过的数据供后续访问时使用,以此来达到提速的效果。

其实除此之外,缓存还有另外两个重要的运用方式:预读取和延迟写。

三、预读取

预读取就是预先读取将要载入的数据,也可以称作“预存预热”,它是在系统中先将硬盘中的一部分加载到内存中,然后再对外提供服务。

为什么要这么做呢?因为有些系统一旦启动就要面临数以万计的请求进来,如果直接让这些请求打到数据库上,非常大的可能是数据库压力暴增,直接被反扒下,无法正常响应。

为了缓解这个问题,就需要通过“预读取”来解决。

可能你会玩,哪怕用了缓存还是扛不住吗?那就是做横向扩展和负载均衡的时候了,这不是本文讨论的内容,有机会再专门分享吧。

如果说“预读取”是在“数据出口”加了一道前置的缓冲区的话,那么下面要说的“延迟写”就是在“数据入口”后面加了一道后置的缓冲区。

四、延迟写

你可能知道,数据库的写入速度是慢于读取速度的,因为写入的时候有一系列的保证数据准确性的机制。所以,如果想提升写入速度的话,要么做分库分表,要么就是通过缓存来进行一道缓冲,再一次性批量写到磁盘,以此来提速。

由于分库分表对跨表操作以及多条件组合查询的副作用巨大,所以引入它的复杂度大于引入缓存,我们应当优先考虑引入缓存的方案。

那么,通过缓存机制来加速“写”的过程就可以称作“延迟写”,它是预先将需要写入磁盘或数据库的数据,暂时写入到内存,然后返回成功,再定时将内存中的数据批量写入到磁盘。

可能你会想,写到内存就认为成功,万一中途出现意外、断电、停机等导致程序异常终止的情况,数据不就丢失了吗?

是的,所以“延迟写”一般仅用于对数据完整性要求不是那么苛刻的场景,比如点赞数、参与用户数等等,可以大大缓解对数据库频繁修改所带来的压力。

其实在我们熟知的分布式缓存 Redis 中,其默认运用的持久化机制---RDB,也是这样的思路。

在一个成熟的系统中,能够运用到缓存的地方其实并不是一处。下面就来梳理一下我们在哪些地方可以加“缓存”。

五、哪里可以加缓存?

在说哪里可以加缓存之前我们先搞清楚一个事情,我们要缓存什么?也就是符合什么特点的数据才需要加缓存?毕竟加缓存是一个额外的成本投入,得物有所值。

一般来说你可以用两个标准来衡量:

热点数据:被高频访问,如几十次/秒以上。

静态数据:很少变化,读大于写。

接下来就可以替他们找到合适的地方加缓存了。

缓存的本质是一个“防御性”的机制,而系统之间的数据流转是一个有序的过程,所以,选择在哪里加缓存就相当于选择在一条马路的哪个位置设路障。在这个路障之后的道路都能受到保护,不被车流碾压。

那么在以终端用户为起点,系统所用的数据库为终点的这条道路上可以作为缓存设立点的位置大致有以下这些:

每个设立点可以挡掉一些流量,最终形成一个漏斗状的拦截效果,以此保护最后面的系统以及最终的数据库。

下面简要描述一下每个运用场景以及需要注意的点。

六、缓存类别

1、浏览器缓存

这是离用户最近的可以作为缓存的地方,而且借助的是用户的“资源”(缓存的数据在用户的终端设备上),性价比可谓最好,让用户帮你分担压力。

当你打开浏览器的开发者工具,看到 from cache 或者 from memory cache、from disk cache 的时候,就意味着这些数据已经被缓存在了用户的终端设备上了,没网的时候也能访问到一部分内容就是这个原因。

这个过程是浏览器替我们完成的,一般用于缓存图片、js 与 css 这些资源,我们可以通过 Http 消息头中的 Cache-Control 来控制它,具体细节这里就不展开了。此外,js 里的全局变量、cookie 等运用也属于该范畴。

浏览器缓存是在用户侧的缓存点,所以我们对它的掌控力比较差,在没有发起新请求的情况下,你无法主动去更新数据。

2、CDN 缓存

提供 CDN 服务的服务商,在全国甚至是全球部署着大量的服务器节点(可以叫做“边缘服务器”)。那么将数据分发到这些遍布各地服务器上作为缓存,让用户访问就近的服务器上的缓存数据,就可以起到压力分摊和加速效果。这在 TOC 类型的系统上运用,效果格外显著。

但是需要注意的是,由于节点众多,更新缓存数据比较缓慢,一般至少是分钟级别,所以一般仅适用于不经常变动的静态数据。

解决方式也是有的,就是在 url 后面带个自增数或者唯一标示,如 ?v=1001。因为不同的 url 会被视作“新”的数据和文件,被重新 create 出来。

3、网关(代理)缓存

很多时候我们在源站前面加一层网关,为的是做一些安全机制或者作为统一分流策略的入口。

同时这里也是做缓存的一个好场所,毕竟网关是“业务无关”的,它能够拦下来请求,对背后的源站也有很大的受益,减少了大量的 CPU 运算。

常用的网关缓存有 Varnish、Squid 与 Ngnix。一般情况下,简单的缓存运用场景,用 Ngnix 即可,因为大部分时候我们会用它做负载均衡,能少引入一个技术就少一分复杂度。如果是大量的小文件可以使用 Varnish,而 Squid 则相对大而全,运用成本也更高一些。

4、进程内缓存

可能我们大多数程序员第一次刻意使用缓存的场景就是这个时候。

一个请求能走到这里说明它是“业务相关”的,需要经过业务逻辑的运算。

也正因如此,从这里开始对缓存的引入成本比前面 3 种大大增加,因为对缓存与数据库之间的“数据一致性”要求更高了。

5、进程外缓存

这个大家也熟悉,就是 Redis 与 Memcached 之类,甚至也可以自己单独写一个程序来专门存放缓存数据,供其它程序远程调用。

这里先多说几句关于 Redis 和 Memcached 该怎么选择的思路。

对资源(CPU、内存等)利用率格外重视的话可以使用 Memcached ,但程序在使用的时候需要容忍可能发生的数据丢失,因为纯内存的机制。如果无法容忍这单,并对资源利用率也比较豪放的话就可以使用 Redis。而且 Redis 的数据库结构更多, Memcached 只有 key-value,更像是一个 NoSQL 存储。

6、数据库缓存

数据库本身是自带缓存模块的,否则也不会叫它内存杀手,基本上你给多少内存它就能吃多少内存。数据库缓存是数据库的内部机制,一般都会给出设置缓存空间大小的配置来让你进行干预。

最后,其实磁盘本身也有缓存。所以你会发现,为了让数据能够平稳地写到物理磁盘中真的是一波三折。

七、缓存是银弹吗?

可能你会想缓存那么好,那么应该多多益善,只要慢就上缓存来解决?

一个事物看上去再好,也有它负面的一面,缓存也有一系列的副作用需要考虑。除了前面提到的“缓存更新”和“缓存与数据的一致性”问题,还有诸如下边的这些问题:

1、缓存雪崩

大量的请求并发进入时,由于某些原因未起到预期的缓冲效果,哪怕只是很短的一段时间,导致请求全部转到数据库,造成数据库压力过重。解决它可以通过“加锁排队”或者“缓存时间增加随机值”。

2、缓存穿透

和缓存雪崩类似,区别是这会持续更长的时间,因为每次“cache miss”后依然无法从数据源加载数据到缓存,导致持续产生“cache miss”。解决它可以通过“布隆过滤器”或者“缓存空对象”。

3、缓存并发

一个缓存 key 下的数据被同时 set,怎么保证业务的准确性?再加上数据库的话?进程内缓存、进程外缓存与数据库三者皆用的情况下呢?用一句话来概括建议的方案是:使用“先 DB 再缓存”的方式,并且缓存操作用 delete 而不是 set。

4、缓存无底洞

虽然分布式缓存是可以无限横向扩展的,但是,集群下的节点真的是越多越好吗?当然不是,缓存也是符合“边际效用递减”规律的。

5、缓存淘汰

内存总是有限的,如果数据量很大,那么根据具体的场景定制合理的淘汰策略是必不可少的, 如 LRU、LFU 与 FIFO 等等。

Java 动态代理

一、代理模式

著名的代理模式例子为引用计数(英语:reference counting)指针对象。

当一个复杂对象的多份副本须存在时,代理模式可以结合享元模式以减少存储器用量。典型做法是创建一个复杂对象及多个代理者,每个代理者会引用到原本的复杂对象。而作用在代理者的运算会转送到原本对象。一旦所有的代理者都不存在时,复杂对象会被移除。

二、组成

抽象角色:通过接口或抽象类声明真实角色实现的业务方法。

代理角色:实现抽象角色,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。

真实角色:实现抽象角色,定义真实角色所要实现的业务逻辑,供代理角色调用。

三、优点

1、职责清晰

真实的角色就是实现实际的业务逻辑,不用关心其他非本职责的事务,通过后期的代理完成一件完成事务,附带的结果就是编程简洁清晰。

2、保护对象

代理对象可以在客户端和目标对象之间起到中介的作用,这样起到了中介的作用和保护了目标对象的作用。

3、高扩展性

四、模式结构

一个是真正的你要访问的对象(目标类),一个是代理对象,真正对象与代理

对象实现同一个接口,先访问代理类再访问真正要访问的对象。

代理模式分为静态代理、动态代理。

静态代理是由程序员创建或工具生成代理类的源码,再编译代理类。所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。

动态代理是在实现阶段不用关心代理类,而在运行阶段才指定哪一个对象。

五、静态代理

创建一个接口,然后创建被代理的类实现该接口并且实现该接口中的抽象方法。之后再创建一个代理类,同时使其也实现这个接口。在代理类中持有一个被代理对象的引用,而后在代理类方法中调用该对象的方法。

使用静态代理很容易就完成了对一个类的代理操作。但是静态代理的缺点也暴露了出来:由于代理只能为一个类服务,如果需要代理的类很多,那么就需要编写大量的代理类,比较繁琐。

六、动态代理

1、动态代理流程图

2、动态代理代码实现

(1)代理类

利用反射机制在运行时创建代理类。接口、被代理类不变,我们构建一个 ProxyInvocationHandler 类来实现 InvocationHandler 接口。

package com.guor.aop.dynamicproxy;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;public class ProxyInvocationHandler implements InvocationHandler {	private Object target;	public Object getTarget() {		return target;	}	public void setTarget(Object target) {		this.target = target;	}	//生成得到代理类	public Object getProxy() {		return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(),this);	}	//处理代理实例,并返回结果	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {		log(method.getName());		//动态代理的本质就是使用反射机制来实现		Object result = method.invoke(target, args);		return result;	}	public void log(String msg) {		System.out.println("执行了"+msg+"方法");	}}
复制代码

通过 Proxy 类的静态方法 newProxyInstance 返回一个接口的代理实例。针对不同的代理类,传入相应的代理程序控制器 InvocationHandler。

(2)被代理类 UserService


(3)执行动态代理

(4)控制台输出

七、动态代理底层实现

1、动态代理具体步骤

通过实现 InvocationHandler 接口创建自己的调用处理器;

通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;

通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;

通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。

2、源码分析

(1)newProxyInstance

既然生成代理对象是用的 Proxy 类的静态方 newProxyInstance,那么我们就去它的源码里看一下它到底都做了些什么?


(2)getProxyClass0

利用 getProxyClass0(loader, intfs)生成代理类 Proxy 的 Class 对象。

(3)ProxyClassFactory

ProxyClassFactory 内部类创建、定义代理类,返回给定 ClassLoader 和 interfaces 的代理类。

private static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>>{	// prefix for all proxy class names	private static final String proxyClassNamePrefix = "$Proxy";	// next number to use for generation of unique proxy class names	private static final AtomicLong nextUniqueNumber = new AtomicLong();	@Override	public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {		Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);		for (Class<?> intf : interfaces) {			/*			 * Verify that the class loader resolves the name of this			 * interface to the same Class object.			 */			Class<?> interfaceClass = null;			try {				interfaceClass = Class.forName(intf.getName(), false, loader);			} catch (ClassNotFoundException e) {			}			if (interfaceClass != intf) {				throw new IllegalArgumentException(					intf + " is not visible from class loader");			}			/*			 * Verify that the Class object actually represents an			 * interface.			 */			if (!interfaceClass.isInterface()) {				throw new IllegalArgumentException(					interfaceClass.getName() + " is not an interface");			}			/*			 * Verify that this interface is not a duplicate.			 */			if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {				throw new IllegalArgumentException(					"repeated interface: " + interfaceClass.getName());			}		}		String proxyPkg = null;     // package to define proxy class in		int accessFlags = Modifier.PUBLIC | Modifier.FINAL;		/*		 * Record the package of a non-public proxy interface so that the		 * proxy class will be defined in the same package.  Verify that		 * all non-public proxy interfaces are in the same package.		 */		for (Class<?> intf : interfaces) {			int flags = intf.getModifiers();			if (!Modifier.isPublic(flags)) {				accessFlags = Modifier.FINAL;				String name = intf.getName();				int n = name.lastIndexOf('.');				String pkg = ((n == -1) ? "" : name.substring(0, n + 1));				if (proxyPkg == null) {					proxyPkg = pkg;				} else if (!pkg.equals(proxyPkg)) {					throw new IllegalArgumentException(						"non-public interfaces from different packages");				}			}		}		if (proxyPkg == null) {			// if no non-public proxy interfaces, use com.sun.proxy package			proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";		}		/*		 * Choose a name for the proxy class to generate.		 */		long num = nextUniqueNumber.getAndIncrement();		String proxyName = proxyPkg + proxyClassNamePrefix + num;		/*		 * Generate the specified proxy class.		 */		byte[] proxyClassFile = ProxyGenerator.generateProxyClass(			proxyName, interfaces, accessFlags);		try {			return defineClass0(loader, proxyName,								proxyClassFile, 0, proxyClassFile.length);		} catch (ClassFormatError e) {			/*			 * A ClassFormatError here means that (barring bugs in the			 * proxy class generation code) there was some other			 * invalid aspect of the arguments supplied to the proxy			 * class creation (such as virtual machine limitations			 * exceeded).			 */			throw new IllegalArgumentException(e.toString());		}	}}
复制代码

(4)generateProxyClass

一系列检查后,调用 ProxyGenerator.generateProxyClass 来生成字节码文件。


(5)generateClassFile

生成代理类字节码文件的 generateClassFile 方法:






字节码生成后,调用 defineClass0 来解析字节码,生成了 Proxy 的 Class 对象。在了解完代理类动态生成过程后,生产的代理类是怎样的,谁来执行这个代理类。

其中,在 ProxyGenerator.generateProxyClass 函数中 saveGeneratedFiles 定义如下,其指代是否保存生成的代理类 class 文件,默认 false 不保存。

在前面的示例中,我们修改了此系统变量:

System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");


小结:

本文介绍了运用缓存的三种思路,然后梳理了在一个完整的系统中可以设立缓存的几个位置,并且分享了关于浏览器、CDN 与网关(代理)等缓存的一些实用经验,没有具体展开来讲细节,只是希望我们对缓存有一个更加体系化的认识,希望能让我们变得更加全面。


今日份分享已结束,请大家多多包涵和指点!

用户头像

还未添加个人签名 2021.04.20 加入

还未添加个人简介

评论

发布
暂无评论
小白也能看懂!清华学霸整理的Java知识点手册,让你从0到1