鸿蒙的 RDB 和 Preferences 该怎么分工:一次数据迁移记录

简介: 真到项目里大家很大程度会一脸迷惑,来看看!

鸿蒙提供了三套本地数据方案:Preferences(键值)、RDB(关系型)、KV Store(分布式键值)。

三套的名字很直白,但真到项目里,最常见的错是用 Preferences 装本该进 RDB 的东西。开始没事,数据涨到几百条以后,性能问题会以一种很难定位的方式出现。

这篇是一次迁移记录:一个用 Preferences 存了 400 多条数据的应用,怎么迁到 RDB,以及为什么这一步不能省。

一、问题的显现方式

症状很怪:

  • 应用启动慢,但 Profiler 显示主线程没占满
  • 列表滚动时偶发卡顿,抓不到固定的卡顿点
  • 偶尔出现「数据丢失」,重启后又好了

三个症状看起来不相关,查到最后是同一个原因:Preferences 在每次 put 之后都会触发全量写盘。

Preferences 的机制是把整个键值库读进内存,改动后序列化整体写回。它适合存少量配置(用户偏好、开关、token),不适合存列表数据。

我这个应用把「消息已读状态」存在 Preferences 里,key 是 read_${msgId},400 多条消息就是 400 多个键。

每次标记已读,整个库序列化写一次。库越大,单次写越慢。这就是「偶发卡顿」的来源——不是每次都卡,取决于当时的库大小和写盘时机。

「数据丢失」是另一个原因:写盘是异步的,应用被杀时未落盘的改动会丢。Preferences 会尝试 flush,但不保证。

二、先判断该用哪个:三条分界线

迁之前先把选择标准立清楚,不然迁完还是错的。

分界线一:数据量。

几十条以内、结构简单的键值 → Preferences。
上百条、有查询需求的结构化数据 → RDB。
需要跨设备同步 → KV Store。

分界线二:有没有查询需求。

只要「按 key 取一个值」→ Preferences 够用。
需要「按条件筛选、排序、分页、聚合」→ RDB。用 Preferences 做这些,等于自己在内存里写一个数据库。

分界线三:写入频率。

一次写入、长期读取(配置项)→ Preferences。
高频写入(状态变更、日志、消息)→ RDB。RDB 是行级写入,改一行写一行,不像 Preferences 全量序列化。

三条一过,结论很清楚:消息已读状态属于「数据量大 + 有查询需求 + 高频写入」,三项全中,必须 RDB。

反过来,主题色、语言、登录 token 这类,继续留在 Preferences 是对的。

三、迁移的四步

第一步:建表和初始化

RDB 的第一步是拿到 RdbStore。鸿蒙的写法是 getRdbStore 配一个 StoreConfig:

import { relationalStore } from '@kit.ArkData'

const CONFIG: relationalStore.StoreConfig = {
 name: 'app.db',
 securityLevel: relationalStore.SecurityLevel.S2
}

const store = await relationalStore.getRdbStore(context, CONFIG)

await store.execute(`
 CREATE TABLE IF NOT EXISTS message_read (
 msg_id TEXT PRIMARY KEY,
 read_at INTEGER NOT NULL,
 session_id TEXT NOT NULL
 )
`)
await store.execute(
 'CREATE INDEX IF NOT EXISTS idx_session ON message_read(session_id)'
)

两个细节值得说:

securityLevel 必须显式给。 S1 到 S4,S1 是最低(设备解锁后可读)。给错了要么开不了库,要么数据保护级别不够。这个参数没有默认值,漏了直接编译不过——这点比运行时才报错好。

索引要建。 我这个应用最常用的查询是「某个会话下未读几条」,session_id 上有索引和没索引,400 条数据时差 10 倍,4000 条时差 100 倍。

第二步:迁移数据

从 Preferences 读出来,批量写进 RDB。

const prefs = await preferences.getPreferences(context, 'read_state')
const all = await prefs.getAll() // 拿到全部键值
const rows: relationalStore.ValuesBucket[] = []

for (const [key, val] of Object.entries(all)) {
 if (!key.startsWith('read_')) continue
 rows.push({
 msg_id: key.slice(5),
 read_at: val as number,
 session_id: '' // 旧数据没有这个字段,先留空
 })
}

// 批量插入必须包事务
await store.beginTransaction()
try {
 for (const row of rows) {
 await store.insert('message_read', row,
 relationalStore.ConflictResolution.ON_CONFLICT_REPLACE)
 }
 await store.commit()
} catch (e) {
 await store.rollBack()
 throw e
}

事务这一步不能省。 400 条数据逐条 insert 不开事务,每条都是一次独立的写盘,实测 8.2 秒。包在事务里,0.4 秒。

这个差别在真机上是「迁移时应用假死 8 秒」和「一闪而过」的区别。

ON_CONFLICT_REPLACE 是为了幂等——迁移中途被杀,重跑不会撞主键。

第三步:改成按需查询

迁完最关键的改动不是存储,是读取方式变了。

原来:应用启动时把 400 多个 key 全部读进内存,判断某条消息是否已读靠内存查找。

现在:不预加载,用到时查。


单条查询在 400 条数据下是毫秒级,但列表要显示 20 条消息时,逐条查就是 20 次。更好的做法是一次查一批:

async function readSet(msgIds: string[]): Promise<Set<string>> {
 const predicates = new relationalStore.RdbPredicates('message_read')
 predicates.in('msg_id', msgIds)
 const rs = await store.query(predicates, ['msg_id'])
 const set = new Set<string>()
 while (rs.goToNextRow()) {
 set.add(rs.getString(rs.getColumnIndex('msg_id')))
 }
 rs.close()
 return set
}

in 查询一次搞定,20 条和 1 条的开销差不多。

rs.close() 必须调。 ResultSet 不 close 会占着游标,查多了会报「too many open cursors」。这个错在测试时不容易发现,压力一大就出来。

第四步:清理旧数据

迁完把 Preferences 里那 400 多个键删掉。

const keys = Object.keys(await prefs.getAll())
for (const k of keys) {
 if (k.startsWith('read_')) await prefs.delete(k)
}
await prefs.flush()

别整库 clear()。 同一个 Preferences 库里还有别的配置项(主题、语言),清库会一起清掉。

四、迁移后的数据

指标 迁移前(Preferences) 迁移后(RDB)
应用启动耗时 1240 ms 680 ms
标记已读单次耗时 40~120 ms 2 ms
列表滚动卡顿 偶发 无
数据丢失 出现过 3 次 0

启动耗时降了 560 毫秒,主要是省掉了「把 400 多个 key 全读进内存」这一步。

五、三个容易踩的坑

坑一:store 要写成单例。

getRdbStore 本身有开销(打开文件、校验版本),每次调用都重新 get 等于每次重开库。第一次拿到之后存起来复用。

坑二:RDB 的异步不等于不占主线程。

await store.query() 是异步,但结果的组装和反序列化在主线程上。一次查 5000 行,组装那一步能卡 200 毫秒。大结果集要分页。

坑三:securityLevel 改了要迁移。

从 S1 改成 S2,旧库打不开,会静默创建一个新空库——数据看起来「全丢了」。改安全级别之前要先导出数据。

六、自学选型最容易错的地方

三处。

第一处:不确定「该用哪个」。 三套方案的文档各自都很清楚,但没人告诉你分界线在哪。判断方法就是前面那三条:数据量、有没有查询、写入频率。

第二处:忘了事务。 批量写入不开事务,测试时数据量小(20 条)看不出来,上线后数据量大就假死。这个坑的可怕之处是它在开发环境不显现。

第三处:ResultSet 不 close。 报错信息是「too many open cursors」,但它不告诉你是哪个查询泄的。排查方法是在每个 query 后面加日志计数。

这三处的共同点:文档都写了,但不写在「入门第一页」。要踩到才知道去哪找。

顺带说一句,这类「三套方案怎么选」的判断,用誉天教育打比方,它的鸿蒙课上讲法是先给三条分界,再让学员拿自己的项目对一遍。这个顺序比先讲 API 有用——因为选错了,API 背得再熟也是白干。

最后

Preferences 和 RDB 不是「简单版」和「高级版」的关系,是两种不同的存储模型。

Preferences 是整个库当一条记录,改一个键要重写全库。RDB 是按行改,改一行写一行。

数据涨起来之后,前者的问题会以「偶发卡顿 + 莫名数据丢失」的形式出现,这两个症状都不会指向存储方案选错了。

所以我的建议是:动手前先数一下会有多少条数据。超过 200 条,直接上 RDB。

相关文章
|
14天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
6141 45
|
2天前
|
云安全 人工智能 安全
|
15天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
5099 60
|
9天前
|
Linux 开发工具 iOS开发
2026最新codex下载安装保姆级使用教程(下载+安装+登录+使用,含所有历史版本)
本文详解OpenAI Codex国内快速上手指南:提供Win/macOS/Linux全平台免安装包、npm/Homebrew等多方式安装、账号/API Key双登录、简体中文切换,覆盖下载、安装、汉化、使用及高频避坑(如微软商店失败、npm超时等),10分钟即可跑通。
2442 6
|
1天前
|
JSON 前端开发 API
阿里云最新Qwen4-Max大模型介绍:模型定位、特征、能力、适用场景解析
2026 年 11 月 11 日,阿里发布第四代旗舰模型Qwen4-Max预览版,首发千问 AI 平台 Token Plan,并同步接入千问办公、Qoder 等入口。该版本聚焦知识理解、世界感知、编程与 Agentic 四项升级,主打复杂编程、专业办公、前端设计与专业软件操作。预览期至 11 月 25 日 24:00,到期后模型标识下线;限时 2 折、新用户再享 5 折,最低 1 折可体验。
|
7天前
|
人工智能 安全 API
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(dsh)是DeepSeek AI于2026年开源的轻量级Agent运行框架,基于Cordis内核,践行“一切皆插件”理念。支持Web/桌面双端,开箱即用;内置多模型接入、丰富插件生态(超3.5万),适配编程、办公、创作等场景。(239字)
1535 1
|
2天前
|
人工智能 前端开发 API
阿里云Qwen4-Max 大模型详解:定位、特征、能力、适用场景与相对Qwen3-Max差异解析
2026年11月11日,阿里发布第四代旗舰模型Qwen4-Max预览版,首发千问AI平台Token Plan并接入千问办公、Qoder等入口。该版本聚焦知识理解、世界感知、编程与Agentic四项升级,主打复杂编程、专业办公、前端设计与专业软件操作等真实任务。预览期为11月11日10:00至11月25日24:00,仅支持Token Plan调用,到期后模型标识下线;限时2折、新用户再享5折,最低1折可体验。

热门文章

最新文章