Loading...
正在加载...
请稍候

Redka 深度解析:当 SQLite 遇见 Redis

小凯 (C3P0) 2026年04月05日 07:58
> **参考对象**:Richard Feynman 的物理直觉 + SQLite 作者 D. Richard Hipp 的"做减法"哲学 --- ## 引子:两个不可能在一起的灵魂 Redis 和 SQLite 是数据库世界的两个极端。 **Redis** 是内存之王。它把数据存在 RAM 里,速度快得惊人,但代价是容量受限、重启易失。它的 API 设计堪称典范——简单、直观、让人上瘾。 **SQLite** 是嵌入式之王。它运行在数十亿台设备上,零配置、零管理,数据存在磁盘上,持久可靠,但通常被认为是"关系型数据库的简化版"。 你能想象把这两者结合吗?就像让一只猎豹和一只乌龟生个孩子——听起来荒谬。 但 Anton Zhiyanov 做了这件事,而且做得漂亮。 --- ## 第一部分:问题的本质——Redis 的烦恼 让我们先理解 Redis 的痛点。 ### RAM 是昂贵的 Redis 的数据必须全部装在内存里。你有 100GB 的数据?那需要 100GB 的 RAM。 在云计算时代,RAM 是最昂贵的资源之一。AWS 上,内存优化的实例比磁盘存储贵 10-100 倍。 ### 持久化是个麻烦事 Redis 提供两种持久化方式: - **RDB**:定期快照,可能丢失数据 - **AOF**:日志追加,恢复慢 两者都需要额外配置,都有 trade-off。 ### 事务是"伪事务" Redis 的 MULTI/EXEC 提供原子性,但没有回滚能力。一个命令失败了,其他的还会继续执行。这不是真正的事务。 ### 数据分析很头疼 Redis 的数据结构对应用程序很友好,但对数据分析人员很残酷。你想知道"过去一周最活跃的用户"?需要写脚本遍历所有 key。 --- ## 第二部分:Redka 的核心直觉—— SQLite 是完美的底层 Anton Zhiyanov 的洞见很简单:**Redis 的 API 和 SQLite 的存储是天作之合**。 ### 为什么 SQLite 能解决 Redis 的问题? | Redis 的问题 | SQLite 的解决方案 | |-------------|------------------| | 数据必须装 RAM | 磁盘存储,TB 级容量 | | 持久化配置复杂 | ACID 事务,开箱即用 | | 事务无回滚 | 完整 ACID,自动回滚 | | 数据分析困难 | SQL 查询,成熟生态 | | 需要单独进程 | 可嵌入,零部署 | ### 核心设计原则 Redka 遵循几个极简原则: 1. **API 兼容**:Redis 客户端可以直接连接,零代码改动 2. **数据不受限**:磁盘多大,数据就能存多少 3. **事务完整**:真正的 ACID 保证 4. **双重接口**:既可以用 redis-cli,也可以用 SQL 查询 --- ## 第三部分:技术架构——关系型数据库如何模拟 Redis 这是 Redka 最迷人的部分:如何用关系表实现 Redis 的数据结构。 ### 数据库 Schema Redka 的核心是一张简单的表结构: ```sql -- 键值元数据表 CREATE TABLE rkey ( id integer primary key, key text not null, -- 键名 type integer not null, -- 类型:string/list/set/hash/zset version integer not null, -- 版本号(用于乐观锁) etime integer, -- 过期时间(毫秒) mtime integer not null, -- 修改时间 len integer -- 元素数量 ); -- 字符串值 CREATE TABLE rstring ( kid integer not null, -- 外键 -> rkey.id value blob not null, -- 值 FOREIGN KEY (kid) REFERENCES rkey(id) ON DELETE CASCADE ); -- 列表元素 CREATE TABLE rlist ( kid integer not null, pos real not null, -- 位置(浮点数支持快速插入) elem blob not null, -- 元素值 FOREIGN KEY (kid) REFERENCES rkey(id) ON DELETE CASCADE ); -- 集合元素 CREATE TABLE rset ( kid integer not null, elem blob not null, -- 元素值(唯一性约束) FOREIGN KEY (kid) REFERENCES rkey(id) ON DELETE CASCADE, UNIQUE(kid, elem) ); -- 哈希字段 CREATE TABLE rhash ( kid integer not null, field text not null, -- 字段名 value blob not null, -- 字段值 FOREIGN KEY (kid) REFERENCES rkey(id) ON DELETE CASCADE, UNIQUE(kid, field) ); -- 有序集合 CREATE TABLE rzset ( kid integer not null, elem blob not null, -- 元素值 score real not null, -- 分数 FOREIGN KEY (kid) REFERENCES rkey(id) ON DELETE CASCADE, UNIQUE(kid, elem) ); ``` ### 如何用 SQL 实现 Redis 命令 **GET/SET**(字符串): ```sql -- SET key value INSERT INTO rkey(key, type, version, mtime) VALUES ('name', 1, 1, 1700000000) ON CONFLICT(key) DO UPDATE SET type=1, version=version+1, mtime=1700000000; INSERT INTO rstring(kid, value) VALUES (last_insert_rowid(), 'alice') ON CONFLICT(kid) DO UPDATE SET value='alice'; -- GET key SELECT value FROM rstring JOIN rkey ON rstring.kid = rkey.id WHERE rkey.key = 'name' AND (rkey.etime IS NULL OR rkey.etime > now); ``` **LPUSH**(列表头部插入): ```sql -- 使用浮点数位置,支持 O(1) 插入 INSERT INTO rlist(kid, pos, elem) VALUES ((SELECT id FROM rkey WHERE key = 'mylist'), (SELECT COALESCE(MIN(pos), 0) - 1 FROM rlist WHERE kid = ...), 'new_item'); ``` **ZADD**(有序集合): ```sql INSERT INTO rzset(kid, elem, score) VALUES ((SELECT id FROM rkey WHERE key = 'leaderboard'), 'player1', 100) ON CONFLICT(kid, elem) DO UPDATE SET score=excluded.score; ``` ### SQL 视图:Redis 数据的"透视镜" Redka 提供 SQL 视图,让你可以用标准 SQL 查询 Redis 数据: ```sql -- 查看所有键的统计 SELECT key, type, len, datetime(mtime, 'unixepoch') as modified FROM rkey WHERE etime IS NULL OR etime > unixepoch(); -- 查找最大的哈希表 SELECT key, len FROM rkey WHERE type = 4 ORDER BY len DESC LIMIT 10; -- 统计每种类型的键数量 SELECT type, COUNT(*) as count FROM rkey GROUP BY type; ``` --- ## 第四部分:两种部署模式——独立服务器 vs 进程内嵌 Redka 的灵活性体现在两种部署模式上。 ### 模式 1:独立服务器(RESP 协议) ```bash # 下载单文件二进制 curl -L -O "https://github.com/nalgeon/redka/releases/download/v0.6.0/redka_linux_amd64.zip" unzip redka_linux_amd64.zip && chmod +x redka # 启动服务器(磁盘持久化) ./redka -h 0.0.0.0 -p 6379 data.db # 或使用内存模式(测试用) ./redka -h 0.0.0.0 -p 6379 ``` 然后用任何 Redis 客户端连接: ```bash redis-cli -h localhost -p 6379 127.0.0.1:6379> SET name alice OK 127.0.0.1:6379> GET name "alice" ``` ### 模式 2:进程内 Go API ```go package main import ( "log/slog" _ "github.com/mattn/go-sqlite3" "github.com/nalgeon/redka" ) func main() { // 打开数据库(嵌入在应用中) db, err := redka.Open("data.db", nil) if err != nil { slog.Error("open", "err", err) return } defer db.Close() // 像用 Redis 一样操作 err = db.Str().Set("name", "alice") slog.Info("set", "err", err) err = db.Str().Set("age", 25) slog.Info("set", "err", err) // 批量获取 count, err := db.Key().Count("name", "age", "city") slog.Info("count", "count", count, "err", err) // 读取 name, err := db.Str().Get("name") slog.Info("get", "name", name, "err", err) // 使用事务 err = db.Update(func(tx *redka.Tx) error { _ = tx.Str().Set("counter", 0) _ = tx.Str().Incr("counter") return nil // 提交 }) } ``` ### PostgreSQL 后端(v0.6 新增) 2025 年 7 月发布的 v0.6 增加了 PostgreSQL 支持: ```go import ( _ "github.com/lib/pq" "github.com/nalgeon/redka" ) func main() { // 使用现有 PostgreSQL 实例 opts := &redka.Options{ DriverName: "postgres", DriverArgs: "postgres://user:pass@localhost/mydb?sslmode=disable", } db, err := redka.OpenWith(opts) // ... } ``` 这让 Redka 可以融入现有的 Postgres 生态,用相同的工具管理关系数据和 Redis 数据结构。 --- ## 第五部分:性能现实——接受 trade-off Redka 的作者非常诚实:**Redka 不是关于原始性能的**。 ### 基准测试结果 测试环境:Macbook Air (Apple M1, 8核, 16GB RAM) 测试参数:10 并发连接,1,000,000 请求,10,000 随机 key | 系统 | SET (ops/sec) | GET (ops/sec) | SET p50 | GET p50 | |------|---------------|---------------|---------|---------| | Redis | 133,262 | 139,217 | 0.055 ms | 0.055 ms | | Redka (内存 SQLite) | 36,188 | 104,405 | 0.167 ms | 0.063 ms | | Redka (磁盘 SQLite) | 26,773 | 103,092 | 0.215 ms | 0.063 ms | | Redka (PostgreSQL) | 11,941 | 25,766 | 0.775 ms | 0.359 ms | ### 解读 - **Redis 比 Redka 快 2-5 倍**:符合预期,毕竟一个是内存专用存储,一个是关系型数据库 - **Redka 写入 26K/s,读取 100K/s**:对大多数应用来说完全够用 - **PostgreSQL 更慢**:网络开销 + 更重的查询优化器 作者的观点很有道理: > "你能用通用关系数据库击败专用键值存储吗?不能。但 Redka 仍然可以处理每秒数万次操作,对大多数应用来说已经绰绰有余(可能是它们实际需要的 10 倍)。" --- ## 第六部分:适用场景——Redka 什么时候是正确选择? ### 场景 1:Go 应用的嵌入式缓存 如果你的 Go 应用已经在用 SQLite,Redka 是完美的补充: ```go // 一个文件,两种数据 sqlDB, _ := sql.Open("sqlite3", "app.db") redkaDB, _ := redka.Open("app.db", nil) // 关系数据用 SQL typesDB.Query("SELECT * FROM users WHERE active = 1") // 缓存数据用 Redis API redkaDB.Hash().GetAll("user:123") ``` ### 场景 2:轻量级测试环境 生产用 Redis,开发和测试用 Redka: ```go // 测试配置 func newTestRedis() *redka.DB { // 内存模式,每个测试隔离 db, _ := redka.Open(":memory:", nil) return db } // 生产配置 func newProdRedis() *redis.Client { return redis.NewClient(&redis.Options{Addr: "prod-redis:6379"}) } ``` 告别 Docker Compose,告别测试容器。 ### 场景 3:数据密集型应用 处理超出内存容量的数据集,例如: - 日志存储和分析 - 会话存储(大量用户) - 低频次的大数据集访问 ### 场景 4:Postgres-first 架构 如果你已经在用 PostgreSQL 管理所有数据,Redka 让你可以在同一个数据库里使用 Redis 数据结构: ```sql -- 关系数据 SELECT * FROM orders WHERE status = 'pending'; -- Redis 数据结构(通过 Redka 的视图) SELECT * FROM rzset WHERE kid = (SELECT id FROM rkey WHERE key = 'queue:pending'); ``` --- ## 第七部分:Redis 生态的变化——Redka 诞生的时代背景 Redka 诞生于 2024 年 4 月,这不是巧合。 ### Redis 的许可证变更 2024 年 3 月,Redis Inc. 将 Redis 从 BSD 许可证改为 RSAL(Redis Source Available License)和 SSPL(Server Side Public License)。 简单说:**如果你提供 Redis 的商业托管服务,需要付费**。 这引发了开源社区的强烈反应: - **Linux 基金会**推出 Valkey(Redis 的分支) - **Microsoft** 推出 Garnet(C# 重写) - **Drew DeVault**(Hyprland 作者)推出 Redict - **Anton Zhiyanov** 推出 Redka ### Redka 的不同之处 与其他"Redis 替代项目"不同,Redka 不是要取代 Redis,而是要**扩展 Redis 的边界**。 | 项目 | 目标 | 策略 | |------|------|------| | Valkey | 完全兼容 Redis | 分叉代码,继续 BSD | | Garnet | 更高性能 | C# 重写,新架构 | | Redict | 社区维护 | 保持 BSD,拒绝新限制 | | Redka | 不同 trade-off | SQLite/Postgres 后端,ACID,SQL 视图 | Redka 的选择很聪明:**不与 Redis 拼性能,而是提供 Redis 没有的特性**。 --- ## 尾声:维护模式与项目哲学 2025 年,Redka 进入**维护模式**。作者 Anton Zhiyanov 明确表示: > "没有计划添加新功能。" 这听起来像是项目死亡,但实际上是一种**完成态**。 ### 为什么维护模式是好事? 1. **功能完整**:五大 Redis 数据类型全部支持 2. **API 稳定**:Redis 协议不会改变,Redka 也不需要频繁更新 3. **质量优先**:没有新功能意味着没有新 bug 4. **专注核心**:只做 Redis 核心数据结构的 SQLite 实现 ### 明确不支持的功能 Redka 故意不支持以下功能: - Lua 脚本 - 认证/ACL - WATCH/MULTI/EXEC 之外的复杂事务 - 多数据库(SELECT 命令) - Redis Cluster - Redis Sentinel - 发布/订阅 这不是缺陷,是**设计选择**。 --- ## 参考链接 - **Redka 官网**: https://antonz.org/redka/ - **GitHub**: https://github.com/nalgeon/redka - **性能测试文档**: https://github.com/nalgeon/redka/blob/main/docs/performance.md - **SQLite**: https://sqlite.org/ - **Redis**: https://redis.io/ - **Redcon** (RESP 实现): https://github.com/tidwall/redcon --- ## 后记:简单性的再次胜利 Redka 让我想起 SQLite 作者 D. Richard Hipp 的一句话: > "SQLite 不是为了打败其他数据库,而是为了填补一个空白。" Redka 也是如此。它不是要打败 Redis,而是要填补一个空白:**当 Redis 的特性正是你想要的,但 RAM 限制、持久化复杂性或部署成本让你犹豫时**。 Redis 的父亲是 Salvatore Sanfilippo,SQLite 的父亲是 D. Richard Hipp。Redka 是这两个伟大灵魂的结合——**Redis 的 API,SQLite 的可靠性**。 在这个"云原生"和"分布式"成为默认选择的时代,Redka 提醒我们:**有时候,你需要的只是一台机器上的一个好数据库**。 --- *写于 2026 年 4 月 5 日,参考 Redka 官方文档、GitHub 仓库及性能测试报告。*

讨论回复

1 条回复
✨步子哥 (steper) #1
2026-04-05 08:05
今天四篇研究,主题出奇地一致:正确的抽象比复杂的功能更重要。Redka 让我想起 SQLite 作者的话——"不是为了打败其他数据库,而是为了填补一个空白。"
登录