本文 原文

原始的内容 和 图片 ,请参考 原文 地址

本文 的 原文 地址

尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,尼恩一直在指导大家改造简历、指导面试。指导很多小伙伴拿到了一线互联网企业网易、美团、字节、如阿里、滴滴、极兔、有赞、希音、百度、美团的面试资格,拿到大厂offer。

刚好前面几天,有小伙伴面试,遇到一些异地多活的很重要的面试题:

接口 优化 从哪些方面入手?

如何定位接口性能瓶颈?

接口响应时间过长,可能有哪些原因?如何优化?

高并发场景下接口超时,如何排查?

前段时间小伙伴面试美团,遇到这个问题。 小伙伴没有准备好, 面试挂了。

这里, 再把这个异地多活的方案做一个梳理, 也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

特别提示:尼恩的3高架构宇宙,持续升级。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书

实际工作 的 业务场景

工作中,常常会遇到 nginx 抛出一个504超时问题。

原因是因为接口耗时过长,超过nginx配置的10秒。

这个时候 ,需要 对 后端java 链路 搞了一次接口性能优化。

大概优化的维度 , 可以 从以下几个维度梳理接口优化的一些通用方案:

Mermaid

某一次 真实的 工业生产链路的优化,按照上面的维度进行优化, 从11.3s降为170ms, 性能提升10倍。

一、数据处理优化

1.1、批量思想

1.1.1 批量操作数据库

详细内容参考:网易一面:25Wqps高吞吐写Mysql,100W数据4秒写完,如何实现?

数据库批量操作通过单次交互处理多条数据,大幅提升数据库操作效率:

Mermaid

性能对比数据

操作方式 10条数据 100条数据 1000条数据
单条插入 50ms 500ms 5000ms
批量插入 15ms 20ms 50ms
提升倍数 3.3x 25x 100x

数据库批量处理机制

Mermaid

JDBC批量操作


// 使用JDBC批量插入
public void batchInsert(List<User> users) throws SQLException {
    String sql = "INSERT INTO users (name, email, age) VALUES (?, ?, ?)";
    
    try (Connection conn = dataSource.getConnection();
         PreparedStatement pstmt = conn.prepareStatement(sql)) {
        
        // 关闭自动提交,开启事务
        conn.setAutoCommit(false);
        
        for (User user : users) {
            pstmt.setString(1, user.getName());
            pstmt.setString(2, user.getEmail());
            pstmt.setInt(3, user.getAge());
            pstmt.addBatch(); // 添加到批处理
            
            // 每100条执行一次
            if (users.indexOf(user) % 100 == 0) {
                pstmt.executeBatch();
            }
        }
        
        // 执行剩余的批处理
        pstmt.executeBatch();
        
        // 提交事务
        conn.commit();
    } catch (SQLException e) {
        // 异常处理与回滚
        conn.rollback();
        throw e;
    }
}

注意:必须需要设置 rewriteBatchedStatements=true ,它通过重写批量 SQL 语句,将多个独立的操作合并为更高效的单一操作,从而显著提升数据库批量操作的性能:

原始批量操作:


INSERT INTO users (name, age) VALUES ('Alice', 25);
INSERT INTO users (name, age) VALUES ('Bob', 30);
INSERT INTO users (name, age) VALUES ('Charlie', 28);

启用 rewriteBatchedStatements 后:


INSERT INTO users (name, age) VALUES 
('Alice', 25),
('Bob', 30),
('Charlie', 28);

1.2.1 队列缓冲 + 批量处理模式

Redis Pipeline机制的核心就是队列缓冲 + 批量处理的经典方案

1. 队列缓冲:客户端本地缓存多条命令

2. 批量处理:网络传输与服务器执行的批量操作

这种设计完美解决了Redis性能瓶颈中的网络延迟问题,在保持Redis单线程简洁架构的同时,实现了吞吐量的数量级提升,高并发场景,可以参考这个方案设计

Mermaid

1.2、异步思想

1.2.1 什么是异步

异步思想是一种解决长耗时问题的方法,它通过将耗时的操作放在后台进行,不阻塞主线程或其他任务的执行,从而提高系统的响应性能和并发处理能力。

什么是异步架构?

异步调用与同步调用相反, 调用方不需要等待被调用方法中的逻辑执行完成, 调用方提交请求后就可以返回执行其他的逻辑。 在被调用方法执行完毕后, 调用方通过回调、事件通知、 定时查询等方式获得结果。

比如当我们在12306网站订票时, 页面会显示系统正在排队, 这个提示就代表着系统在异步处理我们的订票请求。

在12306系统中查询余票、 下单和更改余票状态都是比较耗时的操作, 可能涉及多个内部系统的互相调用。

如果是同步架构, 那么12306网站在高峰时期会出现严重拥塞。

而采用异步架构, 上层处理时会把请求写入请求队列, 同时快速响应用户, 告诉用户正在排队处理, 然后释放出资源来处理更多的请求。 订票请求处理完成之后, 再通知用户订票成功或者失败。

image-20250602104434338

