1.如何分批处理数据?

1.1 使用LIMIT和OFFSET子句。

这是最常用的分批查询方法。

例如,你可以使用以下SQL语句来分批查询数据:

SELECT * FROM your_table LIMIT 1000 OFFSET 0;

分批查询到的数据在后端进行处理,达到分批处理数据的效果。

2.使用多线程的方式。

如果你需要用多线程分批处理数据,并且数据所在表的主键id是递增的,可以使用取模的方式进行分批查询。

例如:

import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
 
public class DatabaseUtils {
    // 数据库连接信息
    private static final String URL = "jdbc:mysql://localhost:3306/your_database";
    private static final String USER = "your_username";
    private static final String PASSWORD = "your_password";
 
    // 获取数据库连接
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(URL, USER, PASSWORD);
    }
 
    // 异步查询数据库的方法
    //第一个参数表示偏移量,表示当前已经查询到的数据id
    //第二个参数表示从当前偏移量开始,查询多少条数据
    public static CompletableFuture<List<String>> queryBatchAsync(int offset, int limit) {
        // 使用CompletableFuture.supplyAsync来异步执行数据库查询
        return CompletableFuture.supplyAsync(() -> {
            List<String> results = new ArrayList<>();
            try (Connection conn = getConnection();
                 PreparedStatement stmt = conn.prepareStatement("SELECT id, data FROM your_table LIMIT ? OFFSET ?")) {
                // 设置查询的LIMIT和OFFSET
                stmt.setInt(1, limit);
                stmt.setInt(2, offset);
                // 执行查询
                try (ResultSet rs = stmt.executeQuery()) {
                    // 遍历结果集,将结果添加到列表中
                    while (rs.next()) {
                        results.add(rs.getString("id") + ": " + rs.getString("data"));
                    }
                }
            } catch (SQLException e) {
                // 如果发生异常,抛出运行时异常
                throw new RuntimeException(e);
            }
            // 返回查询结果
            return results;
        });
    }
}

这个类只是负责连接数据库,以及一个异步查询数据库的方法。

注意这个方法的返回结果是CompletableFuture<List>,返回一个异步任务,异步任务中的返回结果是根据偏移量和批量查询条数的查询结果,封装成一个list集合。

注意数据库中的id应该是自增的。

import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class MultiThreadedBatchProcessing {
    public static void main(String[] args) {
        // 假设我们有1000条记录需要处理,每批处理100条记录
        int totalRecords = 1000;
        int batchSize = 100;
        // 创建一个有10个线程的线程池
        ExecutorService executor = Executors.newFixedThreadPool(10);
 
        // 创建一个CompletableFuture数组来存储每个批次的异步任务
        CompletableFuture<?>[] futures = new CompletableFuture[10];
 
        // 循环创建并启动每个批次的异步查询任务
        for (int i = 0; i < totalRecords; i += batchSize) {
            int offset = i; // 计算当前批次的起始位置
            int limit = batchSize; // 每批处理的记录数
            // 启动异步查询任务
            futures[i / batchSize] = DatabaseUtils.queryBatchAsync(offset, limit).thenAccept(batchResult -> {
                // 处理每个批次的结果
                for (String record : batchResult) {
                    System.out.println(record);
                }
            });
        }
 
        // 使用CompletableFuture.allOf等待所有批次的任务完成
        CompletableFuture.allOf(futures).thenRun(() -> {
            // 所有批次处理完成后,关闭线程池
            System.out.println("All batches have been processed.");
            executor.shutdown();
        }).exceptionally(e -> {
            // 如果发生异常,打印错误信息,并尝试紧急关闭线程池
            System.err.println("An error occurred: " + e.getMessage());
            executor.shutdownNow();
            return null;
        });
    }
}

追问:若多线程分批查询过程中有数据插入或者删除,则数据缺漏,如何解决问题?

使用事务保证数据一致性: 可以通过事务来确保数据的一致性。在事务中执行查询、插入或删除操作,如果中途发生错误,可以通过回滚操作来撤销所有已执行的步骤,确保数据的完整性。这样可以避免因并发操作导致的数据不一致问题。

追问:多线程共享事务存在问题,不合适,有其他方式吗?

消息队列和异步重试:在执行更新数据库和删除缓存的操作时,可以使用消息队列和异步重试机制。这样,即使某个操作失败,也可以通过消息队列进行补偿操作,确保数据的最终一致性。

