在这里插入图片描述

📖 引言

你有没有过这样的经历:

  • 上班路上用手机看「民族图鉴」,看到傣族的介绍,觉得有意思,收藏了。到了公司,拿出平板想接着看——哎?收藏呢?怎么没了?
  • 晚上躺床上用手机看藏族的介绍,看到一半睡着了。第二天起来,想用电脑接着看——哎?我看到哪了来着?
  • 手机上做了民族灵魂测试,结果是"傣族"。换了个新手机,重新下载「民族图鉴」——又要重新测一遍?

这些问题的本质是什么?——数据没有同步

你的数据困在一台设备里,换个设备就没了。这在"单设备时代"还行,但在"多设备时代",这就成了大问题。

鸿蒙的分布式能力,就是来解决这个问题的。

分布式——听起来很高大上,说白了就是:你的数据、你的服务,不局限在一台设备里,而是在所有设备之间流动

  • 手机上收藏,平板上立刻就能看到
  • 手机上看到一半,流转到平板接着看
  • 换了新手机,登录账号,所有数据都回来了
  • 没网的时候也能用,有网了自动同步

鸿蒙7.0(API 26)在分布式能力上做了更多增强:

  • 📊 分布式数据管理:KV存储、对象存储、订阅通知,跨设备实时同步
  • ☁️ 端云协同:云端存储、端云同步、离线可用
  • 🔄 智能冲突解决:最后写入赢、自动合并、用户选择
  • 🚀 性能优化:增量同步、压缩、WiFi-only、流量控制

「民族图鉴」作为一个文化类App,分布式同步非常重要:

  • ❤️ 收藏同步:手机上收藏,平板上也能看到
  • 📜 浏览历史同步:看到哪,同步到哪
  • 📱 跨设备接续:手机上看一半,流转到平板继续看
  • ☁️ 云备份:换手机了,数据不丢

这一篇,我们就从分布式能力全景讲起,深入解析分布式数据管理、端云协同、冲突解决、性能优化,并手把手带你实现「民族图鉴」收藏数据的多端同步。


🎯 学习目标

完成本文后,你将能够:

  • ✅ 理解鸿蒙分布式能力的全景与核心价值
  • ✅ 掌握分布式数据管理的三种模式:KV、对象、订阅
  • ✅ 理解端云协同的架构与工作原理
  • ✅ 学会冲突解决策略:最后写入赢、自动合并、用户选择
  • ✅ 掌握性能与流量优化技巧:增量同步、压缩、WiFi-only
  • ✅ 理解安全与加密机制:端到端加密、身份认证
  • ✅ 能够实现「民族图鉴」收藏数据的多端同步
  • ✅ 掌握分布式开发中的常见问题与解决方案

💡 需求分析

分布式能力全景

鸿蒙的分布式能力,不只是"数据同步"——它是一整套的"跨设备协作"框架。

我们来看看分布式能力的全景图:

┌─────────────────────────────────────────────────────────┐
│                    鸿蒙分布式能力全景                     │
├─────────────┬─────────────┬─────────────┬───────────────┤
│  分布式数据   │  分布式文件   │  分布式任务   │  分布式硬件    │
│             │             │             │               │
│ - KV 存储    │ - 文件同步   │ - 任务调度   │ - 摄像头      │
│ - 对象存储   │ - 跨设备访问 │ - 跨端流转   │ - 麦克风      │
│ - 订阅通知   │ - 共享文件夹 │ - 原子化服务 │ - 屏幕        │
│ - 端云协同   │ - 云盘      │ - 分布式计算 │ - 传感器      │
└─────────────┴─────────────┴─────────────┴───────────────┘
能力类型 说明 「民族图鉴」用途
分布式数据 跨设备数据同步,实时一致 收藏、历史、设置同步
分布式文件 跨设备文件共享、同步 民族图片、音乐同步
分布式任务 跨设备任务流转、调度 跨设备接续阅读
分布式硬件 调用其他设备的硬件 用平板音箱播放音乐

本文我们重点讲分布式数据端云协同——这是最常用、最基础的分布式能力。

分布式数据管理

分布式数据管理(Distributed Data Management,简称 DDM),是鸿蒙提供的一套跨设备数据同步框架

它的核心思想是:你只管读写本地数据,同步的事交给系统

设备 A                          设备 B
   │                              │
   │  写入数据(本地数据库)        │
   │       ↓                      │
   │  分布式数据服务               │
   │       ↓                      │
   │  自动同步 ──────────────►    │
   │                              │  分布式数据服务
   │                              │       ↓
   │                              │  写入数据(本地数据库)
   │                              │       ↓
   │                              │  通知 App 数据更新了
   │ ◄────────────────────── 自动同步
   │

你不需要管"怎么连另一台设备"、“数据怎么传”、“传失败了怎么办”——这些系统都帮你搞定了。你只需要:

  1. 把数据存到分布式数据库里
  2. 订阅数据变化通知
  3. 数据变了,更新UI

就这么简单。

分布式数据的三种模式

鸿蒙的分布式数据管理,主要有三种模式:

模式 说明 适用场景
KV 存储 键值对存储,最简单 配置、设置、小数据
对象存储 结构化对象存储,支持查询 列表、详情等结构化数据
订阅通知 发布-订阅模式 实时消息、状态同步

「民族图鉴」中:

  • KV 存储:用户设置(主题、语言)、阅读进度
  • 对象存储:收藏列表、浏览历史
  • 订阅通知:跨设备消息通知

端云协同

端云协同是什么?——就是本地设备 + 云端服务器,一起协同工作

只有端到端同步还不够,因为:

  • 你不可能同时把所有设备都开着
  • 换了新设备,老设备的数据怎么过来?
  • 设备都丢了,数据怎么办?

所以还需要一朵"云"——把数据同步到云端,作为"中枢":

设备 A ────┐
           │
设备 B ────┼──► 云端 ───► 所有设备同步
           │
设备 C ────┘

端云协同的优势

优势 说明
多设备同步 所有设备的数据都通过云端同步
数据备份 数据存在云端,不会丢
离线可用 没网的时候用本地数据,有网了自动同步
跨账号 登录同一个账号,数据就跟着你走

💡 端云 vs 点对点

你可能会问:鸿蒙不是有"点对点"的分布式吗?为什么还要云?

答案是:两者是互补的。

  • 点对点(端到端):两台设备在一起时,直接同步,速度快、不需要流量
  • 端云协同:设备不在一起时,通过云端中转,随时同步

鸿蒙的分布式是"近场点对点 + 远端云同步"结合的——设备近了走直连,远了走云端。
两者无缝切换,用户感知不到。

「民族图鉴」分布式应用场景