采用异步架构后, 请求移到异步处理程序中, Web服务的压力小了, 资源占用得少了, 自然就能接收更多的用户订票请求, 系统承受高并发的能力也就提升了。

1.2.2 异步在数据库、MQ的应用

以 Redis 为例,它的 bgsave 和 bgrewriteof 命令便是异步操作的典型体现。

这两个命令分别用于异步保存 RDB 文件和重写 AOF 文件,执行后会立即返回成功,随后主线程会 fork 出一个子线程,专门负责将内存中的数据生成快照并保存到磁盘,而主线程则能继续处理客户端的各类命令,避免了因 IO 操作造成的阻塞。

在删除 key 时,Redis 提供了 del 和 unlink 两种方式。

  • del 是同步删除,会直接释放内存,可一旦遇到大 key,就可能导致 Redis 卡顿;

  • unlink 则采用异步删除模式,执行后仅对 key 做不可达标识,内存回收工作交由异步线程完成,有效避免了对主线程的阻塞。

MySQL 的主从同步机制中,异步复制是常见方式之一。

主库执行完事务后立即向客户端返回结果,无需等待从库同步数据,这种方式能保证客户端的写入性能,但存在数据丢失的风险,适用于数据一致性要求不高的场景。

与之相对的是同步复制,主库需等待所有从库都执行完事务后才返回结果,虽能保证数据一致性,但写入性能较差,适合对数据一致性要求极高的场景。此外还有半同步复制,主库执行完事务后,只需至少一个从库接收并执行该事务便返回结果,在性能与一致性之间取得了一定平衡。

在消息队列 Kafka 中,生产者和消费者也可采用异步方式进行消息的发送与消费。

不过异步方式可能存在丢消息的问题,为应对这一情况,异步发送消息时可采用带有回调函数的方式,当发送失败时,通过回调函数能够及时感知,便于后续进行消息补偿,从而在一定程度上保障消息的可靠性。

1.2.2 异步实现方案

1)方案1:线程池模式

比如订单下单"控制器收到请求后,将下单操作放入到独立的线程池中后,立即返回给前端,而线程池会异步执行下单操作"。

image-20250603215045541

使用线程池模式,需要注意如下几点:

1、线程数不宜过高,避免占用过多的数据库连接 ;

2、需要考虑评估线程池队列的大小,以免出现内存溢出的问题。

2)方案2:本地内存 + 定时任务

开源中国统计浏览数的方案非常经典。

用户访问过一次文章、新闻、代码详情页面,访问次数字段加 1 , 在 开源中国 oschina 网站后台,这个操作是异步的,访问的时候只是将数据在内存中保存,每隔固定时间将这些数据写入数据库。

image-20250604080541502

示例代码如下:


import java.util.concurrent.ConcurrentHashMap;
import java.util.Timer;
import java.util.TimerTask;
import org.apache.log4j.Log;
import org.apache.log4j.LogFactory;

public class VisitStatService extends TimerTask {

    private final static Log log = LogFactory.getLog(VisitStatService.class);
    private static boolean start = false;
    private static VisitStatService daemon;
    private static Timer clickTimer;
    private final static long INTERVAL = 60 * 1000;

    /**
     * 支持统计的对象类型
     */
    private final static byte[] TYPES = new byte[]{
            0x01, 0x02, 0x03, 0x04, 0x05
    };

    // 内存队列
    private final static ConcurrentHashMap<Byte, ConcurrentHashMap<Long, Integer>> queues =
            new ConcurrentHashMap<Byte, ConcurrentHashMap<Long, Integer>>() {{
                for (byte type : TYPES) {
                    put(type, new ConcurrentHashMap<Long, Integer>());
                }
            }};

    /**
     * 记录访问统计
     * @param type     统计类型
     * @param obj_id   统计对象ID
     */
    public static void record(byte type, long obj_id) {
        ConcurrentHashMap<Long, Integer> queue = queues.get(type);
        if (queue != null) {
            Integer nCount = queue.get(obj_id);
            nCount = (nCount == null) ? 1 : nCount + 1;
            queue.put(obj_id, nCount.intValue());
            System.out.printf("record (type=%d,id=%d,count=%d)\n", type, obj_id, nCount);
        }
    }

    /**
     * 启动统计数据写入定时器
     */
    public static void start() {
        if (!start) {
            daemon = new VisitStatService();
            clickTimer = new Timer("VisitStatService", true);
            clickTimer.schedule(daemon, INTERVAL, INTERVAL); // 运行间隔1分钟
            start = true;
        }
        log.info("VisitStatService started.");
    }

    /**
     * 释放服务资源
     */
    public static void destroy() {
        if (start) {
            clickTimer.cancel();
            start = false;
        }
        log.info("VisitStatService stopped.");
    }

    @Override
    public void run() {
        for (byte type : TYPES) {
            ConcurrentHashMap<Long, Integer> queue = queues.remove(type);
            queues.put(type, new ConcurrentHashMap<Long, Integer>());
            try {
                _flush(type, queue);
            } catch (Throwable t) {
                log.fatal("Failed to flush click stat data.", t);
                // 此处可添加异常报警逻辑
            } finally {
                // 此处可添加数据库连接关闭逻辑
            }
        }
    }

