Hotdry.
database-systems

SQLNet:用SQL作为社交网络交互语言的工程架构与CDC同步实现

深入解析SQLNet的三数据库架构设计,探讨Turso CDC在SQL社交网络中的双向同步机制、权限控制策略与扩展性挑战。

在传统社交网络中,每一次点赞、评论或发帖都被精心包装成友好的 UI 交互,背后却是简单的数据库操作。SQLNet.cc 反其道而行之,将 SQL 查询直接作为用户界面,让用户通过编写 SQL 语句来参与社交互动。这种设计不仅是对社交网络本质的回归,更是一次数据库架构的极限挑战。

三数据库模型:集体幻觉的工程实现

SQLNet 的核心架构被称为 "三数据库模型",旨在为每个用户提供独立的数据库体验,同时维持统一的社交网络感知。

系统数据库:身份验证的守门人

系统数据库负责处理账户管理、密码验证和租户映射。用户不直接与之交互,它仅作为后端服务的身份验证层存在。这种分离确保了核心业务逻辑与身份管理的解耦。

主数据库:唯一真相源

主数据库存储所有社交行为的规范记录,采用简洁的规范化模式:

posts(id, author_id, content, created_at, updated_at)
likes(id, post_id, user_id, created_at)
comments(id, post_id, user_id, content, created_at, updated_at)
follows(follower_id, following_id, created_at)

这个数据库不包含任何衍生字段,如like_countcomment_count,确保数据的一致性和可追溯性。

租户数据库:个性化的幻觉空间

每个用户注册时,系统会为其分配一个完整的 SQLite 数据库(通过 Turso 提供)。这些数据库不仅包含主数据库的所有数据,还添加了便利字段:

posts(id, author_id, content, created_at, updated_at, like_count, comment_count)

这种设计允许用户编写更简洁的查询,例如ORDER BY like_count DESC,而无需执行复杂的 JOIN 操作。用户直接与自己的数据库交互,但所有数据都通过同步机制与主数据库保持一致性。

CDC 同步引擎:Turso 不稳定特性的工程化应用

SQLNet 最核心的技术挑战是实现数千个独立数据库之间的实时同步。解决方案依赖于 Turso 的 Change Data Capture(CDC)功能,具体通过一个不稳定的 PRAGMA 实现。

CDC 配置的关键细节

每个数据库连接都必须执行以下命令:

PRAGMA unstable_capture_data_changes_conn('full');

这个unstable_前缀明确表明该功能尚未稳定,可能在未来的 Turso 版本中被修改或移除。但正是这个功能为 SQLNet 提供了必要的变更捕获能力。

执行该 PRAGMA 后,Turso 会创建一个特殊的turso_cdc表,记录所有数据变更。读取变更的查询如下:

SELECT
    table_name,
    change_type,        -- 1=INSERT, 0=UPDATE, -1=DELETE
    id,
    bin_record_json_object(table_columns_json_array(table_name), before) as before_json,
    bin_record_json_object(table_columns_json_array(table_name), after) as after_json
FROM turso_cdc
ORDER BY change_id ASC

其中bin_record_json_objecttable_columns_json_array是 Turso 的内部函数,用于将二进制记录转换为 JSON 格式。

连接管理的陷阱

关键工程经验:CDC PRAGMA 必须在每个连接上执行,而不是每个数据库。如果某个连接池漏掉了这个步骤,该连接将无法捕获变更,导致同步 "随机" 失效。SQLNet 开发者曾花费 4 小时调试这个问题,最终发现是一个连接池配置错误。

正确的 Go 实现:

db, err := sql.Open("turso", dbPath)
if err != nil {
    return nil, err
}

// 必须在每个连接上执行
_, err = db.Exec("PRAGMA unstable_capture_data_changes_conn('full');")

双向同步机制:上游与下游的数据流

SQLNet 的同步系统分为两个方向,每个方向都有独特的挑战和解决方案。

上游同步:用户→主数据库