结合「民族图鉴」的产品特性,我们设计了这些分布式场景:

场景1:收藏同步(核心场景)

场景描述
用户在手机上收藏了傣族,拿出平板打开「民族图鉴」,收藏列表里自动就有傣族了。

用户价值

  • 数据跟着账号走,不跟着设备走
  • 不用在每个设备上都收藏一遍
  • 换手机了,收藏不丢
场景2:浏览历史同步

场景描述
用户在手机上看藏族的介绍,看到一半出门了。到家用平板打开「民族图鉴」,自动跳转到藏族的详情页,并且定位到上次看到的位置。

用户价值

  • “无缝接续”——拿起任何设备都能接着看
  • 不用找"我上次看到哪了"
场景3:跨设备接续

场景描述
用户在手机上看苗族的介绍,觉得屏幕太小,想在平板上看。点一下"流转到平板",平板自动打开「民族图鉴」,直接跳转到苗族详情页,连滚动位置都一模一样。

用户价值

  • 大屏小屏无缝切换
  • 不用手动找、手动翻
场景4:设置同步

场景描述
用户在手机上把主题改成深色、语言改成英文。换了个平板,登录同一个账号,主题和语言自动就同步过去了。

用户价值

  • 个性化设置跟着账号走
  • 不用每个设备都重新设置一遍

分布式设计原则

做分布式功能时,我们遵循几个原则:

原则 说明
本地优先 先读写本地数据,保证速度,同步在后台做
离线可用 没网的时候也要能用,有网了自动同步
最终一致 不追求"实时强一致",保证"最终一致"就行
自动处理 冲突、重试、失败,系统自动处理,不让用户操心
隐私安全 端到端加密,数据只有用户自己能看
流量友好 尽量用WiFi同步,减少用户流量消耗

💡 CAP 定理与最终一致性

分布式系统有个著名的 CAP 定理:

  • Consistency(一致性):所有节点数据一致
  • Availability(可用性):服务一直可用
  • Partition tolerance(分区容错性):网络分区时还能用

这三者不可兼得,最多只能满足两个。

对于移动端应用来说,P(分区容错)是必须的(因为网络时好时坏、设备时开时关),所以只能在 C 和 A 之间取舍。

「民族图鉴」选择的是 AP(高可用 + 分区容错)——保证随时可用,数据"最终一致"就好。

什么是最终一致?就是说,数据可能暂时不一致,但过一会儿(同步完成后),最终会变成一致的。

对于收藏、历史这种数据,最终一致完全够用——用户不会在意"晚个几秒钟才同步过来"。


🛠️ 核心实现

步骤1:分布式数据管理——KV 存储

1.1 什么是 KV 存储?

KV(Key-Value,键值对)存储是最简单的分布式数据模式——就像一个大 Map,你存一个 key 和一个 value,取的时候用 key 去拿 value。

Key(键)         Value(值)
──────────────────────────────
"theme"      →   "dark"
"language"   →   "zh_CN"
"lastReadId" →   "dai"
"lastReadPos"→   "1230"

适用场景

  • 配置、设置
  • 简单的状态记录
  • 数据量小、结构简单
1.2 KV 存储的使用

鸿蒙的分布式 KV 存储,使用起来非常简单:

// common/utils/DistributedKVManager.ets

import { distributedKVStore } from '@kit.DistributedDataKit';
import { hilog } from '@kit.PerformanceAnalyticsKit';

const TAG = 'DistributedKVManager';

export class DistributedKVManager {
  private static instance: DistributedKVManager;
  private kvStore: distributedKVStore.KVStore | null = null;
  private isInitialized: boolean = false;

  private constructor() {}

  static getInstance(): DistributedKVManager {
    if (!DistributedKVManager.instance) {
      DistributedKVManager.instance = new DistributedKVManager();
    }
    return DistributedKVManager.instance;
  }

  /**
   * 初始化分布式 KV 存储
   */
  async init(context: Context): Promise<boolean> {
    if (this.isInitialized) return true;

    try {
      // 1. 创建 KV 管理器
      const kvManager = distributedKVStore.createKVManager({
        context: context,
        bundleName: 'com.example.ethnicchronicles'
      });

      // 2. 配置 KV 存储选项
      const options: distributedKVStore.Options = {
        createIfMissing: true,
        encrypt: false,
        backup: true,
        autoSync: true,  // 自动同步
        kvStoreType: distributedKVStore.KVStoreType.DEVICE_COLLABORATION,
        securityLevel: distributedKVStore.SecurityLevel.S2
      };

      // 3. 获取或创建 KV 存储
      this.kvStore = await kvManager.getKVStore('app_config', options);

      // 4. 订阅数据变化
      this.subscribeDataChange();

      this.isInitialized = true;
      hilog.info(0x0000, TAG, '分布式 KV 存储初始化成功');
      return true;
    } catch (error) {
      hilog.error(0x0000, TAG, `初始化失败: ${JSON.stringify(error)}`);
      return false;
    }
  }

  /**
   * 订阅数据变化
   */
  private subscribeDataChange(): void {
    if (!this.kvStore) return;

    try {
      this.kvStore.on('dataChange', distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL,
        (data: distributedKVStore.ChangeNotification) => {
          hilog.info(0x0000, TAG, `数据变化: 新增${data.insertEntries.length}条, 修改${data.updateEntries.length}条, 删除${data.deleteEntries.length}`);
          
          // 通知所有订阅者
          this.notifySubscribers(data);
        }
      );
    } catch (error) {
      hilog.error(0x0000, TAG, `订阅失败: ${error}`);
    }
  }

  /**
   * 写入数据
   */
  async put(key: string, value: string | number | boolean): Promise<boolean> {
    if (!this.kvStore) {
      hilog.warn(0x0000, TAG, 'KVStore 未初始化');
      return false;
    }

    try {
      await this.kvStore.put(key, value);
      return true;
    } catch (error) {
      hilog.error(0x0000, TAG, `写入失败: ${error}`);
      return false;
    }
  }

  /**
   * 读取数据
   */
  async get(key: string, defaultValue: string | number | boolean = ''): Promise<string | number | boolean> {
    if (!this.kvStore) {
      return defaultValue;
    }

    try {
      const value = await this.kvStore.get(key);
      return value ?? defaultValue;
    } catch (error) {
      hilog.error(0x0000, TAG, `读取失败: ${error}`);
      return defaultValue;
    }
  }

  /**
   * 删除数据
   */
  async delete(key: string): Promise<boolean> {
    if (!this.kvStore) return false;

    try {
      await this.kvStore.delete(key);
      return true;
    } catch (error) {
      hilog.error(0x0000, TAG, `删除失败: ${error}`);
      return false;
    }
  }