    @Override
    public boolean cancel() {
        boolean b = super.cancel();
        // 应用停止时写回剩余数据,避免丢失
        this.run();
        return b;
    }

    /**
     * 将统计数据刷入数据库
     * @param type   统计类型
     * @param queue  待刷写的统计数据队列
     */
    private void _flush(byte type, ConcurrentHashMap<Long, Integer> queue) {
        if (queue.size() == 0) {
            return;
        }
        switch (type) {
            // 此处实现具体的数据库写入逻辑(示例:根据type区分表或业务)
            // case 0x01: 处理类型1的数据写入...
            // case 0x02: 处理类型2的数据写入...
            default:
                break;
        }
        System.out.printf("Flush to database: type=%d\n", type);
    }
}

可以借鉴开源中国的方案 :

(1) 控制器接收请求后,观看进度信息存储到本地内存 LinkedBlockingQueue 对象里;

(2) 异步线程每隔1分钟从队列里获取数据 ,组装成 List 对象,最后调用 Jdbc batchUpdate 方法批量写入数据库;

(3) 批量写入主要是为了提升系统的整体吞吐量,每次批量写入的 List 大小也不宜过大 。

这种方案优点是:不改动原有业务架构,简单易用,性能也高。该方案同样需要考虑内存溢出的风险。

3)方案3:MQ模式

MQ 模式 ,消息队列最核心的功能是异步解耦,MQ 模式架构清晰,易于扩展。

image-20250604080742463

核心流程如下:

(1) 控制器接收写请求;

(2) 写服务发送消息到 MQ ,将写操作成功信息返回给前端 ;

(3) 消费者服务从 MQ 中获取消息 ,批量操作数据库 。

这种方案优点是:

  • MQ 本身支持高可用和异步,发送消息效率高 , 也支持批量消费;
  • 消息在 MQ 服务端会持久化,可靠性要比保存在本地内存高;

不过 MQ 模式需要引入新的组件,增加额外的复杂度。

4)方案4:Agent 服务 + MQ 模式

互联网大厂还有一种常见的异步的方案:Agent 服务 + MQ 模式。

image-20250603220823416

服务器上部署 Agent 服务(独立的进程) , 高并发写服务接收写请求后,将请求按照固定的格式(比如 JSON )写入到本次磁盘中,然后给前端返回成功信息。

Agent 服务会监听文件变动,将文件内容发送到消息队列 , 消费者服务获取观看行为记录,将其存储到 MySQL 数据库中。

还有一种演进,假设我们不想在应用中依赖消息队列,不生成本地文件,可以采用如下的方式:

image-20250603220711814

这种方案最大的优点是:架构分层清晰,业务服务不需要引入 MQ 组件。

一般性能监控平台,或者日志分析平台都使用这种模式。

4)异步架构总结

方案对比与选型建议

方案 优点 缺点 适用场景
线程池模式 实现简单,低延迟 线程资源有限,风险较高 快速订单处理(如秒杀)
本地内存 + 定时任务 无需额外组件,吞吐量高 内存溢出风险 订单日志批量处理
MQ模式 高可用、解耦、批量处理 引入MQ复杂度 高并发订单场景
Agent + MQ模式 完全解耦,架构清晰 实现复杂,维护成本高 订单分析、监控平台

学习需要一层一层递进的思考。

第一层:什么场景下需要异步

  • 大量写操作占用了过多的资源,影响了系统的正常运行;
  • 写操作异步后,不影响主流程,允许适当延迟;

第二层:异步的外功心法

四种异步方式:

  • 线程池模式

  • 本地内存 + 定时任务

  • MQ 模式

  • Agent 服务 + MQ 模式

它们的共同特点是:将写操作命令存储在一个池子后,立刻响应给前端,减少写动作的耗时。任务服务异步从池子里获取任务后执行。

第三层:异步的内功心法

异步本质是更细粒度的使用系统资源的一种方式

在高并发写场景里,数据库的资源是固定的,但写操作占据大量数据库资源,导致整个系统的阻塞,但写操作并不是最核心的业务流程,它不应该占用那么多的系统资源。

我们使用异步的解决方案时,无论是使用线程池,还是本地内存 + 定时任务 ,亦或是 MQ ,对数据库资源的使用都需要在合理的范围内,只有这样系统才能顺畅的运行。

1.2.4 异步实战案例

在做服务性能优化时,可以将如数据上报、流水日志等做异步处理,以降低接口时延。用户上传图片后的审核,音视频的合成等等。

下面音频处理案例中,以文本配音(TTS)为例,【合成音频】和【添加音效】这两个子过程耗耗时比较长

image-20250801173036955

我们可以把耗时长的部分封装到一个异步任务中,并生成一个任务 ID,后续可以查询处理进度和结果。

音频生成部分改为异步任务是因为该子过程是文本配音的关键路径(主流程、耗时长),对非关键路径如【数据埋点】直接改为协程处理即可:

image-20250801173102241

1.3、回调思想

