传承程序猿的开源精神,分享Java场景的问题和思路,供大家一起探讨学习
1. 分布式架构MySQL的主键设置
1.1 MySQL分布式环境中,为啥不推荐设置主键自增?
在单机MySQL中,自增主键非常方便,能保证主键唯一且递增。但在分布式环境中(如分库分表、多主节点)下会出现如下问题:
(1)各数据表独立分配自增ID,比如都从1开始,无法保证全局唯一性;
(2)即使各个数据表设置不同自增步长,比如表1起始1步长2,表2起始2步长2,但扩展不好,一旦新增或删除 数据表/节点,如何设置新的自增起点和步长?难以维护;
(3)自增ID强依赖锁,在高并发下写入会影响性能;
1.2 UUID可以作为主键吗,会存在什么问题?
UUID(通用唯一标识符)确实能在分布式环境下保证唯一性,但仍然存在以下问题:
(1)存储空间大:UUID通常为16字节(128位)相比BigInt(8字节)大得多,浪费空间;格式:550e8400-e29b-41d4-a716-446655440000
(2)无序性:UUID随机生成,聚簇索引根据主键大小排序,插入索引页会造成频繁页分裂,影响B+树性能;
(3)查询慢:随机性导致索引局部性差,影响缓存命中率;
1.3 雪花算法可以吗,原理是什么,有什么优缺点,如何解决这些缺点?
雪花算法是分布式唯一ID生成算法,一共64位:1位符号位,固定为0表示整数;41位时间戳,从1970年起大约可使用69年;10位机器标识,最多支持1024个节点;12位序列号,同一毫秒内可生成4096个ID;
优点如下:
(1)全局唯一:时间戳+机器标识+序列号,保证唯一性;
(2)高性能:本地生成,不受数据库锁影响,QPS高达百万;
(3)趋势递增:基于时间戳,趋势递增,有利于索引性能;
缺点和解决方案如下:
(1)强依赖系统时间,如果发生时钟回拨问题,会导致ID重复或乱序:生成ID后检测时间是否回拨,可使用“时间回拨补偿算法”(如漂移时钟或逻辑时钟机制)
(2)机器ID分配冲突:多个机器节点ID重复可能生成重复ID:使用ZooKeeper/Redis分配机器ID或手动配置机器ID
2. 深分页如何进行性能优化?
深分页的危害:比如我们有一张大表 orders,执行SELECT * FROM orders ORDER BY id LIMIT 1000000, 10; MySQL实际上会扫描1000010条数据,只返回最后十条,导致磁盘IO量暴增,性能下降;
解决方案如下:
(1)主键子查询+覆盖索引:用子查询快速定位主键ID,再根据ID做表连接,实现覆盖索引
- SELECT o.*
- FROM orders o
- JOIN (
- SELECT id
- FROM orders
- ORDER BY id
- LIMIT 1000000, 10
- ) AS temp ON o.id = temp.id;
(2)基于“上次最大ID”分页(游标分页),只能支持“顺序翻页”,不能直接跳转到第N页,依赖连续递增主键
- -- 第一页
- SELECT * FROM orders ORDER BY id LIMIT 10;
- -- 下一页(上次最大id为1000010)
- SELECT * FROM orders WHERE id > 1000010 ORDER BY id LIMIT 10;
(3)可以根据业务限制最大分页深度,比如最多翻到第100页;借助Redis缓存或ES等搜索引擎;
3. 动态查询条件导致索引失效如何优化?
(1)建立联合索引:将最常用的字段建立联合索引,为了遵循联合索引的最左前缀原则,根据常用性来设置联合索引的顺序,根据业务需求,可以将最左边字段设置成默认值;
(2)分库分表:当条件变化范围大、数据量极大时,如果单表索引难以支撑,可以根据时间或其他字段进行水平分表,将数据扩散开来;
4. 如何设计秒杀系统?
秒杀系统功能需求大致为:商品在某一时间点开启,用户能抢购商品并下单支付;每人每个账号或设备能抢购的数量;抢到的用户进行支付,超时自动释放库存。对于这种场景,需要做到高并发、峰值QPS量大、低延迟、强一致性/最终一致性、抗压能力强,安全防护,主要面临的问题有:安全攻击、库存超卖、一人一单/n单、请求雪崩
(1)前端:前端可部署在Nginx集群,实现负载均衡或设置限流;
(2)设置一些验证码和排队图标,进行安全校验,增加用户体检,防止无脑刷新;
(3)网关:对于请求进行权限检验,也根据漏桶/令牌桶算法实现限流;
(4)重复下单:利用幂等性防止用户重复下单,用户成功下单后根据用户ID和商品ID生成唯一标识,放到数据库或缓存中,之后每次下单后先检验唯一标识是否存在;
(5)库存超卖:可以利用CAS防止库存超卖,每次扣减库存时先校验库存数量是否大于0,如果并发量高可以考虑Redis的setIfAbsent命令或Redission分布式锁来确保强一致性;
(6)流量削峰:使用MQ中间件实现异步操作,避免业务阻塞;
(7)订单超时释放:可利用RabbitMQ的延迟队列,订单创建后超时未支付设置为取消,释放库存;
5. 商品类型树怎么优化?
5.1数据表设计:
这里列举几种类型的类型表存储方式,大型电商(如京东、阿里)多采用 Closure Table + 缓存 + 层级 JSON 混合方案
(1)邻接表:每个分类存储父级分类ID
- CREATE TABLE category (
- id BIGINT PRIMARY KEY,
- name VARCHAR(50),
- parent_id BIGINT
- );
特点:结构简单,增删改效率高;但查询整个树或子树需要多次递归查询,性能差;
(2)路径枚举:每个分类保存自己的路径
| id | name | path |
| 1 | 服装 | 1 |
| 2 | 男装 | 1/2 |
| 3 | 外套 | 1/2/3 |
特点:查询子节点快:where path like ‘1/2/%’; 更新复杂,移动节点要更新整条路径;
(3)闭包表:新建一张关系表,保存所有父子关系
一般包含两张表:
1️⃣ 分类表(category)
- CREATE TABLE category (
- id BIGINT PRIMARY KEY,
- name VARCHAR(100)
- );
2️⃣ 闭包表(category_closure)
- CREATE TABLE category_closure (
- ancestor BIGINT NOT NULL, -- 祖先分类ID
- descendant BIGINT NOT NULL, -- 子孙分类ID
- depth INT NOT NULL, -- 层级深度(自己到自己为0)
- PRIMARY KEY (ancestor, descendant)
- );
当插入新分类时,闭包表会自动维护「祖先-子孙」关系。例如:
| ancestor | descendant | depth |
| 1 | 1 | 0 |
| 1 | 2 | 1 |
| 1 | 3 | 2 |
| 2 | 2 | 0 |
| 2 | 3 | 1 |
| 3 | 3 | 0 |
这里表示:分类 1 是分类 2 的父级,分类 3 的祖父;分类 2 是分类 3 的父级。
特点:查询任意节点的所有祖先/子孙方便;插入只需要增加若干关系记录;表数据多,增加维护成本;
5.2 缓存:
为了提升性能,可以将分类树缓存在Redis中,可配合SpringTask或xxl-job定时更新缓存;修改分类树可以使用延迟双删,追求高性能可以考虑使用消息中间件;
当商品分类树数据量极大时,可以通过Gzip压缩降低Redis占用量
5.3 前端懒加载:
可以选择不一次性加载整棵树,而是第一级分类加载后,用户展开某节点时再查询该节点的子节点;
6. Excel导出百万数据,内存占用高、接口响应慢、CPU占用高怎么解决?
6.1 Excel导出数据的三种方法:
| 方案 | 常用库 | 文件格式 | 特点 |
| Apache POI | org.apache.poi | .xls、.xlsx | ✅ 功能最全;❌ 内存占用高 |
| EasyExcel(推荐) | com.alibaba:easyexcel | .xlsx | ✅ 性能高,API 简洁;❌ 功能稍有限 |
| Hutool ExcelUtil | cn.hutool.poi.excel | .xlsx | ✅ 快速简单;❌ 不适合大文件导出 |
实例代码:
Apache POI 示例(基础方案)
- import org.apache.poi.ss.usermodel.*;
- import org.apache.poi.xssf.usermodel.XSSFWorkbook;
- import java.io.*;
- import java.util.*;
- public class PoiExcelExport {
- public static void main(String[] args) throws Exception {
- // 模拟数据
- List<Map<String, Object>> data = List.of(
- Map.of("id", 1, "name", "张三", "score", 90),
- Map.of("id", 2, "name", "李四", "score", 88)
- );
- // 创建工作簿和表
- Workbook wb = new XSSFWorkbook();
- Sheet sheet = wb.createSheet("学生成绩");
- // 表头
- Row header = sheet.createRow(0);
- header.createCell(0).setCellValue("ID");
- header.createCell(1).setCellValue("姓名");
- header.createCell(2).setCellValue("成绩");
- // 数据
- int rowNum = 1;
- for (Map<String, Object> rowData : data) {
- Row row = sheet.createRow(rowNum++);
- row.createCell(0).setCellValue((Integer) rowData.get("id"));
- row.createCell(1).setCellValue((String) rowData.get("name"));
- row.createCell(2).setCellValue((Integer) rowData.get("score"));
- }
- // 输出到文件
- try (FileOutputStream fos = new FileOutputStream("学生成绩.xlsx")) {
- wb.write(fos);
- }
- wb.close();
- System.out.println("✅ Excel 导出完成!");
- }
- }
✅ 优点:控制灵活,支持合并单元格、图片、样式等
❌ 缺点:一次性加载全部数据,大文件(>10万行)容易OOM
EasyExcel 示例(企业常用)
- import com.alibaba.excel.annotation.ExcelProperty;
- import lombok.Data;
- @Data
- public class Student {
- @ExcelProperty("编号")
- private Integer id;
- @ExcelProperty("姓名")
- private String name;
- @ExcelProperty("成绩")
- private Integer score;
- }
- import com.alibaba.excel.EasyExcel;
- import java.util.List;
- public class EasyExcelExport {
- public static void main(String[] args) {
- List<Student> list = List.of(
- new Student(1, "张三", 90),
- new Student(2, "李四", 88)
- );
- String fileName = "学生成绩.xlsx";
- EasyExcel.write(fileName, Student.class)
- .sheet("成绩表")
- .doWrite(list);
- System.out.println("✅ EasyExcel 导出成功!");
- }
- }
✅ 优点:
- 支持 大数据量导出(分批写入,内存友好)
- 代码简洁,企业项目最常用
❌ 缺点:不适合复杂样式(可扩展但较麻烦)
解决思路如下:
(1)分页查询+流式写出:不要一次性查询所有数据,而是分批分页读取数据库,写入后立即flush到磁盘,降低内存消耗;磁盘指的是磁盘临时文件,就是将数据在内存中分批处理,处理后放在磁盘临时文件,处理完生成最终的Excel.
- int pageSize = 10000;
- int pageNum = 1;
- ExcelWriter excelWriter = EasyExcel.write("导出.xlsx", Student.class).build();
- WriteSheet writeSheet = EasyExcel.writerSheet("学生").build();
- while (true) {
- List<Student> data = studentMapper.selectPage(pageNum, pageSize);
- if (data.isEmpty()) break;
- excelWriter.write(data, writeSheet);
- pageNum++;
- }
- excelWriter.finish();
(2)异步导出+文件缓存:用户点击“导出”后,不立即生成文件,而是提交一个导出任务(后台线程池或MQ异步执行),导出后将文件存到OSS或文件管理系统,返回前端下载链接。加快接口响应,不阻塞请求,提高系统吞吐;
(3)限流:设置线程池最大线程数,防止用户同时导出多个百万级任务,确保导出请求稳定性,降低CPU占用
(4)Gzip压缩:如果导出文件较大,可以在服务器将Excel压缩成zip,降低带宽,但会增加一定的CPU压缩开销,建议配合异步操作处理;
- try (FileOutputStream fos = new FileOutputStream("export.zip");
- ZipOutputStream zos = new ZipOutputStream(fos)) {
- ZipEntry entry = new ZipEntry("data.xlsx");
- zos.putNextEntry(entry);
- Files.copy(Paths.get("data.xlsx"), zos);
- zos.closeEntry();
- }
7. 如何定位和解决线上的OOM?
OOM是JVM在堆、方法区、栈、本地内存等区域内存不足时抛出的异常,常见的异常如下:
java.lang.OutOfMemoryError: Java heap space,堆内存溢出(最常见);java.lang.OutOfMemoryError: Metaspace,元空间不足(类加载过多);java.lang.OutOfMemoryError: GC overhead limit exceeded,GC时间过长仍无法回收空间;
java.lang.OutOfMemoryError: Direct buffer memory,直接内存(堆外内存)溢出;
java.lang.OutOfMemoryError: Unable to create new native thread,线程太多系统资源耗尽
定位线上OOM:
(1)生成dump文件:当发生 OOM时,如果 JVM 启动参数中配置了
-XX:+HeapDumpOnOutOfMemoryError,JVM 会自动生成堆内存的 dump 文件。
当然,我们也可以在运行时通过 jmap 命令手动生成 dump 文件。
(2)分析dump文件:使用 Eclipse MAT(Memory Analyzer Tool) 或 VisualVM 打开并分析dump文件(内存快照);
OOM解决思路:
| 场景 | 原因 | 解决方案 |
| 堆内存溢出(Java heap space) | 对象过多或未及时释放 | 分析堆 dump,优化缓存、循环、集合 |
| 元空间溢出(Metaspace) | 动态加载过多类(如反射、代理) | 增加 -XX:MaxMetaspaceSize,减少重复加载 |
| Direct Memory 溢出 | 使用 ByteBuffer.allocateDirect() 过多 | 显式释放 buffer、或增大 -XX:MaxDirectMemorySize |
| GC overhead limit exceeded | GC 占用过多时间 | 检查是否有内存泄漏、对象创建过快 |
| Unable to create new native thread | 线程创建过多 | 优化线程池配置、减少并发线程数 |
| 堆外缓存泄漏(Netty、Kafka、MapDB) | 没有释放 DirectBuffer | 检查 ReferenceCountUtil.release() 调用 |
8. 扫码登录怎么实现?
扫码登录时两台设备(手机和电脑)之间的“确认登陆”过程,步骤如下:
(1)PC端请求登录二维码:用户在PC端打开登陆页面,前端向后端发送生成登录token(UUID)的请求,后端生成并将这个token存储在Redis中,并生成一张二维码(内容包含UUID和登录URL),接着前端显示二维码。二维码内容一般是一个URL,例如:https://example.com/qrcodeLogin?uuid=abc123xyz
(2)手机端扫码:用户用手机(已登录APP)扫描二维码,手机端解析二维码中的URL,并向服务器发送请求,服务器根据请求携带的信息(比如用户ID)校验用户是否已登录,并将该登录token对应的状态改为“已确认登录”。
- POST /api/qrcode/confirmLogin
- body: { uuid: "abc123xyz", userId: 101 }
(3)PC端检测:可通过轮询或WebSocket长连接实现。PC端在展示二维码后,前端会每隔1-2秒轮询发送一次后端接口,服务器返回该登录token的状态
(4)登录成功:当服务器返回“登陆成功”的信息后,PC前端调用登陆成功相关的接口,获取并保存用户的JWT等登录信息,跳转到系统首页。
9. redis如何实现上亿用户实时积分排行?
海量数据积分排行(比如游戏积分榜、直播打赏榜、活动排名榜等)中非常常见,有三个关键点:实时性:积分变化后,排行榜能立即更新、高并发:可能有成千上万个用户同时加分、高性能:不能每次都全表扫描或排序;
(1)分片:当数据量达到一定程度的时候,单凭一个ZSet是无法满足需求的,可以按照用户ID或业务维度拆分成多个排行榜,比如score:rank:0存放用户ID为0-10^7, score:rank:1 存放用户ID为10^7-2*10^7,总榜求前N名时各分片取前N名在合并排序;
(2)热榜缓存:排行榜一般只需要排前面的数据,可以结合SpringTask或XXl-job定时扫描各个Zset,生成一个热榜缓存起来;
(3)异步更新:对于实时性要求不高的积分变化,可以通过MQ异步批量更新
10. 如何使用redis记录用户连续登录天数/系统签到系统?
用redis中的Bitmap(位图)存储每个用户时间区间内的签到情况,每个key对应一个用户,每天对应一个bit(0表示未签到,1表示已签到),统计累计签到次数可以使用BITCOUNT命令,计算连续签到次数可以取出最近N天的比特,遍历1的连续段。
虽然每个用户对应一个key好像占用很多内存,实际上,位图每一位占用一个比特,一年365天等于365bite约等于46个字节,加上key大小每个key一年占用内存不到80字节,即使有一亿用户也不超过10G内存占用。
11. 订单超时自动取消怎么实现?
(1)Redis的TTL:对于一些中小型项目或简单业务场景,可以使用Redis的TTL实现。创建订单时,在Redis中写入一个带过期时间的key,在配置文件中开启key过期事件,当key过期时,在回调中执行订单取消逻辑;
(2)MQ:企业常用的是使用Kafka、Rabbit MQ、RocketMQ等提供的延迟队列;
(3)Redisson的DelayedQueue:可以使用Redission提供的延迟队列,它是基于Redis的ZSet和List队列实现的,将消息的过期时间戳作为score,消息本身作为member,确保最早到期的消息排在最前面。通过定时任务定时扫描Zset,将到期的消息在Zset中删除,并转移到目标队列中,通过发布订阅模式发送通知,唤醒阻塞在目标队列中的消费者进行消费。
| 实现方式 | 核心原理 | 优点 | 缺点 | 适用场景 |
| 1️⃣ 定时任务轮询 | 定期扫描数据库中未支付且超时的订单 | 简单易实现 | 扫描延迟、性能差 | 低并发系统、小项目 |
| 2️⃣ 延时队列(消息队列) | 下单时投递延时消息,延迟时间到再检查订单 | 精准、性能好 | MQ依赖强 | 中高并发系统(推荐) |
| 3️⃣ Redis TTL + Keyspace 事件 | 利用 Redis 过期事件触发取消逻辑 | 实时、轻量 | Redis 宕机会丢事件 | 中小型系统(推荐) |
| 4️⃣ Redisson DelayedQueue | Redisson 内置延时队列,底层基于 Redis zset + 轮询 | 易用、可靠 | Redis 依赖强 | 中高并发(常用于微服务) |
12. volatile有哪些应用场景?
Volatile是用来确保变量的可见性和有序性(禁止指令重排序),但不保证原子性;
可见性:JMM将内存分为两部分,一个是线程的工作内存和共享的主内存,线程会将主内存中的共享变量缓存副本到自己的工作内存,目的是不用每次都到主内存当中读取数据从而提升性能,这个特性也引发了多个线程之间对数据修改的不可见。Volatile修饰变量后,JVM会强制把变量刷新到主内存,并让其他线程的本地缓存失效;
有序性:指令重排序问题,当给CPU三条指令:1、到磁盘获取数据;2、拿到数据赋值;3、执行计算任务,由于第一条指令磁盘IO慢,为了提高CPU利用率,会在第一条指令执行后执行第三条指令。JVM在volatile修饰的变量的读写操作添加内存屏障,防止编译器/CPU指令重排序。
volatile 常见应用场景如下:
状态标志(Flag)变量:
- class Worker implements Runnable {
- private volatile boolean running = true;
- public void run() {
- while (running) {
- // 执行任务
- }
- System.out.println("线程安全停止");
- }
- public void stop() {
- running = false;
- }
- }
🔹 为什么要加 volatile?
否则线程可能一直读到旧值(被 CPU 缓存了),导致 stop() 调用后线程仍不退出。
双重检查锁(DCL)单例模式:
- class Singleton {
- private static volatile Singleton instance;
- private Singleton() {}
- public static Singleton getInstance() {
- if (instance == null) {
- synchronized (Singleton.class) {
- if (instance == null) {
- instance = new Singleton(); // 可能被指令重排!
- }
- }
- }
- return instance;
- }
- }
如果没有 volatile,可能发生指令重排:
- 分配内存
- 引用赋值给 instance(此时对象还没构造完成)
- 调用构造函数
另一个线程看到 instance != null,却访问了“半初始化”的对象 → 错误!
配置刷新(动态感知变化):
在服务中某些全局配置项(如开关、阈值)需要实时生效,可以用 volatile:
- class Config {
- public static volatile int maxConnections = 100;
- }
- // 某线程修改配置
- Config.maxConnections = 200;
- // 其他线程立即看到更新
- System.out.println(Config.maxConnections);
无需加锁即可实现低延迟的配置热更新。
13. 查询两百条数据耗时200毫秒,怎么在500毫秒内查询1000条数据?
数据库查询是IO密集型操作,单线程往往会造成CPU等待磁盘IO,CPU利用率低。因此可以创建线程池,并发查询数据库,提高整体吞吐量。设置线程池的核心线程数为5,将1000条数据分为5份,每条线程负责查询200条数据。
- import java.util.*;
- import java.util.concurrent.*;
- public class ParallelQueryExample {
- private static final int THREAD_COUNT = 5;
- private static final ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
- public static void main(String[] args) throws Exception {
- long start = System.currentTimeMillis();
- // 模拟分页查询参数
- List<Future<List<String>>> futures = new ArrayList<>();
- for (int i = 0; i < THREAD_COUNT; i++) {
- int offset = i * 200;
- futures.add(executor.submit(() -> queryData(offset, 200)));
- }
- // 汇总结果
- List<String> allData = new ArrayList<>();
- for (Future<List<String>> future : futures) {
- allData.addAll(future.get()); // 阻塞等待结果
- }
- long end = System.currentTimeMillis();
- System.out.println("查询完成, 总共获取: " + allData.size() + " 条数据");
- System.out.println("耗时: " + (end - start) + " ms");
- executor.shutdown();
- }
- // 模拟查询数据库(假设每次查询200条需要200ms)
- private static List<String> queryData(int offset, int limit) {
- try {
- Thread.sleep(200); // 模拟数据库查询时间
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- List<String> list = new ArrayList<>();
- for (int i = 0; i < limit; i++) {
- list.add("Record-" + (offset + i));
- }
- return list;
- }
- }
14. 分布式架构怎么确保并发安全?
在单机系统中,可以通过synchronized关键字和Lock接口确保并发安全,但在分布式架构下由于每个JVM、进程都独立,这些单机锁就会失效,可以使用分布式锁:Redis的SetNX命令或Redission框架。
锁的key为UUID+线程ID,确保释放锁的是获取锁的线程;通过看门狗机制定期给锁续期,避免由于业务阻塞导致锁被提前释放;加锁、给锁续期和释放锁等操作都是基于Redis的SetNX命令和lua脚本实现的,确保了操作原子性;
在 Redis 集群模式下,Redisson 默认遵循 AP 模式,即优先保证系统的高可用性和性能。
加锁操作通常只需在主节点执行成功即可返回结果,从而实现快速响应。如果主节点刚好加锁,在同步给从节点前宕机可能导致数据不一致问题,若对一致性要求较高,可以使用 Redisson 的 RedLock(红锁)机制。红锁通过在多个独立 Redis 节点上同时加锁,并在 超过半数节点成功 时才认为加锁成功,从而在一定程度上增强一致性(接近 CP 模式),但会牺牲部分性能与可用性。
15. 内存200M怎么读取1G文件并统计重复内容?
我们不能一次性将1G文件直接加载到内存中,否则会触发OOM,可以采用分块处理、哈希分桶的思想。步骤如下:
步骤1:按哈希分桶:将大文件切分为多个小文件(例如分为20个桶,每个50M左右),思路:对每行内容去哈希值,根据哈希值决定写入哪个桶;
步骤2:桶内统计:逐个读取桶文件,用HashMap统计词频;
步骤3:结果合并:直接汇总所有桶的结果即可;
- public class BigFileCounter {
- private static final String INPUT_FILE = "data.txt";
- private static final String TMP_DIR = "tmp";
- private static final int BUCKET_COUNT = 20; // 分桶数,可根据内存大小调整
- public static void main(String[] args) throws Exception {
- new File(TMP_DIR).mkdirs();
- splitToBuckets();
- List<File> resultFiles = countEachBucket();
- mergeResults(resultFiles);
- System.out.println("✅ 全部统计完成!");
- }
- /**
- * 第一步:按哈希分桶,将1GB文件拆成多个小文件
- */
- private static void splitToBuckets() throws Exception {
- System.out.println("开始分桶...");
- BufferedReader reader = new BufferedReader(
- new InputStreamReader(new FileInputStream(INPUT_FILE), StandardCharsets.UTF_8), 10 * 1024 * 1024);
- BufferedWriter[] writers = new BufferedWriter[BUCKET_COUNT];
- for (int i = 0; i < BUCKET_COUNT; i++) {
- writers[i] = new BufferedWriter(
- new OutputStreamWriter(new FileOutputStream(TMP_DIR + "/bucket_" + i + ".txt"), StandardCharsets.UTF_8));
- }
- String line;
- while ((line = reader.readLine()) != null) {
- String word = line.trim();
- if (word.isEmpty()) continue;
- int bucketId = getHashBucket(word);
- writers[bucketId].write(word);
- writers[bucketId].newLine();
- }
- reader.close();
- for (BufferedWriter w : writers) w.close();
- System.out.println("✅ 分桶完成!");
- }
- /**
- * 第二步:统计每个桶内部的词频
- */
- private static List<File> countEachBucket() throws IOException {
- System.out.println("开始统计每个桶...");
- List<File> resultFiles = new ArrayList<>();
- for (int i = 0; i < BUCKET_COUNT; i++) {
- File bucketFile = new File(TMP_DIR + "/bucket_" + i + ".txt");
- if (!bucketFile.exists()) continue;
- Map<String, Integer> countMap = new HashMap<>();
- try (BufferedReader br = new BufferedReader(new FileReader(bucketFile))) {
- String line;
- while ((line = br.readLine()) != null) {
- countMap.put(line, countMap.getOrDefault(line, 0) + 1);
- }
- }
- // 写入结果文件
- File resultFile = new File(TMP_DIR + "/result_" + i + ".txt");
- try (BufferedWriter bw = new BufferedWriter(new FileWriter(resultFile))) {
- for (Map.Entry<String, Integer> entry : countMap.entrySet()) {
- bw.write(entry.getKey() + "\t" + entry.getValue());
- bw.newLine();
- }
- }
- System.out.printf("桶 %d 统计完成,共 %d 项%n", i, countMap.size());
- resultFiles.add(resultFile);
- }
- return resultFiles;
- }
- /**
- * 第三步:合并结果文件
- */
- private static void mergeResults(List<File> resultFiles) throws IOException {
- System.out.println("开始合并结果...");
- Map<String, Integer> finalCount = new HashMap<>();
- for (File file : resultFiles) {
- try (BufferedReader br = new BufferedReader(new FileReader(file))) {
- String line;
- while ((line = br.readLine()) != null) {
- String[] parts = line.split("\t");
- if (parts.length != 2) continue;
- String word = parts[0];
- int count = Integer.parseInt(parts[1]);
- finalCount.put(word, finalCount.getOrDefault(word, 0) + count);
- }
- }
- }
- // 写入最终结果
- try (BufferedWriter bw = new BufferedWriter(new FileWriter("final_result.txt"))) {
- for (Map.Entry<String, Integer> entry : finalCount.entrySet()) {
- bw.write(entry.getKey() + "\t" + entry.getValue());
- bw.newLine();
- }
- }
- System.out.println("✅ 合并完成!结果已写入 final_result.txt");
- }
- /**
- * 简单哈希函数(可换成更均匀的 MD5 哈希)
- */
- private static int getHashBucket(String str) {
- return Math.abs(str.hashCode()) % BUCKET_COUNT;
- }
- }
16. Java中如何定位和防止死锁?
定位死锁的方式:
(1)在终端输入jps会打印所有进程ID,输入jstack <pid>会打印相关信息,明确显示哪个线程持有哪个锁,等待哪个锁;
(2)使用Java编程的API(ThreadMXBean)监控接口用while定时检测死锁;
防止死锁:
防止死锁的核心是破坏死锁形成的四个条件:互斥、占有且等待、不可抢夺、循环等待,解决方法如下:
(1)保持锁的获取顺序一致;
(2)使用tryLock() 方法,设置尝试获取锁和持有锁的时间;
(3)使用别的替代,比如高并发容器ConcurrentHashMap、CopyOnWriteArrayList,使用CAS乐观锁或synchronzied同步锁
(4)定期用ThreadMXBean.findDeallockedThreads() 方法检测,一旦发现死锁就采取响应措施,比如报警/自动重启模块
17. 如何防止重复下单?
重复下单指同一个用户对同一商品,在短时间内重复提交支付请求,导致系统重复创建订单、重复支付、重复口库存。常见场景有:用户手抖点了两次“立即支付”;网络慢,用户刷新页面;前端、后端重试机制触发;解决方案如下:
(1)前端按钮设置“防抖”和禁止多次点击;
(2)确保幂等性:根据下单业务生成唯一标识,缓存在Redis中或利用数据库的唯一索引,以此判断订单是否已经创建;
(3)使用分布式锁,确保高并发下同一用户同一商品只有一个下单线程;
(4)支付回调添加判断逻辑,查询订单状态如果为“已支付”直接返回;
18. 用户忘记密码,系统为啥不直接提供密码,而是修改密码?
在安全系统设计中,用户密码不会以明文形式保存在数据库,而是通过不可逆哈希算法对密码加密,这也意味着,系统根本不知道你的原始密码,只能重置密码;
如果系统能“告诉你密码”,说明在数据库存储的是明文密码或是可逆加密密码,可能会造成攻击者暴力破解密码或内部人员窃取密码的风险;
重置密码的流程:用户点击“忘记密码”,系统生成随机token,通过已绑定邮箱/手机号发送,用户验证身份后重置新密码,系统保存新密码的哈希值;
现Web系统最常用的加密方式为bcrypt,基于Blowfish算法设计,采用可调节工作因子,可以设置哈希计算的复杂度,内置随机盐,保证每次生成的结果不一样,防暴力破解;
19. 怎么防止刷单?
常见的刷单手段如下:
1、同一用户多次注册:使用不同手机号、邮箱或设备注册多个账号;
2、虚假交易:通过虚拟账号下单再退款,制造销量;
3、批量请求接口:脚本或机器人刷优惠卷、抽奖、积分;
4、协同刷单:通过微信群、QQ群组织多人互刷,数据更分散、难测;
常见的技术防御手段如下:
1、身份与设备防控:限制一个身份只能绑定一个账号,防止批量注册;采集浏览器UA、分辨率、字体等识别设备唯一性;进行登录地异常检测,如用户频繁切换登陆地点,可判为风险账号;
2、行为检测:通过时间窗口内点击下单、领取优惠卷的次数校验脚本;对直接下单或批量接口调用的行为检测;添加行为验证码;
3、IP与网络风控:通过黑名单/灰名单封禁代理IP、异常请求IP;限流与防爬虫,限制接口调用频率;
20. 说一下SQL执行过程?

当客户端向MySQL发送一条SQl语句,MySQL的执行大致分为两个层面:
- Service层:负责连接管理、SQL解析、优化、缓存等逻辑处理;
- 存储引擎层:负责具体数据的存储和读取;
首先,客户端发送请求,MySQL通过连接器建立连接,对用户进行身份验证,接着优先查询缓存查找是否存在相同的SQL,如果命中缓存直接返回结果,在MySQL8之后废弃了查询缓存,因为在高发下缓存频繁失效导致性能下降;未命中缓存接着往下走,通过解析器进行词法分析和语法分析,预处理器验证表、字段是否存在,优化器生成最佳执行计划,输出最优的SQL执行路径给执行器,执行器调根据执行计划,调用存储引擎接口获取数据,将结果返回给客户端;
21. 单表数据量多少才推荐分库分表?
单表数据量多少才分库分表,这没有一个绝对数值,当有一套经验阈值和性能判断依据。分库分表主要为了解决三大问题:性能瓶颈;存储瓶颈;可用性和扩展性;
在分库分表之前可以先采用如下优化手段:
1、建立合适的索引(联合索引、覆盖索引);
2、搭建主从集群,实现读写分离;
3、冷热数据分离;
4、优化缓存层;
5、SQL优化;
只有当这些手段不足以支撑业务需求,再考虑真正的分库分表。理论上来说,当单表行数据大于1000万或单表大小大于10GB,就应该分库分表;
22. 场景题问怎么设计表?
数据库三范式是设计高效、无冗余数据表和和新准则,核心目标是减少数据冗余和避免更新异常。
第一范式:原子性,数据表中的每个字段必须是不可再分的最小数据单位。例如联系方式同时存储电话或邮箱,需要拆分成两个字段;
第二范式:完全依赖,在满足第一范式基础上,表中非主键字段必须完全依赖主键,避免部分依赖。例如订单表用订单ID+商品ID作为联合主键,“订单日期”仅依赖“订单ID”,需要将这个表拆分成“订单表”和“订单详情表”;
第三范式:传递依赖,在满足第二范式基础上,表中非主键列不能传递依赖于主键(即非主键列之间不能有依赖关系),例如用户表中存在“所在城市”,“城市邮编”,两者存在依赖关系,需拆分成用户表和城市表;
设计表的思路如下(举例学生选课系统):
(1)明确业务场景和查询需求:了解业务流程是什么,谁在用,系统的核心操作是什么。一个学生可以选多门课,一门课可以被多个学生选,能查询出学生成绩、课程老师,学生表和课程表是多对多的关系;
(2)抽取实体:将业务对象抽取成实体,比如学生、课程作为单独表,选课作为中间表;
(3)设计字段:为每个实体设计主键和根据实际业务需求设计对应字段;
(4)确定表关联字段:比如选课表的学生ID跟学生表关联,课程ID跟课程表关联;
被问到的场景问题:
(1)存在多个用户、多个角色(对应权限),怎么确定某一个用户的权限,某个角色对应的用户?
用户表和角色表之间是多对多的关系,可以建一个中间表用户角色表,用用户ID关联用户表,用角色ID关联角色表,根据用户ID查询对应多个角色,根据角色ID查询对应多个用户;
(2)购物车下单多个商家的商品,购物清单有所有商品信息,但每个商家只要对自己商铺的商品负责配送,怎么设计?
这里涉及到三级表,一级表是订单表(1对1对应下单信息),字段有支付金额、支付状态、商家数量、商品数量、下单时间等,二级表是订单商家表,根据订单Id跟订单表关联,一个订单表对应多个订单商家表,三级表是商品表,订单商家表根据商品ID关联,一个订单商家表对应多个商品表;
23. count(1),count(*),count(字段)有什么区别?
Count(*)和count(1)一样,统计表中所有记录的行数,包括 NULL 值所在的行、空值行、重复行等,count(字段) 统计指定字段非 NULL 的行数,优化器会将count(*)自动转化为count(常量),因此count(1)和count(*)性能上基本没有差别。
24. RestTemplate如何优化连接池?
RestTemplate默认每次请求都会新建一个HttpRULConnection,不支持连接复用和连接池管理,这会导致频繁的域名解析+TCP握手/挥手,从而造成性能瓶颈。优化思路是使用连接池,通常用HttpClien或OKHttp实现。
推荐使用 Apache HttpClient(或 OkHttp3),并开启连接池与超时控制。
- @Configuration
- public class RestTemplateConfig {
- @Bean
- public RestTemplate restTemplate() {
- // 1️⃣ 连接池管理器
- PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
- connectionManager.setMaxTotal(200); // 最大连接数
- connectionManager.setDefaultMaxPerRoute(50); // 每个路由的最大连接数
- // 2️⃣ 构建 HttpClient
- CloseableHttpClient httpClient = HttpClients.custom()
- .setConnectionManager(connectionManager)
- .evictIdleConnections(30, TimeUnit.SECONDS) // 定期清理空闲连接
- .disableAutomaticRetries() // 禁止自动重试(可选)
- .build();
- // 3️⃣ 设置请求工厂
- HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
- factory.setConnectTimeout(5000); // 连接超时
- factory.setReadTimeout(10000); // 读超时
- factory.setConnectionRequestTimeout(5000); // 从连接池获取连接的超时
- return new RestTemplate(factory);
- }
- }
25. SpringBoot可以同时处理多少请求?
SpringBoot内嵌Tomcat,这取决于Tomcat的线程池参数,最大工作线程数默认为200,空闲线程数默认为10,请求等待队列默认为100。因此,SpringBoot能同时处理200个请求,并允许额外100个请求排队等待。
- server:
- tomcat:
- max-threads: 200 # 最大工作线程数(默认200)
- min-spare-threads: 10 # 最小空闲线程数(默认10)
- accept-count: 100 # 请求等待队列长度(默认100)
可以在配置文件配置这些属性来提高SpringBoot的并发处理请求数量;
CPU核心数、内存大小、JVM垃圾回收策略等也会影响吞吐量;
26. Git怎么修复线上突发bug?
假设你的项目分支模型如下:
| 分支类型 | 用途 | 生命周期 |
| main / master | 线上版本 | 长期 |
| develop | 主开发分支 | 长期 |
| feature/xxx | 新功能开发 | 临时 |
| release/xxx | 预发布分支 | 临时 |
| hotfix/xxx | 线上紧急修复 | 临时 |
采取的措施为:在生产分支上创建hotfix紧急修复分支,在该分支上修复测试,测试没问题后切换到生产分支,将紧急修复分支合并到生产分支再推送远程仓库,接着切换到开发分支,将生产分支合并到开发分支同样推送远程仓库。注意每次切换都需要当前分支拉取最新代码。
27. 如何防止Springboot反编译?
SpringBoot打包后的jar或war包本质上是一个ZIP压缩包,里面包含字节码文件、第三方依赖、配置文件和静态资源,这些内容通过反编译工具就能轻易查看源码逻辑,因此发布商业项目时,防止反编译和源码泄露是一个常见的安全需求。方式如下:
✅ 方案一:使用 ProGuard 混淆
Spring Boot 可通过 ProGuard 或 yGuard 混淆 .class 文件,让反编译结果变成一堆乱码。
步骤
(1)添加 Maven 插件
- <build>
- <plugins>
- <plugin>
- <groupId>com.github.wvengen</groupId>
- <artifactId>proguard-maven-plugin</artifactId>
- <version>2.0.17</version>
- <executions>
- <execution>
- <goals>
- <goal>proguard</goal>
- </goals>
- </execution>
- </executions>
- <configuration>
- <proguardVersion>7.2.2</proguardVersion>
- <injar>${project.build.finalName}.jar</injar>
- <outjar>${project.build.finalName}-obf.jar</outjar>
- <options>
- <option>-keep public class * { public *; }</option>
- <option>-dontwarn</option>
- <option>-dontnote</option>
- <option>-dontoptimize</option>
- <option>-renamesourcefileattribute SourceFile</option>
- <option>-keepattributes *Annotation*</option>
- </options>
- </configuration>
- </plugin>
- </plugins>
- </build>
(2)打包执行
mvn clean package proguard:proguard
混淆完成后生成 xxx-obf.jar。
✅ 方案二:使用 yGuard 混淆(更适合 Spring Boot)
yGuard 支持更好,兼容性比 ProGuard 高。
- <plugin>
- <groupId>com.yworks</groupId>
- <artifactId>yguard-maven-plugin</artifactId>
- <version>3.1.0</version>
- <executions>
- <execution>
- <phase>package</phase>
- <goals>
- <goal>yguard</goal>
- </goals>
- </execution>
- </executions>
- <configuration>
- <inoutpairs>
- <inoutpair>
- <in>${project.build.finalName}.jar</in>
- <out>${project.build.directory}/${project.build.finalName}-yguard.jar</out>
- </inoutpair>
- </inoutpairs>
- <keep>
- <!-- 保留Spring Boot入口 -->
- <classes>
- <class name="com.fzz.learnitservice.LearnItServiceApplication"/>
- </classes>
- </keep>
- </configuration>
- </plugin>
28. 如何对SpringBoot配置文件敏感信息加密?
(1)把敏感信息从配置文件抽离,运行时通过ENV或-D传入(推荐与容器/与服务器结合)
(2)使用Jasypt把配置文件中的敏感信息加密,运行时提供主密码解密(适合中小项目/企业快速落地)
(3)使用Config Server管理配置,支持服务端加密或客户端加密,适合多服务场景
22万+

被折叠的 条评论
为什么被折叠?