  // ===== 订阅机制 =====
  private subscribers: Array<(data: distributedKVStore.ChangeNotification) => void> = [];

  subscribe(callback: (data: distributedKVStore.ChangeNotification) => void): void {
    this.subscribers.push(callback);
  }

  unsubscribe(callback: (data: distributedKVStore.ChangeNotification) => void): void {
    const idx = this.subscribers.indexOf(callback);
    if (idx > -1) this.subscribers.splice(idx, 1);
  }

  private notifySubscribers(data: distributedKVStore.ChangeNotification): void {
    this.subscribers.forEach(cb => {
      try {
        cb(data);
      } catch (e) {
        hilog.error(0x0000, TAG, `订阅者异常: ${e}`);
      }
    });
  }
}
1.3 KV 存储配置详解

上面的代码里,有几个重要的配置项,我们来详细解释一下:

配置项 说明 推荐值
kvStoreType KV 存储类型 DEVICE_COLLABORATION(多设备协同)
autoSync 是否自动同步 true(自动同步,不用手动管)
securityLevel 安全级别 S2(中等安全,适合普通数据)
encrypt 是否加密 敏感数据设为 true
backup 是否备份 true(支持云端备份)

安全级别说明

级别 说明 适用数据
S0 无安全要求 公开数据
S1 低安全级别 普通配置
S2 中安全级别 用户数据、收藏、历史
S3 高安全级别 密码、密钥等敏感数据

「民族图鉴」的收藏、历史数据,用 S2 就够了。


步骤2:分布式对象存储——收藏列表同步

2.1 为什么需要对象存储?

KV 存储很方便,但它也有局限:

  • 只能存简单的键值对
  • 不能查询(只能按 key 拿)
  • 不能存复杂的列表结构

比如收藏列表,用户收藏了几十个民族,用 KV 存储怎么存?——把整个列表序列化成 JSON,存在一个 key 里?

这样的问题是:

  • 每次修改一个收藏,都要读写整个列表,效率低
  • 多设备同时修改时,容易冲突(一个加了A,一个加了B,最后只有一个生效)
  • 不能做查询、筛选

这时候就需要对象存储了。

2.2 什么是分布式对象存储?

分布式对象存储(Distributed Object Store),可以理解为一个轻量级的分布式数据库——你可以存结构化的对象,支持查询、筛选、分页,还能跨设备同步。

收藏对象(FavoriteObject)
┌──────────────────┐
│  id: string      │  ← 主键
│  ethnicId: string│
│  ethnicName: string
│  addTime: number │
│  syncStatus: number
└──────────────────┘

你可以:

  • 插入、更新、删除对象
  • 按条件查询(比如"所有收藏"、“最近收藏的10个”)
  • 订阅数据变化
  • 跨设备自动同步
2.3 「民族图鉴」收藏同步实现

我们来实现收藏功能的分布式同步:

// common/services/DistributedService.ets

import { distributedKVStore } from '@kit.DistributedDataKit';
import { hilog } from '@kit.PerformanceAnalyticsKit';

const TAG = 'DistributedService';

export interface FavoriteItem {
  id: string;           // 唯一ID(用 ethnicId + 时间戳?或者就用 ethnicId)
  ethnicId: string;     // 民族ID
  ethnicName: string;   // 民族名称
  addTime: number;      // 收藏时间
  isDeleted: boolean;   // 是否删除(软删除,用于同步)
  updateTime: number;   // 最后更新时间
}

export class DistributedService {
  private static instance: DistributedService;
  private kvStore: distributedKVStore.KVStore | null = null;
  private isInitialized: boolean = false;
  private favoriteCache: Map<string, FavoriteItem> = new Map();

  private constructor() {}

  static getInstance(): DistributedService {
    if (!DistributedService.instance) {
      DistributedService.instance = new DistributedService();
    }
    return DistributedService.instance;
  }

  /**
   * 初始化
   */
  async init(context: Context): Promise<boolean> {
    if (this.isInitialized) return true;

    try {
      const kvManager = distributedKVStore.createKVManager({
        context: context,
        bundleName: 'com.example.ethnicchronicles'
      });

      const options: distributedKVStore.Options = {
        createIfMissing: true,
        backup: true,
        autoSync: true,
        kvStoreType: distributedKVStore.KVStoreType.DEVICE_COLLABORATION,
        securityLevel: distributedKVStore.SecurityLevel.S2
      };

      this.kvStore = await kvManager.getKVStore('favorites', options);
      
      // 加载初始数据
      await this.loadFavoritesFromStore();
      
      // 订阅数据变化
      this.subscribeDataChange();

      this.isInitialized = true;
      hilog.info(0x0000, TAG, '分布式服务初始化成功');
      return true;
    } catch (error) {
      hilog.error(0x0000, TAG, `初始化失败: ${JSON.stringify(error)}`);
      return false;
    }
  }

  /**
   * 从 KV 存储加载收藏数据
   * 我们用 KV 存储来存收藏列表(用前缀区分)
   * 更复杂的场景可以用分布式对象存储
   */
  private async loadFavoritesFromStore(): Promise<void> {
    if (!this.kvStore) return;

    try {
      // 获取所有以 "fav_" 开头的 key
      const entries = await this.kvStore.getEntries('fav_');
      
      this.favoriteCache.clear();
      for (const entry of entries) {
        try {
          const item = JSON.parse(entry.value as string) as FavoriteItem;
          if (!item.isDeleted) {
            this.favoriteCache.set(item.ethnicId, item);
          }
        } catch (e) {
          hilog.warn(0x0000, TAG, `解析收藏数据失败: ${e}`);
        }
      }
      
      hilog.info(0x0000, TAG, `加载了 ${this.favoriteCache.size} 条收藏`);
    } catch (error) {
      hilog.error(0x0000, TAG, `加载收藏失败: ${error}`);
    }
  }

  /**
   * 订阅数据变化
   */
  private subscribeDataChange(): void {
    if (!this.kvStore) return;

    try {
      this.kvStore.on('dataChange', 
        distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL,
        (data: distributedKVStore.ChangeNotification) => {
          hilog.info(0x0000, TAG, 
            `收藏数据变化: +${data.insertEntries.length} ~${data.updateEntries.length} -${data.deleteEntries.length}`);
          
          // 更新本地缓存
          this.handleDataChange(data);
          
          // 通知监听者
          this.notifyFavoriteChanged();
        }
      );
    } catch (error) {
      hilog.error(0x0000, TAG, `订阅数据变化失败: ${error}`);
    }
  }