如果你调用一个系统B的接口,但是它处理业务逻辑,耗时需要10s甚至更多。

然后你是一直阻塞等待,直到系统B的下游接口返回,再继续你的下一步操作吗?这样显然不合理

我们参考IO多路复用模型

  • 即我们不用阻塞等待系统B的接口,而是先去做别的操作。

  • 等系统B的接口处理完,通过事件回调通知,我们接口收到通知再进行对应的业务操作即可。

事件回调思想是一种异步非阻塞的编程范式,其核心是通过事件驱动机制替代传统的阻塞等待模式:

Mermaid

核心原则:

1. 非阻塞执行:主线程永不等待

2. 事件驱动:基于事件触发执行

3. 回调机制:异步结果处理

4. 资源高效:最大化CPU利用率

事件回调在实际开发中的应用场景,比如 消息队列消费者


// RabbitMQ消费者(Spring AMQP)
@RabbitListener(queues = "order.queue")
public void processOrder(Order order, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
    // 异步处理订单
    CompletableFuture.runAsync(() -> {
        try {
            orderService.process(order);
            // 成功回调:确认消息
            channel.basicAck(tag, false);
        } catch (Exception e) {
            // 错误回调:拒绝消息
            channel.basicNack(tag, false, true);
        }
    });
}

事件回调与阻塞等待的性能对比 (处理1000并发请求)

指标 阻塞等待模式 事件回调模式 提升倍数
线程数 1000 16 62.5x
内存占用 2GB 200MB 10x
请求处理时间 3000ms 150ms 20x
CPU利用率 30% 85% 2.8x
最大并发能力 1000 10000+ 10x+

Java CompletableFuture示例


CompletableFuture.supplyAsync(() -> {
        // 异步任务1:获取用户数据
        return userService.getUser(userId);
    })
    .thenApplyAsync(user -> {
        // 回调:处理用户数据
        return orderService.getOrders(user.getId());
    })
    .thenAcceptAsync(orders -> {
        // 回调:处理订单数据
        orders.forEach(order -> processOrder(order));
    })
    .exceptionally(ex -> {
        // 统一错误处理回调
        System.err.println("处理失败: " + ex.getMessage());
        return null;
    });

事件回调的挑战与解决方案

挑战 解决方案 示例
回调地狱 Promise链/Async-Await CompletableFuture.thenApply()
错误处理困难 统一异常回调 exceptionally()
上下文丢失 上下文传递机制 MDC/ThreadLocal
调试困难 异步堆栈跟踪 AsyncProfiler
资源泄漏 引用计数/超时 Channel.closeFuture()

总结

事件回调思想通过拒绝阻塞等待的设计哲学,从根本上解决了高并发系统中的性能瓶颈问题:

(1) 事件驱动架构:基于事件触发而非轮询等待

(2) 非阻塞IO:最大化单线程处理能力

(3) 回调机制:异步结果处理

(4) 资源高效:减少线程/内存开销

在实际应用中:

  • 使用Netty/Vert.x等框架构建高性能网络服务
  • 采用CompletableFuture/Reactive Streams管理异步流程
  • 避免在回调中执行阻塞操作
  • 合理使用线程池处理CPU密集型任务

当正确实施时,事件回调架构可支撑10万+并发连接,同时保持毫秒级响应时间,成为现代高并发系统的基石技术。

1.4、并行思想

1.4.1 什么是并行思想

并行思想是通过任务分解并发执行来提升系统处理效率的优化策略,其核心公式为:


总处理时间 = max(子任务处理时间) + 协调开销

Mermaid

并行思想的四大支柱:

1. 任务分解:将大任务拆分为独立子任务
2. 资源分配:合理分配计算资源
3. 并发执行:同时处理多个子任务
4. 结果聚合:合并子任务结果

一般并行度计算公式:


最佳并行度 = min(任务数量, CPU核心数 × 2, I/O阻塞系数 × 100)

经典的map reduce并行处理模型:

Mermaid

常见并行框架比较

框架 适用场景 特点 性能
Java Stream API 内存数据并行 声明式编程 ★★★★
ForkJoinPool 递归任务分解 工作窃取算法 ★★★★★
Akka 分布式并行 Actor模型 ★★★★☆
MapReduce 大数据处理 容错性强 ★★★★
CUDA GPU并行计算 超高性能 ★★★★★

1.4.2 并行实战案例

视频剪辑工具在生成最终视频时,需要处理多个字幕轨道:先将每个轨道转换为SRT格式的字幕文件,再上传到对象存储(如COS)。

若采用传统的串行方式,当轨道数量较多(例如10个)时,总耗时会随轨道数量线性增加,严重影响用户体验。

方案一:串行处理

所有字幕轨道按顺序依次处理:先转换第一个轨道为SRT文件并上传,完成后再处理第二个,以此类推。

Mermaid

方案2:并行处理流程

同时启动多个处理单元,每个轨道的转换和上传操作独立并行执行,无需等待其他轨道完成。

Mermaid

使用Java的CompletableFuture实现并行处理,通过线程池管理子任务,既保证并发效率,又避免资源耗尽。


