长期的应试教育让我花了太久才明白：学习其实是一件只与自己相关的事。 现在的我，更愿意主动探索那些真正能激发好奇心、让生活变得更有趣的事物。 如果你有能够启发我、让我产生兴趣的想法、机会或故事，欢迎写信给我： （ljr0115@outlook.com） 我会优先回复那些 与我的时间安排契合、或与我当下关注方向相吻合 的邮件。
AI 摘要 文章汇总JDK8常用JVM参数：堆-Xms/-Xmx、新生代-Xmn/–XX:NewRatio、元空间-XX:MetaspaceSize/MaxMetaspaceSize；GC算法选择-XX:+UseG1GC等；日志-XX:+PrintGCDetails；OOM转储-XX:+HeapDumpOnOutOfMemoryError；并附性能优化与案例链接。 此摘要根据文章内容自动生成，仅用于文章内容的解释与总结title: 最重要的JVM参数总结category: Javatag:JVMJDK 版本：1.81.概述在本篇文章中，你将掌握最常用的 JVM 参数配置。2.堆内存相关Java 虚拟机所管理的内存中最大的一块，Java 堆是所有线程共享的一块内存区域，在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例，几乎所有的对象实例以及数组都在这里分配内存。2.1.显式指定堆内存–Xms和-Xmx与性能有关的最常见实践之一是根据应用程序要求初始化堆内存。如果我们需要指定最小和最大堆大小（推荐显示指定大小），以下参数可以帮助你实现：-Xms<heap size>[unit] -Xmx<heap size>[unit]heap size 表示要初始化内存的具体大小。unit 表示要初始化内存的单位。单位为 “ g” (GB)、“ m”（MB）、“ k”（KB）。举个栗子 🌰，如果我们要为 JVM 分配最小 2 GB 和最大 5 GB 的堆内存大小，我们的参数应该这样来写：-Xms2G -Xmx5G2.2.显式新生代内存(Young Generation)根据Oracle 官方文档，在堆总可用内存配置完成之后，第二大影响因素是为 Young Generation 在堆内存所占的比例。默认情况下，YG 的最小大小为 1310 _MB_，最大大小为 _无限制_。一共有两种指定 新生代内存(Young Generation)大小的方法：1.通过-XX:NewSize和-XX:MaxNewSize指定-XX:NewSize=<young size>[unit] -XX:MaxNewSize=<young size>[unit]举个栗子 🌰，如果我们要为 新生代分配 最小 256m 的内存，最大 1024m 的内存我们的参数应该这样来写：-XX:NewSize=256m -XX:MaxNewSize=1024m2.通过-Xmn<young size>[unit]指定举个栗子 🌰，如果我们要为 新生代分配 256m 的内存（NewSize 与 MaxNewSize 设为一致），我们的参数应该这样来写：-Xmn256mGC 调优策略中很重要的一条经验总结是这样说的：将新对象预留在新生代，由于 Full GC 的成本远高于 Minor GC，因此尽可能将对象分配在新生代是明智的做法，实际项目中根据 GC 日志分析新生代空间大小分配是否合理，适当通过“-Xmn”命令调节新生代大小，最大限度降低新对象直接进入老年代的情况。另外，你还可以通过 -XX:NewRatio=<int> 来设置老年代与新生代内存的比值。比如下面的参数就是设置老年代与新生代内存的比值为 1。也就是说老年代和新生代所占比值为 1：1，新生代占整个堆栈的 1/2。-XX:NewRatio=12.3.显式指定永久代/元空间的大小从 Java 8 开始，如果我们没有指定 Metaspace 的大小，随着更多类的创建，虚拟机会耗尽所有可用的系统内存（永久代并不会出现这种情况）。JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小-XX:PermSize=N #方法区 (永久代) 初始大小 -XX:MaxPermSize=N #方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen相对而言，垃圾收集行为在这个区域是比较少出现的，但并非数据进入方法区后就“永久存在”了。JDK 1.8 的时候，方法区（HotSpot 的永久代）被彻底移除了（JDK1.7 就已经开始了），取而代之是元空间，元空间使用的是本地内存。下面是一些常用参数：-XX:MetaspaceSize=N #设置 Metaspace 的初始大小（是一个常见的误区，后面会解释） -XX:MaxMetaspaceSize=N #设置 Metaspace 的最大大小1、Metaspace 的初始容量并不是 -XX:MetaspaceSize 设置，无论 -XX:MetaspaceSize 配置什么值，对于 64 位 JVM 来说，Metaspace 的初始容量都是 21807104（约 20.8m）。可以参考 Oracle 官方文档 Other Considerations 中提到的：Specify a higher value for the option MetaspaceSize to avoid early garbage collections induced for class metadata. The amount of class metadata allocated for an application is application-dependent and general guidelines do not exist for the selection of MetaspaceSize. The default size of MetaspaceSize is platform-dependent and ranges from 12 MB to about 20 MB.MetaspaceSize 的默认大小取决于平台，范围从 12 MB 到大约 20 MB。另外，还可以看一下这个试验：JVM 参数 MetaspaceSize 的误解。2、Metaspace 由于使用不断扩容到-XX:MetaspaceSize参数指定的量，就会发生 FGC，且之后每次 Metaspace 扩容都会发生 Full GC。也就是说，MetaspaceSize 表示 Metaspace 使用过程中触发 Full GC 的阈值，只对触发起作用。垃圾搜集器内部是根据变量 _capacity_until_GC来判断 Metaspace 区域是否达到阈值的，初始化代码如下所示：void MetaspaceGC::initialize() { // Set the high-water mark to MaxMetapaceSize during VM initialization since // we can't do a GC during initialization. _capacity_until_GC = MaxMetaspaceSize; }相关阅读：issue 更正：MaxMetaspaceSize 如果不指定大小的话，不会耗尽内存 #1204 。3.垃圾收集相关3.1.垃圾回收器为了提高应用程序的稳定性，选择正确的垃圾收集算法至关重要。JVM 具有四种类型的 GC 实现：串行垃圾收集器并行垃圾收集器CMS 垃圾收集器G1 垃圾收集器可以使用以下参数声明这些实现：-XX:+UseSerialGC -XX:+UseParallelGC -XX:+UseParNewGC -XX:+UseG1GC有关垃圾回收实施的更多详细信息，请参见此处。3.2.GC 日志记录生产环境上，或者其他要测试 GC 问题的环境上，一定会配置上打印 GC 日志的参数，便于分析 GC 相关的问题。# 必选 # 打印基本 GC 信息 -XX:+PrintGCDetails -XX:+PrintGCDateStamps # 打印对象分布 -XX:+PrintTenuringDistribution # 打印堆数据 -XX:+PrintHeapAtGC # 打印Reference处理信息 # 强引用/弱引用/软引用/虚引用/finalize 相关的方法 -XX:+PrintReferenceGC # 打印STW时间 -XX:+PrintGCApplicationStoppedTime # 可选 # 打印safepoint信息，进入 STW 阶段之前，需要要找到一个合适的 safepoint -XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1 # GC日志输出的文件路径 -Xloggc:/path/to/gc-%t.log # 开启日志文件分割 -XX:+UseGCLogFileRotation # 最多分割几个文件，超过之后从头文件开始写 -XX:NumberOfGCLogFiles=14 # 每个文件上限大小，超过就触发分割 -XX:GCLogFileSize=50M4.处理 OOM对于大型应用程序来说，面对内存不足错误是非常常见的，这反过来会导致应用程序崩溃。这是一个非常关键的场景，很难通过复制来解决这个问题。这就是为什么 JVM 提供了一些参数，这些参数将堆内存转储到一个物理文件中，以后可以用来查找泄漏:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./java_pid<pid>.hprof -XX:OnOutOfMemoryError="< cmd args >;< cmd args >" -XX:+UseGCOverheadLimit这里有几点需要注意:HeapDumpOnOutOfMemoryError 指示 JVM 在遇到 OutOfMemoryError 错误时将 heap 转储到物理文件中。HeapDumpPath 表示要写入文件的路径; 可以给出任何文件名; 但是，如果 JVM 在名称中找到一个 <pid> 标记，则当前进程的进程 id 将附加到文件名中，并使用.hprof格式OnOutOfMemoryError 用于发出紧急命令，以便在内存不足的情况下执行; 应该在 cmd args 空间中使用适当的命令。例如，如果我们想在内存不足时重启服务器，我们可以设置参数: -XX:OnOutOfMemoryError="shutdown -r" 。UseGCOverheadLimit 是一种策略，它限制在抛出 OutOfMemory 错误之前在 GC 中花费的 VM 时间的比例5.其他-server : 启用“ Server Hotspot VM”; 此参数默认用于 64 位 JVM-XX:+UseStringDeduplication : Java 8u20 引入了这个 JVM 参数，通过创建太多相同 String 的实例来减少不必要的内存使用; 这通过将重复 String 值减少为单个全局 char [] 数组来优化堆内存。-XX:+UseLWPSynchronization: 设置基于 LWP (轻量级进程)的同步策略，而不是基于线程的同步。-XX:LargePageSizeInBytes: 设置用于 Java 堆的较大页面大小; 它采用 GB/MB/KB 的参数; 页面大小越大，我们可以更好地利用虚拟内存硬件资源; 然而，这可能会导致 PermGen 的空间大小更大，这反过来又会迫使 Java 堆空间的大小减小。-XX:MaxHeapFreeRatio : 设置 GC 后, 堆空闲的最大百分比，以避免收缩。-XX:SurvivorRatio : eden/survivor 空间的比例, 例如-XX:SurvivorRatio=6 设置每个 survivor 和 eden 之间的比例为 1:6。-XX:+UseLargePages : 如果系统支持，则使用大页面内存; 请注意，如果使用这个 JVM 参数，OpenJDK 7 可能会崩溃。-XX:+UseStringCache : 启用 String 池中可用的常用分配字符串的缓存。-XX:+UseCompressedStrings : 对 String 对象使用 byte [] 类型，该类型可以用纯 ASCII 格式表示。-XX:+OptimizeStringConcat : 它尽可能优化字符串串联操作。文章推荐这里推荐了非常多优质的 JVM 实践相关的文章，推荐阅读，尤其是 JVM 性能优化和问题排查相关的文章。JVM 参数配置说明 - 阿里云官方文档 - 2022JVM 内存配置最佳实践 - 阿里云官方文档 - 2022求你了，GC 日志打印别再瞎配置了 - 思否 - 2022一次大量 JVM Native 内存泄露的排查分析（64M 问题） - 掘金 - 2022一次线上 JVM 调优实践，FullGC40 次/天到 10 天一次的优化过程 - HeapDump - 2021听说 JVM 性能优化很难？今天我小试了一把！ - 陈树义 - 2021你们要的线上 GC 问题案例来啦 - 编了个程 - 2021Java 中 9 种常见的 CMS GC 问题分析与解决 - 美团技术团队 - 2020从实际案例聊聊 Java 应用的 GC 优化-美团技术团队 - 美团技术团队 - 2017
AI 摘要 文章剖析了自研 RPC 框架的网络传输模块：先定义 RpcRequest/Response 实体，再抽象 RpcRequestTransport 接口，分别用 Socket 与 Netty 实现客户端与服务端收发；Netty 版引入编解码器、自定义协议及心跳保活，并借助 CompletableFuture 实现异步回调，完成高性能远程调用。 此摘要根据文章内容自动生成，仅用于文章内容的解释与总结09 RPC 框架代码分析之网络传输模块以下提到的 服务端 指的是提供服务/方法的一端，客户端 指的是调用远程(服务端)服务/方法的一端。我们之前在“如何自己实现一个 RPC 框架？”这篇文章中介绍到说：既然我们要调用远程的方法，就要发送网络请求来传递目标类和方法的信息以及方法的参数等数据到服务端。 这就涉及到了网络传输！网络传输具体实现你可以使用 Socket （ Java 中最原始、最基础的网络通信方式。但是，Socket 是阻塞 IO、性能低并且功能单一）。你也可以使用同步非阻塞的 I/O 模型 NIO ，但是用它来进行网络编程真的太麻烦了。不过没关系，你可以使用基于 NIO 的网络编程框架 Netty ，它将是你最好的选择！guide-rpc-framework 使用了一种基于 Socket，一种基于 Netty 的方式（循序渐进）。网络传输模块整体结构如下：一共被分为了 4 个包constants : 存放一些网络传输模块共用的常量dto : 用于网络传输的类。handler : 里面只有一个用于处理 rpc 请求的类RpcRequestHandler（根据 rpc 请求调用目标类的目标方法）。transport : 用户网络传输相关类（真正传输网络请求的地方。提供了 Socket 和 Netty 两种网络传输方式）。网络传输实体类网络传输实体类在 dto 包下，主要有两个类。**RpcRequest.java**rpc 请求实体类。当你要调用远程方法的时候，你需要先传输一个 RpcRequest 给对方，RpcRequest 里面包含了要调用的目标方法和类的名称、参数等数据。另外，version 字段（服务版本）主要是为后续不兼容升级提供可能。group 字段主要用于处理一个接口有多个类实现的情况。@AllArgsConstructor @NoArgsConstructor @Getter @Builder @ToString public class RpcRequest implements Serializable { private static final long serialVersionUID = 1905122041950251207L; private String requestId; private String interfaceName; private String methodName; private Object[] parameters; private Class<?>[] paramTypes; private RpcMessageType rpcMessageType; private String version; private String group; public RpcServiceProperties toRpcProperties() { return RpcServiceProperties.builder().serviceName(this.getInterfaceName()) .version(this.getVersion()) .group(this.getGroup()).build(); } }**RpcResponse.java**既然有了 rpc 请求实体类，那肯定就要有 rpc 响应实体类了。当服务端通过 RpcRequest 中的相关数据调用到目标服务的目标方法之后，调用结果就通过 RpcResponse 返回给客户端。@AllArgsConstructor @NoArgsConstructor @Getter @Setter @Builder @ToString public class RpcResponse<T> implements Serializable { private static final long serialVersionUID = 715745410605631233L; private String requestId; /** * response code */ private Integer code; /** * response message */ private String message; /** * response body */ private T data; public static <T> RpcResponse<T> success(T data, String requestId) { RpcResponse<T> response = new RpcResponse<>(); response.setCode(RpcResponseCode.SUCCESS.getCode()); response.setMessage(RpcResponseCode.SUCCESS.getMessage()); response.setRequestId(requestId); if (null != data) { response.setData(data); } return response; } public static <T> RpcResponse<T> fail(RpcResponseCode rpcResponseCode) { RpcResponse<T> response = new RpcResponse<>(); response.setCode(rpcResponseCode.getCode()); response.setMessage(rpcResponseCode.getMessage()); return response; } }网络传输由于，这部分我提供了一种基于 Socket，一种基于 Netty 的网络传输方式（循序渐进）。因此，我先定义了一个发送 RPC 请求的顶层接口，然后我们分别使用 Socket 和 Netty 两种方式对这个接口进行实现即可！RpcRequestTransport.java 传输请求的接口/** * send RpcRequest。 * * @author shuang.kou * @createTime 2020年05月29日 13:26:00 */ @SPI public interface RpcRequestTransport { /** * send rpc request to server and get result * * @param rpcRequest message body * @return data from server */ Object sendRpcRequest(RpcRequest rpcRequest); }下面，我们先来看一下比较简单点的使用 Socket 进行网络传输的方式。Socket客户端这里的客户端实际就是发送 RPC 请求的一端，可以对照我们之间画的 RPC 调用的原理图来理解。客户端主要用于发送网络请求到服务端（目标方法所在的服务器）。当我们知道了服务端的地址之后，我们就可以通过 SocketRpcClient 发送 rpc 请求(RpcRequest) 到服务端了(如果我们要找到服务端的地址，涉及到了注册中心相关的知识。下一节会提到。)。我们直接实现上面定义的 RpcRequestTransport.java 即可。这样的话，通过 Socket 来传输消息的模块就写好了！/** * 基于 Socket 传输 RpcRequest * * @author shuang.kou * @createTime 2020年05月10日 18:40:00 */ @AllArgsConstructor @Slf4j public class SocketRpcClient implements RpcRequestTransport { private final ServiceDiscovery serviceDiscovery; public SocketRpcClient() { this.serviceDiscovery = ExtensionLoader.getExtensionLoader(ServiceDiscovery.class).getExtension("zk"); } @Override public Object sendRpcRequest(RpcRequest rpcRequest) { // build rpc service name by rpcRequest String rpcServiceName = RpcServiceProperties.builder().serviceName(rpcRequest.getInterfaceName()) .group(rpcRequest.getGroup()).version(rpcRequest.getVersion()).build().toRpcServiceName(); InetSocketAddress inetSocketAddress = serviceDiscovery.lookupService(rpcServiceName); try (Socket socket = new Socket()) { socket.connect(inetSocketAddress); ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream()); // Send data to the server through the output stream objectOutputStream.writeObject(rpcRequest); ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream()); // Read RpcResponse from the input stream return objectInputStream.readObject(); } catch (IOException | ClassNotFoundException e) { throw new RpcException("调用服务失败:", e); } } }上面的逻辑很简单，就是对 Socket 发送网络请求这个基础知识的运用。我这里就不再对上面的代码进行解析了，看不懂的小伙伴自行翻看之前关于 Socket 讲解的章节。服务端**SocketRpcServer.java**Socket 服务端。用于等待客户端连接。当客户端成功连接之后，就可以发送 rpc 请求(RpcRequest) 到服务端了。然后，服务端拿到 RpcRequest就会去执行对应的方法。执行完对应的方法之后，就把执行得到的结果放在 RpcResponse 中返回给客户端。/** * @author shuang.kou * @createTime 2020年05月10日 08:01:00 */ @Slf4j public class SocketRpcServer { private final ExecutorService threadPool; private final ServiceProvider serviceProvider; public SocketRpcServer() { threadPool = ThreadPoolFactoryUtils.createCustomThreadPoolIfAbsent("socket-server-rpc-pool"); serviceProvider = SingletonFactory.getInstance(ServiceProviderImpl.class); } public void registerService(Object service) { serviceProvider.publishService(service); } public void registerService(Object service, RpcServiceProperties rpcServiceProperties) { serviceProvider.publishService(service, rpcServiceProperties); } public void start() { try (ServerSocket server = new ServerSocket()) { String host = InetAddress.getLocalHost().getHostAddress(); server.bind(new InetSocketAddress(host, PORT)); CustomShutdownHook.getCustomShutdownHook().clearAll(); Socket socket; while ((socket = server.accept()) != null) { log.info("client connected [{}]", socket.getInetAddress()); threadPool.execute(new SocketRpcRequestHandlerRunnable(socket)); } threadPool.shutdown(); } catch (IOException e) { log.error("occur IOException:", e); } } }NettyNetty 这部分的原理也差不多，不过实现代码差别很大。客户端**NettyClient.java**Netty 客户端主要提供了:**doConnect()** :用于连接服务端（目标方法所在的服务器）并返回对应的 Channel。当我们知道了服务端的地址之后，我们就可以通过 NettyClient 成功连接服务端了。（有了 Channel 之后就能发送数据到服务端了）**sendRpcRequest()** : 用于传输 rpc 请求(RpcRequest) 到服务端。@Slf4j public final class NettyRpcClient implements RpcRequestTransport { private final ServiceDiscovery serviceDiscovery; private final UnprocessedRequests unprocessedRequests; private final ChannelProvider channelProvider; private final Bootstrap bootstrap; private final EventLoopGroup eventLoopGroup; @SneakyThrows public Channel doConnect(InetSocketAddress inetSocketAddress) { CompletableFuture<Channel> completableFuture = new CompletableFuture<>(); bootstrap.connect(inetSocketAddress).addListener((ChannelFutureListener) future -> { if (future.isSuccess()) { log.info("The client has connected [{}] successful!", inetSocketAddress.toString()); completableFuture.complete(future.channel()); } else { throw new IllegalStateException(); } }); return completableFuture.get(); } @Override public Object sendRpcRequest(RpcRequest rpcRequest) { // build return value CompletableFuture<RpcResponse<Object>> resultFuture = new CompletableFuture<>(); // build rpc service name by rpcRequest String rpcServiceName = rpcRequest.toRpcProperties().toRpcServiceName(); // get server address InetSocketAddress inetSocketAddress = serviceDiscovery.lookupService(rpcServiceName); // get server address related channel Channel channel = getChannel(inetSocketAddress); if (channel.isActive()) { // put unprocessed request unprocessedRequests.put(rpcRequest.getRequestId(), resultFuture); RpcMessage rpcMessage = new RpcMessage(); rpcMessage.setData(rpcRequest); rpcMessage.setCodec(SerializationTypeEnum.PROTOSTUFF.getCode()); rpcMessage.setCompress(CompressTypeEnum.GZIP.getCode()); rpcMessage.setMessageType(RpcConstants.REQUEST_TYPE); channel.writeAndFlush(rpcMessage).addListener((ChannelFutureListener) future -> { if (future.isSuccess()) { log.info("client send message: [{}]", rpcMessage); } else { future.channel().close(); resultFuture.completeExceptionally(future.cause()); log.error("Send failed:", future.cause()); } }); } else { throw new IllegalStateException(); } return resultFuture; } }**UnprocessedRequests.java**用于存放未被服务端处理的请求（建议限制 map 容器大小，避免未处理请求过多 OOM)。public class UnprocessedRequests { private static final Map<String, CompletableFuture<RpcResponse<Object>>> UNPROCESSED_RESPONSE_FUTURES = new ConcurrentHashMap<>(); public void put(String requestId, CompletableFuture<RpcResponse<Object>> future) { UNPROCESSED_RESPONSE_FUTURES.put(requestId, future); } public void complete(RpcResponse<Object> rpcResponse) { CompletableFuture<RpcResponse<Object>> future = UNPROCESSED_RESPONSE_FUTURES.remove(rpcResponse.getRequestId()); if (null != future) { future.complete(rpcResponse); } else { throw new IllegalStateException(); } } }**NettyClientHandler**自定义客户端 ChannelHandler 用于处理服务器发送的数据。@Slf4j public class NettyClientHandler extends ChannelInboundHandlerAdapter { private final UnprocessedRequests unprocessedRequests; private final ChannelProvider channelProvider; public NettyClientHandler() { this.unprocessedRequests = SingletonFactory.getInstance(UnprocessedRequests.class); this.channelProvider = SingletonFactory.getInstance(ChannelProvider.class); } /** * 读取从服务端返回的消息 */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { try { log.info("client receive msg: [{}]", msg); if (msg instanceof RpcResponse) { RpcResponse<Object> rpcResponse = (RpcResponse<Object>) msg; unprocessedRequests.complete(rpcResponse); } } finally { ReferenceCountUtil.release(msg); } } // Netty 心跳机制相关。保证客户端和服务端的连接不被断掉，避免重连。 @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { //省略部分代码 } }从代码中，可以看出当 rpc 请求被成功处理（客户端收到服务端的执行结果）之后，我们调用了 unprocessedRequests.complete(rpcResponse) 方法，这样的话，你只需要通过下面的方式就能成功接收到服务端返回的结果。CompletableFuture<RpcResponse> completableFuture = (CompletableFuture<RpcResponse>) clientTransport.sendRpcRequest(rpcRequest); rpcResponse = completableFuture.get();**ChannelProvider.java**用于存放 Channel（Channel用于在服务端和客户端之间传输数据）。@Slf4j public class ChannelProvider { private final Map<String, Channel> channelMap; public ChannelProvider() { channelMap = new ConcurrentHashMap<>(); } public Channel get(InetSocketAddress inetSocketAddress) { String key = inetSocketAddress.toString(); // determine if there is a connection for the corresponding address if (channelMap.containsKey(key)) { Channel channel = channelMap.get(key); // if so, determine if the connection is available, and if so, get it directly if (channel != null && channel.isActive()) { return channel; } else { channelMap.remove(key); } } return null; } public void set(InetSocketAddress inetSocketAddress, Channel channel) { String key = inetSocketAddress.toString(); channelMap.put(key, channel); } public void remove(InetSocketAddress inetSocketAddress) { String key = inetSocketAddress.toString(); channelMap.remove(key); log.info("Channel map size :[{}]", channelMap.size()); } }服务端相关**NettyRpcServer.java**Netty 服务端。并监听客户端的连接。另外，还提供了两个用户手动注册服务的方法（_还可以通过注解__RpcService__注册服务，这个后面也会介绍到_）。@Slf4j @Component public class NettyRpcServer { public static final int PORT = 9998; private final ServiceProvider serviceProvider = SingletonFactory.getInstance(ServiceProviderImpl.class); public void registerService(Object service, RpcServiceProperties rpcServiceProperties) { serviceProvider.publishService(service, rpcServiceProperties); } @SneakyThrows public void start() { CustomShutdownHook.getCustomShutdownHook().clearAll(); String host = InetAddress.getLocalHost().getHostAddress(); EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); DefaultEventExecutorGroup serviceHandlerGroup = new DefaultEventExecutorGroup( RuntimeUtil.cpus() * 2, ThreadPoolFactoryUtils.createThreadFactory("service-handler-group", false) ); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) // TCP默认开启了 Nagle 算法，该算法的作用是尽可能的发送大数据快，减少网络传输。TCP_NODELAY 参数的作用就是控制是否启用 Nagle 算法。 .childOption(ChannelOption.TCP_NODELAY, true) // 是否开启 TCP 底层心跳机制 .childOption(ChannelOption.SO_KEEPALIVE, true) //表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁，服务器处理创建新连接较慢，可以适当调大这个参数 .option(ChannelOption.SO_BACKLOG, 128) .handler(new LoggingHandler(LogLevel.INFO)) // 当客户端第一次进行请求的时候才会进行初始化 .childHandler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) { // 30 秒之内没有收到客户端请求的话就关闭连接 ChannelPipeline p = ch.pipeline(); p.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS)); p.addLast(new RpcMessageEncoder()); p.addLast(new RpcMessageDecoder()); p.addLast(serviceHandlerGroup, new NettyRpcServerHandler()); } }); // 绑定端口，同步等待绑定成功 ChannelFuture f = b.bind(host, PORT).sync(); // 等待服务端监听端口关闭 f.channel().closeFuture().sync(); } catch (InterruptedException e) { log.error("occur exception when start server:", e); } finally { log.error("shutdown bossGroup and workerGroup"); bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); serviceHandlerGroup.shutdownGracefully(); } } }**NettyServerHandler.java**自定义服务端 ChannelHandler 用于处理客户端发送的数据。当客户端发的 rpc 请求(RpcRequest) 来了之后，服务端就会处理 rpc 请求(RpcRequest) ，处理完之后就把得到 rpc 相应(RpcResponse)传输给客户端。@Slf4j public class NettyServerHandler extends ChannelInboundHandlerAdapter { private final RpcRequestHandler rpcRequestHandler; public NettyServerHandler() { this.rpcRequestHandler = SingletonFactory.getInstance(RpcRequestHandler.class); } /** * 读取从客户端消息，然后调用目标服务的目标方法并返回给客户端。 */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { // 省略部分代码 } // Netty 心跳机制相关。保证客户端和服务端的连接不被断掉，避免重连。 @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { // 省略部分代码 } }传输协议在《如何自己实现一个 RPC 框架》这一节，我们就提到了传输协议的作用。简单来说：通过设计协议，我们定义需要传输哪些类型的数据， 并且还会规定每一种类型的数据应该占多少字节。这样我们在接收到二级制数据之后，就可以正确的解析出我们需要的数据。这有一点像密文传输的感觉。以下便是我们设计的传输协议（编解码器这里会用到！！！）： * 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 * +-----+-----+-----+-----+--------+----+----+----+------+-----------+-------+----- --+-----+-----+-------+ * | magic code |version | full length | messageType| codec|compress| RequestId | * +-----------------------+--------+---------------------+-----------+-----------+-----------+------------+ * | | * | body | * | | * | ... ... | * +-------------------------------------------------------------------------------------------------------+ * 4B magic code（魔法数） 1B version（版本） 4B full length（消息长度） 1B messageType（消息类型） * 1B compress（压缩类型） 1B codec（序列化类型） 4B requestId（请求的Id）魔法数 ： 通常是 4 个字节。这个魔数主要是为了筛选来到服务端的数据包，有了这个魔数之后，服务端首先取出前面四个字节进行比对，能够在第一时间识别出这个数据包并非是遵循自定义协议的，也就是无效数据包，为了安全考虑可以直接关闭连接以节省资源。序列化器类型 ：标识序列化的方式，比如是使用 Java 自带的序列化，还是 json，kyro 等序列化方式。消息长度 ： 运行时计算出来。......编解码器编解码器这里主要用到了 Kryo 序列化和反序列化以及 Netty 网络传输字节容器 ByteBuf 相关的知识。编解码器的作用主要是让我们在 Netty 进行网络传输所用的对象类型 ByteBuf 与 我们代码层面需要的业务对象之间转换。这部分的代码还是比较多的，小伙伴们可以自己阅读以下，整体逻辑还是比较简单的。一定要先搞懂传输协议之后再去看这部分代码。**RpcMessageDecoder.java**自定义解码器。负责处理"入站"消息，将 ByteBuf 消息格式的对象转换为我们需要的业务对象。网络传输需要通过字节流来实现，ByteBuf 可以看作是 Netty 提供的字节数据的容器，使用它会让我们更加方便地处理字节数据。**RpcMessageEncoder.java**自定义编码器。负责处理"出站"消息，将消息格式转换字节数组然后写入到字节数据的容器 ByteBuf 对象中。
AI 摘要 文章解析缓存雪崩、击穿、穿透：雪崩是批量失效或Redis宕机致数据库崩溃；击穿是热点key过期引发高并发查库；穿透是查询既不在缓存也不在数据库的无效数据，使数据库压力激增，并给出随机过期、互斥锁、布隆过滤器等应对方案。 此摘要根据文章内容自动生成，仅用于文章内容的解释与总结什么是缓存雪崩、击穿、穿透？用户的数据一般都是存储于数据库，数据库的数据是落在磁盘上的，磁盘的读写速度可以说是计算机里最慢的硬件了。当用户的请求，都访问数据库的话，请求数量一上来，数据库很容易就崩溃的了，所以为了避免用户直接访问数据库，会用 Redis 作为缓存层。因为 Redis 是内存数据库，我们可以将数据库的数据缓存在 Redis 里，相当于数据缓存在内存，内存的读写速度比硬盘快好几个数量级，这样大大提高了系统性能。引入了缓存层，就会有缓存异常的三个问题，分别是缓存雪崩、缓存击穿、缓存穿透。这三个问题也是面试中很常考察的问题，我们不光要清楚地知道它们是怎么发生，还需要知道如何解决它们。话不多说，发车！缓存雪崩通常我们为了保证缓存中的数据与数据库中的数据一致性，会给 Redis 里的数据设置过期时间，当缓存数据过期后，用户访问的数据如果不在缓存里，业务系统需要重新生成缓存，因此就会访问数据库，并将数据更新到 Redis 里，这样后续请求都可以直接命中缓存。那么，当大量缓存数据在同一时间过期（失效）或者 Redis 故障宕机时，如果此时有大量的用户请求，都无法在 Redis 中处理，于是全部请求都直接访问数据库，从而导致数据库的压力骤增，严重的会造成数据库宕机，从而形成一系列连锁反应，造成整个系统崩溃，这就是缓存雪崩的问题。可以看到，发生缓存雪崩有两个原因：大量数据同时过期；Redis 故障宕机；不同的诱因，应对的策略也会不同。大量数据同时过期针对大量数据同时过期而引发的缓存雪崩问题，常见的应对方法有下面这几种：均匀设置过期时间；互斥锁；双 key 策略；后台更新缓存；1. 均匀设置过期时间如果要给缓存数据设置过期时间，应该避免将大量的数据设置成同一个过期时间。我们可以在对缓存数据设置过期时间时，给这些数据的过期时间加上一个随机数，这样就保证数据不会在同一时间过期。2. 互斥锁当业务线程在处理用户请求时，如果发现访问的数据不在 Redis 里，就加个互斥锁，保证同一时间内只有一个请求来构建缓存（从数据库读取数据，再将数据更新到 Redis 里），当缓存构建完成后，再释放锁。未能获取互斥锁的请求，要么等待锁释放后重新读取缓存，要么就返回空值或者默认值。实现互斥锁的时候，最好设置超时时间，不然第一个请求拿到了锁，然后这个请求发生了某种意外而一直阻塞，一直不释放锁，这时其他请求也一直拿不到锁，整个系统就会出现无响应的现象。3. 双 key 策略我们对缓存数据可以使用两个 key，一个是主 key，会设置过期时间，一个是备 key，不会设置过期，它们只是 key 不一样，但是 value 值是一样的，相当于给缓存数据做了个副本。当业务线程访问不到「主 key」的缓存数据时，就直接返回「备 key」的缓存数据，然后在更新缓存的时候，同时更新「主 key」和「备 key」的数据。双 key 策略的好处是，当主 key 过期了，有大量请求获取缓存数据的时候，直接返回备 key 的数据，这样可以快速响应请求。而不用因为 key 失效而导致大量请求被锁阻塞住（采用了互斥锁，仅一个请求来构建缓存），后续再通知后台线程，重新构建主 key 的数据。4. 后台更新缓存业务线程不再负责更新缓存，缓存也不设置有效期，而是让缓存“永久有效”，并将更新缓存的工作交由后台线程定时更新。事实上，缓存数据不设置有效期，并不是意味着数据一直能在内存里，因为当系统内存紧张的时候，有些缓存数据会被“淘汰”，而在缓存被“淘汰”到下一次后台定时更新缓存的这段时间内，业务线程读取缓存失败就返回空值，业务的视角就以为是数据丢失了。解决上面的问题的方式有两种。第一种方式，后台线程不仅负责定时更新缓存，而且也负责频繁地检测缓存是否有效，检测到缓存失效了，原因可能是系统紧张而被淘汰的，于是就要马上从数据库读取数据，并更新到缓存。这种方式的检测时间间隔不能太长，太长也导致用户获取的数据是一个空值而不是真正的数据，所以检测的间隔最好是毫秒级的，但是总归是有个间隔时间，用户体验一般。第二种方式，在业务线程发现缓存数据失效后（缓存数据被淘汰），通过消息队列发送一条消息通知后台线程更新缓存，后台线程收到消息后，在更新缓存前可以判断缓存是否存在，存在就不执行更新缓存操作；不存在就读取数据库数据，并将数据加载到缓存。这种方式相比第一种方式缓存的更新会更及时，用户体验也比较好。在业务刚上线的时候，我们最好提前把数据缓存起来，而不是等待用户访问才来触发缓存构建，这就是所谓的缓存预热，后台更新缓存的机制刚好也适合干这个事情。Redis 故障宕机针对 Redis 故障宕机而引发的缓存雪崩问题，常见的应对方法有下面这几种：服务熔断或请求限流机制；构建 Redis 缓存高可靠集群；1. 服务熔断或请求限流机制因为 Redis 故障宕机而导致缓存雪崩问题时，我们可以启动服务熔断机制，暂停业务应用对缓存服务的访问，直接返回错误，不用再继续访问数据库，从而降低对数据库的访问压力，保证数据库系统的正常运行，然后等到 Redis 恢复正常后，再允许业务应用访问缓存服务。服务熔断机制是保护数据库的正常运行，但是暂停了业务应用访问缓存服系统，全部业务都无法正常工作为了减少对业务的影响，我们可以启用请求限流机制，只将少部分请求发送到数据库进行处理，再多的请求就在入口直接拒绝服务，等到 Redis 恢复正常并把缓存预热完后，再解除请求限流的机制。2. 构建 Redis 缓存高可靠集群服务熔断或请求限流机制是缓存雪崩发生后的应对方案，我们最好通过主从节点的方式构建 Redis 缓存高可靠集群。如果 Redis 缓存的主节点故障宕机，从节点可以切换成为主节点，继续提供缓存服务，避免了由于 Redis 故障宕机而导致的缓存雪崩问题。缓存击穿我们的业务通常会有几个数据会被频繁地访问，比如秒杀活动，这类被频繁访问的数据被称为热点数据。如果缓存中的某个热点数据过期了，此时大量的请求访问了该热点数据，就无法从缓存中读取，直接访问数据库，数据库很容易就被高并发的请求冲垮，这就是缓存击穿的问题。可以发现缓存击穿跟缓存雪崩很相似，你可以认为缓存击穿是缓存雪崩的一个子集。应对缓存击穿可以采取前面说到两种方案：互斥锁方案，保证同一时间只有一个业务线程更新缓存，未能获取互斥锁的请求，要么等待锁释放后重新读取缓存，要么就返回空值或者默认值。不给热点数据设置过期时间，由后台异步更新缓存，或者在热点数据准备要过期前，提前通知后台线程更新缓存以及重新设置过期时间；缓存穿透当发生缓存雪崩或击穿时，数据库中还是保存了应用要访问的数据，一旦缓存恢复相对应的数据，就可以减轻数据库的压力，而缓存穿透就不一样了。当用户访问的数据，既不在缓存中，也不在数据库中，导致请求在访问缓存时，发现缓存缺失，再去访问数据库时，发现数据库中也没有要访问的数据，没办法构建缓存数据，来服务后续的请求。那么当有大量这样的请求到来时，数据库的压力骤增，这就是缓存穿透的问题。缓存穿透的发生一般有这两种情况：业务误操作，缓存中的数据和数据库中的数据都被误删除了，所以导致缓存和数据库中都没有数据；黑客恶意攻击，故意大量访问某些读取不存在数据的业务；应对缓存穿透的方案，常见的方案有三种。第一种方案，非法请求的限制；第二种方案，缓存空值或者默认值；第三种方案，使用布隆过滤器快速判断数据是否存在，避免通过查询数据库来判断数据是否存在；第一种方案，非法请求的限制当有大量恶意请求访问不存在的数据的时候，也会发生缓存穿透，因此在 API 入口处我们要判断出请求参数是否合理，请求参数是否含有非法值、请求字段是否存在，如果判断出是恶意请求就直接返回错误，避免进一步访问缓存和数据库。第二种方案，缓存空值或者默认值当我们线上业务发现缓存穿透的现象时，可以针对查询的数据，在缓存中设置一个空值或者默认值，这样后续请求就可以从缓存中读取到空值或者默认值，返回给应用，而不会继续查询数据库。第三种方案，使用布隆过滤器快速判断数据是否存在，避免通过查询数据库来判断数据是否存在。我们可以在写入数据库数据时，使用布隆过滤器做个标记，然后在用户请求到来时，业务线程确认缓存失效后，可以通过查询布隆过滤器快速判断数据是否存在，如果不存在，就不用通过查询数据库来判断数据是否存在。即使发生了缓存穿透，大量请求只会查询 Redis 和布隆过滤器，而不会查询数据库，保证了数据库能正常运行，Redis 自身也是支持布隆过滤器的。那问题来了，布隆过滤器是如何工作的呢？接下来，我介绍下。布隆过滤器由「初始值都为 0 的位图数组」和「N 个哈希函数」两部分组成。当我们在写入数据库数据时，在布隆过滤器里做个标记，这样下次查询数据是否在数据库时，只需要查询布隆过滤器，如果查询到数据没有被标记，说明不在数据库中。布隆过滤器会通过 3 个操作完成标记：第一步，使用 N 个哈希函数分别对数据做哈希计算，得到 N 个哈希值；第二步，将第一步得到的 N 个哈希值对位图数组的长度取模，得到每个哈希值在位图数组的对应位置。第三步，将每个哈希值在位图数组的对应位置的值设置为 1；举个例子，假设有一个位图数组长度为 8，哈希函数 3 个的布隆过滤器。在数据库写入数据 x 后，把数据 x 标记在布隆过滤器时，数据 x 会被 3 个哈希函数分别计算出 3 个哈希值，然后在对这 3 个哈希值对 8 取模，假设取模的结果为 1、4、6，然后把位图数组的第 1、4、6 位置的值设置为 1。当应用要查询数据 x 是否数据库时，通过布隆过滤器只要查到位图数组的第 1、4、6 位置的值是否全为 1，只要有一个为 0，就认为数据 x 不在数据库中。布隆过滤器由于是基于哈希函数实现查找的，高效查找的同时存在哈希冲突的可能性，比如数据 x 和数据 y 可能都落在第 1、4、6 位置，而事实上，可能数据库中并不存在数据 y，存在误判的情况。所以，查询布隆过滤器说数据存在，并不一定证明数据库中存在这个数据，但是查询到数据不存在，数据库中一定就不存在这个数据。总结缓存异常会面临的三个问题：缓存雪崩、击穿和穿透。其中，缓存雪崩和缓存击穿主要原因是数据不在缓存中，而导致大量请求访问了数据库，数据库压力骤增，容易引发一系列连锁反应，导致系统奔溃。不过，一旦数据被重新加载回缓存，应用又可以从缓存快速读取数据，不再继续访问数据库，数据库的压力也会瞬间降下来。因此，缓存雪崩和缓存击穿应对的方案比较类似。而缓存穿透主要原因是数据既不在缓存也不在数据库中。因此，缓存穿透与缓存雪崩、击穿应对的方案不太一样。我这里整理了表格，你可以从下面这张表格很好的知道缓存雪崩、击穿和穿透的区别以及应对方案。