  /**
   * 处理数据变化
   */
  private handleDataChange(data: distributedKVStore.ChangeNotification): void {
    // 处理新增和修改
    const allChanged = [...data.insertEntries, ...data.updateEntries];
    for (const entry of allChanged) {
      if (entry.key.startsWith('fav_')) {
        try {
          const item = JSON.parse(entry.value as string) as FavoriteItem;
          if (item.isDeleted) {
            this.favoriteCache.delete(item.ethnicId);
          } else {
            this.favoriteCache.set(item.ethnicId, item);
          }
        } catch (e) {
          hilog.warn(0x0000, TAG, `解析变化数据失败: ${e}`);
        }
      }
    }

    // 处理删除
    for (const entry of data.deleteEntries) {
      if (entry.key.startsWith('fav_')) {
        // 从 key 中提取 ethnicId(或者从缓存里找)
        // 简化处理,重新加载
      }
    }
  }

  // ===== 收藏操作 =====

  /**
   * 获取所有收藏
   */
  getFavorites(): FavoriteItem[] {
    return Array.from(this.favoriteCache.values())
      .sort((a, b) => b.addTime - a.addTime);
  }

  /**
   * 检查是否已收藏
   */
  isFavorite(ethnicId: string): boolean {
    return this.favoriteCache.has(ethnicId);
  }

  /**
   * 添加收藏
   */
  async addFavorite(ethnicId: string, ethnicName: string): Promise<boolean> {
    if (!this.kvStore) return false;

    try {
      const now = Date.now();
      const item: FavoriteItem = {
        id: `fav_${ethnicId}`,
        ethnicId: ethnicId,
        ethnicName: ethnicName,
        addTime: now,
        isDeleted: false,
        updateTime: now
      };

      // 写入 KV 存储
      const key = `fav_${ethnicId}`;
      await this.kvStore.put(key, JSON.stringify(item));

      // 更新本地缓存
      this.favoriteCache.set(ethnicId, item);

      hilog.info(0x0000, TAG, `添加收藏: ${ethnicId}`);
      return true;
    } catch (error) {
      hilog.error(0x0000, TAG, `添加收藏失败: ${error}`);
      return false;
    }
  }

  /**
   * 移除收藏
   */
  async removeFavorite(ethnicId: string): Promise<boolean> {
    if (!this.kvStore) return false;

    try {
      // 软删除(标记为删除,用于同步)
      const existing = this.favoriteCache.get(ethnicId);
      if (existing) {
        const now = Date.now();
        const updated: FavoriteItem = {
          ...existing,
          isDeleted: true,
          updateTime: now
        };

        const key = `fav_${ethnicId}`;
        await this.kvStore.put(key, JSON.stringify(updated));

        this.favoriteCache.delete(ethnicId);
      }

      hilog.info(0x0000, TAG, `移除收藏: ${ethnicId}`);
      return true;
    } catch (error) {
      hilog.error(0x0000, TAG, `移除收藏失败: ${error}`);
      return false;
    }
  }

  /**
   * 切换收藏状态
   */
  async toggleFavorite(ethnicId: string, ethnicName: string): Promise<boolean> {
    if (this.isFavorite(ethnicId)) {
      return this.removeFavorite(ethnicId);
    } else {
      return this.addFavorite(ethnicId, ethnicName);
    }
  }

  // ===== 监听机制 =====
  private favoriteListeners: Array<() => void> = [];

  onFavoriteChanged(callback: () => void): void {
    this.favoriteListeners.push(callback);
  }

  offFavoriteChanged(callback: () => void): void {
    const idx = this.favoriteListeners.indexOf(callback);
    if (idx > -1) this.favoriteListeners.splice(idx, 1);
  }

  private notifyFavoriteChanged(): void {
    this.favoriteListeners.forEach(cb => {
      try {
        cb();
      } catch (e) {
        hilog.error(0x0000, TAG, `收藏监听异常: ${e}`);
      }
    });
  }
}

💡 为什么用软删除?

你可能注意到了,我们用的是"软删除"(isDeleted 标记),而不是真的把数据删掉。

为什么?因为分布式同步。

想象一下这个场景:

  • 设备A:收藏了傣族(key = fav_dai)
  • 设备A和设备B同步了,设备B也有 fav_dai
  • 设备A:取消收藏傣族(删除 fav_dai)
  • 设备A和设备B同步…

问题来了:设备B怎么知道"fav_dai 被删了"?——如果直接删了,同步过去的时候,设备B可能以为"这是个新数据?不对,我这边没有…"

用软删除就没这个问题——把 isDeleted 设为 true,同步过去,对方看到"哦,这个被删了",然后也删掉。

这是分布式系统的常见做法:用软删除代替硬删除,保证删除操作也能被正确同步


步骤3:冲突解决——多端修改了怎么办?

3.1 什么是冲突?

分布式系统中,冲突是不可避免的。

什么是冲突?——两台设备在没同步的情况下,同时修改了同一份数据

比如:

  • 手机上:收藏了傣族
  • 平板上:取消了傣族的收藏(之前同步过,两边都有)
  • 两台设备都联网了,开始同步…

——最后是收藏还是不收藏?

这就是冲突。

3.2 冲突解决策略

常见的冲突解决策略有这几种:

策略 说明 优点 缺点 适用场景
最后写入赢(LWW) 谁最后改的,听谁的 简单、好实现 可能丢失较早的修改 设置、状态类数据
用户选择 让用户选保留哪个 不会丢数据 用户体验差,要手动选 重要数据、文档
自动合并 自动合并两边的修改 体验好,不丢数据 实现复杂,不是所有数据都能合并 列表、集合类数据
乐观锁 带版本号,冲突了重试 可靠 实现复杂 金融、关键数据
3.3 「民族图鉴」的冲突策略

不同的数据类型,用不同的策略:

数据类型 冲突策略 说明
收藏列表 自动合并(并集) 两边都加了,就都保留;一边删一边加,保留添加的
浏览历史 最后写入赢 最后看的为准
阅读进度 最后写入赢 最后看到的位置为准
设置(主题、语言) 最后写入赢 最后改的为准

收藏列表的自动合并逻辑

设备A:收藏了 [汉族, 傣族]
设备B:收藏了 [汉族, 藏族]
        ↓ 同步后(取并集)
结果:[汉族, 傣族, 藏族]

两边加的都保留,不会丢——这就是"自动合并"的好处。

对于删除呢?

设备A:删除了傣族
设备B:没改
        ↓ 同步后
结果:傣族被删除

因为我们用了软删除 + 时间戳,谁后删的听谁的(一般也不会同时删同一个)。

💡 为什么收藏适合自动合并?

因为收藏是一个集合(Set)——你加你的,我加我的,最后合并到一起,不会有冲突。