当用户在自己的数据库中执行操作时,同步流程如下:

  1. 变更捕获:读取用户数据库的turso_cdc
  2. 来源过滤:排除来自主数据库的变更(防止循环复制)
  3. 字段过滤:移除非规范字段(如like_count
  4. 写入主库:将规范化的变更写入主数据库
  5. 统计更新:重新计算受影响帖子的统计信息
  6. 清理 CDC:清空用户的 CDC 表

字段过滤是关键的安全措施:

func (r *Replicator) isCanonicalField(field string) bool {
    excludedFields := map[string]bool{
        "like_count":    true,
        "comment_count": true,
        "_sync_origin":  true,
    }
    return !excludedFields[field]
}

下游同步:主数据库→所有其他用户

下游同步是一个更复杂的广播过程:

  1. 读取主库变更:定期扫描主数据库的 CDC 表
  2. 识别来源:确定每个变更的发起用户
  3. 广播写入:对除发起者外的所有租户执行INSERT OR REPLACE
  4. 清理主 CDC:清空主数据库的 CDC 表
  5. 循环检测:通过_sync_origin字段标记下游写入

循环检测的实现:

func isReplicatedChange(change CDCChange) bool {
    if change.Operation == "INSERT" {
        if origin, ok := change.NewData["_sync_origin"].(string); ok {
            return origin == "primary"
        }
    }
    // UPDATE: was null before, is "primary" now? That's a sync write.
    if change.Operation == "UPDATE" {
        oldOrigin, _ := change.OldData["_sync_origin"].(string)
        newOrigin, _ := change.NewData["_sync_origin"].(string)
        return oldOrigin == "" && newOrigin == "primary"
    }
    return false
}

权限控制与安全策略

me () 函数:上下文感知的查询注入

SQLNet 引入了一个特殊的me()函数,在查询执行时自动注入当前用户的 ID:

-- 用户查询自己的帖子
SELECT * FROM posts WHERE author_id = me();

-- 查看谁关注了自己
SELECT * FROM follows WHERE following_id = me();

后端实现会拦截 SQL 查询,将me()替换为从 JWT 令牌中提取的实际用户 ID。这种设计既保持了查询的简洁性,又确保了数据隔离。

本地表:用户的私有空间

并非所有表都参与同步。用户可以创建仅存在于自己数据库中的表:

-- 草稿表,仅本地可见
CREATE TABLE drafts (
    id TEXT PRIMARY KEY,
    content TEXT,
    created_at DATETIME
);

-- 保存的查询,个性化算法
CREATE TABLE saved_queries (
    id TEXT PRIMARY KEY,
    name TEXT,
    query TEXT
);

这些表完全由用户控制,上游同步会忽略它们,为用户提供了真正的私有空间。

"破坏性" 功能:自我修正的幻觉

SQLNet 允许用户修改非规范字段,如将like_count设置为任意值:

UPDATE posts SET like_count = 999999 WHERE id = 'some-post';

这种修改会在用户的数据库中生效,但不会传播到主数据库。当下游同步运行时,真实的统计信息会覆盖用户的修改。这种设计创造了一种 "数字涂鸦" 体验 —— 用户可以暂时修改现实,但系统最终会恢复真相。

扩展性挑战与优化方向

当前架构的复杂度分析

下游同步的复杂度为 O (变更数 × 租户数)。假设有 1,000 个用户,每个同步周期有 100 个变更,那么需要执行 100,000 次数据库写入。随着用户数量增长,这种复杂度将变得不可持续。

优化策略建议

  1. 批量处理:将多个变更合并为单个事务,减少网络往返
  2. 分区同步:将用户分组,只向相关组广播变更
  3. 增量 CDC:只同步自上次同步以来的变更,而不是全量扫描
  4. 连接池优化:复用数据库连接,减少 CDC 配置开销
  5. 异步队列:使用消息队列缓冲同步任务,平滑负载

分阶段扩展方案

  • 阶段 1(<1,000 用户):当前架构足够
  • 阶段 2(1,000-10,000 用户):引入批量处理和分区
  • 阶段 3(>10,000 用户):重构为基于事件的架构,使用专门的同步服务

工程实践建议

CDC 使用的注意事项

  1. 版本兼容性unstable_capture_data_changes_conn可能在未来版本中变更,需要密切跟踪 Turso 更新
  2. 连接管理:确保所有连接池都正确配置 CDC PRAGMA
  3. 错误处理:实现健壮的重试机制,处理 CDC 读取失败的情况
  4. 监控指标:跟踪 CDC 表大小、同步延迟和失败率

数据库设计最佳实践

  1. 明确字段分类:严格区分规范字段和衍生字段
  2. 版本化模式:为每个表添加_version字段,支持乐观并发控制
  3. 审计日志:保留关键操作的审计记录,便于调试和合规
  4. 索引策略:为同步查询创建合适的索引,特别是 CDC 表的change_id

安全加固措施

  1. SQL 注入防护:虽然用户编写 SQL,但仍需验证查询的合法性
  2. 速率限制:防止恶意用户通过大量查询耗尽资源
  3. 查询复杂度限制:限制 JOIN 数量和结果集大小
  4. 敏感数据过滤:确保用户无法访问其他用户的私有信息

结论:SQL 作为交互语言的未来

SQLNet 展示了将数据库查询语言直接作为用户界面的可能性。这种设计虽然增加了用户的学习成本,但提供了前所未有的透明度和控制力。对于技术社区而言,SQLNet 不仅是一个社交网络,更是一个数据库架构的实验场。

CDC 同步机制虽然目前依赖于不稳定的 Turso 特性,但为多租户数据库同步提供了有价值的参考模式。随着边缘计算和分布式数据库的发展,类似的架构模式可能在更多场景中找到应用。

最终,SQLNet 的成功不在于用户规模,而在于它挑战了我们对 "用户友好" 的传统认知。在算法推荐和 UI 抽象盛行的时代,直接暴露底层操作可能正是某些用户渴望的 "友好" 体验。

资料来源

  • SQLNet 官方网站:https://sqlnet.cc
  • "I Built a Social Network Where You Have to Write SQL to Post" (Dev.to, 2026-01-03)
  • Turso 文档:Data & Connections 章节
查看归档