Skip to main content

本地存储与并发锁工具

本节介绍前端框架提供的两类基础设施:

  • LocalStorage 包装类:用于安全地读写本地存储
  • 一组并发锁工具:ReentrantLock、ReadWriteLock、StampedLock、CountDownLatch、CyclicBarrier 等

这些工具在复杂前端场景下(多请求并发、Web Worker 协作、跨组件共享状态)能显著简化代码。

1. 本地存储封装:LocalStorage​

框架对浏览器的 localStorage 做了一层简单的类型安全封装,避免:

  • 重复的 JSON.stringify/parse 代码
  • key 命名分散、难以统一管理

具体实现可在 projects/framework/src/lib/local.storage.ts 中查看,这里只给出使用示例。

import {LocalStorage} from '@rydeen/angular-framework';

const storage = new LocalStorage('APP_'); // 可选前缀

storage.set('token', 'xxx');
const token = storage.get<string>('token');

storage.setObject('profile', { name: 'Alice', age: 18 });
const profile = storage.getObject<{ name: string; age: number }>('profile');

建议在应用中再封装一层「领域化」的存储服务,例如 AuthStorageService、SettingsStorageService,统一管理 key 与结构。

2. 并发锁工具​

在传统前端开发中,我们较少显式使用「锁」的概念,但在以下场景中却非常有用:

  • 避免同一段初始化逻辑被多次并发执行(如数据库 schema 加载)
  • 多个异步任务需要等待同一批操作完成后再继续(如多接口聚合)
  • 在 Web Worker / Service 层控制对某些资源的串行访问

框架提供的锁工具 API 设计上大量借鉴了 Java java.util.concurrent 包中的类,便于后端开发者迁移思维模式。

2.1 ReentrantLock(可重入锁)​

import {ReentrantLock} from '@rydeen/angular-framework';

const lock = new ReentrantLock();

async function criticalSection() {
await lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
}

生活化类比

  • 想象你家厨房只有一把煤气灶,做饭时需要「轮流进厨房」:
    • 谁先拿到门口的钥匙,谁就先进去做菜;
    • 做完以后把钥匙放回去,下一个人才能进去。
  • 这把「厨房钥匙」就是一把最简单的互斥锁(ReentrantLock),保证同一时间只有一个人操作煤气灶,避免混乱和危险。

适用的前端业务场景

  • 数据库迁移 / 本地初始化:确保迁移脚本、IndexedDB 初始化只会有一个任务在跑,避免并发执行导致重复写入或锁死。
  • 全局缓存刷新:在同一时间只允许一个请求去刷新大缓存,其它请求直接复用结果或等待,避免服务端被瞬间打爆。
  • 关键配置更新:比如 App 主题、语言、全局设置的变更,保证同一时间不会有两个并发更新流程互相覆盖。

带来的提升

  • 从「用各种 flag/if 控制时序」变成「显式声明这段代码‘同一时间只能跑一次’」,业务意图更清晰,AI 和人都更容易读懂与重构。

2.2 ReadWriteLock(读写锁)​

在读多写少的场景中,可以用读写锁提升并发度:

import {ReadWriteLock} from '@rydeen/angular-framework';

const lock = new ReadWriteLock();

async function readConfig() {
await lock.readLock();
try {
// 允许多个读取同时进行
} finally {
lock.readUnlock();
}
}

async function updateConfig() {
await lock.writeLock();
try {
// 写操作期间会阻塞所有其它读写
} finally {
lock.writeUnlock();
}
}

生活化类比

  • 图书馆的阅览室:
    • 读者很多,大家可以同时看同一本书(读锁,共享);
    • 但当管理员要「更新/修复」这本书时,必须先把书收回来,这时所有读者都暂时不能看(写锁,独占)。
  • 读写锁就是把这种规则用代码表达出来:多数情况下大家都可以并发读取,只有在修改时才会短暂阻塞所有读写。

适用的前端业务场景

  • 全局配置/字典数据:例如从服务端拉取的业务配置(菜单配置、权限配置、字典项等),大部分时间只是读,偶尔刷新。
  • 本地缓存视图:多个组件同时读取同一份缓存(如用户信息、主题配置)时,刷新缓存的逻辑可以使用写锁保护,避免在更新过程中读到一半。

带来的提升

  • 比全局互斥锁更「温柔」:
    • 大量读请求之间不会互相阻塞;
    • 只有真正需要写时,才会短暂停止其它访问。
  • 在高并发页面(多组件依赖同一份配置)下,可以同时保证正确性与响应速度。

2.3 CountDownLatch / CyclicBarrier​

当你需要「等待 N 个异步任务全部完成」时,可以使用 CountDownLatch:

import {CountDownLatch} from '@rydeen/angular-framework';

async function loadAll() {
const latch = new CountDownLatch(3);

this.loadMenu().finally(() => latch.countDown());
this.loadUsers().finally(() => latch.countDown());
this.loadSettings().finally(() => latch.countDown());

await latch.await(); // 等待三个任务全部完成
// 继续后续流程
}

CyclicBarrier 则适合「阶段性汇合点」的场景,例如多 Worker 在每一轮计算结束后统一对齐状态。

生活化类比

  • CountDownLatch:像「集齐三个人就开饭」——
    • 你约了 3 个朋友吃火锅,只有当三个人都到达后才正式开锅;每来一个就在清单上打勾,直到勾满。
  • CyclicBarrier:像「游戏副本里的下一阶段倒计时」——
    • 多个玩家分别在不同区域打怪,当所有人都完成当前阶段后,系统才一起进入下一阶段,这个过程会重复多轮。

适用的前端业务场景

  • CountDownLatch:
    • 多接口聚合首页:首页需要菜单、用户信息、配置三类数据,全部准备好后再一次性展示,避免闪烁与多次布局抖动。
    • 预加载资源:等待几类图片/配置/缓存全部初始化后再进入主界面。
  • CyclicBarrier:
    • 多 Web Worker 协作计算:比如在离线模式下做本地大数据统计,每一轮统计需要多个 Worker 各自处理一部分数据,全部完成后统一合并结果,再开始下一轮。

带来的提升

  • 让「等所有人都准备好再开始」这种业务规则有了显式表达,而不是到处散落 Promise.all、回调地狱或一堆状态 flag。
  • 对 AI 和后续维护者来说,一看到 CountDownLatch / CyclicBarrier,就能快速理解这是在做「聚合点/阶段同步」,便于自动生成和重构。

3. 场景示例:数据库迁移与锁​

在 cross-platform-app 中,数据库迁移与查询构建较为复杂,通常需要保证:

  • 迁移脚本只执行一次
  • 不同模块在访问数据库时遵循某种顺序或隔离策略

可以使用锁工具将这些约束显式地表达出来,而不是依赖「约定」:

// 伪代码示例
export class MigrationService {
private readonly lock = new ReentrantLock();

async migrateIfNeeded() {
await this.lock.lock();
try {
const done = await this.checkMigrationDone();
if (!done) {
await this.runMigrations();
}
} finally {
this.lock.unlock();
}
}
}

4. 小结​

与传统「完全依赖 Promise / Observable,不显式使用锁」的写法相比:

  • 意图更清晰:看到 lock / latch 就能立即明白这是在控制并发,而不是一堆杂乱的 if / flag。
  • 迁移成本低:后端同学可以几乎原样搬运 Java 里常见并发模式到前端。
  • 更易维护:对于复杂场景,显式锁要比隐式的「时序假设」更容易排查问题与重构。