但如果是"修改同一条数据的同一个字段"(比如把民族名称从"傣族"改成"泰族"),那就没法自动合并了,只能用"最后写入赢"或者"用户选择"。

「民族图鉴」里大部分同步数据都是"集合操作"(加收藏、加历史),所以冲突很少,大部分时候都能自动合并。


步骤4:端云协同——云端 + 本地

4.1 端云协同的架构

端云协同的架构很简单——本地是缓存,云端是真相(Source of Truth):

     设备端(手机/平板)                    云端
  ┌─────────────────────┐            ┌──────────────┐
  │  本地数据库(KV/对象)│            │  云数据库      │
  │  (读写都走这里)     │ ◄────────► │  (数据中枢)  │
  └─────────────────────┘            └──────────────┘
            │
            ▼
         应用 UI
         (读本地数据,保证速度)

工作原理

  1. 读取:直接读本地数据库,快
  2. 写入:先写本地,再异步同步到云端
  3. 同步:有网的时候,后台自动和云端同步
  4. 冲突:同步时发现冲突,按策略解决
4.2 离线可用原则

端云协同最重要的原则就是:离线可用

没网的时候怎么办?——照常使用

没网的时候:
  读 → 读本地数据
  写 → 写本地数据,记个"待同步"标记

有网的时候:
  把"待同步"的数据批量同步到云端
  把云端的新数据拉下来
  解决冲突
  通知 UI 更新

用户根本感知不到"同步"的过程——打开App就能用,数据就在那里。

4.3 鸿蒙的端云服务

鸿蒙提供了端云协同服务(HarmonyOS Connect + 云侧服务),你可以用:

  • 华为云:华为提供的云端服务
  • 自建服务:自己搭后端,通过标准接口对接

对于个人开发者或小团队,用华为云的端云服务是最省心的——不用自己搭服务器、不用管运维,直接用就行。

💡 端云协同 vs 自己写同步

你可能会想:“不就是数据同步吗?我自己写个接口,每次启动拉一下,不就行了?”

理论上可以,但实际上有很多坑:

  • 增量同步:每次都全量拉?数据多了慢死
  • 冲突解决:两边都改了,听谁的?
  • 失败重试:网不好,传一半断了,怎么办?
  • 实时性:改了数据,另一台设备多久能收到?
  • 离线队列:离线时的操作,上线了按什么顺序同步?

这些问题,看起来简单,做起来全是坑。用系统提供的端云服务,这些都帮你搞定了。


步骤5:性能与流量优化

分布式同步虽好,但也要注意性能和流量——不能为了同步,把用户的流量都耗光了,或者把手机弄卡了。

5.1 增量同步

原则:只同步变化的部分,不同步全部数据

比如收藏列表:

  • ❌ 不好:每次同步都把整个收藏列表传一遍
  • ✅ 好:只传"新增的"、“修改的”、“删除的”

鸿蒙的分布式数据服务默认就是增量同步的——你不用管,系统帮你做了。

5.2 数据压缩

原则:传输前先压缩,减少流量消耗

JSON 数据压缩率很高,压缩后体积能小 60%-80%。

系统层面一般会做,但我们自己也可以注意:

  • 只传必要的字段
  • 不传大文件(大文件用对象存储或文件服务)
  • 数据结构尽量精简
5.3 WiFi-only 同步

原则:大的数据、非紧急的数据,只在 WiFi 下同步

比如:

  • 收藏、设置:数据小,用流量同步也没关系
  • 图片、音乐:数据大,只在 WiFi 下同步

代码中可以判断当前网络类型:

import { connection } from '@kit.ConnectivityKit';

async function isWifi(): Promise<boolean> {
  try {
    const netType = await connection.getDefaultNet();
    return netType === connection.NetBearType.WIFI;
  } catch (error) {
    return false;
  }
}

// 同步大文件前先判断
async function syncLargeData(): Promise<void> {
  const wifiOnly = await this.getWifiOnlySetting();
  if (wifiOnly && !(await isWifi())) {
    // WiFi-only 且当前不是 WiFi,不同步
    hilog.info(TAG, '非WiFi环境,跳过大数据同步');
    return;
  }
  
  // 同步...
}
5.4 同步频率控制

原则:不要太频繁地同步

比如:

  • 用户连续收藏了5个民族——不要收藏一个同步一次,等用户操作完了批量同步
  • 阅读进度——不要每滚动一像素就同步,几秒钟同步一次就行

节流(Throttle)

// 阅读进度同步:最多 5 秒一次
private lastSyncTime: number = 0;
private readonly SYNC_INTERVAL = 5000; // 5秒

updateReadPosition(ethnicId: string, position: number): void {
  // 更新本地
  this.localReadPos.set(ethnicId, position);
  
  // 节流同步
  const now = Date.now();
  if (now - this.lastSyncTime > this.SYNC_INTERVAL) {
    this.syncReadPosition(ethnicId, position);
    this.lastSyncTime = now;
  }
}

💡 同步的"度"

同步不是越频繁越好。

太频繁了:费电、费流量、可能还会卡
太不频繁了:数据不一致,用户体验差

找一个平衡点——

  • 重要的操作(收藏、取消收藏):立即同步
  • 不重要的(阅读进度):延迟几秒批量同步
  • 大数据(图片、音乐):WiFi 下再同步

步骤6:安全与加密

用户的数据,特别是收藏、历史这些隐私数据,安全非常重要。

6.1 端到端加密

原则:数据在传输和存储过程中,都是加密的。只有用户自己的设备能解密。

设备A → 加密 → 云端(看不懂) → 解密 → 设备B

就算云端数据泄露了,没有密钥也看不懂。

鸿蒙的分布式数据服务支持加密,你只要在初始化时设置 encrypt: true 就行:

const options: distributedKVStore.Options = {
  encrypt: true,  // 开启加密
  securityLevel: distributedKVStore.SecurityLevel.S2,
  // ...
};
6.2 身份认证

原则:只有登录同一个账号的设备,才能同步数据。

这样才能保证"你的数据只有你能看到"。

鸿蒙的分布式数据是和 Huawei ID 绑定的——同一个 Huawei ID 登录的设备,才能互相同步数据。

6.3 权限控制

原则:最小化权限,只申请需要的。

同步需要的权限:

// module.json5
{
  "requestPermissions": [
    {
      "name": "ohos.permission.DISTRIBUTED_DATASYNC",
      "reason": "$string:distributed_permission_reason"
    }
  ]
}

只申请分布式数据同步的权限,不要多申请。


步骤7:实战——「民族图鉴」收藏同步

理论讲了这么多,我们来完整实现「民族图鉴」的收藏同步功能。