分布式锁:在高并发场景下,可以使用分布式锁来保证同一时间只有一个线程能修改特定的数据行。这可以通过在应用程序层面采用分布式锁、Redis等中间件实现锁机制来完成

2.大数据外部排序

场景:

假设我们有一个包含1亿条记录的大型数据文件,这些记录存储在磁盘上,而我们的计算机内存有限,例如只能一次性处理10万条记录。

2.1 初始划分(生成初始归并段)

  1. 我们从磁盘中读取10万条记录到内存中。
  2. 在内存中使用常规的归并排序算法对这10万条记录进行排序,然后将排好序的这10万条记录写回磁盘,形成一个初始归并段。
  3. 重复这个过程,直到整个1亿条记录都被划分成1000个初始归并段(100000000÷100000 = 1000),每个归并段包含10万条已经排好序的记录。

2.2 多路归并过程

  1. 我们开始进行多路归并。假设我们采用二路归并(每次合并两个归并段)。
  2. 我们从磁盘中读取两个初始归并段(每个10万条记录)到内存中的不同缓冲区。
  3. 然后比较这两个缓冲区中的第一条记录,将较小的那条记录写入到一个新的临时文件(在磁盘上)。
  4. 接着继续比较这两个缓冲区中的下一条记录,不断重复这个过程,直到其中一个缓冲区中的记录全部被写入到临时文件。
  5. 然后将另一个缓冲区中剩余的记录也写入到临时文件,这样就完成了一次二路归并,得到了一个包含20万条记录的新归并段。
  6. 重复这个二路归并过程,不断合并归并段。例如,经过一系列的二路归并,我们可以将1000个初始归并段逐步合并成500个、250个等,最终合并成一个完整的有序文件。

2.3 优化(采用多路归并)

  1. 为了提高效率,我们可以采用多路归并(如五路归并)。在这种情况下,我们从磁盘中同时读取五个初始归并段到内存中的不同缓冲区。
  2. 然后比较这五个缓冲区中的第一条记录,将最小的那条记录写入到一个新的临时文件。
  3. 不断重复这个过程,直到这五个归并段中的所有记录都被合并到新的临时文件中,这样就完成了一次五路归并。多路归并可以减少归并的趟数,从而减少磁盘I/O的次数,提高外部排序的效率。

3.大数据中找到最大的前10条

问题:MySQL中如果是非常大的数据量,比如说要从几千万条数据中,找到最大的前10条。

Java后端开多个进程,每个进程负责一部分数据,找出这部分数据中的最大的十条数据,然后再从每个线程中的最大十条数据中找出最大的十条,就是所有数据中最大的十条数据。

注意,由于MySQL没用top(SqlServer中的)方法,所以采用 order by + limit 实现找出最大几条数据。

4.用户登录的表设计

问题:一个用户登录页面,涉及不同用户,不同用户有多个不同权限。应该设计多少张表?

设计用户登录和权限管理系统的数据库时,通常需要考虑以下几个核心实体:用户、角色、权限,以及它们之间的关系。以下是这些实体和关系的简要说明:

  1. 用户(Users):存储用户的基本信息,如用户名、密码、邮箱等。
  2. 角色(Roles):定义不同的角色,每个角色代表一组权限。
  3. 权限(Permissions):定义具体的操作权限,如读取、写入、删除等。
  4. 用户-角色关系(User-Role):一个用户可以有多个角色,一个角色可以被多个用户拥有,这是一个多对多的关系。
  5. 角色-权限关系(Role-Permission):一个角色可以有多个权限,一个权限可以被多个角色拥有,这也是一个多对多的关系。

基于这些实体和关系,通常需要设计以下几张表:

Users 表:存储用户信息。

  1. 用户ID
  2. 用户名
  3. 密码(通常是加密存储)
  4. 邮箱
  5. 其他个人信息

Roles 表:存储角色信息。

  1. 角色ID
  2. 角色名称
  3. 角色描述

Permissions 表:存储权限信息。

  1. 权限ID
  2. 权限名称
  3. 权限描述

User_Roles 表(用户-角色关系表):存储用户和角色的多对多关系。

  1. 用户ID
  2. 角色ID

Role_Permissions 表(角色-权限关系表):存储角色和权限的多对多关系。

  1. 角色ID
  2. 权限ID

