如何降低接口的响应时间(RT)

概述

接口的响应时间(RT, Response Time)是衡量系统性能的一个重要指标。在本篇文档中,我们介绍几种常用的降低接口响应时间的策略和思想,包括 缓存、池化、异步处理、任务拆解等

缓存

缓存是提升系统性能最常见的方法之一,尤其适用于那些计算复杂且不经常变化的数据。缓存可以避免重复计算,减少数据库和其他IO操作的次数。

本地缓存

•	使用场景:当数据访问频率高且不容易发生变化时,可以考虑在应用服务器本地进行缓存。
•	实现方式:
•	常用的本地缓存工具类有 Caffeine 和 Guava Cache。
•	配置 TTL(存活时间)和最大缓存大小,防止缓存雪崩和内存溢出。
LoadingCache<String, Data> cache = Caffeine.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).maximumSize(1000).build(key -> getDataFromDatabase(key)); // 延迟加载

缓存估量

在本地缓存中,内存始终是服务器珍贵资源,而有些场景下,我们不能缓存所有业务数据。之前也说了,缓存的数据应该是高频访问且低频修改的,从业务的角度看,基本上就是基础类档案数据。

那么要估算 JVM 缓存的内存占用,首先需要了解以下几个要素:

  1. 缓存对象的大小:每个缓存的对象在内存中占据的大小。
  2. 缓存的条目数:要缓存的数据总条目数。
  3. 缓存管理的开销:缓存框架本身(如 Caffeine 或 Guava)的元数据开销。
  4. JVM 内存结构:堆内存、非堆内存以及垃圾回收对内存的影响。
估算步骤
确定每个缓存对象的大小

对于缓存的业务数据,首先需要知道每个对象在 JVM 中的占用大小。可以通过以下方式进行估算:

  • 手动估算:根据对象的属性大小进行估计。每种 Java 类型在内存中的大小如下:

假设你有一个 BasicData 对象,如下:

public class BasicData {private String id;        // 16 字节(对象头)+ 8 字节(引用) + 字符串内部数据大小private int value;        // 4 字节private Date timestamp;   // 8 字节(引用) + Date 内部的具体占用// 其他属性...
}