7.1 需求回顾
  • 收藏数据跨设备同步
  • 离线可用
  • 冲突自动合并
  • WiFi-only 可选
  • 实时更新UI
7.2 DistributedService 完整实现
// common/services/DistributedService.ets

import { distributedKVStore } from '@kit.DistributedDataKit';
import { hilog } from '@kit.PerformanceAnalyticsKit';
import { Context } from '@kit.AbilityKit';

const TAG = 'DistributedService';

export interface FavoriteItem {
  ethnicId: string;
  ethnicName: string;
  addTime: number;
  isDeleted: boolean;
  updateTime: number;
}

export interface ReadProgress {
  ethnicId: string;
  scrollOffset: number;
  updateTime: number;
}

export class DistributedService {
  private static instance: DistributedService;
  private context: Context | null = null;
  private kvStore: distributedKVStore.KVStore | null = null;
  private isInitialized: boolean = false;
  
  private favoriteCache: Map<string, FavoriteItem> = new Map();
  private readProgressCache: Map<string, ReadProgress> = new Map();
  
  private favoriteListeners: Array<() => void> = [];
  private progressListeners: Array<(ethnicId: string) => void> = [];

  private constructor() {}

  static getInstance(): DistributedService {
    if (!DistributedService.instance) {
      DistributedService.instance = new DistributedService();
    }
    return DistributedService.instance;
  }

  async init(context: Context): Promise<boolean> {
    if (this.isInitialized) return true;
    this.context = context;

    try {
      const kvManager = distributedKVStore.createKVManager({
        context: context,
        bundleName: 'com.example.ethnicchronicles'
      });

      const options: distributedKVStore.Options = {
        createIfMissing: true,
        backup: true,
        autoSync: true,
        kvStoreType: distributedKVStore.KVStoreType.DEVICE_COLLABORATION,
        securityLevel: distributedKVStore.SecurityLevel.S2
      };

      this.kvStore = await kvManager.getKVStore('ethnic_data', options);
      
      await this.loadFromStore();
      this.subscribeDataChange();

      this.isInitialized = true;
      hilog.info(0x0000, TAG, '分布式服务初始化成功');
      return true;
    } catch (error) {
      hilog.error(0x0000, TAG, `初始化失败: ${JSON.stringify(error)}`);
      return false;
    }
  }

  private async loadFromStore(): Promise<void> {
    if (!this.kvStore) return;

    try {
      // 加载收藏
      const favEntries = await this.kvStore.getEntries('fav_');
      this.favoriteCache.clear();
      for (const entry of favEntries) {
        try {
          const item = JSON.parse(entry.value as string) as FavoriteItem;
          if (!item.isDeleted) {
            this.favoriteCache.set(item.ethnicId, item);
          }
        } catch (e) {
          hilog.warn(0x0000, TAG, `解析收藏失败: ${e}`);
        }
      }

      // 加载阅读进度
      const progEntries = await this.kvStore.getEntries('prog_');
      this.readProgressCache.clear();
      for (const entry of progEntries) {
        try {
          const item = JSON.parse(entry.value as string) as ReadProgress;
          this.readProgressCache.set(item.ethnicId, item);
        } catch (e) {
          hilog.warn(0x0000, TAG, `解析进度失败: ${e}`);
        }
      }

      hilog.info(0x0000, TAG, 
        `加载完成: 收藏${this.favoriteCache.size}条, 阅读进度${this.readProgressCache.size}`);
    } catch (error) {
      hilog.error(0x0000, TAG, `加载数据失败: ${error}`);
    }
  }

  private subscribeDataChange(): void {
    if (!this.kvStore) return;

    try {
      this.kvStore.on('dataChange',
        distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL,
        (data: distributedKVStore.ChangeNotification) => {
          this.handleDataChange(data);
        }
      );
    } catch (error) {
      hilog.error(0x0000, TAG, `订阅失败: ${error}`);
    }
  }

  private handleDataChange(data: distributedKVStore.ChangeNotification): void {
    const allChanged = [...data.insertEntries, ...data.updateEntries];
    let favoriteChanged = false;
    let progressChanged = false;
    let changedEthnicId = '';

    for (const entry of allChanged) {
      if (entry.key.startsWith('fav_')) {
        try {
          const item = JSON.parse(entry.value as string) as FavoriteItem;
          if (item.isDeleted) {
            this.favoriteCache.delete(item.ethnicId);
          } else {
            this.favoriteCache.set(item.ethnicId, item);
          }
          favoriteChanged = true;
        } catch (e) {
          // ignore
        }
      } else if (entry.key.startsWith('prog_')) {
        try {
          const item = JSON.parse(entry.value as string) as ReadProgress;
          this.readProgressCache.set(item.ethnicId, item);
          changedEthnicId = item.ethnicId;
          progressChanged = true;
        } catch (e) {
          // ignore
        }
      }
    }

    if (favoriteChanged) {
      this.notifyFavoriteChanged();
    }
    if (progressChanged && changedEthnicId) {
      this.notifyProgressChanged(changedEthnicId);
    }
  }

  // ===== 收藏相关 =====
  getFavorites(): FavoriteItem[] {
    return Array.from(this.favoriteCache.values())
      .sort((a, b) => b.addTime - a.addTime);
  }

  isFavorite(ethnicId: string): boolean {
    return this.favoriteCache.has(ethnicId);
  }

  async addFavorite(ethnicId: string, ethnicName: string): Promise<boolean> {
    if (!this.kvStore) return false;
    if (this.favoriteCache.has(ethnicId)) return true;

    try {
      const now = Date.now();
      const item: FavoriteItem = {
        ethnicId,
        ethnicName,
        addTime: now,
        isDeleted: false,
        updateTime: now
      };

      await this.kvStore.put(`fav_${ethnicId}`, JSON.stringify(item));
      this.favoriteCache.set(ethnicId, item);
      this.notifyFavoriteChanged();
      
      hilog.info(0x0000, TAG, `添加收藏: ${ethnicId}`);
      return true;
    } catch (error) {
      hilog.error(0x0000, TAG, `添加收藏失败: ${error}`);
      return false;
    }
  }

  async removeFavorite(ethnicId: string): Promise<boolean> {
    if (!this.kvStore) return false;
    if (!this.favoriteCache.has(ethnicId)) return true;

    try {
      const existing = this.favoriteCache.get(ethnicId)!;
      const now = Date.now();
      const updated: FavoriteItem = {
        ...existing,
        isDeleted: true,
        updateTime: now
      };

      await this.kvStore.put(`fav_${ethnicId}`, JSON.stringify(updated));
      this.favoriteCache.delete(ethnicId);
      this.notifyFavoriteChanged();
      
      hilog.info(0x0000, TAG, `移除收藏: ${ethnicId}`);
      return true;
    } catch (error) {
      hilog.error(0x0000, TAG, `移除收藏失败: ${error}`);
      return false;
    }
  }

