Skip to content

---

[!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 并配置全局策略。对于匹配规则,只有 STRICTSTANDARDLOOSE 三个,一般推荐 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] 注意事项

  1. 命名建议:为了不和 MyBatis 的 Mapper 冲突,建议将 MapStruct 的接口命名为 XXXConverterXXXMapping

  2. 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+B2C3*2 递归求值左右子节点,执行运算符
FunctionNode SUM(A1:A10) 从注册表获取函数实现,传入参数求值
CellRefNode A1Sheet2!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)或监控时可以快速发现哪个线程出了问题,进而逐步确定问题环节,缩小范围。

命名方式

  1. 利用 Guava 的 ThreadFactoryBuilder(个人偏向,链式调用更优雅)。

  2. 实现 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 + 服务端版本补偿

  1. 客户端通过 localStorage 记录最近的增量操作日志(带有唯一 Sequence ID)。

  2. 服务端重启后,客户端自动上报本地最大版本号。

  3. 服务端发现版本空洞,触发重放(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(增量逻辑)

  • 数据的变动量即增加了什么改动了什么等