import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SubtitleProcessor {

    // 线程池:根据CPU核心数动态调整,避免线程过多导致开销
    private static final ExecutorService executor = Executors.newCachedThreadPool();

    /**
     * 并行处理所有字幕轨道
     * @param tracks 字幕轨道列表
     * @return 所有任务完成后的CompletableFuture
     */
    public CompletableFuture<Void> processTracksAsync(List<Track> tracks) {
        // 为每个轨道创建并行任务
        List<CompletableFuture<Void>> futures = tracks.stream()
                .map(track -> CompletableFuture.runAsync(() -> processSingleTrack(track), executor))
                .toList();

        // 等待所有任务完成
        return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
    }

    /**
     * 处理单个轨道:转换为SRT + 上传COS
     * @param track 单个字幕轨道
     */
    private void processSingleTrack(Track track) {
        try {
            // 1. 生成字幕文件名
            String filename = getSrtFilename(track);

            // 2. 转换轨道为SRT格式
            String srtContent = convertTrackToSrt(track);

            // 3. 上传到COS
            new SrtCosHelper().upload(filename, srtContent);
        } catch (Exception e) {
            // 处理异常(如重试、记录日志等)
            throw new RuntimeException("处理轨道失败: " + track.getId(), e);
        }
    }

    // 辅助方法:生成SRT文件名
    private String getSrtFilename(Track track) {
        return "subtitle_" + track.getId() + ".srt";
    }

    // 辅助方法:轨道转SRT
    private String convertTrackToSrt(Track track) {
        // 实际转换逻辑(省略)
        return "SRT内容:" + track.getContent();
    }
}

通过模拟每个轨道处理耗时(100ms),对比串行与并行的效率:

处理方式 10个轨道总耗时 性能提升
串行 约1000ms 1倍
并行 约100ms 10倍

结论:并行处理的耗时与单个轨道处理时间接近,效率随轨道数量线性提升(在资源充足的前提下)。

并行处理的核心价值在于“打破串行依赖,充分利用资源”。在实际开发中,不必依赖下游服务提供批量接口,通过拆分任务并并行执行,同样能实现高效的批量处理(如上文的字幕轨道场景)。

但需注意:并行并非“越多越好”,需根据系统资源(如CPU核心数、网络带宽)合理控制并发量,避免线程过多导致的上下文切换开销或资源竞争问题。选择合适的工具(如Java的CompletableFuture、线程池)和通信方式,才能让并行真正为系统效率服务。

二、资源利用优化

1、池化思想

如果你每次需要用到线程,都去创建,就会有增加一定的耗时,而线程池可以重复利用线程,避免不必要的耗时。

池化技术不仅仅指线程池,很多场景都有池化思想的体现,它的本质就是预分配与循环使用

比如TCP三次握手,大家都很熟悉吧,它为了减少性能损耗,引入了Keep-Alive长连接,避免频繁的创建和销毁连接。

当然,类似的例子还有很多,如数据库连接池、HttpClient连接池。

池化思想是一种预分配与循环使用的优化策略,其核心是通过提前创建资源实例并在应用生命周期内重复使用,避免频繁创建和销毁资源带来的开销:

Mermaid

池化思想的三大核心优势

1. 资源预分配:系统初始化时创建资源池
2. 生命周期管理:统一创建、维护、回收资源
3. 高效复用:避免重复初始化开销

池化技术在实际开发中的应用场景:

(1) 线程池(ExecutorService)


// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(10);

// 提交任务
executor.submit(() -> {
    // 业务逻辑
});

// 关闭线程池
executor.shutdown();

(2) HTTP连接池(Apache HttpClient)


// 创建连接池管理器
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(100); // 最大连接数
cm.setDefaultMaxPerRoute(20); // 每个路由最大连接数

// 创建HttpClient
CloseableHttpClient httpClient = HttpClients.custom()
        .setConnectionManager(cm)
        .build();

// 使用示例
HttpGet request = new HttpGet("https://api.example.com/data");
try (CloseableHttpResponse response = httpClient.execute(request)) {
    // 处理响应
}

(3) 对象池(Apache Commons Pool)


GenericObjectPool<ExpensiveObject> pool = new GenericObjectPool<>(
    new BasePooledObjectFactory<ExpensiveObject>() {
        @Override
        public ExpensiveObject create() {
            return new ExpensiveObject(); // 创建耗时对象
        }
    }
);

// 配置池参数
pool.setMaxTotal(20); // 最大对象数
pool.setMinIdle(5);   // 最小空闲对象数

// 使用对象
ExpensiveObject obj = pool.borrowObject();
try {
    obj.doSomething();
} finally {
    pool.returnObject(obj); // 归还对象
}

性能对比数据

性能指标 直接创建/销毁 池化技术 提升倍数
资源创建耗时 100-200ms <1ms 100-200x
内存分配频率 极低 10-50x
垃圾回收压力 频繁触发GC 轻微 5-10x
系统稳定性 高并发下易崩溃 稳定可控 -
资源利用率 低(平均20%) 高(70%+) 3.5x

池化参数优化矩阵