  async toggleFavorite(ethnicId: string, ethnicName: string): Promise<boolean> {
    if (this.isFavorite(ethnicId)) {
      return this.removeFavorite(ethnicId);
    } else {
      return this.addFavorite(ethnicId, ethnicName);
    }
  }

  // ===== 阅读进度相关 =====
  getReadProgress(ethnicId: string): number {
    const progress = this.readProgressCache.get(ethnicId);
    return progress?.scrollOffset || 0;
  }

  private lastProgressSync: number = 0;
  private readonly PROGRESS_SYNC_INTERVAL = 5000;

  updateReadProgress(ethnicId: string, scrollOffset: number): void {
    // 更新本地缓存
    this.readProgressCache.set(ethnicId, {
      ethnicId,
      scrollOffset,
      updateTime: Date.now()
    });

    // 节流同步
    const now = Date.now();
    if (now - this.lastProgressSync > this.PROGRESS_SYNC_INTERVAL) {
      this.syncReadProgress(ethnicId);
      this.lastProgressSync = now;
    }
  }

  private async syncReadProgress(ethnicId: string): Promise<void> {
    if (!this.kvStore) return;

    const progress = this.readProgressCache.get(ethnicId);
    if (!progress) return;

    try {
      await this.kvStore.put(
        `prog_${ethnicId}`,
        JSON.stringify(progress)
      );
    } catch (error) {
      hilog.error(0x0000, TAG, `同步阅读进度失败: ${error}`);
    }
  }

  // ===== 监听机制 =====
  onFavoriteChanged(callback: () => void): void {
    this.favoriteListeners.push(callback);
  }

  offFavoriteChanged(callback: () => void): void {
    const idx = this.favoriteListeners.indexOf(callback);
    if (idx > -1) this.favoriteListeners.splice(idx, 1);
  }

  private notifyFavoriteChanged(): void {
    this.favoriteListeners.forEach(cb => {
      try { cb(); } catch (e) {
        hilog.error(0x0000, TAG, `收藏监听异常: ${e}`);
      }
    });
  }

  onProgressChanged(callback: (ethnicId: string) => void): void {
    this.progressListeners.push(callback);
  }

  offProgressChanged(callback: (ethnicId: string) => void): void {
    const idx = this.progressListeners.indexOf(callback);
    if (idx > -1) this.progressListeners.splice(idx, 1);
  }

  private notifyProgressChanged(ethnicId: string): void {
    this.progressListeners.forEach(cb => {
      try { cb(ethnicId); } catch (e) {
        hilog.error(0x0000, TAG, `进度监听异常: ${e}`);
      }
    });
  }
}
7.3 在收藏页中使用
// pages/CollectionPage.ets

import { DistributedService, FavoriteItem } from '../common/services/DistributedService';

@Entry
@Component
struct CollectionPage {
  @State favoriteList: FavoriteItem[] = [];
  @State isLoading: boolean = true;
  
  private distributedService: DistributedService = DistributedService.getInstance();

  aboutToAppear(): void {
    this.loadFavorites();
    
    // 监听收藏变化
    this.distributedService.onFavoriteChanged(() => {
      this.loadFavorites();
    });
  }

  aboutToDisappear(): void {
    // 移除监听(注意保存引用,这里简化了)
  }

  private loadFavorites(): void {
    this.favoriteList = this.distributedService.getFavorites();
    this.isLoading = false;
  }

  build() {
    Column() {
      if (this.isLoading) {
        this.buildLoading()
      } else if (this.favoriteList.length === 0) {
        this.buildEmpty()
      } else {
        this.buildList()
      }
    }
    .width('100%')
    .height('100%')
  }

  @Builder
  buildList(): void {
    List() {
      ForEach(this.favoriteList, (item: FavoriteItem) => {
        ListItem() {
          this.buildFavoriteItem(item)
        }
      }, item => item.ethnicId)
    }
    .width('100%')
    .layoutWeight(1)
  }

  @Builder
  buildFavoriteItem(item: FavoriteItem): void {
    Row({ space: 12 }) {
      Text(item.ethnicName.charAt(0))
        .width(48)
        .height(48)
        .borderRadius(24)
        .backgroundColor('#E8F0FE')
        .fontSize(20)
        .fontColor('#1976D2')
        .textAlign(TextAlign.Center)

      Column({ space: 4 }) {
        Text(item.ethnicName)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .fontColor($r('app.color.text_primary'))

        Text(this.formatDate(item.addTime))
          .fontSize(12)
          .fontColor($r('app.color.text_hint'))
      }
      .layoutWeight(1)
      .alignItems(HorizontalAlign.Start)

      Text('❤️')
        .fontSize(20)
        .onClick(() => {
          this.distributedService.removeFavorite(item.ethnicId);
        })
    }
    .width('100%')
    .padding(16)
    .backgroundColor(Color.White)
    .onClick(() => {
      router.pushUrl({
        url: 'pages/EthnicDetailPage',
        params: { ethnicId: item.ethnicId }
      });
    })
  }

  private formatDate(timestamp: number): string {
    const date = new Date(timestamp);
    return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
  }