这样设计的好处是:

  1. 灵活性:可以灵活地为用户分配角色,为角色分配权限,而不需要修改用户信息或角色信息。
  2. 扩展性:当需要添加新的角色或权限时,不需要修改现有的用户信息,只需添加新的角色或权限记录即可。
  3. 安全性:通过控制角色的权限,可以方便地管理不同用户的访问控制,而不需要直接操作用户数据。

这种设计模式通常被称为角色基于访问控制(RBAC),是实现用户权限管理的一种常见和有效的方法。

5.从大量单词中找到出现频率最高的单词

问题:有1gb大小的文件,文件中每行是一个单词,每个单词大小不超过16kb,现在内存大小只有1mb,请问怎么找出出现频率最高的100个单词。

5.1 分块处理

文件分块

  1. 由于内存只有1MB,而文件有1GB,无法一次性将整个文件读入内存进行处理。可以将1GB的文件按照一定的大小进行分块,例如每次读取1MB(考虑到内存还要用于其他操作,可能要读取稍小于1MB的数据块)。
  2. 假设我们每次读取900KB的数据块。

单词计数(针对每个块)

  1. 对于每个数据块,使用一个哈希表。由于每个单词大小不超过16KB,在900KB的数据块中能容纳相当数量的单词。
  2. 当读取完一个数据块后,将这个数据块中的单词计数结果保存到磁盘上的临时文件中。

5.2 合并计数结果

读取临时文件

  1. 读取之前保存的各个临时文件中的单词计数结果。由于这些临时文件的大小相对较小,可以逐个读取并合并。
  2. 再次使用一个哈希表来合并这些计数结果。例如,如果在一个临时文件中单词“apple”出现了5次,在另一个临时文件中出现了3次,合并后“apple”的计数就是8次。

注意这里map要统计所有的单词出现频率,因此key不能存储整个单词。具体如何实现等我再研究一下。

找出高频词

  1. 在合并后的哈希表中,找出出现频率最高的100个单词。可以通过对哈希表中的值(即单词出现的频率)进行排序,然后取前100个对应的单词。

6.有没有了解过内存占比高或者cpu占比高的情况

内存占比高的情况

内存占比高通常指的是Java应用程序在运行过程中消耗了大量的内存资源,可能导致系统性能下降,甚至出现OutOfMemoryError异常。以下是一些可能导致内存占比高的情况:

  1. 内存泄漏:如果应用程序中存在未被正确释放的对象,可能会导致内存泄漏,最终耗尽可用内存。
  2. 对象生命周期过长:如果某些对象的生命周期过长,即使不再需要,也不会被垃圾回收器回收,这也会导致内存占用增加。
  3. 缓存不当:如果应用程序中使用了缓存,但没有正确管理缓存的大小和过期策略,可能会导致内存占用过高。
  4. 堆内存设置不合理:如果JVM的堆内存设置过小,可能会导致频繁的垃圾回收,影响性能。

CPU占比高的情况

CPU占比高通常指的是Java应用程序在运行过程中消耗了大量的CPU资源,可能导致系统响应变慢,甚至出现卡顿现象。以下是一些可能导致CPU占比高的情况:

  1. 死循环或无限递归:如果应用程序中存在死循环或无限递归,可能会导致CPU占用率达到100%。
  2. 高并发处理:如果应用程序需要处理大量并发请求,可能会导致CPU占用率升高。
  3. 复杂算法或计算密集型任务:如果应用程序中包含复杂的算法或计算密集型任务,可能会导致CPU占用率升高。
  4. 频繁的GC操作:如果JVM的垃圾回收器过于频繁地运行,可能会导致CPU占用率升高。

追问:如何排查CPU飙高的情况

第一步,使用 top 找到占用 CPU 最高的 Java 进程

image

使用 top命令发现占用 CPU 99.7% 的线程是 Java 进程,进程 PID 为 13731。

第二步,用 top -Hp 命令查看占用 CPU 最高的线程 上一步用 top命令找到了那个 Java 进程。那一个进程中有那么多线程,不可能所有线程都一直占着 CPU 不放,这一步要做的就是揪出这个罪魁祸首,当然有可能不止一个。

执行top -Hp pid命令,pid 就是前面的 Java 进程,我这个例子中就是 13731 。

完整命令为:

top -Hp 13731

执行之后的效果如下:

image

以看到占用 CPU 最高的那个线程 PID 为 13756。

然后将 13756转换为 16 进制的,后面会用到,可以用在线进制转换的网站直接转换,转换结果为 0x35bc。

第三步,保存线程栈信息