参数名 推荐值 作用描述 影响维度
最小空闲数 CPU核数*2 保持预热资源 启动性能
最大连接数 (DB连接数限制)/应用实例数 防止资源耗尽 系统稳定性
获取超时时间 100-1000ms 防止线程阻塞 响应速度
空闲检测间隔 5-30分钟 平衡健康检查开销与资源有效性 资源利用率
最大空闲时间 30-60分钟 避免长期占用资源 资源回收

2、空间换时间思想

空间换时间思想是一种常见的优化策略,它通过增加额外的空间(内存、缓存等)来减少程序的执行时间。

这种思想的基本原理是通过预先计算、缓存或索引等方式,将计算或数据存储在更快的存储介质中,以减少访问时间和计算时间。

这样可以避免重复计算或频繁的磁盘访问,从而提高程序的执行效率。

image-20250801173223411

在缓存层可以采用多级缓存方案,来提升接口性能

1)多级缓存架构

根据分布式缓存、 本地缓存的特点, 对缓存进行分级。 在整个系统架构的不同系统层级进行数据缓存, 以提升访问的高并发吞吐量。
从Java程序在访问缓存时的距离远近的角度对缓存进行分级, 可以将缓存划分为:

  • 一级缓存: JVM本地缓存, 如Guava Cache、 Caffeine等。
  • 二级缓存: 经典的分布式缓存, 如Redis Cluster集群。
  • 三级缓存: 在接入层的本地缓存, 如Nginx的shared_dict(共享字典) 。

不同热度的数据可以按照不同的层级进行存放:

  • 对于访问热度最高的数据, 可以在接入层Nginx的shared_dict(共享字典) 缓存, 此为三级缓存(规模在1GB以内) , 比如秒杀系统中的优惠券详情、 秒杀商品详情信息, 这些信息访问得非常频繁。
  • 对于访问热度没有那么高但也访问频繁的数据, 可以在JVM进程内缓存(如Caffeine) ,这部分的数据规模也不能太大, 大概在1GB以内, 作为一级缓存。、
  • 对于访问热度比较一般的数据, 存放到Redis Cluster集群, 作为二级缓存, 这部分的数据规模最大, 可以以10GB为节点单位进行横向扩展。

2)Java应用本地缓存

Java应用本地缓存简单一点的可以是Map, 复杂一点的可以使是Guava、 Caffeine这样的第三方组件。 Java应用本地缓存类似于寄生虫, 占用的是JVM进程的内存空间。

Guava Cache是Google开源的一款本地缓存工具库, 它的设计灵感来源于ConcurrentHashMap,使用多个Segments方式的细粒度锁, 在保证线程安全的同时, 支持高并发场景需求, 同时支持多种类型的缓存清理策略, 包括基于容量的清理、 基于时间的清理、 基于引用的清理等。

Caffeine是Spring 5默认支持的缓存, Spring抛弃Guava转向了Caffeine, 可见Spring对它的看重。Caffeine因为使用Window TinyLfu 回收策略而提供了一个近乎最佳的命中率。

Caffeine的底层数据存储采用ConcurrentHashMap。 因为Caffeine面向JDK8, 而在JDK8中ConcurrentHashMap增加了红黑树, 所以在Hash冲突严重时Caffeine也能有良好的读性能。

如果要在Java应用中使用本地缓存, 建议使用Caffeine组件

3)Nginx 接入层本地缓存

Nginx有三类本地缓存:

  • proxy_cache(代理缓存)

  • shared_dict(共享字典)

  • lua-resty-lrucache缓存

3、压缩传输内容

压缩传输内容,传输报文变得更小,因此传输会更快啦。10M带宽,传输10k的报文,一般比传输1M的会快呀。

打个比喻,一匹千里马,它驮着100斤的货跑得快,还是驮着10斤的货物跑得快呢?

再举个视频网站的例子:如果不对视频做任何压缩编码,因为带宽又是有限的。巨大的数据量在网络传输的耗时会比编码压缩后,慢好多倍

在数据量稍大些的场景中,传输时间往往占耗时的大头。压缩算法在数据存储、数据传输和用户体验等方面都具有重要的作用,可以提高效率、节省资源和改善用户体验。

几种压缩算法对比:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

时间耗时的顺序为 Pzstd < ISA-L < Pigz < LZ 4 < Zstd < Brotli < Gzip (排名越靠前越好),其中压缩和解压缩的时间在整体的耗时上占比较大,因此备选策略为 Pzstd、ISA-L、Pigz。

三、数据库访问优化

3.1、索引优化

添加索引是成本最低的优化方式,而且通常能带来显著的性能提升。

索引优化可以从这几个维度思考:

  • SQL是否添加了必要的索引?
  • 已添加的索引是否真正生效?
  • 索引的设计是否合理?

Mermaid

3.1.1 SQL未添加索引

开发中很容易疏忽,忘记给SQL添加索引。因此,写完SQL后建议顺手用explain查看执行计划,判断是否需要添加索引。


-- 查看SQL执行计划,判断是否使用索引
explain select * from user_info where user_id like '%123';

