Java场景汇总

传承程序猿的开源精神,分享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做表连接,实现覆盖索引

  1. SELECT o.*  
  2. FROM orders o  
  3. JOIN (  
  4.     SELECT id  
  5.     FROM orders  
  6.     ORDER BY id  
  7.     LIMIT 1000000, 10  
  8. ) AS temp ON o.id = temp.id;  

(2)基于“上次最大ID”分页(游标分页),只能支持“顺序翻页”,不能直接跳转到第N页,依赖连续递增主键

  1. -- 第一页  
  2. SELECT * FROM orders ORDER BY id LIMIT 10;  
  3.   
  4. -- 下一页(上次最大id1000010  
  5. 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

  1. CREATE TABLE category (  
  2.   id BIGINT PRIMARY KEY,  
  3.   name VARCHAR(50),  
  4.   parent_id BIGINT  
  5. );  

特点:结构简单,增删改效率高;但查询整个树或子树需要多次递归查询,性能差;

(2)路径枚举:每个分类保存自己的路径

id

name

path

1

服装

1

2

男装

1/2

3

外套

1/2/3

特点:查询子节点快:where path like ‘1/2/%’; 更新复杂,移动节点要更新整条路径;

(3)闭包表:新建一张关系表,保存所有父子关系

一般包含两张表:

1️分类表(category

  1. CREATE TABLE category (  
  2.   id BIGINT PRIMARY KEY,  
  3.   name VARCHAR(100)  
  4. );  

2️⃣ 闭包表(category_closure)

  1. CREATE TABLE category_closure (  
  2.   ancestor BIGINT NOT NULL,   -- 祖先分类ID  
  3.   descendant BIGINT NOT NULL, -- 子孙分类ID  
  4.   depth INT NOT NULL,         -- 层级深度(自己到自己为0  
  5.   PRIMARY KEY (ancestor, descendant)  
  6. );  

当插入新分类时,闭包表会自动维护「祖先-子孙」关系。例如:

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 示例(基础方案)

  1. import org.apache.poi.ss.usermodel.*;  
  2. import org.apache.poi.xssf.usermodel.XSSFWorkbook;  
  3.   
  4. import java.io.*;  
  5. import java.util.*;  
  6.   
  7. public class PoiExcelExport {  
  8.     public static void main(String[] args) throws Exception {  
  9.         // 模拟数据  
  10.         List<Map<String, Object>> data = List.of(  
  11.                 Map.of("id", 1, "name""张三""score", 90),  
  12.                 Map.of("id", 2, "name""李四""score", 88)  
  13.         );  
  14.   
  15.         // 创建工作簿和表  
  16.         Workbook wb = new XSSFWorkbook();  
  17.         Sheet sheet = wb.createSheet("学生成绩");  
  18.   
  19.         // 表头  
  20.         Row header = sheet.createRow(0);  
  21.         header.createCell(0).setCellValue("ID");  
  22.         header.createCell(1).setCellValue("姓名");  
  23.         header.createCell(2).setCellValue("成绩");  
  24.   
  25.         // 数据  
  26.         int rowNum = 1;  
  27.         for (Map<String, Object> rowData : data) {  
  28.             Row row = sheet.createRow(rowNum++);  
  29.             row.createCell(0).setCellValue((Integer) rowData.get("id"));  
  30.             row.createCell(1).setCellValue((String) rowData.get("name"));  
  31.             row.createCell(2).setCellValue((Integer) rowData.get("score"));  
  32.         }  
  33.   
  34.         // 输出到文件  
  35.         try (FileOutputStream fos = new FileOutputStream("学生成绩.xlsx")) {  
  36.             wb.write(fos);  
  37.         }  
  38.   
  39.         wb.close();  
  40.         System.out.println(" Excel 导出完成!");  
  41.     }  
  42. }  

✅ 优点:控制灵活,支持合并单元格、图片、样式等
❌ 缺点:一次性加载全部数据,大文件(>10万行)容易OOM

EasyExcel 示例(企业常用)

  1. import com.alibaba.excel.annotation.ExcelProperty;  
  2. import lombok.Data;  
  3.   
  4. @Data  
  5. public class Student {  
  6.     @ExcelProperty("编号")  
  7.     private Integer id;  
  8.   
  9.     @ExcelProperty("姓名")  
  10.     private String name;  
  11.   
  12.     @ExcelProperty("成绩")  
  13.     private Integer score;  
  14. }  

  1. import com.alibaba.excel.EasyExcel;  
  2. import java.util.List;  
  3.   
  4. public class EasyExcelExport {  
  5.     public static void main(String[] args) {  
  6.         List<Student> list = List.of(  
  7.                 new Student(1, "张三", 90),  
  8.                 new Student(2, "李四", 88)  
  9.         );  
  10.   
  11.         String fileName = "学生成绩.xlsx";  
  12.         EasyExcel.write(fileName, Student.class)  
  13.                  .sheet("成绩表")  
  14.                  .doWrite(list);  
  15.   
  16.         System.out.println(" EasyExcel 导出成功!");  
  17.     }  
  18. }  

✅ 优点:

  • 支持 大数据量导出(分批写入,内存友好)
  • 代码简洁,企业项目最常用

❌ 缺点:不适合复杂样式(可扩展但较麻烦)

解决思路如下:

(1)分页查询+流式写出:不要一次性查询所有数据,而是分批分页读取数据库,写入后立即flush到磁盘,降低内存消耗;磁盘指的是磁盘临时文件,就是将数据在内存中分批处理,处理后放在磁盘临时文件,处理完生成最终的Excel.

  1. int pageSize = 10000;  
  2. int pageNum = 1;  
  3. ExcelWriter excelWriter = EasyExcel.write("导出.xlsx", Student.class).build();  
  4. WriteSheet writeSheet = EasyExcel.writerSheet("学生").build();  
  5.   
  6. while (true) {  
  7.     List<Student> data = studentMapper.selectPage(pageNum, pageSize);  
  8.     if (data.isEmpty()) break;  
  9.   
  10.     excelWriter.write(data, writeSheet);  
  11.     pageNum++;  
  12. }  
  13.   
  14. excelWriter.finish(); 

(2)异步导出+文件缓存:用户点击“导出”后,不立即生成文件,而是提交一个导出任务(后台线程池或MQ异步执行),导出后将文件存到OSS或文件管理系统,返回前端下载链接。加快接口响应,不阻塞请求,提高系统吞吐;

(3)限流:设置线程池最大线程数,防止用户同时导出多个百万级任务,确保导出请求稳定性,降低CPU占用

(4)Gzip压缩:如果导出文件较大,可以在服务器将Excel压缩成zip,降低带宽,但会增加一定的CPU压缩开销,建议配合异步操作处理;

  1. try (FileOutputStream fos = new FileOutputStream("export.zip");  
  2.      ZipOutputStream zos = new ZipOutputStream(fos)) {  
  3.     ZipEntry entry = new ZipEntry("data.xlsx");  
  4.     zos.putNextEntry(entry);  
  5.     Files.copy(Paths.get("data.xlsx"), zos);  
  6.     zos.closeEntry();  
  7. }  

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 ToolVisualVM 打开并分析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对应的状态改为“已确认登录”。

  1. POST /api/qrcode/confirmLogin  
  2. 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)变量:

  1. class Worker implements Runnable {  
  2.     private volatile boolean running = true;  
  3.   
  4.     public void run() {  
  5.         while (running) {  
  6.             // 执行任务  
  7.         }  
  8.         System.out.println("线程安全停止");  
  9.     }  
  10.   
  11.     public void stop() {  
  12.         running = false;  
  13.     }  
  14. }  

🔹 为什么要加 volatile?
否则线程可能一直读到旧值(被 CPU 缓存了),导致 stop() 调用后线程仍不退出。

双重检查锁(DCL)单例模式:

  1. class Singleton {  
  2.     private static volatile Singleton instance;  
  3.   
  4.     private Singleton() {}  
  5.   
  6.     public static Singleton getInstance() {  
  7.         if (instance == null) {  
  8.             synchronized (Singleton.class) {  
  9.                 if (instance == null) {  
  10.                     instance = new Singleton(); // 可能被指令重排!  
  11.                 }  
  12.             }  
  13.         }  
  14.         return instance;  
  15.     }  
  16. }  

如果没有 volatile,可能发生指令重排:

  1. 分配内存
  2. 引用赋值给 instance(此时对象还没构造完成)
  3. 调用构造函数

另一个线程看到 instance != null,却访问了“半初始化”的对象 → 错误!

配置刷新(动态感知变化):

在服务中某些全局配置项(如开关、阈值)需要实时生效,可以用 volatile:

  1. class Config {  
  2.     public static volatile int maxConnections = 100;  
  3. }  
  4.   
  5. // 某线程修改配置  
  6. Config.maxConnections = 200;  
  7.   
  8. // 其他线程立即看到更新  
  9. System.out.println(Config.maxConnections);  

无需加锁即可实现低延迟的配置热更新

13. 查询两百条数据耗时200毫秒,怎么在500毫秒内查询1000条数据?

       数据库查询是IO密集型操作,单线程往往会造成CPU等待磁盘IO,CPU利用率低。因此可以创建线程池,并发查询数据库,提高整体吞吐量。设置线程池的核心线程数为5,将1000条数据分为5份,每条线程负责查询200条数据。

  1. import java.util.*;  
  2. import java.util.concurrent.*;  
  3.   
  4. public class ParallelQueryExample {  
  5.   
  6.     private static final int THREAD_COUNT = 5;  
  7.     private static final ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);  
  8.   
  9.     public static void main(String[] args) throws Exception {  
  10.         long start = System.currentTimeMillis();  
  11.   
  12.         // 模拟分页查询参数  
  13.         List<Future<List<String>>> futures = new ArrayList<>();  
  14.   
  15.         for (int i = 0; i < THREAD_COUNT; i++) {  
  16.             int offset = i * 200;  
  17.             futures.add(executor.submit(() -> queryData(offset, 200)));  
  18.         }  
  19.   
  20.         // 汇总结果  
  21.         List<String> allData = new ArrayList<>();  
  22.         for (Future<List<String>> future : futures) {  
  23.             allData.addAll(future.get());  // 阻塞等待结果  
  24.         }  
  25.   
  26.         long end = System.currentTimeMillis();  
  27.         System.out.println("查询完成总共获取: " + allData.size() + 条数据");  
  28.         System.out.println("耗时: " + (end - start) + " ms");  
  29.   
  30.         executor.shutdown();  
  31.     }  
  32.   
  33.     // 模拟查询数据库(假设每次查询200条需要200ms  
  34.     private static List<String> queryData(int offset, int limit) {  
  35.         try {  
  36.             Thread.sleep(200); // 模拟数据库查询时间  
  37.         } catch (InterruptedException e) {  
  38.             Thread.currentThread().interrupt();  
  39.         }  
  40.   
  41.         List<String> list = new ArrayList<>();  
  42.         for (int i = 0; i < limit; i++) {  
  43.             list.add("Record-" + (offset + i));  
  44.         }  
  45.         return list;  
  46.     }  
  47. }  

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:结果合并:直接汇总所有桶的结果即可;

  1. public class BigFileCounter {  
  2.   
  3.     private static final String INPUT_FILE = "data.txt";  
  4.     private static final String TMP_DIR = "tmp";  
  5.     private static final int BUCKET_COUNT = 20; // 分桶数,可根据内存大小调整  
  6.   
  7.     public static void main(String[] args) throws Exception {  
  8.         new File(TMP_DIR).mkdirs();  
  9.         splitToBuckets();  
  10.         List<File> resultFiles = countEachBucket();  
  11.         mergeResults(resultFiles);  
  12.         System.out.println(" 全部统计完成!");  
  13.     }  
  14.   
  15.     /** 
  16.      * 第一步:按哈希分桶,将1GB文件拆成多个小文件 
  17.      */  
  18.     private static void splitToBuckets() throws Exception {  
  19.         System.out.println("开始分桶...");  
  20.         BufferedReader reader = new BufferedReader(  
  21.                 new InputStreamReader(new FileInputStream(INPUT_FILE), StandardCharsets.UTF_8), 10 * 1024 * 1024);  
  22.   
  23.         BufferedWriter[] writers = new BufferedWriter[BUCKET_COUNT];  
  24.         for (int i = 0; i < BUCKET_COUNT; i++) {  
  25.             writers[i] = new BufferedWriter(  
  26.                     new OutputStreamWriter(new FileOutputStream(TMP_DIR + "/bucket_" + i + ".txt"), StandardCharsets.UTF_8));  
  27.         }  
  28.   
  29.         String line;  
  30.         while ((line = reader.readLine()) != null) {  
  31.             String word = line.trim();  
  32.             if (word.isEmpty()) continue;  
  33.             int bucketId = getHashBucket(word);  
  34.             writers[bucketId].write(word);  
  35.             writers[bucketId].newLine();  
  36.         }  
  37.   
  38.         reader.close();  
  39.         for (BufferedWriter w : writers) w.close();  
  40.         System.out.println(" 分桶完成!");  
  41.     }  
  42.   
  43.     /** 
  44.      * 第二步:统计每个桶内部的词频 
  45.      */  
  46.     private static List<File> countEachBucket() throws IOException {  
  47.         System.out.println("开始统计每个桶...");  
  48.         List<File> resultFiles = new ArrayList<>();  
  49.   
  50.         for (int i = 0; i < BUCKET_COUNT; i++) {  
  51.             File bucketFile = new File(TMP_DIR + "/bucket_" + i + ".txt");  
  52.             if (!bucketFile.exists()) continue;  
  53.   
  54.             Map<String, Integer> countMap = new HashMap<>();  
  55.   
  56.             try (BufferedReader br = new BufferedReader(new FileReader(bucketFile))) {  
  57.                 String line;  
  58.                 while ((line = br.readLine()) != null) {  
  59.                     countMap.put(line, countMap.getOrDefault(line, 0) + 1);  
  60.                 }  
  61.             }  
  62.   
  63.             // 写入结果文件  
  64.             File resultFile = new File(TMP_DIR + "/result_" + i + ".txt");  
  65.             try (BufferedWriter bw = new BufferedWriter(new FileWriter(resultFile))) {  
  66.                 for (Map.Entry<String, Integer> entry : countMap.entrySet()) {  
  67.                     bw.write(entry.getKey() + "\t" + entry.getValue());  
  68.                     bw.newLine();  
  69.                 }  
  70.             }  
  71.   
  72.             System.out.printf(" %d 统计完成,共 %d %n", i, countMap.size());  
  73.             resultFiles.add(resultFile);  
  74.         }  
  75.         return resultFiles;  
  76.     }  
  77.   
  78.     /** 
  79.      * 第三步:合并结果文件 
  80.      */  
  81.     private static void mergeResults(List<File> resultFiles) throws IOException {  
  82.         System.out.println("开始合并结果...");  
  83.         Map<String, Integer> finalCount = new HashMap<>();  
  84.   
  85.         for (File file : resultFiles) {  
  86.             try (BufferedReader br = new BufferedReader(new FileReader(file))) {  
  87.                 String line;  
  88.                 while ((line = br.readLine()) != null) {  
  89.                     String[] parts = line.split("\t");  
  90.                     if (parts.length != 2) continue;  
  91.                     String word = parts[0];  
  92.                     int count = Integer.parseInt(parts[1]);  
  93.                     finalCount.put(word, finalCount.getOrDefault(word, 0) + count);  
  94.                 }  
  95.             }  
  96.         }  
  97.   
  98.         // 写入最终结果  
  99.         try (BufferedWriter bw = new BufferedWriter(new FileWriter("final_result.txt"))) {  
  100.             for (Map.Entry<String, Integer> entry : finalCount.entrySet()) {  
  101.                 bw.write(entry.getKey() + "\t" + entry.getValue());  
  102.                 bw.newLine();  
  103.             }  
  104.         }  
  105.   
  106.         System.out.println(" 合并完成!结果已写入 final_result.txt");  
  107.     }  
  108.   
  109.     /** 
  110.      * 简单哈希函数(可换成更均匀的 MD5 哈希) 
  111.      */  
  112.     private static int getHashBucket(String str) {  
  113.         return Math.abs(str.hashCode()) % BUCKET_COUNT;  
  114.     }  
  115. }  

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的执行大致分为两个层面:

  1. Service层:负责连接管理、SQL解析、优化、缓存等逻辑处理;
  2. 存储引擎层:负责具体数据的存储和读取;

首先,客户端发送请求,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),并开启连接池与超时控制。

  1. @Configuration  
  2. public class RestTemplateConfig {  
  3.   
  4.     @Bean  
  5.     public RestTemplate restTemplate() {  
  6.         // 1️ 连接池管理器  
  7.         PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);  
  8.         connectionManager.setMaxTotal(200); // 最大连接数  
  9.         connectionManager.setDefaultMaxPerRoute(50); // 每个路由的最大连接数  
  10.   
  11.         // 2️ 构建 HttpClient  
  12.         CloseableHttpClient httpClient = HttpClients.custom()  
  13.                 .setConnectionManager(connectionManager)  
  14.                 .evictIdleConnections(30, TimeUnit.SECONDS) // 定期清理空闲连接  
  15.                 .disableAutomaticRetries() // 禁止自动重试(可选)  
  16.                 .build();  
  17.   
  18.         // 3️ 设置请求工厂  
  19.         HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);  
  20.         factory.setConnectTimeout(5000); // 连接超时  
  21.         factory.setReadTimeout(10000);   // 读超时  
  22.         factory.setConnectionRequestTimeout(5000); // 从连接池获取连接的超时  
  23.   
  24.         return new RestTemplate(factory);  
  25.     }  

