---
[!note] 😎😎 当笔记出来的时候证明我已撤退,目前已完成接近400+的
commit,由衷感谢遇到的每一个人,让我有不断深入的可能。
📚 PMS 技术开发手册
Tags: #Project/PMS #Java/Standard #Guide #updating
1. 🛠️ 数据处理与对象映射
领域定义:涉及 DTO/Entity 转换、JSON 序列化、字段过滤等数据流转处理核心逻辑。
1.1 对象映射工具
🔹 ModelMapper
Tags: #Java/Lib #Tool/Mapping
引入依赖 org.modelmapper 后,对于 DTO 之间的转换非常方便。
-
基础用法:
当两个类中字段一致情况下,直接使用:
Java modelMapper.map(Entity, DTO.class); -
全局配置 (Bean):
建议注册为 Bean 并配置全局策略。对于匹配规则,只有
STRICT、STANDARD、LOOSE三个,一般推荐 STRICT 以避免模糊匹配导致的错误。Java modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); -
自定义映射 (TypeMap):
对于某些特定字段的匹配,可以使用如下形式:
Java
Java TypeMap<User, UserDTO> typeMap = modelMapper.createTypeMap(User.class, UserDTO.class); typeMap.addMappings(mapper -> { // 将 user.getFirstName() 映射到 userDto.setGivenName() mapper.map(User::getFirstName, UserDTO::setGivenName); // 跳过某个字段 mapper.skip(UserDTO::setPassword); });
🔹 MapStruct (推荐)
Tags: #Java/Lib #Tool/Mapping #Performance
MapStruct 在编译时生成代码,性能远高于反射机制的 ModelMapper。
-
依赖配置:
需要引入
org.mapstruct,且必须在 Maven 插件中配置,因为需要参与编译过程(Annotation Processing):XML
Java <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> -
基础用法:
字段名完全一致时,只需定义接口:
Java @Mapper(componentModel = "spring") public interface UserConverter { UserDTO toDto(UserEntity entity); } -
进阶用法 (字段不一致):
Java @Mapper(componentModel = "spring") public interface UserConverter { @Mapping(source = "fullName", target = "name") // 明确忽略敏感字段 @Mapping(target = "password", ignore = true) UserDTO toDto(UserEntity entity); }
[!WARNING] 注意事项
命名建议:为了不和 MyBatis 的 Mapper 冲突,建议将 MapStruct 的接口命名为
XXXConverter或XXXMapping。Lombok 冲突:如上 XML 所示,务必确保在
maven-compiler-plugin中,Lombok 的处理器在 MapStruct 之前。
1.2 序列化与字段控制 (Serialization)
Tags: #Java/Json #Spring/Data
🔹JsonInclude 去除底噪
当遇到空噪音字段时,使用注解过滤:
@JsonInclude(JsonInclude.Include.NON_NULL):用于去除null字段,减少日志体积或 API 响应大小。
🔹乐观锁版本控制
@Version:对于一些特殊的字段(如version),使用此注解避免每次手动++。save操作时数据库字段会自动自增,用于实现乐观锁。
1.3 Excel 文件解析
Tags: #Java/File #Performance #Tool/POI #Tool/EasyExcel
1.3.1 Excel 文件处理与 MultipartFile API
领域定义:处理外部文件上传,利用 Spring 提供的
MultipartFile接口进行流化解析,并映射为业务 DTO。
🔹 MultipartFile 核心 API 详解
当你通过 Controller 接收上传的文件时,MultipartFile 是最核心的载体。结合源码,其常用方法含义如下:
-
getName(): 返回请求参数的名称(即input标签的name属性)。在多文件上传时,通过此值区分不同的业务文件(如:身份证正反面、简历与附件等)。 -
getOriginalFilename(): 获取文件原始名称(如用户列表.xlsx)。解析前通常用它来校验文件后缀。 -
getContentType(): 获取文件 MIME 类型。Excel 通常是application/vnd.openxmlformats-officedocument.spreadsheetml.sheet。 -
getSize(): 获取文件大小(字节)。建议在解析前判断,防止超大文件导致 OOM。 -
getInputStream(): 核心方法。获取文件的输入流,直接对接 POI 进行解析,无需先存磁盘,实现零拷贝。 -
transferTo(File dest): 将上传的文件直接保存到服务器本地磁盘(用于存档)。
1.3.2 基于 Apache POI 的深度解析策略
当需要获取单元格样式(如背景色、边框、字体)及复杂公式时,必须使用 POI 的 DOM 解析模式。
🔹 核心对象层级
-
Workbook: 整个 Excel 文档。 -
Sheet: 单个工作表(对应源码中的poiSheet)。 -
Row: 行。 -
Cell: 单元格,包含数据值和CellStyle(样式信息)。
🔹 样式与公式提取 (代码参考)
根据源码逻辑(单元格处理详见 `org.apache.poi.ss.usermodel` 中关于API的设计),可以进一步提取样式信息:
private void parseSheet(Sheet poiSheet, String documentId) {
for (Row row : poiSheet) {
if (row == null || row.getLastCellNum() <= 0) continue;
for (Cell cell : row) {
// 1. 提取单元格样式
CellStyle style = cell.getCellStyle();
// 获取背景颜色索引 (对应你的解析场景)
short bgColor = style.getFillForegroundColor();
// 2. 提取单元格公式
if (cell.getCellType() == CellType.FORMULA) {
String formula = cell.getCellFormula();
// 存入你的 formulaBuffer
}
}
}
}
1.3.3 多文件上传与业务逻辑分拣
在多文件场景下,必须利用 getName() 进行逻辑分发。
[!TIP] 多文件上传经验
显式参数映射:
public void upload(@RequestParam("file1") MultipartFile f1, @RequestParam("file2") MultipartFile f2)。Spring 会自动根据getName()匹配。动态列表处理:如果是
List<MultipartFile> files,则需遍历并判断getName():
Java for (MultipartFile file : files) { if ("idCardFront".equals(file.getName())) { /* 处理正面 */ } if ("resume".equals(file.getName())) { /* 处理简历 */ } }
将样式享元池作为 POI 解析的性能优化案例,最合适的做法是将其作为 1.3.4 小节进行补充。
这样安排不仅能完善你的数据处理章节,使其具备从“接口定义”到“底层解析”再到“高级优化”的完整逻辑链条,还能与第四章的设计模式理论形成完美的“实战联动”。
以下是为你整理的补充内容,结合了你提供的 CellStyleFactory 源码:
1.3.4 性能优化:样式享元池 (Style Flyweight Pool)
Tags: #Optimization #Memory #DesignPattern/Flyweight
在深度解析 Excel(如提取颜色、字体等样式)时,单元格样式具有极高的重复性。若为每个单元格独立创建样式对象,将导致严重的资源浪费。
🔹 为什么要使用享元池?
-
内存占用:POI 解析时,每个
Cell若都关联一个重复的CellStyle包装对象,会迅速撑爆堆内存。 -
句柄限制:Excel 文件本身对唯一样式总数有限制。
-
解决之道:利用享元模式,通过“无则创建,有则复用”的原则,将具有相同特征的样式进行内存级共享。
🔹 核心实现:CellStyleFactory
如享元池案例,通过 ConcurrentHashMap 维护一个全局唯一的样式池:
public class CellStyleFactory {
// 享元池:存储已解析出的唯一共享样式,SharedStyle为定义的record样式对象
private static final Map<SharedStyle, SharedStyle> STYLE_POOL = new ConcurrentHashMap<>();
// 全局默认样式(兜底方案)
public static final SharedStyle DEFAULT_STYLE = new SharedStyle(
"宋体", 11, false, false, null, null, null, false, null, null
);
/**
* 无则创建,有则复用 (享元模式核心实现)
* @return 内存池中唯一可复用的共享对象
*/
public static SharedStyle getSharedStyle(SharedStyle prototype) {
if (prototype == null) return DEFAULT_STYLE;
// 利用 computeIfAbsent 确保相同特征的样式只在池中存在一份
return STYLE_POOL.computeIfAbsent(prototype, key -> key);
}
}
🔹 业务实践建议
-
指纹计算:
SharedStyle类必须重写hashCode()和equals()方法,确保编译器能准确识别“属性完全一致”的样式对象。 -
分批处理:在
parseSheet循环中解析单元格时,应先构建临时的prototype样式对象,通过getSharedStyle转换为池化对象后,再存入batchBuffer进行后续处理。
1.4 Excel 公式引擎
Tags: #Excel/Formula #AST #GraphTheory #Performance #Optimization
1.4.1 公式语义分析 (AST 构建与求值)
🔹 为什么用 AST?
- 通过 SPI(服务发现机制)和 静态代码块自动注册的方式,配合函数式接口实现公式逻辑的动态注入,计算引擎只负责解析 AST 树和调度,具体的算子(Operator)完全解耦
- DFS 检测环,利用 拓扑排序(Kahn 算法)确定计算优先级
AST 将公式转换为树形结构,通过递归求值天然支持复杂表达式。
例如 =SUM(A1:B2)*0.1+C3 的 AST 树:
BinaryOpNode(+)
/ \
BinaryOpNode(*) CellRefNode(C3)
/ \
FunctionNode LiteralNode(0.1)
(SUM)
🔹 核心抽象类:AstNode
所有节点继承统一接口,实现多态求值:
public abstract class AstNode {
// 基于上下文计算节点值
public abstract Object evaluate(Map<String, Object> cellValues);
// 收集依赖单元格(用于构建依赖图)
public abstract Set<String> collectDependencies();
}
关键子类:
| 节点类型 | 用途 | evaluate 逻辑 |
|---|---|---|
BinaryOpNode |
A1+B2、C3*2 |
递归求值左右子节点,执行运算符 |
FunctionNode |
SUM(A1:A10) |
从注册表获取函数实现,传入参数求值 |
CellRefNode |
A1、Sheet2!B5 |
从 cellValues 或内存/MongoDB 加载值 |
LiteralNode |
123、"text" |
直接返回字面值 |
🔹 函数注册机制:静态映射池
通过 AstFormulaRegistry 实现零反射的函数调用:
private static final Map<String, AstFormula> FUNCTIONS = new HashMap<>();
static {
AstFormulaEngine engine = new AstFormulaEngine();
FUNCTIONS.put("SUM", engine::sum);
FUNCTIONS.put("VLOOKUP", engine::vlookup);
FUNCTIONS.put("IF", engine::ifFunc);
// ... 50+ 标准 Excel 函数
}
public static AstFormula get(String name) {
return FUNCTIONS.get(name.toUpperCase());
}
✅ 设计优势:
- 编译时绑定,无反射开销
- 新增函数只需实现方法并注册
- 支持 Java 8 方法引用,代码简洁
1.4.2 依赖拓扑管理 (有向图 + 环路检测)
🔹 全局依赖图构建时机
在用户首次加入文档时异步构建:
// JoinFileMessageHandler.java
CompletableFuture.runAsync(() -> {
List<Sheet> allSheets = excelService.getAllSheets(documentId);
dependencyGraphManager.buildFullGraph(documentId, allSheets);
});
依赖图数据结构:
public static class DocumentGraph {
// 正向:cellKey -> 它依赖的单元格集合
Map<String, Set<String>> dependencies = new ConcurrentHashMap<>();
// 反向:cellKey -> 依赖它的单元格集合
Map<String, Set<String>> dependents = new ConcurrentHashMap<>();
// 公式缓存:cellKey -> 公式字符串
Map<String, String> formulas = new ConcurrentHashMap<>();
}
cellKey 格式:sheetId:cellRef(如 64a2f1b3c:A1)或 sheetId:RANGE:A1:B10。
🔹 拓扑排序:保证计算顺序
当 A1 更新时,使用 Kahn 算法对受影响公式排序:
private List<String> topologicalSortDependents(String documentId, Set<String> dependents) {
Map<String, Integer> inDegree = new HashMap<>();
// 1. 计算每个节点的入度
for (String depKey : dependents) {
Set<String> deps = documentDependencyGraphManager
.getDependencies(documentId, depKey)
.stream()
.filter(dependents::contains) // 只考虑受影响的子图
.collect(Collectors.toSet());
inDegree.put(depKey, deps.size());
}
// 2. 入度为 0 的节点入队
Queue<String> queue = new LinkedList<>();
inDegree.entrySet().stream()
.filter(e -> e.getValue() == 0)
.forEach(e -> queue.offer(e.getKey()));
// 3. Kahn 算法
List<String> result = new ArrayList<>();
while (!queue.isEmpty()) {
String current = queue.poll();
result.add(current);
// 减少依赖当前节点的节点入度
for (String dependent : dependents) {
if (getDeps(dependent).contains(current)) {
int newDegree = inDegree.merge(dependent, -1, Integer::sum);
if (newDegree == 0) queue.offer(dependent);
}
}
}
return result;
}
🔹 循环依赖检测与容错
if (result.size() < dependents.size()) {
log.warn("⚠️ 检测到循环依赖,将剩余节点按字典序添加");
Set<String> remaining = new HashSet<>(dependents);
result.forEach(remaining::remove);
result.addAll(remaining.stream().sorted().toList());
}
✅ 容错策略:即使存在循环依赖,也强制完成所有计算(可能产生
#REF!错误值),而非系统崩溃。⚠️ 实战经验:循环依赖通常由用户误操作(如
A1=B1+1, B1=A1*2),前端应配合高亮提示。
1.4.3 计算优化策略
🔹 懒加载:大范围引用的流式处理
触发阈值:当引用单元格数 > 50,000 时启用。
// CellRefNode.evaluate() 中的判断
if (rows * cols > 50000) {
boolean inLookup = Arrays.stream(Thread.currentThread().getStackTrace())
.anyMatch(e -> Set.of("vlookup", "hlookup", "lookup").contains(
e.getMethodName().toLowerCase()));
if (inLookup) {
// 返回懒加载代理,而非立即展开
return new LazyLookupRange(sheetId, startRow, endRow, startCol, endCol,
() -> mongoArchiveService);
}
}
流式 VLOOKUP 实现(分批加载 + 按需查询):
public Object streamingVLookup(LazyLookupRange range, Object lookupValue,
int colIndex, boolean exactMatch) {
int batchSize = 1000; // 每批 1000 行
for (int bStart = range.startRow(); bStart <= range.endRow(); bStart += batchSize) {
int bEnd = Math.min(bStart + batchSize - 1, range.endRow());
// 仅加载查找列
List<String> lookupRefs = IntStream.rangeClosed(bStart, bEnd)
.mapToObj(row -> FormulaUtils.cellPositionToRef(row, lookupCol))
.toList();
Map<String, Cell> batch = range.getMongoService()
.getCellsByRefs(range.sheetId(), lookupRefs);
// 在当前批次中查找
for (int row = bStart; row <= bEnd; row++) {
String ref = FormulaUtils.cellPositionToRef(row, lookupCol);
if (matches(batch.get(ref), lookupValue)) {
// 找到匹配后,单独加载返回列
String returnRef = FormulaUtils.cellPositionToRef(row, returnCol);
return mongoService.getCellsByRefs(sheetId, List.of(returnRef))
.get(returnRef).getValue();
}
}
}
return FormulaError.NA.getString();
}
✅ 性能数据:百万行数据的 VLOOKUP,内存占用从 ~2GB 降至 ~50MB,耗时从超时优化至 <500ms。
⚠️ 注意:
batchSize=1000是经验值,需根据实际单元格大小调整(如单元格含富文本可调至 500)。
🔹 计算快照:版本化隔离并发修改
核心 API:
// 1. 创建计算快照(冻结依赖单元格状态)
long snapshotVersion = snapshot.snapshotDependencies(operationId, allNeededCells);
snapshot.registerCalculation(cellRef, snapshotVersion, operationId);
// 2. 从快照读取数据(隔离并发修改)
Cell cell = snapshot.readFromSnapshot(operationId, cellKey);
// 3. 计算完成后释放
snapshot.releaseCalculationSnapshot(operationId);
snapshot.unregisterCalculation(cellRef, operationId);
版本过期检测(避免基于脏数据的计算):
for (String depKey : sortedCells) {
if (snapshot.isVersionStale(snapshotVersion)) {
log.warn("检测到新版本,放弃剩余 {} 个公式的计算", sortedCells.size() - i);
break; // 终止过期计算
}
// 执行公式计算...
}
✅ 并发安全保证:即使 100 个用户同时编辑同一文档,每次计算都基于"创建快照时刻的数据",避免读到中间态。
⚠️ 内存管理:快照在计算完成后立即释放。若计算链路过长(如依赖 1000+ 公式),需监控内存占用。
🔹 批量更新与 Redis 同步
发送端(计算完成后批量通知):
List<Operation> operations = new ArrayList<>();
// 添加主单元格 + 所有受影响公式
operations.add(Operation.builder()
.operationType("UPDATE_CELL")
.cellData(new Cell(mainCell))
.build());
for (FormulaCell fc : affectedFormulas) {
operations.add(Operation.builder()
.cellData(memoryDataManager.getCell(sheetId, fc.getCellRef()))
.build());
}
// 🔑 携带版本号发送
long currentVersion = snapshot.getCurrentVersion();
redisSyncService.notifyBatchOperations(documentId, sheetId, operations, currentVersion);
接收端(原子性应用批量操作):
private void handleBatchOperation(BatchOperationMessage message) {
DocumentSnapshot snapshot = snapshotManager.getOrCreate(documentId);
// 应用所有操作
for (CellUpdateMessage msg : message.getOperations()) {
snapshot.getLatestCellState().put(cellKey, operation);
memoryDataManager.updateCell(sheetId, msg.getCellRef(), cell);
}
// 🔑 原子性同步版本号
snapshot.getGlobalVersion().set(message.getSnapshotVersion());
snapshot.confirmVersionSync(message.getSnapshotVersion());
}
✅ 版本号作用:
- 发送端:标识这批操作对应的快照版本
- 接收端:验证是否有中间版本丢失(若跳号则触发全量同步)
- 并发控制:后到达的旧版本消息会被自动忽略
1.4.4 性能
🔹公式缓存:LRU 淘汰策略
private final Map<String, FormulaParser.ParsedFormula> formulaCache =
Collections.synchronizedMap(new LinkedHashMap<>(1000, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<...> eldest) {
return size() > 1000; // 最多缓存 1000 个公式
}
});
缓存键设计:直接使用公式字符串(如 =SUM(A1:A10)),而非 cellRef,因为不同单元格可能有相同公式。
🔹上下文累积更新
for (String depKey : sortedCells) {
CalculationResult result = calculate(formulaCell, accumulatedContext);
// 🔑 计算完成后立即更新上下文,供下一个公式使用
accumulatedContext.put(depCellRef, result.result());
globalCache.put(depCellRef, result.result());
}
反例:若等所有公式计算完再统一更新上下文,后续公式会读到旧值。
🔹日志采样(避免洪流)
if (successCount < 3 || ThreadLocalRandom.current().nextDouble() < 0.005) {
log.debug("计算完成: cellRef={}, result={}", depCellRef, newValue);
}
🔹已知限制
- 循环依赖:检测机制无法阻止用户创建,只能容错计算并返回错误值。
- 超大范围公式:
SUM(A:Z)这种全列引用即使懒加载也会触发 MongoDB 慢查询(建议前端限制或提示用户)。 - 版本冲突:极端高并发下(如 200+ 用户同时编辑同一单元格),可能出现快照版本竞争,当前采用"后写覆盖"策略。
📊 完整计算流程图
用户编辑 A1
↓
AST 解析 & 立即计算 A1
↓
依赖图查询 → getDependents("sheetId:A1") → [B2, C3, D4]
↓
拓扑排序(Kahn 算法)→ [B2 → C3 → D4]
↓
创建计算快照 (版本 V123,冻结 A1、依赖的其他单元格)
↓
顺序计算:
B2 (从快照读取 A1 值) → 更新上下文 & 内存
C3 (从快照读取 A1、B2 值) → 更新上下文 & 内存
D4 (从快照读取 A1、C3 值) → 更新上下文 & 内存
↓
批量保存 + Redis 广播 (携带版本号 V123)
↓
释放计算快照
↓
其他节点接收消息 → 原子性应用批量操作 + 同步版本号
1.5 数据一致性处理预研
数据一致性
‘内存快照 + 客户端增量日志(Redo Log)’ 方案,服务端异步刷盘提升吞吐量,利用版本号(Sequence ID)处理冲突,实现最终一致性
2. 日志与可观测性
领域定义:涉及系统输出规范、日志分级标准及洪峰流量控制。
2.1 核心日志等级规范
Tags: #Log/Standard
由高向下优先级:
TRACE -> DEBUG -> INFO -> WARN -> ERROR -> FATAL/OFF
2.2 日志洪流治理 (Log Sampling)
Tags: #Log/Optimization
场景:关于大量循环日志的抽样检查 (避免日志洪流打满磁盘)。
方案:如果只是调试使用,可以通过以下线程随机采样的方法进行操作。通过对样本抽样的概率进行调试,确保日志数量稳定。
Java
// 示例:仅打印前几次或 5% 的概率打印
if (i++ < 10 || ThreadLocalRandom.current().nextDouble() < 0.05) {
log.info("💡[addOperation] key={}, opType={}", key, operation.getOperationType());
}
3. 并发编程与性能
领域定义:涉及线程池管理、线程命名及参数调优策略。
3.1 线程命名
Tags: #Java/Thread #Debug
原则:在项目实际开发中,必须手动为线程进行命名。
目的:确保调试(jstack)或监控时可以快速发现哪个线程出了问题,进而逐步确定问题环节,缩小范围。
命名方式:
-
利用 Guava 的
ThreadFactoryBuilder(个人偏向,链式调用更优雅)。 -
实现
ThreadFactory接口(原生方式)。
3.2 线程池参数配置 (Pool Sizing)
Tags: #Java/Performance #Tuning
利用 Linux 命令 lscpu 查看核心数。根据任务类型决定核心线程数:
-
CPU 密集型任务:
-
配置:N + 1 (N 为 CPU 核心数)。
-
原理:最大化利用 CPU,减少上下文切换。
-
-
IO 密集型任务:
-
配置:M * N (M 通常 > 1,推荐 2,N 为 CPU 核心数)。
-
原理:此类任务不一直占用 CPU(大部分时间在等待 IO),因此需要更多线程来分摊等待时间。具体 M 值可根据 IO 等待时间调整。
-
3.3性能处理
调优
通过 ‘流式处理(Streaming)+ 线程池并行(Concurrency)+ 结果缓存(Caching)’ 三位,具体到文件解析,利用流处理规避 OOM,利用分片加载解决首屏白屏
4. 设计模式
领域定义:解决特定架构问题的通用解决方案。
4.1享元模式 (Flyweight Pattern)
Tags: #DesignPattern #Structural #Optimization
4.1.1. 核心定义
享元模式 是一种结构型设计模式,它通过共享技术有效地支持大量细粒度的对象,从而避免大量相似对象带来的内存开销。
4.1.2. 内部状态 vs 外部状态
理解享元模式的关键在于区分两种状态:
-
内部状态 (Intrinsic State):存储在享元对象内部,不随环境改变而改变,是可以共享的内容(例如:围棋棋子的颜色)。
-
外部状态 (Extrinsic State):随环境改变而改变,不可以共享,通常由客户端保存并传入(例如:围棋棋子的坐标位置)。
4.1.3. 代码示例 (以“棋局”为例)
假设开发一个围棋游戏,场上有几百个棋子。如果每个棋子都创建一个对象,内存会爆炸。
// 享元类:棋子颜色是共享的
class GoPiece {
private final String color; // 内部状态
public GoPiece(String color) {
this.color = color;
}
public void display(int x, int y) { // x, y 是外部状态
System.out.println("棋子颜色:" + color + " | 位置:(" + x + "," + y + ")");
}
}
// 享元工厂:负责管理共享对象
class GoPieceFactory {
private static final Map<String, GoPiece> pool = new HashMap<>();
public static GoPiece getPiece(String color) {
if (!pool.containsKey(color)) {
pool.put(color, new GoPiece(color));
}
return pool.get(color);
}
}
4.1.4. 优缺点分析
-
优点:
-
极大减少内存中对象的数量。
-
降低程序运行时的内存占用。
-
-
缺点:
-
提高了系统的复杂性(需要分离内外状态)。
-
读取外部状态会稍微增加运行时间。
-
4.2TTL-Redis(Time to live)
雪崩 #优化 #过期
对于级联依赖大于万级别的或者当前计算性能问题频出的部分,需要通过给特定表的cell键设置逻辑过期,同时需要削峰处理,避免高并发情况下发生雪崩问题,去分散流量问题。
4.3TDD
开发泛型
测试驱动开发,同时需要以SonarQube为基本开发规范,Resutful API、OpenAPI等
4.4服务器宕机快照 处理方案
预研方案
针对宕机导致快照丢失的极端情况,我打算考虑使用的预研方案是采用 客户端 Redo Log + 服务端版本补偿。
-
客户端通过
localStorage记录最近的增量操作日志(带有唯一 Sequence ID)。 -
服务端重启后,客户端自动上报本地最大版本号。
-
服务端发现版本空洞,触发重放(Replay)机制,将丢失的增量补回 DB,这样即便是‘内存快照’模式,也能达到秒级故障恢复(RPO≈0)。 当前系统将单元格引用关系建模为有向无环图(DAG),通过拓扑排序,确定‘计算链’,确保被引用的单元格永远在引用者之前完成计算,针对性能,用入度计数法(Kahn算法),避免递归导致的栈溢出。
4.5用户在使用但锁过期
过期
打算在初期采用Redisson 的看门狗(Watchdog),不断延迟TTL来预防这样的事件发生,但是对于后续规模的不断增加,这里的方案也需要调整
5. 语言特性
局部变量类型推断:var
Tags: #Java/Syntax #Java10
1. 核心定义
var 是 Java 10 引入的一个关键字(准确说是“保留类型名称”),允许编译器根据变量的初始化表达式自动推断出变量的实际类型。
2. 使用限制
-
必须初始化:由于编译器需要根据右侧的值推断类型,所以
var声明时必须立即赋值。 -
局部变量限定:只能用于方法内部的局部变量、
for循环索引或try-with-resources声明中,不能用于类成员变量或方法参数。 -
非延迟绑定:推断是在编译期完成的,一旦推断出类型,该变量的类型就固定了。
代码对比:
// 传统写法
Map<String, List<GoPiece>> pool = new HashMap<>();
// var 写法
var pool = new HashMap<String, List<GoPiece>>();
数据模型: record
Tags: #数据结构 #数据建模
对于定义只存储数据的类如DTO、VO等类型,可以采用record类型,可以极大的简化代码并提高对应处理。
// 一行搞定
public record User(Long id, String name, String email) {}
// 甚至可以在方法内部定义局部 Record
public void process() {
record TempResult(int count, double average) {}
var result = new TempResult(10, 5.5);
}
并发编程:虚拟线程问题
Tags: #虚拟线程
在处理大量IO密集型来说如调用API、查数据库等,不再建议手动维护庞大的线程池,建议:
- 写法:使用 Executors.newVirtualThreadPerTaskExecutor()。
- Java
// 为每个任务创建一个虚拟线程,轻量且高效
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
// 执行 IO 任务
});
} // 自动关闭
模式匹配instanceOf消除强转
Tags: #模式匹配 - 具体写法对比:
if (obj instanceof String) {
String s = (String) obj; // 多余的强转
System.out.println(s.length());
}
----
if (obj instanceof String s) { // 判断的同时定义变量 s
System.out.println(s.length()); // 直接使用
}
简化流操作Stream.toList()
- 在
java16之前大多使用比较多的是.collect(Collectors.toList()) Java List<String> list = Stream.of("A", "B", "C") .filter(s -> !s.isEmpty()) .toList();
6.命名处理
语境法命名:
Delta(增量逻辑)
- 数据的变动量即增加了什么改动了什么等