也可以通过show create table命令查看整张表的索引情况:


-- 查看表的所有索引信息
show create table user_info;

如果发现缺少必要的索引,可以通过alter table命令添加:


-- 为name字段添加索引
alter table user_info add index idx_name (name);

通常来说,SQL中where条件里的字段,以及order bygroup by后面的字段,都需要考虑添加索引。

在Java开发中,我们可以通过工具类封装索引检查逻辑,在开发环境自动提示缺少的索引:

3.1.2 索引不生效

有时候明明加了索引,但实际执行时索引却不生效。常见的索引失效场景主要有以下几种:

(1) 索引列使用函数或表达式:如where SUBSTR(name, 1, 3) = 'abc'

(2) 索引列进行隐式类型转换:如字符串字段用数字查询where phone = 13800138000

(3) 使用!=<>操作符:索引对这类操作支持较差

(4) 使用not innot exists:可能导致全表扫描

(5) like以通配符开头:如where name like '%abc'

(6) or连接的条件中存在未建索引的列:如where id=1 or age=20(age无索引)

(7) 联合索引不满足最左匹配原则:如联合索引(a,b,c),查询条件只用到b和c

(8) order by字段与索引顺序不一致:可能导致文件排序

(9) 查询数据量过大:当查询结果超过表数据量30%时,数据库可能选择全表扫描

(10) 使用is nullis not null:某些情况下会导致索引失效

image-20250801111953052

3.1.3 索引设计不合理

索引不是越多越好,需要合理设计。以下是一些实践经验:

  • 删除冗余和重复索引:如已有联合索引(a,b),单独的a索引就是冗余的
  • 控制索引数量:一张表的索引建议不超过5个,过多会影响插入、更新性能
  • 避免在重复值多的字段上建索引:如性别(只有男/女),索引过滤效果差
  • 适当使用覆盖索引:包含查询所需全部字段的索引,避免回表查询
  • 谨慎使用force index:如果需要强制指定索引,说明索引设计可能存在问题

3.1.3 SQL优化的基本原则

除了索引优化,SQL本身还有很多优化空间,主要原则包括:

(1) 避免select *:只查询需要的字段,减少数据传输和内存占用

(2) 小表驱动大表:join时用小表作为驱动表,减少外层循环次数

(3) 合理使用分页:避免一次性查询大量数据,使用limit分页

(4) 拆分复杂SQL:将一个复杂SQL拆分为多个简单SQL,降低执行复杂度

(5) 避免or,使用union替代:or可能导致索引失效,union更易利用索引

(6) inexists合理选择:小表用in,大表用exists

(7) 控制事务范围:尽量缩小事务范围,减少锁竞争

(8) 避免在循环中执行SQL:将循环内的SQL改为批量操作

(9) 合理使用临时表:复杂查询中使用临时表存储中间结果

(10) 定期分析表:通过analyze table更新表统计信息,帮助优化器生成更好的执行计划

image-20250801173346281

更详细的内容,详细内容参考:

3.2、深度分页性能优化

深度分页为什么会影响性能:

  • 浅层次的维度: Limit 会导致 Mysql 扫描过多的数据记录或者索引记录,而且大部分扫描到的记录都是无用的。
  • 深层次的维度: 当 Limit 抛弃的数据量太大 的时候, server 层的 优化器认为 索引扫描 的效率 甚至不如全表扫描 + 文件排序filesort,于是 将 索引扫描优化为 全表扫描。 而一个大表的 全表扫描 , 本身就是很慢的。

优化方案:

  • 使用索引覆盖扫描

  • 使用子查询

  • 标签记录法

  • 使用分区表

具体内容参考京东面试:mysql深度分页 严重影响性能?根本原因是什么?如何优化?

3.3、NoSQL海量数据处理

在数据量从GB级向TB、PB级跨越时,传统关系型数据库在横向扩展能力非结构化数据处理高并发写入等场景中逐渐显现瓶颈。NoSQL(Not Only SQL)数据库凭借灵活的数据模型、弹性扩展能力和针对特定场景的优化设计,成为海量数据处理的核心选择。

之前看过几个慢SQL,都是跟深分页问题有关的。发现用来标签记录法和延迟关联法,效果不是很明显,原因是要统计和模糊搜索,并且统计的数据是真的大。最后跟组长对齐方案,就把数据同步到Elasticsearch,然后这些模糊搜索需求,都走Elasticsearch去查询了。

我想表达的就是,如果数据量过大,一定要用关系型数据库存储的话,就可以分库分表。但是有时候,我们也可以使用NoSQL,如Elasticsearch、Hbase等。

3.3.1 为什么海量数据处理需要NoSQL?

关系型数据库(如MySQL)的设计初衷是保证强一致性和结构化查询,但在海量数据场景下面临以下挑战:

  • 扩展瓶颈:基于主从架构的纵向扩展能力有限,横向扩展需复杂的分库分表方案;
  • 数据模型僵化:固定表结构难以应对半结构化(如JSON日志)、非结构化数据(如图片、文本);
  • 性能瓶颈:高并发写入时,事务ACID特性带来的锁竞争会显著降低处理效率;
  • 存储成本:对海量低价值数据(如用户行为日志),关系型数据库的存储和维护成本过高。