  @Builder
  buildLoading(): void {
    Column() {
      Text('加载中...')
        .fontSize(14)
        .fontColor($r('app.color.text_hint'))
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  @Builder
  buildEmpty(): void {
    Column({ space: 16 }) {
      Text('📚')
        .fontSize(64)

      Text('还没有收藏的民族')
        .fontSize(16)
        .fontColor($r('app.color.text_hint'))

      Button('去探索')
        .width(120)
        .height(40)
        .borderRadius(20)
        .backgroundColor($r('app.color.primary_color'))
        .fontColor('#FFFFFF')
        .onClick(() => {
          router.pushUrl({ url: 'pages/EthnicListPage' });
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

⚠️ 常见问题与解决方案

问题1:同步延迟、数据不同步

现象

  • 设备A收藏了,设备B很久都没收到
  • 或者根本同步不过去

常见原因

原因 概率 说明
网络问题 30% 设备没联网,或者网络不好
没登录账号 25% 两台设备登录的不是同一个账号
不同一个局域网 20% 近场同步需要在同一个WiFi下
同步被系统限制 15% 省电模式、后台限制
初始化失败 10% 权限没给、初始化失败

排查步骤

第1步:检查设备是否都联网了?
    ↓ 是
第2步:检查是不是同一个 Huawei ID?
    ↓ 是
第3步:检查权限给了吗?(分布式数据同步权限)
    ↓ 给了
第4步:检查是不是在同一个WiFi下?(近场同步)
    ↓ 是
第5步:重启App试试?
    ↓ 还不行
第6步:看日志,找具体错误

问题2:冲突了怎么办?

现象
两台设备都修改了数据,同步后发现一边的修改"丢了"。

解决方案

1. 选择合适的冲突策略

不同数据用不同策略:

数据类型 推荐策略
收藏列表 自动合并(并集)
阅读进度 最后写入赢
设置 最后写入赢
用户编辑的内容 用户选择

2. 给用户提示

如果发生了冲突,并且是用户可能在意的数据,弹个提示告诉用户:

if (conflictDetected) {
  this.showAlert(
    '数据冲突',
    '检测到其他设备也修改了数据,已自动合并。',
    ['知道了']
  );
}

3. 保留历史版本

重要数据可以保留几个历史版本,出问题了可以回滚。


问题3:同步太费流量

现象
用户反馈:“这个App怎么这么费流量?”

优化方案

1. WiFi-only 开关

给用户一个选项:“仅在WiFi下同步大数据”

// 设置里的开关
@State wifiOnlySync: boolean = true;

// 同步前判断
async syncLargeData(): Promise<void> {
  if (this.wifiOnlySync && !this.isWifi()) {
    hilog.info(TAG, '非WiFi,跳过大数据同步');
    return;
  }
  // 同步...
}

2. 增量同步

只传变化的部分,不传全部数据。

3. 数据压缩

传输前压缩。

4. 减少同步频率

非关键数据,降低同步频率。


问题4:离线操作后同步乱了

现象

  • 离线时收藏了A,取消了B
  • 一联网,同步后数据不对

原因
操作顺序不对——比如先取消了B,又收藏了A,但同步的时候顺序反了。

解决方案

用时间戳 + 操作日志

每个操作都带时间戳,同步时按时间顺序重放:

// 操作日志
interface Operation {
  type: 'add' | 'remove';
  ethnicId: string;
  timestamp: number;
}

// 同步后按时间排序,依次应用
operations.sort((a, b) => a.timestamp - b.timestamp);
for (const op of operations) {
  if (op.type === 'add') {
    // 添加收藏
  } else {
    // 移除收藏
  }
}

只要时间戳是对的,最终结果就是对的。


问题5:数据安全与隐私顾虑

现象
用户担心:“我的收藏数据存在云端,安全吗?”

解答思路

  1. 端到端加密:数据在云端是加密的,只有用户自己的设备能解密
  2. 账号绑定:只有登录同一个账号才能看到数据
  3. 用户可控:用户可以随时清除云端数据、关闭同步
  4. 合规:符合隐私法规,不滥用用户数据

设计建议

  • 在设置里加个"同步设置",让用户可以开关
  • 明确告知用户"同步了什么数据"、“存在哪里”
  • 给用户"清除云端数据"的入口

📝 本章小结

核心知识点

本文深入讲解了分布式同步与端云协同:

1. 分布式能力全景

  • 四大能力:分布式数据、分布式文件、分布式任务、分布式硬件
  • 核心思想:数据和服务不局限在单设备,在设备间流动
  • 「民族图鉴」主要用分布式数据

2. 分布式数据管理

  • KV 存储:键值对,简单高效,适合配置和小数据
  • 对象存储:结构化数据,支持查询,适合列表
  • 订阅通知:数据变化时自动通知
  • 自动同步:系统帮你搞定同步细节

3. 端云协同

  • 架构:本地是缓存,云端是真相
  • 离线可用:没网照常使用,有网自动同步
  • 优势:多设备同步、数据备份、跨账号

4. 冲突解决策略

  • 最后写入赢(LWW):简单,适合状态类数据
  • 自动合并:适合集合类数据(如收藏)
  • 用户选择:适合重要数据,体验较差
  • 「民族图鉴」:收藏用自动合并,设置/进度用 LWW

5. 性能与流量优化

  • 增量同步:只传变化的部分
  • 数据压缩:减少传输体积
  • WiFi-only:大数据只在WiFi下同步
  • 节流同步:非关键数据降低同步频率

6. 安全与加密

  • 端到端加密:云端也看不懂
  • 身份认证:同账号才能同步
  • 权限控制:最小化权限申请

最佳实践总结

本地优先,异步同步

读 → 直接读本地,快
写 → 先写本地,再后台同步
用户感知不到同步过程,打开就用

最终一致,不追强一致

对于收藏、历史这类数据
"最终一致"就够了
不用追求"实时强一致"

数据分类,不同策略

- 收藏列表 → 自动合并(并集)
- 阅读进度 → 最后写入赢 + 节流
- 设置配置 → 最后写入赢
- 大数据(图片)→ WiFi-only

软删除代替硬删除

分布式同步中
删除操作也要能被同步
用 isDeleted 标记,而不是直接删掉

节流与批量

高频操作(如阅读进度)
不要每次都同步
用节流(throttle),几秒同步一次

用户可控,透明告知

- 给用户开关同步的选项
- 告诉用户同步了什么
- 给用户清除数据的入口
- 尊重用户隐私

下一步预告

分布式同步搞定了,下一篇我们来聊一个更"颠覆"的概念——原子化服务

什么是原子化服务?——不用安装、即点即用、服务直达。

想查个民族,不用下载「民族图鉴」App,搜一下就能用;想分享给朋友,不用让对方下载,碰一下对方直接就能看。

下一篇(第85篇)我们将讲解:

  • ⚡ 什么是原子化服务:免安装、即点即用、服务直达
  • 🆚 原子化服务 vs 传统App:形态、分发、使用方式
  • 🎯 「民族图鉴」原子化服务设计:民族查询、每日推荐、扫一扫
  • 🔧 原子化服务开发:工程结构、Ability配置、打包发布
  • 🚀 实战:将民族查询功能做成原子化服务

App的未来,是"原子化"吗?我们下篇揭晓。


🔗 相关链接


💡 提示:很多开发者刚接触分布式同步时,容易陷入"完美主义"的误区——追求"实时强一致"、追求"零冲突"、追求"100%可靠"。

但实际上,对于大多数应用来说,最终一致就够了。

用户不会在意"收藏晚了3秒才同步过来",也不会在意"两台设备数据暂时不一样"。他们在意的是:

  • 我能不能随时用(可用性)
  • 我的数据会不会丢(可靠性)
  • 换了设备数据在不在(一致性)

把这三点做好了,用户就满意了。不要为了追求"理论上的完美",把系统搞得很复杂、性能搞得很差。

分布式的精髓不是"完美",而是"够用就好"——在可接受的范围内,提供最好的体验。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