25. SpringBoot可以同时处理多少请求?

       SpringBoot内嵌Tomcat,这取决于Tomcat的线程池参数,最大工作线程数默认为200,空闲线程数默认为10,请求等待队列默认为100。因此,SpringBoot能同时处理200个请求,并允许额外100个请求排队等待。

  1. server:  
  2.   tomcat:  
  3.     max-threads: 200        # 最大工作线程数(默认200  
  4.     min-spare-threads: 10   # 最小空闲线程数(默认10  
  5.     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 插件

  1. <build>  
  2.   <plugins>  
  3.     <plugin>  
  4.       <groupId>com.github.wvengen</groupId>  
  5.       <artifactId>proguard-maven-plugin</artifactId>  
  6.       <version>2.0.17</version>  
  7.       <executions>  
  8.         <execution>  
  9.           <goals>  
  10.             <goal>proguard</goal>  
  11.           </goals>  
  12.         </execution>  
  13.       </executions>  
  14.       <configuration>  
  15.         <proguardVersion>7.2.2</proguardVersion>  
  16.         <injar>${project.build.finalName}.jar</injar>  
  17.         <outjar>${project.build.finalName}-obf.jar</outjar>  
  18.         <options>  
  19.           <option>-keep public class * { public *; }</option>  
  20.           <option>-dontwarn</option>  
  21.           <option>-dontnote</option>  
  22.           <option>-dontoptimize</option>  
  23.           <option>-renamesourcefileattribute SourceFile</option>  
  24.           <option>-keepattributes *Annotation*</option>  
  25.         </options>  
  26.       </configuration>  
  27.     </plugin>  
  28.   </plugins>  
  29. </build>  