对象中除了基本类型外,像 StringDate 这样的对象也需要进一步估算其大小。

 `byte`、`boolean`:1 字节short`、`char`:2 字节int`、`float`:4 字节long`、`double`:8 字节对象引用(64 位 JVM):8 字节
  • 使用工具估算:可以使用工具如 Java Instrumentation APIApache Commons LangSizeOf 工具来直接计算对象的大小。例如:
public static long getObjectSize(Object object) {Instrumentation instrumentation = getInstrumentation();return instrumentation.getObjectSize(object);
}
计算缓存条目数

确定需要缓存多少条数据。如果你打算缓存 10,000 个 BasicData 对象,那么可以使用以下公式计算总占用内存:

总内存占用 = 每个对象大小 * 缓存的条目数
考虑缓存管理的开销

CaffeineGuava Cache 这样的缓存库会有一定的元数据开销,比如缓存条目的哈希表、时间戳、命中率统计等。这些管理开销通常是缓存对象总大小的一小部分,但需要考虑。

以 Caffeine 为例:

  • 大约需要 64 字节来管理每个缓存条目(包含键、值和相关的元数据)。
JVM 堆内存与非堆内存的分配

需要考虑 JVM 内存分配的整体情况:

  • 堆内存:用于存储缓存数据的主要部分。
  • 非堆内存:缓存框架的运行时元数据可能会消耗部分非堆内存。
示例估算

假设 BasicData 对象大小为 100 字节,并且你打算缓存 10,000 条数据,使用 Caffeine 缓存框架:

  1. 每个 BasicData 对象约占 100 字节。
  2. 每个缓存条目的元数据占 64 字节。

总内存占用计算为:

缓存对象总大小 = 100 字节 * 10,000 = 1,000,000 字节(约 1 MB)
缓存管理开销 = 64 字节 * 10,000 = 640,000 字节(约 0.64 MB)总内存占用 = 1 MB + 0.64 MB = 1.64 MB

这个估算结果表明,缓存 10,000 条 BasicData 对象大约会占用 1.64 MB 的堆内存。

工具辅助监控

为了更精确地管理和监控 JVM 内存使用,可以使用以下工具:

  1. JVisualVM:JDK 自带的监控工具,可以监控堆内存、线程和垃圾回收。
  2. JProfilerYourKit:专业的 Java 性能分析工具,能够查看对象的内存占用情况,分析垃圾回收行为。

通过这些工具,你可以动态地观察缓存对内存的实际影响并进行优化。

分布式缓存

使用场景:当应用是分布式部署时,本地缓存无法满足需求,这时可以引入分布式缓存,如 Redis

实现方式

将热点数据存储在 Redis 中,减少直接访问数据库的压力。

设置合理的失效时间,避免缓存穿透或缓存击穿问题。

String key = "user_" + userId;
User user = redisTemplate.opsForValue().get(key);
if (user == null) {user = getUserFromDatabase(userId);redisTemplate.opsForValue().set(key, user, 10, TimeUnit.MINUTES);
}

建议使用redission:以下是常见 Redis 客户端(包括 redisTemplateRedisson)的优劣差异的对比表格:

客户端优点缺点适用场景
redisTemplate- Spring 官方提供,易于与 Spring 生态集成 - API 简单、直观,常用操作支持全面 - 支持多种 Redis 数据结构的直接操作,包括 StringListSetZSetHash- 无分布式锁、限流等高级特性,需要自行实现 - 不支持异步操作和复杂的 Redis 集群管理适用于 Spring 项目中的简单缓存操作场景
Redisson- 提供了丰富的分布式工具支持,如分布式锁、限流、信号量等高级功能 - 支持异步、反应式编程(Reactive) - 内置 Redis 集群和哨兵模式的支持 - 支持多种复杂数据结构,如分布式集合、列表、队列等- 相较于 redisTemplate,依赖库体积更大 - API 相对复杂,学习成本较高适用于需要分布式锁、限流、异步操作等复杂功能场景
Jedis- 性能较高,提供了对 Redis 协议的直接支持 - 支持异步编程和管道操作 - API 简单直接- 线程不安全,必须每个线程独立创建连接实例 - 不支持自动重连、集群模式下配置较复杂适用于对性能有较高要求,且能自己管理线程安全的场景
Lettuce- 线程安全,所有 Redis 操作都基于 Netty - 支持异步、响应式编程(与 Spring WebFlux 完美兼容) - 支持 Redis 集群模式和自动重连 - API 灵活,支持高级数据类型和功能- 较低级别的 API,相比于 Redisson 的高级分布式工具缺少封装 - 配置略微复杂适用于高并发、异步任务、响应式编程的场景

说明:

  1. redisTemplate 是 Spring 官方提供的 Redis 客户端,适合与 Spring 框架集成的简单 Redis 操作,但缺乏分布式相关的高级功能。
  2. Redisson 提供了丰富的分布式工具,特别适合需要分布式锁、异步操作等复杂应用场景,但相对学习成本较高。
  3. Jedis 是性能较高的 Redis 客户端,适合对性能有高要求的场景,但线程不安全,需要自己管理线程安全性。
  4. Lettuce 是 Netty 驱动的 Redis 客户端,线程安全且支持异步和响应式编程,适合高并发和 WebFlux 等响应式场景。

根据项目需求选择合适的 Redis 客户端,可以提高开发效率和系统的可维护性。

缓存设计注意事项

缓存穿透:针对大量请求访问不存在的数据,缓存层和数据库都会收到大量请求。解决方案可以使用布隆过滤器。

缓存雪崩:大量缓存同时过期,导致所有请求都打到数据库。可以通过设置缓存过期时间的随机偏移量来缓解。

缓存更新机制:定期更新缓存数据或使用缓存失效时更新策略,确保数据的实时性。

集群环境下的缓存策略

在集群环境中使用多级缓存(JVM 本地缓存 + Redis 分布式缓存)时,处理缓存一致性问题是关键,尤其是在数据变更时同步更新各级缓存。在我理解中,一级缓存是 JVM 本地缓存,二级缓存是 Redis,读多写少的场景下缓存一致性可以通过以下几种策略来保证:

方案设计目标

  • 多级缓存一致性:确保一级缓存(JVM 本地缓存)和二级缓存(Redis)在数据发生变更时同步更新。
  • 高效缓存使用:优先使用速度更快的本地缓存,减少对 Redis 和数据库的访问。
  • 避免缓存穿透、雪崩问题

方案 1:Cache Aside(旁路缓存)模式

Cache Aside 是最常见的缓存更新策略,即应用程序负责更新和删除缓存。基本流程如下:

数据读取流程:
  1. 先从 本地缓存(JVM 缓存)中查询数据。
  2. 如果本地缓存未命中,再从 Redis 中读取。
  3. 如果 Redis 也未命中,则从数据库中读取,数据读出后更新 Redis本地缓存
数据更新流程:
  1. 数据发生变更时,直接更新数据库。
  2. 更新成功后 清除 Redis 中的缓存,通过 del 操作删除。
  3. 通过消息机制或者广播通知其他服务 本地缓存失效(或者直接清除,可以考虑CAS乐观版本锁机制)。
  4. 在下次读取时,重新从数据库加载数据并更新 Redis 和本地缓存。

优点

  • 简单易理解,数据库是主导更新的一方。
  • 多级缓存只需要在变更时同步,不需要每次都去强制一致。

缺点

  • 写操作的性能开销较大,因为需要清理 Redis 和本地缓存。
  • 数据变更量较大时,可能频繁清理缓存导致缓存命中率下降。

实现步骤

  1. JVM 本地缓存更新:使用 Caffeine 或 Guava 作为本地缓存。
  2. Redis 二级缓存:数据变更时,通过 Redis 的 del操作清理缓存。
  3. 同步缓存失效:可以使用消息队列(如 Kafka、RocketMQ)或 Redis 发布订阅机制,通知其他节点清理本地缓存。
伪代码示例:
// 读取数据逻辑
public Object getData(String key) {// Step 1: 先从 JVM 本地缓存读取Object data = localCache.get(key);if (data != null) {return data;}// Step 2: 如果本地缓存没有,再从 Redis 中读取data = redisTemplate.opsForValue().get(key);if (data != null) {localCache.put(key, data); // 更新本地缓存return data;}// Step 3: 如果 Redis 也没有,从数据库加载data = database.load(key);if (data != null) {redisTemplate.opsForValue().set(key, data); // 更新 RedislocalCache.put(key, data); // 更新本地缓存}return data;
}// 更新数据逻辑
public void updateData(String key, Object newValue) {database.update(key, newValue); // 更新数据库redisTemplate.delete(key);      // 清理 Redis 缓存publishCacheInvalidation(key);  // 通知其他节点清理本地缓存
}// 使用消息队列或发布订阅同步缓存失效
public void onCacheInvalidation(String key) {localCache.remove(key);         // 本地缓存失效
}

方案 2:消息队列实现缓存同步

利用 消息队列(如 Kafka、RabbitMQ、RocketMQ)在数据更新后同步缓存变更。这种方式比直接清理缓存要灵活,适合更复杂的集群环境。

实现思路:
  1. 更新数据库 时,生产一条消息到消息队列,消息中包含变更的键值。
  2. 集群中所有服务节点监听此消息,收到消息后主动 清除本地缓存,Redis 也可以选择更新或清除。
  3. 在下次访问该缓存时,各节点重新从 Redis 或数据库加载最新数据。

优点

  • 消息队列支持异步处理,性能影响较小。
  • 可以实现较精准的缓存同步,不需要主动频繁清理缓存。

缺点

  • 依赖消息队列,增加了系统复杂性。
  • 消息的可靠性和延迟需要保证,否则可能会造成短期的不一致性。
伪代码示例:
// 数据更新时生产缓存失效消息
public void updateData(String key, Object newValue) {database.update(key, newValue);          // 更新数据库redisTemplate.delete(key);               // 清除 Redis 缓存messageQueue.publish("cache_invalidate", key); // 发送缓存失效消息
}// 监听消息队列,清除本地缓存
public void onMessage(String key) {localCache.remove(key);                  // 清除本地缓存
}

方案 3:基于 Redis 的发布/订阅机制

Redis 提供了 <font style="color:#0e0e0e;">pub/sub</font>(发布/订阅)机制,可以用来通知多个节点同步缓存失效。

实现思路:
  1. 数据更新时,清除 Redis 缓存后,通过 Redis 发布订阅系统,发布一条缓存失效的消息。
  2. 所有节点订阅该 Redis 频道,收到消息后,清除本地缓存。
  3. pub/sub是单向的,不能保证消息的可靠性,但对于实时性要求不高的场景是可行的。

优点

  • 简单直接,不需要引入额外的消息中间件。
  • 适合对一致性要求不高的场景。

缺点

  • `pub/sub 不支持持久化消息,消息可能丢失。
  • 如果有大量缓存同步需求,Redis 发布/订阅可能会有性能瓶颈。