当前 Java 程序的所有线程信息都可以通过 jstack命令查看,我们用jstack命令将第一步找到的 Java 进程的线程栈保存下来。

jstack 13731 > thread_stack.log

第四步,在线程栈中查找最贵祸首的线程

第二步已经找到了这个罪魁祸首的线程 PID,并把它转换成了 16 进制的,第三步保存下来的线程栈中有所有线程的 PID 16 进制信息,我们在线程栈中查找这个16进制的线程 id (0x35bc)。

image

7.断点续传是怎么做的?

我们是基于分块上传的模式实现断点续传的需求,当文件上传一部分断网后前边已经上传过的不再上传。

  1. 前端对文件分块。
  2. 前端使用多线程一块一块上传,上传前给服务端发一个消息校验该分块是否上传,如果已上传则不再上传。如果从该断点处断网了,下次上传时,前面的分块已经存在了,就不会重复上传,会在断点处重新上传。从而实现断点续传。
  3. 等所有分块上传完毕,服务端合并所有分块,校验文件的完整性。 因为分块全部上传到了服务器,服务器将所有分块按顺序进行合并,就是写每个分块文件内容按顺序依次写入一个文件中。使用字节流去读写文件。
  4. 前端给服务传了一个md5值 (前端根据用户上传文件计算出来的md5值) ,服务端合并文件后计算合并后文件的md5是否和前端传的一样,如果一样则说文件完整,服务端可以删除分块的文件,只保留合并成功的文件。如果不一样说明可能由于网络丢包导致文件不完整,这时上传失败需要重新上传。

8.分块文件清理问题?

上传一个文件进行分块上传,上传一半不传了,之前上传到minio的分块文件要清理吗?怎么做的?

  1. 在数据库中有一张文件表记录minio(或者oss等分布式存储系统)中存储的文件信息。
  2. 文件开始上传时会写入文件表,状态为上传中,上传完成会更新状态为上传完成。并且还会存储文件开始上传时间。
  3. 当一个文件传了一半不再上传了说明该文件没有上传完成(比如说状态为上传中),会有定时任务去查询文件表中的记录,如果文件超过一定时间 (比如说24h) 未上传完成则删除minio中没有上传成功的文件目录。

9.xxl-job是什么怎么工作?

XXL-JOB分布式任务调度服务由调用中心和执行器组成,调用中心负责按任务调度策略向执行器下发任务,执行器负责接收任务执行任务。

  1. 首先部署并启动xxl-job调度中心。(一个java工程)
  2. 首先在微服务添加xxl-job依赖,在微服务中配置执行器
  3. 启动微服务,执行器向调度中心上报自己。
  4. 在微服务中写一个任务方法并用xxl-job的注解去标记执行任务的方法名称。
  5. 在调度中心配置任务调度策略,调度策略就是每隔多长时间执行还是在每天或每月的固定时间去执行,比如每天0点执行,或每隔1小时执行一次等。
  6. 在调度中心启动任务。
  7. 调度中心根据任务调度策略,到达时间就开始下发任务给执行器。
  8. 执行器收到任务就开始执行任务。

10.任务幂等性如何保证?

幂等性是指一次和多次请求某一个资源对于资源本身应该具有同样的结果。

幂等性是为了解决重复提交问题,比如:恶意刷单,重复支付等。

解决幂等性常用的方案:

  1. 数据库约束: 比如:唯一索引,主键。同一个主键不可能两次都插入成功。
  2. 乐观锁: 常用于数据库,更新数据时根据乐观锁状态去更新。
  3. 悲观锁: 在操作数据时锁定数据,防止其他事务同时修改。
  4. 唯一序列号: 请求前生成唯一的序列号,携带序列号去请求,执行时在redis记录该序列号表示以该序列号的请求执行过了,如果相同的序列号再次来执行说明是重复执行。

比如:表单提交中,进入表单时,后端为每个表单生成一个token,并且放在redis中。表单提交时,会携带这个token,后端判断redis中是否有该token,如果有说明第一次进行提交,同时把redis中token删除。表单重复提交时,该token在redis中就找不到了,该操作就执行失败。

  1. 消息队列: 使用消息队列来处理任务,确保消息只被消费一次。可以结合消息的确认机制和重试策略来实现。

来源:https://blog.csdn.net/qq_64064246/article/details/142762574

最后修改:2026 年 06 月 06 日
如果觉得我的文章对你有用,请随意赞赏