(2)打包执行

mvn clean package proguard:proguard

混淆完成后生成 xxx-obf.jar。

方案二:使用 yGuard 混淆(更适合 Spring Boot

yGuard 支持更好,兼容性比 ProGuard 高。

  1. <plugin>  
  2.     <groupId>com.yworks</groupId>  
  3.     <artifactId>yguard-maven-plugin</artifactId>  
  4.     <version>3.1.0</version>  
  5.     <executions>  
  6.         <execution>  
  7.             <phase>package</phase>  
  8.             <goals>  
  9.                 <goal>yguard</goal>  
  10.             </goals>  
  11.         </execution>  
  12.     </executions>  
  13.     <configuration>  
  14.         <inoutpairs>  
  15.             <inoutpair>  
  16.                 <in>${project.build.finalName}.jar</in>  
  17.                 <out>${project.build.directory}/${project.build.finalName}-yguard.jar</out>  
  18.             </inoutpair>  
  19.         </inoutpairs>  
  20.         <keep>  
  21.             <!-- 保留Spring Boot入口 -->  
  22.             <classes>  
  23.                 <class name="com.fzz.learnitservice.LearnItServiceApplication"/>  
  24.             </classes>  
  25.         </keep>  
  26.     </configuration>  
  27. </plugin>  

28. 如何对SpringBoot配置文件敏感信息加密?

(1)把敏感信息从配置文件抽离,运行时通过ENV或-D传入(推荐与容器/与服务器结合)

(2)使用Jasypt把配置文件中的敏感信息加密,运行时提供主密码解密(适合中小项目/企业快速落地)

(3)使用Config Server管理配置,支持服务端加密或客户端加密,适合多服务场景

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值