伪代码示例:
// 数据更新时发布缓存失效消息
public void updateData(String key, Object newValue) {database.update(key, newValue);          // 更新数据库redisTemplate.delete(key);               // 清除 Redis 缓存redisTemplate.convertAndSend("cache_invalidate", key); // 发布失效消息
}// 订阅 Redis 频道,清除本地缓存
@EventListener
public void onMessage(String key) {localCache.remove(key);                  // 清除本地缓存
}

方案比较

方案优点缺点适用场景
Cache Aside实现简单,数据库是主导更新方更新时需要主动清除缓存,写操作性能较低读多写少的场景,缓存失效可以接受的场景
消息队列缓存同步可以异步处理缓存同步,适合复杂集群环境依赖消息队列,系统复杂性增加,需保证消息可靠性需要强一致性或大规模集群环境中
Redis 发布/订阅不需要引入额外中间件,基于 Redis 直接实现消息不可持久化,可能存在消息丢失,无法保证强一致性对一致性要求不高,变更量不大的场景

总结

对于读多写少的业务场景,使用 Cache Aside 模式结合 Redis 和 JVM 本地缓存是较为普遍且简单的做法。而在分布式集群环境中,消息队列Redis 发布/订阅可以帮助实现缓存的同步和一致性。

你可以根据业务的需求和性能要求,选择合适的缓存同步方案:

  • 强一致性场景:使用消息队列同步本地缓存和 Redis。
  • 弱一致性场景:使用 Redis 的发布/订阅机制通知缓存失效。

池化

池化(Pooling)是一种优化资源管理的设计模式,核心思想是重用资源而不是频繁创建和销毁资源。通过池化,系统可以将常用的对象或资源(如数据库连接、线程、文件句柄等)提前准备好,并在需要时从池中获取,使用后将资源返还池中,从而降低资源创建和销毁的开销。

常见的池化应用包括:数据库连接池线程池、以及在 I/O 模型中的NIOAIOBIO。池化不仅能节省系统开销,还能提升系统的吞吐能力和响应速度。

池化技术通过重复利用资源来减少创建和销毁开销,从而提升系统性能。连接池化在数据库、线程和对象创建方面尤为常见。

数据库连接池

数据库连接的创建和销毁是一个代价较大的操作。在没有连接池的情况下,每次进行数据库操作时,系统都会打开和关闭连接,导致大量的时间浪费。

连接池的工作原理

• 在系统启动时,连接池会创建一定数量的数据库连接,并将其存储在连接池中。

• 当需要执行数据库操作时,应用程序从连接池获取一个连接,使用完后不关闭连接,而是将其放回连接池中以供后续使用。

• 连接池会动态调整连接数量,例如在高负载时增加连接数,低负载时减少连接数。

连接池的优点

性能提升:减少频繁创建和关闭数据库连接的开销。

资源复用:有限数量的连接可以被多个请求重复使用,避免数据库资源枯竭。

自动管理:连接池通常支持连接超时、失效重连等机制。

应用到日常开发

• 常用的数据库连接池框架如 HikariCP、C3P0、DBCP 等都可以直接集成到项目中,提高数据库操作的效率。

NIO、AIO 和 BIO

NIO、AIO 和 BIO 是三种不同的 I/O 模型,用于处理网络通信或文件读写操作。

BIO(Blocking I/O):传统的阻塞式 I/O 模型,每次 I/O 操作都必须等待数据读写完成,线程被阻塞,适合小规模并发场景。

NIO(Non-blocking I/O):非阻塞式 I/O,基于多路复用(Selector)模型。线程可以在数据准备好之前执行其他任务,适合高并发场景。典型应用场景如 Netty。

