多端同步为什么会漏记录:用 SQLite 验证游标和删除事件

举报
yd_234095983 发表于 2026/11/07 14:17:23 2026/11/07
【摘要】 用 8 项可运行断言复现同秒分页漏读和迟到写入,说明按服务端顺序拉取、保留删除事件以及事务更新游标的条件。

为什么第二页会漏掉一条待办变更

鸿蒙开发实践中心的「云同步待办清单:鸿蒙端云一体实战」提到了从全量轮询改成增量拉取。这里把这个思路缩小成一个 SQLite 内存实验:两个设备同步待办变更,每页只取一条。本文不改动案例源码,也未部署云资源。

如果游标只记 changed_at,第一页取走同一秒的第一条变更后,下一页用 changed_at > ? 会跳过同一秒的第二条。把唯一的变更 ID 加入排序与游标,能解决已有数据的并列问题:WHERE (changed_at, change_id) > (?, ?) ORDER BY changed_at, change_id。两个字段都不能为空,且客户端保存的是实际处理完的最后一条,不是客户端当前时间。

复合游标也挡不住迟到写入

复合游标扫过 (100, "change-b") 后,如果补写了 (100, "change-0"),它仍排在游标前。按业务时间分页不等于按可见的变更顺序同步。下面用同一张追加事件表验证两个问题,再对比按服务端顺序 seq 拉取。deleted=1 是删除事件,不是从结果集里过滤掉一行。

import sqlite3
from contextlib import closing

with closing(sqlite3.connect(":memory:")) as connection:
    connection.execute("""CREATE TABLE changes (
        seq INTEGER PRIMARY KEY, changed_at INTEGER NOT NULL,
        change_id TEXT NOT NULL UNIQUE, task_id TEXT NOT NULL,
        deleted INTEGER NOT NULL CHECK (deleted IN (0, 1))
    )""")
    append_sql = """INSERT INTO changes
        (changed_at, change_id, task_id, deleted) VALUES (?, ?, ?, ?)"""
    with connection:
        connection.executemany(append_sql, [
            (100, "change-a", "task-a", 0),
            (100, "change-b", "task-b", 0),
        ])

    first_page = connection.execute("""SELECT changed_at, change_id
        FROM changes ORDER BY changed_at, change_id LIMIT 1""").fetchone()
    assert first_page == (100, "change-a")
    naive = connection.execute("SELECT change_id FROM changes WHERE changed_at > ?",
                               (first_page[0],)).fetchall()
    assert naive == []
    composite_sql = """SELECT changed_at, change_id FROM changes
        WHERE (changed_at, change_id) > (?, ?) ORDER BY changed_at, change_id LIMIT 1"""
    second_page = connection.execute(composite_sql, first_page).fetchone()
    assert second_page == (100, "change-b")

    with connection:
        connection.executemany(append_sql, [
            (100, "change-0", "task-late", 0),
            (101, "change-delete", "task-b", 1),
        ])
    assert connection.execute(composite_sql, second_page).fetchone() == (101, "change-delete")

    pull_sql = """SELECT seq, task_id, deleted FROM changes
        WHERE seq > ? ORDER BY seq LIMIT 1"""
    local_tasks = {"task-a", "task-b"}
    cursor = 2
    late_event = connection.execute(pull_sql, (cursor,)).fetchone()
    assert late_event == (3, "task-late", 0)
    cursor, task_id, deleted = late_event
    local_tasks.add(task_id)
    delete_event = connection.execute(pull_sql, (cursor,)).fetchone()
    assert delete_event == (4, "task-b", 1)
    cursor, task_id, deleted = delete_event
    local_tasks.discard(task_id)
    assert local_tasks == {"task-a", "task-late"}
    assert connection.execute(pull_sql, (cursor,)).fetchone() is None

print("8 项断言通过")

落地时检查什么

  • 服务端排序:示例是 SQLite 单写入者、追加事件且不删除历史的模型。迁移到并发事务数据库时,不能把任何自增 ID 都当成提交顺序:如果小号事务晚于大号提交,客户端照样可能越过它。使用能保证发布顺序的变更日志或数据库变更流位点。
  • 客户端提交:把本页变更和新游标放在同一个本地事务。中断后按旧游标重拉,增改按版本或事件去重,删除按业务键幂等应用。删除事件保留时间必须覆盖增量同步窗口;过期游标改走全量重建。
  • 数据范围:服务端根据已认证身份限定可读的事件,不接受客户端指定任意用户。演示只覆盖单用户分页和删除,不涉及并发写冲突、鉴权或全量快照初始化。

实验使用 Python 标准库的 sqlite3,全部为合成数据。参考:SQLite 官方文档《Row Values》的滚动窗口查询、《Isolation In SQLite》的写入隔离,以及上述鸿蒙公开实践案例。AI 辅助起草和整理,示例在本地独立运行验证。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。