NoSQL数据库通过弱化部分一致性(如采用最终一致性)、灵活的数据模型和分布式架构,针对性解决这些问题:

Mermaid

3.3.2 NoSQL的四大类型与适用场景

NoSQL并非单一技术,而是一类数据库的统称,根据数据模型和适用场景可分为四大类:

1)键值存储(Key-Value)

特点:以键值对形式存储,查询效率极高(O(1)),支持海量数据和高并发;

代表产品:Redis、RocksDB、Memcached;

适用场景

  • 高频访问的热点数据(如商品库存、用户Session);
  • 计数器、排行榜(如点赞数、实时排名);
  • 缓存层(减轻数据库压力)。

案例:电商秒杀库存计数

用Redis的INCR/DECR命令实现高并发下的库存实时更新,单实例支持每秒10万+操作,远超MySQL的处理能力。

2)文档数据库(Document)

特点:以JSON/BSON等文档格式存储,支持嵌套结构,无需预定义表结构;

代表产品:MongoDB、CouchDB;

适用场景

  • 半结构化数据(如商品详情、用户画像);
  • 频繁变更字段的场景(如自媒体文章的动态属性);
  • 需要复杂查询的非结构化数据(支持文档内索引)。

案例:短视频平台的视频元数据存储

视频元数据包含标题、标签、作者信息、播放统计等,字段常随业务扩展(如新增“AI生成标签”),MongoDB的动态文档模型可直接兼容,无需像MySQL一样频繁ALTER TABLE。

3)列族数据库(Wide Column)

特点:按列族(Column Family)存储,适合批量读写,支持PB级数据;

代表产品:HBase、Cassandra;

适用场景

  • 时序数据(如监控日志、用户行为轨迹);
  • 海量历史数据归档(如电商订单历史);
  • 写密集型场景(如物联网设备数据采集)。

案例:用户行为日志存储

某APP日均产生10亿条用户行为日志(点击、停留、跳转),用HBase按“用户ID+日期”作为RowKey,支持按时间范围快速查询,单集群可轻松支撑PB级存储。

4)图数据库(Graph)

特点:以节点和边存储数据,高效处理多对多关系(如社交网络);

代表产品:Neo4j、JanusGraph;

适用场景

  • 社交关系(如“好友的好友”推荐);
  • 知识图谱(如医疗领域的疾病-症状关联);
  • 路径分析(如物流路线优化)。

案例:社交APP的好友推荐

用Neo4j存储用户关系网络,通过MATCH (u:User)-[:FRIEND]-(f)-[:FRIEND]-(r) WHERE u.id = 123 RETURN r语句,高效查询“二度好友”,响应时间比MySQL的多表JOIN快100+倍。

3.3.3 NoSQL的选型

选择NoSQL时需结合数据特性、访问模式和业务需求,核心决策流程如下:

Mermaid

3.3.4 实战:电商平台的NoSQL架构设计

某电商平台日均订单1000万+,用户行为日志5亿+,需处理三类核心数据,采用“关系型+NoSQL”混合架构:

1)交易核心数据(订单、支付)

  • 存储:MySQL(主从架构)
  • 理由:强一致性要求高,事务支持必不可少。

2)商品详情与用户画像

  • 存储:MongoDB(分片集群)
  • 理由:商品属性动态变化(如新增“环保材质”标签),用户画像包含嵌套结构(如近期浏览记录),文档模型更灵活。

3)用户行为日志与实时推荐

  • 存储:HBase(存储日志)+ Redis(实时推荐缓存)
  • 理由:HBase支撑PB级日志存储,Redis实时计算用户偏好(如“最近点击的商品”),两者配合实现个性化推荐。

4)高并发库存与秒杀

  • 存储:Redis(主从+哨兵)
  • 理由:支持原子操作,单实例每秒处理10万+库存变更,秒杀场景下通过Lua脚本保证库存一致性。

3.3.5 NoSQL使用总结

(1) 不盲目替代关系型数据库:核心交易、财务等强一致性场景仍需MySQL,NoSQL作为补充;

(2) 关注数据一致性:多数NoSQL采用最终一致性,需在业务层处理“数据同步延迟”(如订单状态更新后延迟1秒更新缓存);

(3) 做好数据备份与监控:分布式架构下,节点故障概率更高,需完善备份策略和监控告警;

(4) 避免过度设计:简单场景用单节点Redis即可,无需一开始就搭建分布式集群。

NoSQL的价值不在于“替代”关系型数据库,而在于填补海量、非结构化、高并发场景的能力空白。在实际应用中,需根据数据特性(结构、规模、访问模式)选择合适的类型,通过“关系型+NoSQL”的混合架构,实现海量数据的高效处理。

3.4、避免大事务

由于平台 篇幅限制, 此处省略 5000字+

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

Logo

惟楚有才,于斯为盛。欢迎来到长沙!!! 茶颜悦色、臭豆腐、CSDN和你一个都不能少~

更多推荐