AIO(Asynchronous I/O):异步 I/O 操作,由操作系统异步处理读写操作,任务完成后通过回调通知。AIO 适合超高并发场景,但相对复杂,且在 JVM 中应用较少。

池化思想在 NIO 和 AIO 中的体现

NIO 和 AIO 基于事件驱动模型,通过复用少量线程来处理大量的 I/O 请求。

• 线程池可以在 NIO 中与 Selector 配合使用,处理多个通道的 I/O 事件,从而提升并发处理能力。

应用到日常开发

• 如果系统需要处理大量 I/O(如 WebSocket 或 HTTP 服务),可以使用 Netty 框架,它基于 NIO,提供高性能的 I/O 处理能力。

• 针对高并发场景,也可以采用 异步编程(如 Java 的 CompletableFuture 或 Reactor 模型)。

线程池

线程的创建和销毁同样是一个消耗资源的操作,尤其在高并发的场景中,频繁创建和销毁线程会导致系统性能下降。

线程池的工作原理

• 系统初始化时,创建一个线程池,并预先分配一定数量的线程。

• 任务提交后,从线程池中分配线程来执行任务,任务完成后线程不会被销毁,而是返回到线程池中供后续任务使用。

• 线程池可以通过配置核心线程数、最大线程数、任务队列等参数来控制任务并发量。

线程池的优点

减少开销:复用已有的线程,减少频繁创建和销毁线程的开销。

提高性能:避免大量并发任务时创建过多线程导致系统资源耗尽。

自动调度:线程池会根据系统的负载情况自动调度线程执行任务,保证资源利用率。

应用到日常开发

• 可以使用 Java 提供的 ExecutorService、ThreadPoolExecutor 等进行线程池的管理。

• 针对 Web 服务,可以使用 Tomcat 或 Jetty 内置的线程池来优化请求的处理。

池化思想可以应用于多个层面,尤其在提高系统的资源利用率和接口响应效率方面,有着显著作用。

1.	数据库连接池:减少数据库连接的频繁创建和销毁,提升数据库操作的性能。
2.	对象池:例如缓存 StringBuilder、ByteBuffer 等可复用对象,避免重复创建对象,降低内存和垃圾回收压力。
3.	线程池:在线程密集型的场景中,通过线程池管理任务,避免线程资源的浪费。
4.	I/O 模型:通过 NIO 或 AIO 的异步 I/O 模型,提升大规模并发网络请求的处理能力。
5.	内存池化:可以通过复用内存块(如 Netty 的 ByteBuf)减少频繁的内存分配和回收,提升内存使用效率。

通过合理使用池化思想,可以显著提高系统的性能,减少接口的响应时间(RT),提升系统的并发处理能力。例如在高并发的 Web 服务中,可以通过线程池、数据库连接池、缓存等多层次的池化机制提升整体性能。

异步

异步处理可以有效地减少接口响应时间,将一些耗时的操作放到后台处理,避免阻塞主线程。

3.1 异步任务

使用场景:当接口需要执行耗时任务(如发送邮件、生成报告)时,可以通过异步任务减少主线程的负载。

实现方式

•在 Spring 中可以通过 @Async 注解轻松实现异步方法调用。

@Async
public void sendEmail(String email) {// 发送邮件的具体逻辑
}

@Async原理以及使用注意

@Async 注解是 Spring 中用来将方法异步执行的一个功能,它背后的原理确实是通过 AOP(Aspect-Oriented Programming,面向切面编程) 实现的。下

@Async 原理

  1. AOP 拦截:Spring 使用 AOP 机制拦截标注了 @Async 的方法调用,并将其转交给一个线程池进行异步处理。这样的方法不会立即执行,而是被放入一个任务队列,交由线程池在后台执行。
  2. 动态代理:Spring 在 @Async 实现中会创建该类的 代理对象,使用 JDK 动态代理CGLIB 动态代理 来管理异步任务的调用。
    • JDK 动态代理:如果目标类实现了接口,Spring 会使用 JDK 动态代理来创建代理对象。
    • CGLIB 动态代理:如果目标类没有实现接口,Spring 会使用 CGLIB 创建一个子类来实现代理。
  3. 线程池管理@Async 方法会在一个线程池中执行,默认使用 Spring 提供的 SimpleAsyncTaskExecutor 或者用户自定义的线程池(通过 @EnableAsync 配置)。每次调用 @Async 方法时,都会由线程池中的一个线程执行,主线程不会等待该方法的完成。
  4. 返回类型@Async 支持 voidFuture<T>CompletableFuture<T> 类型的返回值。对于返回 Future 的方法,Spring 会将异步执行的结果封装在 Future 中供主线程使用。

@Async 和 CGLIB 代理

  • CGLIB(Code Generation Library) 是 Java 中的一个字节码生成库。Spring 会在需要创建类的代理时使用 CGLIB 动态生成代理类。CGLIB 是基于继承的动态代理机制,它会生成目标类的子类并拦截方法调用。当使用 @Async 时,如果目标类没有实现接口,Spring 会使用 CGLIB 创建代理类,重写被 @Async 注解的方法,并将其放入线程池中异步执行。

@Async 的常见问题和注意事项

  1. 相同类内调用不生效
    这是由于 Spring AOP 的工作机制决定的。当在同一个类中调用 @Async 方法时,这个调用是对 原始对象 的直接调用,而不是通过 Spring 代理类,因此不会触发 AOP 拦截,导致 @Async 不生效。解决方案
    • @Async 方法的调用分离到另一个 Spring 管理的 Bean 中,从而确保通过代理类调用。
    • 使用 ApplicationContext 来获取代理对象,并调用异步方法:
@Autowired
private ApplicationContext applicationContext;public void callAsyncMethod() {MyClass myClassProxy = applicationContext.getBean(MyClass.class);myClassProxy.asyncMethod(); // 通过代理对象调用,确保 AOP 生效
}//当然作为事务是一样的
@Overridepublic void deleteNoApprovalExitsData(String mainFormulaId) {List<CalculationApproval> currentDelApprovals = this.lambdaQuery().eq(CalculationApproval::getId, mainFormulaId).isNull(CalculationApproval::getWorkflowInstanceId).list();if (CollectionUtils.isEmpty(currentDelApprovals)) {return;}SpringUtils.getAopProxy(this).deleteInfos(currentDelApprovals);}@Transactionalpublic void deleteInfos(List<CalculationApproval> currentDelApprovals) {List<String> ids = currentDelApprovals.stream().map(CalculationApproval::getId).collect(Collectors.toList());this.removeBatchByIds(ids);QueryWrapper<CalculationApprovalDetail> queryWrapper = new QueryWrapper<>();queryWrapper.in("parentId", ids);detailMapper.delete(queryWrapper);}
  1. 线程池配置
    默认情况下,Spring 的 @Async 注解使用 SimpleAsyncTaskExecutor,这是一个并不是真正的线程池。为了更好的性能,你应该自定义一个线程池来处理异步任务:
@Configuration
@EnableAsync
public class AsyncConfig {@Bean(name = "taskExecutor")public Executor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(5);executor.setMaxPoolSize(10);executor.setQueueCapacity(25);executor.setThreadNamePrefix("Async-");executor.initialize();return executor;}
}
  1. 异常处理
    异步方法发生的异常不会直接抛给调用者,需要通过 Future.get() 方法捕获。如果返回类型是 void,需要配置异常处理机制,例如:
