HarmonyOS应用《民族图鉴》开发第84篇:分布式同步——分布式数据与端云协同

📖 引言
你有没有过这样的经历:
- 上班路上用手机看「民族图鉴」,看到傣族的介绍,觉得有意思,收藏了。到了公司,拿出平板想接着看——哎?收藏呢?怎么没了?
- 晚上躺床上用手机看藏族的介绍,看到一半睡着了。第二天起来,想用电脑接着看——哎?我看到哪了来着?
- 手机上做了民族灵魂测试,结果是"傣族"。换了个新手机,重新下载「民族图鉴」——又要重新测一遍?
这些问题的本质是什么?——数据没有同步。
你的数据困在一台设备里,换个设备就没了。这在"单设备时代"还行,但在"多设备时代",这就成了大问题。
鸿蒙的分布式能力,就是来解决这个问题的。
分布式——听起来很高大上,说白了就是:你的数据、你的服务,不局限在一台设备里,而是在所有设备之间流动。
- 手机上收藏,平板上立刻就能看到
- 手机上看到一半,流转到平板接着看
- 换了新手机,登录账号,所有数据都回来了
- 没网的时候也能用,有网了自动同步
鸿蒙7.0(API 26)在分布式能力上做了更多增强:
- 📊 分布式数据管理:KV存储、对象存储、订阅通知,跨设备实时同步
- ☁️ 端云协同:云端存储、端云同步、离线可用
- 🔄 智能冲突解决:最后写入赢、自动合并、用户选择
- 🚀 性能优化:增量同步、压缩、WiFi-only、流量控制
「民族图鉴」作为一个文化类App,分布式同步非常重要:
- ❤️ 收藏同步:手机上收藏,平板上也能看到
- 📜 浏览历史同步:看到哪,同步到哪
- 📱 跨设备接续:手机上看一半,流转到平板继续看
- ☁️ 云备份:换手机了,数据不丢
这一篇,我们就从分布式能力全景讲起,深入解析分布式数据管理、端云协同、冲突解决、性能优化,并手把手带你实现「民族图鉴」收藏数据的多端同步。
🎯 学习目标
完成本文后,你将能够:
- ✅ 理解鸿蒙分布式能力的全景与核心价值
- ✅ 掌握分布式数据管理的三种模式:KV、对象、订阅
- ✅ 理解端云协同的架构与工作原理
- ✅ 学会冲突解决策略:最后写入赢、自动合并、用户选择
- ✅ 掌握性能与流量优化技巧:增量同步、压缩、WiFi-only
- ✅ 理解安全与加密机制:端到端加密、身份认证
- ✅ 能够实现「民族图鉴」收藏数据的多端同步
- ✅ 掌握分布式开发中的常见问题与解决方案
💡 需求分析
分布式能力全景
鸿蒙的分布式能力,不只是"数据同步"——它是一整套的"跨设备协作"框架。
我们来看看分布式能力的全景图:
┌─────────────────────────────────────────────────────────┐
│ 鸿蒙分布式能力全景 │
├─────────────┬─────────────┬─────────────┬───────────────┤
│ 分布式数据 │ 分布式文件 │ 分布式任务 │ 分布式硬件 │
│ │ │ │ │
│ - KV 存储 │ - 文件同步 │ - 任务调度 │ - 摄像头 │
│ - 对象存储 │ - 跨设备访问 │ - 跨端流转 │ - 麦克风 │
│ - 订阅通知 │ - 共享文件夹 │ - 原子化服务 │ - 屏幕 │
│ - 端云协同 │ - 云盘 │ - 分布式计算 │ - 传感器 │
└─────────────┴─────────────┴─────────────┴───────────────┘
| 能力类型 | 说明 | 「民族图鉴」用途 |
|---|---|---|
| 分布式数据 | 跨设备数据同步,实时一致 | 收藏、历史、设置同步 |
| 分布式文件 | 跨设备文件共享、同步 | 民族图片、音乐同步 |
| 分布式任务 | 跨设备任务流转、调度 | 跨设备接续阅读 |
| 分布式硬件 | 调用其他设备的硬件 | 用平板音箱播放音乐 |
本文我们重点讲分布式数据和端云协同——这是最常用、最基础的分布式能力。
分布式数据管理
分布式数据管理(Distributed Data Management,简称 DDM),是鸿蒙提供的一套跨设备数据同步框架。
它的核心思想是:你只管读写本地数据,同步的事交给系统。
设备 A 设备 B
│ │
│ 写入数据(本地数据库) │
│ ↓ │
│ 分布式数据服务 │
│ ↓ │
│ 自动同步 ──────────────► │
│ │ 分布式数据服务
│ │ ↓
│ │ 写入数据(本地数据库)
│ │ ↓
│ │ 通知 App 数据更新了
│ ◄────────────────────── 自动同步
│
你不需要管"怎么连另一台设备"、“数据怎么传”、“传失败了怎么办”——这些系统都帮你搞定了。你只需要:
- 把数据存到分布式数据库里
- 订阅数据变化通知
- 数据变了,更新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
(读本地数据,保证速度)
工作原理:
- 读取:直接读本地数据库,快
- 写入:先写本地,再异步同步到云端
- 同步:有网的时候,后台自动和云端同步
- 冲突:同步时发现冲突,按策略解决
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. 分布式数据管理
- KV 存储:键值对,简单高效,适合配置和小数据
- 对象存储:结构化数据,支持查询,适合列表
- 订阅通知:数据变化时自动通知
- 自动同步:系统帮你搞定同步细节
3. 端云协同
- 架构:本地是缓存,云端是真相
- 离线可用:没网照常使用,有网自动同步
- 优势:多设备同步、数据备份、跨账号
4. 冲突解决策略
- 最后写入赢(LWW):简单,适合状态类数据
- 自动合并:适合集合类数据(如收藏)
- 用户选择:适合重要数据,体验较差
- 「民族图鉴」:收藏用自动合并,设置/进度用 LWW
5. 性能与流量优化
- 增量同步:只传变化的部分
- 数据压缩:减少传输体积
- WiFi-only:大数据只在WiFi下同步
- 节流同步:非关键数据降低同步频率
6. 安全与加密
- 端到端加密:云端也看不懂
- 身份认证:同账号才能同步
- 权限控制:最小化权限申请
最佳实践总结
✅ 本地优先,异步同步
读 → 直接读本地,快
写 → 先写本地,再后台同步
用户感知不到同步过程,打开就用
✅ 最终一致,不追强一致
对于收藏、历史这类数据
"最终一致"就够了
不用追求"实时强一致"
✅ 数据分类,不同策略
- 收藏列表 → 自动合并(并集)
- 阅读进度 → 最后写入赢 + 节流
- 设置配置 → 最后写入赢
- 大数据(图片)→ WiFi-only
✅ 软删除代替硬删除
分布式同步中
删除操作也要能被同步
用 isDeleted 标记,而不是直接删掉
✅ 节流与批量
高频操作(如阅读进度)
不要每次都同步
用节流(throttle),几秒同步一次
✅ 用户可控,透明告知
- 给用户开关同步的选项
- 告诉用户同步了什么
- 给用户清除数据的入口
- 尊重用户隐私
下一步预告
分布式同步搞定了,下一篇我们来聊一个更"颠覆"的概念——原子化服务。
什么是原子化服务?——不用安装、即点即用、服务直达。
想查个民族,不用下载「民族图鉴」App,搜一下就能用;想分享给朋友,不用让对方下载,碰一下对方直接就能看。
下一篇(第85篇)我们将讲解:
- ⚡ 什么是原子化服务:免安装、即点即用、服务直达
- 🆚 原子化服务 vs 传统App:形态、分发、使用方式
- 🎯 「民族图鉴」原子化服务设计:民族查询、每日推荐、扫一扫
- 🔧 原子化服务开发:工程结构、Ability配置、打包发布
- 🚀 实战:将民族查询功能做成原子化服务
App的未来,是"原子化"吗?我们下篇揭晓。
🔗 相关链接
- 项目源码: GitCode 仓库
- 分布式数据管理: 官方文档
- 分布式 KV 存储: 官方文档
- 端云协同: 官方文档
💡 提示:很多开发者刚接触分布式同步时,容易陷入"完美主义"的误区——追求"实时强一致"、追求"零冲突"、追求"100%可靠"。
但实际上,对于大多数应用来说,最终一致就够了。
用户不会在意"收藏晚了3秒才同步过来",也不会在意"两台设备数据暂时不一样"。他们在意的是:
- 我能不能随时用(可用性)
- 我的数据会不会丢(可靠性)
- 换了设备数据在不在(一致性)
把这三点做好了,用户就满意了。不要为了追求"理论上的完美",把系统搞得很复杂、性能搞得很差。
分布式的精髓不是"完美",而是"够用就好"——在可接受的范围内,提供最好的体验。
更多推荐



所有评论(0)