HarmonyOS 7 RelationalStore + ArkUI:收藏列表乐观更新中的事务回滚与页面状态纠偏【鸿蒙心迹】
最近我把一个资讯 Demo 的收藏按钮重新做了一遍。功能很小:点一下爱心,列表立即变成“已收藏”,数据库随后落盘。真正麻烦的是连续点击、事务失败、页面切换撞在一起以后,UI 很容易先跑到前面,数据却停在后面。
最初是“先改 UI,再 update RDB”。单次点击正常,但我连续点五次,并在第三次写入时故意制造异常,就看到 article_317 页面已经是绿色爱心,RDB 却仍然是未收藏;退出详情再回来,状态又突然变回去。
这次只处理一个问题:乐观更新以后,怎样让 ArkUI 即时反馈、RelationalStore 事务结果和异步回调顺序重新对齐。

一、一个收藏按钮其实有三套状态
Demo 叫 FavoriteTxnLab,最终稳定数据是:Session fav_txn_20261001_11,Item article_317,Mutation Seq 5,Rollback Count 1,Pending 0,UI Favorite true,DB Favorite true,State CONSISTENT,Last Cost 37 ms。
一开始我只维护 isFavorite。后来发现它同时承担了三个职责:用户眼前看到的值、数据库最终确认的值、正在执行中的目标值。
如果先点收藏,UI 立刻从 false 变成 true;数据库没写完又点取消;前一次 Promise 比后一次更晚返回,就会出现旧结果覆盖新操作。
所以页面侧拆成 uiFavorite、dbFavorite、pending 和 mutationSeq。其中 mutationSeq 每点击一次递增,用来判断异步结果是不是已经过期。
二、乐观更新先设计“怎么退”
第一段代码解决即时响应和过期回调。
@State uiFavorite: boolean = false
@State dbFavorite: boolean = false
@State pending: number = 0
@State mutationSeq: number = 0
private async onFavoriteClick(): Promise<void> {
const seq = ++this.mutationSeq
const before = this.dbFavorite
const target = !this.uiFavorite
this.uiFavorite = target
this.pending++
try {
const result = await this.favoriteService.toggleFavorite(
'article_317', target, seq
)
if (seq !== this.mutationSeq) return
this.dbFavorite = result.favorite
this.uiFavorite = result.favorite
} catch (_) {
if (seq === this.mutationSeq) {
this.uiFavorite = before
}
} finally {
this.pending = Math.max(0, this.pending - 1)
}
}
关键不是 try/catch,而是 seq !== this.mutationSeq。第五次点击发生以后,第三次、第四次请求即使稍后返回,也不能覆盖最新页面状态。
正式项目里 before 最好保存轻量快照,例如爱心、收藏数和卡片版本号。否则事务失败只回退图标,计数仍然多一次。
三、数据库事务把业务写入绑成一次提交
收藏状态更新成功、审计记录插入失败时,如果两步分开执行,后续会出现“状态变了,却找不到对应操作记录”。
import { relationalStore } from '@kit.ArkData'
async toggleFavorite(
itemId: string,
target: boolean,
seq: number
): Promise<{ favorite: boolean }> {
const tx = await this.store.createTransaction({
transactionType: relationalStore.TransactionType.IMMEDIATE
})
try {
const predicates =
new relationalStore.RdbPredicates('article')
.equalTo('id', itemId)
await tx.update(
{ favorite: target ? 1 : 0 },
predicates
)
await tx.insert('favorite_audit', {
item_id: itemId,
mutation_seq: seq,
target: target ? 1 : 0,
created_at: Date.now()
})
await tx.commit()
return { favorite: target }
} catch (err) {
await tx.rollback()
this.rollbackCount++
throw err
}
}
这段只解决数据库内部一致性,不会自动修正 UI。rollback() 后页面如果已经显示绿色爱心,仍要用上一节的快照恢复。
也不要把网络请求塞进事务。进入 transaction 前准备好外部数据,事务里只做必要本地读写并尽快提交。
第三个 mutation 我故意让审计插入失败,因此日志出现一次 rollback count=1;第五次操作成功提交,数据库和页面最终都落在 true。
四、commit 成功后再做一次最终对账
快速点击很多次时,commit 成功不等于“当前 UI 就是最新状态”。所以事务完成后重新读一次数据库。
private async reconcileLatest(
itemId: string,
seq: number
): Promise<void> {
if (seq !== this.mutationSeq) return
const dbValue =
await this.favoriteRepository.getFavorite(itemId)
if (seq !== this.mutationSeq) return
this.dbFavorite = dbValue
this.uiFavorite = dbValue
if (this.pending === 0 &&
this.uiFavorite === this.dbFavorite) {
this.state = 'CONSISTENT'
}
}
两次序号判断分别挡住“查询前已过期”和“查询过程中又发生新点击”。
我把 CONSISTENT 定义得很严格:没有 pending、UI 与 DB 相同、当前序号仍然最新。只有三条同时满足才显示最终稳定状态。
五、日志要把失败过程留下来
页面只管视觉状态和点击序号,FavoriteTxnService 管事务,FavoriteRepository 管最终查询。这样注入失败时,不需要把数据库细节塞回页面。
HiLog 固定输出:
optimistic ui=true seq=5
createTransaction IMMEDIATE
rollback count=1
commit seq=5
db=true ui=true
State: APPLYING -> CONSISTENT
如果只看 commit success,很容易漏掉之前确实发生过 rollback。

六、连续五次操作,才能真正看出问题
第一次收藏;第二次取消;第三次再次收藏但事务回滚;第四次重试;第五次最终提交为收藏。
最终页面数据是 fav_txn_20261001_11、article_317、Mutation Seq=5、Rollback Count=1、Pending=0、UI Favorite=true、DB Favorite=true、State=CONSISTENT、Last Cost=37 ms。
真正有价值的不是绿色 CONSISTENT,而是中间出现过一次失败,系统仍能把状态恢复并完成最新操作。

七、生命周期和正式项目边界
页面离开时,我不会强行取消已经开始的本地事务;数据库继续 commit 或 rollback,页面只停止接收过期结果。mutationSeq 防操作乱序,页面 generation 防销毁后的旧回调。
事务对象也不缓存成全局单例。应复用的是 RdbStore 和 Repository。若还要同步云端,先记录待同步事件再交给同步层;多设备场景则需要服务端版本号或冲突规则。
八、最后真正改掉的是“谁说了算”
有效改动只有三层:页面用序号挡掉过期异步结果,数据库用 Transaction 保证一次业务写入的原子性,事务结束后再查询最终状态,把 UI 和 DB 对齐。
完整链路是:
OPTIMISTIC → APPLYING → ROLLBACK → RETRY → CONSISTENT
乐观更新的价值是快,不是让页面永远领先数据库。把即时反馈和最终确认拆开以后,失败就只是状态机里正常的一步。
更多推荐





所有评论(0)