@Async
public void asyncMethod() {try {// 业务逻辑} catch (Exception e) {handleException(e);}
}
  1. 返回值注意事项
    如果异步方法返回 CompletableFutureFuture,可以使用 get() 方法等待任务完成,或通过回调机制处理结果。
  2. 事务支持
    @Async 方法默认不在同一个事务上下文中。如果希望在异步任务中使用事务,需要手动声明事务管理。

使用 @Async 的最佳实践

  • 分离异步方法调用:避免在同一个类中调用 @Async 方法,确保使用代理对象。
  • 配置线程池:在高并发场景中,应合理配置线程池的大小、队列容量等参数,以提升性能和资源利用率。
  • 处理异常:异步方法中的异常不会直接反馈给调用方,需显式捕获或使用 Future 获取。
  • 保证线程安全:异步任务执行时,要注意线程安全问题,避免并发修改共享资源。

总结:@Async 通过 AOP 实现异步方法调用,在实际使用时应注意相同类内调用的无效问题、合理配置线程池,以及通过代理对象调用异步方法。在适当的场景下使用 @Async,可以显著提高应用的并发处理能力和性能。

异步消息队列

使用场景:当需要处理大量异步任务时,可以使用消息队列(如 RabbitMQKafka)来解耦和异步化操作。

实现方式:生产者将任务发送到消息队列,消费者从消息队列中获取任务进行处理,任务的处理结果可以异步反馈给用户。

任务拆解与并行计算

任务拆解(Task Decomposition)是将复杂的大任务分解为可以独立执行的较小任务,然后将这些小任务并行执行。这个过程通常会遵循以下几个步骤:

•	任务分解:将一个复杂的大任务拆解为多个独立的子任务。
•	递归执行:每个子任务可以进一步拆解,直到达到可以直接执行的粒度。
•	任务合并:子任务完成后,将各个子任务的结果合并为最终结果。

在 Java 并发编程中,常用的工具类 ForkJoinPool 就是任务拆解的典型实现。

任务拆解

•	使用场景:当一个操作需要执行多个子任务时,可以将它们拆解并行执行。
•	实现方式:
•	通过多线程、线程池或并行流执行多个子任务。
•	常见使用场景包括批量处理、复杂计算等。
List<Future<Result>> futures = new ArrayList<>();
for (Task task : tasks) {Future<Result> future = executor.submit(() -> task.execute());futures.add(future);
}for (Future<Result> future : futures) {Result result = future.get();  // 获取子任务结果
}

并行流处理

使用场景:对于可以并行处理的数据集(如集合),可以使用 Java 8 的并行流来提高处理速度。

实现方式

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> results = numbers.parallelStream().map(n -> n * n).collect(Collectors.toList());

ForkJoinPool

ForkJoinPool 是 Java 7 引入的一个并行任务执行框架,专为处理 任务拆解 场景设计。它通过递归拆解任务并使用多线程并行处理,能够充分利用多核 CPU 的能力。

工作原理

fork:将任务拆解成更小的子任务。

join:等待子任务完成,并将结果合并。

ForkJoinPool 的关键类是 RecursiveTask 和 RecursiveAction,前者用于有返回值的任务,后者用于没有返回值的任务。

class FibonacciTask extends RecursiveTask<Integer> {private final int n;FibonacciTask(int n) {this.n = n;}@Overrideprotected Integer compute() {if (n <= 1) {return n;}FibonacciTask f1 = new FibonacciTask(n - 1);f1.fork(); // fork一个子任务FibonacciTask f2 = new FibonacciTask(n - 2);return f2.compute() + f1.join(); //等待f1完成并合并结果}
}

在这个例子中,计算斐波那契数列的任务被拆解为两个子任务,通过 fork() 并行执行,并使用 join() 方法等待子任务完成并获取结果。

Work-Stealing 机制

ForkJoinPool 使用 work-stealing 算法来优化线程的利用率。work-stealing 是指当一个线程完成了它的任务后,如果它的任务队列为空,它会从其他仍有未完成任务的线程的任务队列中“偷取”任务来执行。

工作流程

每个线程有一个双端任务队列。线程将分解出来的子任务放在队列的末尾,并从队列的头部取任务执行。

当线程完成了自己队列中的任务后,它会尝试从其他线程的队列中“偷取”任务,从队列的末端取出并执行。

这种机制的优势在于:

• 避免某些线程在繁忙工作时,其他线程处于闲置状态,从而提升线程池的整体效率。

• 尤其在不均匀负载的情况下,能够动态平衡任务,充分利用系统资源。

  1. ForkJoinPool 与传统线程池的比较
特性ForkJoinPool传统线程池(ThreadPoolExecutor)
任务分解支持任务的递归拆解与合并不支持任务拆解,任务独立执行
工作窃取机制支持,通过窃取其他线程的任务提高利用率不支持,每个线程固定执行自己的任务
并行任务支持优化多核 CPU 的使用,适合并行任务适合执行独立任务
适用场景大任务拆解成小任务并行执行适合较为独立的并发任务
任务类型递归任务(RecursiveTask/RecursiveAction)Runnable 或 Callable

ForkJoinPool 的优势

高效处理并行任务:ForkJoinPool 特别适合需要递归拆解的并行任务,能够充分利用多核 CPU 进行计算。

动态任务平衡:通过 work-stealing 机制可以有效避免线程闲置,提高资源利用率。

可扩展性好:在大规模并发计算时,ForkJoinPool 可以通过任务拆解和工作窃取机制将任务合理分配到多个线程,具备更好的扩展性。

日常开发中的应用

复杂计算任务:如需要处理大量复杂计算的场景,例如递归的算法(斐波那契数列、合并排序等),ForkJoinPool 是一个理想的选择。

批量任务处理:对于需要处理大量子任务(如批量数据处理、文件解析等)时,任务拆解模式和 ForkJoinPool 可以有效提升任务处理的并行效率。

异步处理:在日常开发中,也可以使用 ForkJoinPool 来处理一些耗时的任务,使得接口响应更加快速。

缓存数据更新:当缓存数据较大时,可以将更新操作拆分为多个子任务并行执行,减少单一线程的负载。

文件/数据分片处理:如果需要处理大型文件或数据集,可以将文件或数据分成若干块,使用 ForkJoinPool 并行处理每一块,最后合并结果,提升整体处理速度。

使用 ForkJoinPool 的注意事项

任务的粒度:任务拆解不能过度或过少。过度拆解会导致线程管理的开销变大,降低效率;而拆解不足可能导致任务无法充分并行执行。

避免共享资源冲突:并行任务之间共享资源时要特别小心,可能会引发线程安全问题。建议尽量减少共享状态或使用合适的同步机制。

线程池配置:默认 ForkJoinPool 的线程数是基于可用处理器核心数,可以根据实际需要调整线程池的大小,避免过多的任务切换开销。

批量处理

批量处理是指在一次请求中处理大量的数据或操作,而不是逐条处理,从而提高效率和减少资源消耗。在数据库操作、文件处理、消息发送等场景中,批量处理是一种常见的优化方式。

批量处理的核心思想

批量处理的核心思想可以总结为以下几点:

  1. 减少交互次数
    • 无论是数据库操作还是与外部服务的交互,频繁地发送请求都会带来巨大的开销。批量处理的核心就是尽量减少这些请求次数,将多个操作合并成一次请求,从而降低频繁网络传输、IO 和连接管理的开销。
  2. 减少上下文切换
    • 每次处理单条数据时,系统都会发生上下文切换,消耗时间和资源。通过批量处理,可以减少 CPU 和内存的上下文切换,提升性能。
  3. 优化事务管理
    • 对于数据库的批量处理,可以将多条 SQL 语句放在一个事务中执行,减少事务的开启和提交次数,从而减少锁争用的开销,提高整体的吞吐量。
  4. 最大化利用资源
    • 通过将多个操作一起执行,可以充分利用网络带宽、CPU 计算能力和数据库连接等资源,减少资源的空闲时间,提高吞吐量。

批量处理在数据库中的应用

在数据库操作中,批量处理是一个非常有效的优化手段,特别是在处理大量数据时。单条操作与批量操作的区别可以体现在 SQL 执行的次数、网络交互的次数、事务提交的次数等方面。

  1. 单条插入与批量插入对比

优点

单条插入:

每次插入时都会发送一条 INSERT 语句,然后数据库执行一次事务提交。这样会频繁地打开、关闭数据库连接,并且每次插入一条记录都需要网络交互,效率较低。示例:

INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30);
INSERT INTO users (id, name, age) VALUES (2, 'Bob', 25);
INSERT INTO users (id, name, age) VALUES (3, 'Charlie', 35);

批量插入:

将多个插入操作合并成一个 INSERT 语句,可以显著减少网络传输、数据库解析 SQL 语句的次数,以及事务提交的开销。示例:

INSERT INTO users (id, name, age) VALUES 
(1, 'Alice', 30),
(2, 'Bob', 25),
(3, 'Charlie', 35);
- 减少数据库交互次数,降低网络传输的延迟。
- 提高数据库的处理效率,尤其是在事务提交的场景中,一次提交多个操作,减少了事务开销。
  1. JDBC 批量处理:在 Java 中,使用 JDBC 操作数据库时可以利用 addBatch()executeBatch() 方法进行批量处理,避免频繁的数据库连接开销。示例代码:
Connection conn = DriverManager.getConnection(...);
String sql = "INSERT INTO users (id, name, age) VALUES (?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);for (User user : userList) {pstmt.setInt(1, user.getId());pstmt.setString(2, user.getName());pstmt.setInt(3, user.getAge());pstmt.addBatch();  // 将 SQL 添加到批量
}pstmt.executeBatch();  // 批量执行
conn.commit();
pstmt.close();
conn.close();
  1. Hibernate 批量操作:在 ORM 框架 Hibernate 中,批量处理也非常重要,特别是在处理大量数据时。通过设置 JPAHibernate 的批量处理参数,可以提升批量操作的效率。Hibernate 批量插入的设置:
hibernate.jdbc.batch_size=50
hibernate.order_inserts=true
hibernate.order_updates=true

批量插入的代码示例:

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();for (int i = 0; i < 1000; i++) {User user = new User(i, "Name" + i, i % 30);session.save(user);if (i % 50 == 0) { // 每50个插入执行一次flush并清理缓存session.flush();session.clear();}
}tx.commit();
session.close();

批量处理的其他场景

  1. 批量文件处理
    在处理大量文件时,可以将文件分批处理。例如,批量上传文件到服务器、批量读取大文件等。通过将文件切片并行处理,可以提高处理效率。
  2. 消息队列批量处理
    在消息队列系统(如 Kafka、RabbitMQ)中,消息生产者和消费者都可以批量处理消息。例如,消费者可以每次消费多个消息,然后批量提交确认(ack),这样可以减少消息的确认和网络交互次数,提高吞吐量。
  3. 批量更新缓存
    如果需要更新大量缓存数据,可以使用批量操作。例如,Redis 支持 pipeline 模式,将多个命令一起发送到 Redis 服务器,避免每条命令执行时的网络交互,显著提高 Redis 的性能。Redis Pipeline 示例:
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {for (int i = 0; i < 1000; i++) {connection.set(("key" + i).getBytes(), ("value" + i).getBytes());}return null;
});

批量处理的注意事项

  1. 批量大小
    批量处理的大小需要根据具体场景来调整。批量过大可能导致内存溢出或数据库锁争用,而批量过小则不能发挥批量处理的优势。通常可以通过监控系统的资源利用率来调整批量的大小。
  2. 事务处理
    批量操作如果失败,可能会导致部分数据写入成功而部分失败,造成数据不一致。解决办法是将批量操作放入一个事务中,确保数据的一致性。
  3. 错误处理
    在进行批量操作时,需要考虑如何处理批量中的错误。例如,在数据库批量插入时,如果某条数据失败,可以选择回滚整个批次,或者跳过错误的数据,继续处理其他数据。
  4. 延迟问题
    批量处理会导致一定的延迟,因为需要等待足够的请求或数据积累到一定数量时才执行批量操作。需要权衡实时性和吞吐量之间的关系。

总结

  • 批量处理通过合并多个操作来减少网络交互、资源消耗和事务管理的开销。
  • 在数据库操作中,通过批量插入、更新等操作可以显著提升性能。
  • 批量处理还可以应用于消息队列、文件处理和缓存更新等多个场景。
  • 在实际应用中需要根据系统资源、延迟要求等进行调整,确保批量操作能够提升系统的吞吐量而不会引发其他性能问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/438009.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

react-问卷星项目(3)

项目实战 React Hooks 缓存&#xff0c;性能优化&#xff0c;提升时间效率&#xff0c;但是不要为了技术而优化&#xff0c;应该是为了业务而进行优化 内置Hooks保证基础功能&#xff0c;灵活配合实现业务功能&#xff0c;抽离公共部分&#xff0c;自定义Hooks或者第三方&am…

【Linux】包管理器、vim详解及简单配置

&#x1f680;个人主页&#xff1a;小羊 &#x1f680;所属专栏&#xff1a;Linux 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 前言一、包管理器1.1 apt1.2 yum 二、Linux编辑器——vim2.1 vim的三种模式2.2 vim普通模式常用命令2.2.1 移动…

【C++复习】C++11经典语法

文章目录 {}列表初始化1. 初始化内置类型变量2. 初始化数组3. 初始化标准容器4. 初始化自定义类型5. 构造函数初始化列表6. 初始化列表&#xff08;initializer_list&#xff09;7. 返回值初始化8. 静态成员变量和全局变量的就地初始化9. 防止类型收窄总结 decltype右值引用完美…

使用Pytorch构建自定义层并在模型中使用

使用Pytorch构建自定义层并在模型中使用 继承自nn.Module类&#xff0c;自定义名称为NoisyLinear的线性层&#xff0c;并在新模型定义过程中使用该自定义层。完整代码可以在jupyter nbviewer中在线访问。 import torch import torch.nn as nn from torch.utils.data import T…

IP 数据包分包组包

为什么要分包 由于数据链路层MTU的限制,对于较⼤的IP数据包要进⾏分包. 什么是MTU MTU相当于发快递时对包裹尺⼨的限制.这个限制是不同的数据链路对应的物理层,产⽣的限制. • 以太⽹帧中的数据⻓度规定最⼩46字节,最⼤1500字节,ARP数据包的⻓度不够46字节,要在后⾯补填 充…

IDEA在git提交时添加忽略文件

在IntelliJ IDEA中&#xff0c;要忽略target目录下所有文件的Git提交&#xff0c;你可以通过设置.gitignore文件来实现。以下是步骤和示例代码&#xff1a; 1、打开项目根目录下的.gitignore文件。也可以先下载这个.ignore插件。 2、如果不存在&#xff0c;利用上面的插件新建…

Stable Diffusion绘画 | 来训练属于自己的模型:炼丹参数调整--步数设置与计算

要想训练一个优质的模型&#xff0c;一定要认识和了解模型训练中&#xff0c;参数的作用和意义。 整个模型训练的过程&#xff0c;参数并不是一成不变的&#xff0c;也没有固定的模板&#xff0c; 当我们修改了模型训练里面的某个参数&#xff0c;很可能就需要连带其他一系列…

五.运输层

目录 5.1概述 5.2传输层的寻址与端口 熟知端口号 套接字(Socket) 5.3 UDP 特点 UDP报文格式 UDP校验 二进制反码求和 5.4 TCP 特点 可靠传输 停止等待协议 流水线方式 累计应答 流量控制 滑动窗口 拥塞控制 三次握手&#xff0c;四次握手 5.1概述 只有主机…

首屏优化之:SSR(服务端渲染)

引言 今天我们来聊一下首屏优化之SSR-服务端渲染&#xff08;Server-Side Rendering&#xff09;。 可能很多朋友并不了解什么是 SSR&#xff0c;包括在工作中写的网站是什么类型的也不太清楚&#xff0c;是 CSR 还是 SSR&#xff1f;作者在阅读过大量的文章之后&#xff0c;…

MySQL进阶篇 - 存储引擎

01 MySQL体系结构 【1】索引是在存储引擎层实现的&#xff0c;不同的存储引擎&#xff0c;索引的结构是不一样的。 【2】InnoDB引擎是MySQL5.5版本之后默认的存储引擎。 【3】MySQL体系结构分为客户端和服务器&#xff0c;服务器又分为4个层次。 02 存储引擎简介 【1】引擎…

C--编译和链接见解

欢迎各位看官&#xff01;如果您觉得这篇文章对您有帮助的话 欢迎您分享给更多人哦 感谢大家的点赞收藏评论 感谢各位看官的支持&#xff01;&#xff01;&#xff01; 一&#xff1a;翻译环境和运行环境 在ANSIIC的任何一种实现中&#xff0c;存在两个不同的环境1&#xff0c;…

BugReport中的App Processor wakeup字段意义

一、功耗字段意义&#xff1a; App processor wakeup:Netd基于xt_idletimer 待机下监视网络设备的收发工作状态&#xff0c;即当设备发生联网从休眠态变成为唤醒态时&#xff0c;会记录打醒者的uid(uid大于0)和网络类型(wifi或数据类型)、时间戳 实际日志&#xff1a;我们在B…

【Streamlit案例】制作销售数据可视化看板

目录 一、案例效果 二、数据分析 三、加载数据 四、网站前端 &#xff08;一&#xff09;网页标题和图标 &#xff08;二&#xff09;侧边栏和多选框 &#xff08;三&#xff09;主页面信息 ​&#xff08;四&#xff09;主页面图表 &#xff08;五&#xff09;隐藏部…

微信小程序操作蓝牙

主要流程&#xff1a; 1.初始化蓝牙适配器openBluetoothAdapter&#xff0c;如果不成功就onBluetoothAdapterStateChange监听蓝牙适配器状态变化事件 2.startBluetoothDevicesDiscovery开始搜寻附近的蓝牙外围设备 3.onBluetoothDeviceFound监听寻找到新设备的事件&#xff0c;…

用Python+flask+mysql等开发的Excel数据资产落地工具

话不多说 1)Excel文件上传,列表预览 2)选中要导入结构及数据的Excel文件 约束说明: 2.1)Excel文件的第一行约定为表头名称 2.2)系统自动识别字段列名及数据类型,目前不支持合并表头 3)Excel建表导入数据成功后,可在表源列表中预览查看 4)对数据表源可进行透视图设计管理,可对…

可以无限次使用o1-mini和o1-preview模型API接口的方法,并且比便宜便宜7倍以上

打开网站 https://open.xiaojingai.com 然后点击令牌页面&#xff0c;生成令牌&#xff0c;令牌就是api-key

Hive数仓操作(一)

Hive 介绍 Hive 是一个基于 Hadoop 的数据仓库工具&#xff0c;旨在简化大规模数据集的管理和分析。它将结构化数据文件映射为表&#xff0c;并提供类似 SQL 的查询功能。Hive 的数据存储在 Hadoop 分布式文件系统&#xff08;HDFS&#xff09;中&#xff0c;使用 Hive 查询语…

12.梯度下降法的具体解析——举足轻重的模型优化算法

引言 梯度下降法(Gradient Descent)是一种广泛应用于机器学习领域的基本优化算法&#xff0c;它通过迭代地调整模型参数&#xff0c;最小化损失函数以求得到模型最优解。 通过阅读本篇博客&#xff0c;你可以&#xff1a; 1.知晓梯度下降法的具体流程 2.掌握不同梯度下降法…

数据仓库简介(一)

数据仓库概述 1. 什么是数据仓库&#xff1f; 数据仓库&#xff08;Data Warehouse&#xff0c;简称 DW&#xff09;是由 Bill Inmon 于 1990 年提出的一种用于数据分析和挖掘的系统。它的主要目标是通过分析和挖掘数据&#xff0c;为不同层级的决策提供支持&#xff0c;构成…

云服务架构与华为云架构

目录 1.云服务架构是什么&#xff1f; 1.1 云服务模型 1.2 云部署模型 1.3 云服务架构的组件 1.4 云服务架构模式 1.5 关键设计考虑 1.6 优势 1.7 常见的云服务架构实践 2.华为云架构 2.1 华为云服务模型 2.2 华为云部署模型 2.3 华为云服务架构的核心组件 2.4 华…