随着数据存储和访问需求的不断增长,不同类型的数据库在各自的领域中发挥着重要的作用。Redis 以其高性能的内存数据库特性,广泛应用于需要快速响应的场景;SQLite 则以其轻量级的嵌入式关系数据库,被广泛应用于移动设备和小型应用中。那么,如果将两者的优点结合起来,会产生怎样的火花呢? Redka就是这样一个旨在利用 SQLite 重新实现 Redis 优秀部分的项目,同时保持与 Redis API 的兼容性。
Redka 的核心理念是在保留 Redis 便捷 API 和操作方式的前提下,利用 SQLite 实现数据的持久化和关系型特性。它的出现为开发者提供了一种新的选择,可以在不牺牲性能的情况下,享受到关系型数据库的优势。
虽然 Redka 可能无法完全达到 Redis 的极致性能,但在许多场景下,它的性能表现并不逊色。考虑到它采用了关系型数据库 SQLite 作为存储引擎,这样的性能表现已经非常出色。更重要的是,Redka 为开发者提供了一种可以同时享受 Redis 快速操作和 SQLite 持久化存储的解决方案。
Redka 旨在最大程度上兼容 Redis 的 API。以下是目前已经支持的部分命令,按照数据类型进行分类。
命令 | Go API | 描述 |
---|---|---|
DECR | DB.Str().Incr |
将键的整数值减一 |
DECRBY | DB.Str().Incr |
将键的整数值减去指定值 |
GET | DB.Str().Get |
获取键的值 |
GETSET | DB.Str().SetWith |
设置键的值,并返回旧值 |
INCR | DB.Str().Incr |
将键的整数值加一 |
INCRBY | DB.Str().Incr |
将键的整数值加上指定值 |
INCRBYFLOAT | DB.Str().IncrFloat |
将键的浮点数值加上指定值 |
MGET | DB.Str().GetMany |
获取所有给定键的值 |
MSET | DB.Str().SetMany |
同时设置多个键的值 |
PSETEX | DB.Str().SetExpires |
以毫秒为单位设置键的过期时间 |
SET | DB.Str().Set |
设置键的值 |
SETEX | DB.Str().SetExpires |
以秒为单位设置键的过期时间 |
SETNX | DB.Str().SetWith |
仅当键不存在时,设置键的值 |
暂不支持的命令:APPEND、GETDEL、GETEX、GETRANGE、LCS、MSETNX、SETRANGE、STRLEN、SUBSTR。
命令 | Go API | 描述 |
---|---|---|
LINDEX | DB.List().Get |
获取列表中指定索引的元素 |
LINSERT | DB.List().Insert* |
在列表中某个元素前或后插入元素 |
LLEN | DB.List().Len |
获取列表长度 |
LPOP | DB.List().PopFront |
移除并返回列表的第一个元素 |
LPUSH | DB.List().PushFront |
将一个或多个值插入到列表头部 |
LRANGE | DB.List().Range |
获取列表指定范围内的元素 |
LREM | DB.List().Delete* |
移除列表中与指定值相等的元素 |
LSET | DB.List().Set |
设置列表中指定索引的元素 |
LTRIM | DB.List().Trim |
修剪列表,使其只保留指定范围内的元素 |
RPOP | DB.List().PopBack |
移除并返回列表的最后一个元素 |
RPOPLPUSH | DB.List().PopBackPushFront |
移除列表的最后一个元素,并将其添加到另一个列表的头部 |
RPUSH | DB.List().PushBack |
将一个或多个值插入到列表尾部 |
暂不支持的命令:BLMOVE、BLMPOP、BLPOP、BRPOP、BRPOPLPUSH、LMOVE、LMPOP、LPOS、LPUSHX、RPUSHX。
命令 | Go API | 描述 |
---|---|---|
SADD | DB.Set().Add |
向集合添加一个或多个成员 |
SCARD | DB.Set().Len |
获取集合的成员数 |
SDIFF | DB.Set().Diff |
返回所有给定集合的差集 |
SDIFFSTORE | DB.Set().DiffStore |
将差集存储在指定的集合中 |
SINTER | DB.Set().Inter |
返回所有给定集合的交集 |
SINTERSTORE | DB.Set().InterStore |
将交集存储在指定的集合中 |
SISMEMBER | DB.Set().Exists |
判断成员是否存在于集合中 |
SMEMBERS | DB.Set().Items |
返回集合中的所有成员 |
SMOVE | DB.Set().Move |
将成员从一个集合移动到另一个集合 |
SPOP | DB.Set().Pop |
移除并返回集合中的一个随机成员 |
SRANDMEMBER | DB.Set().Random |
返回集合中的一个随机成员 |
SREM | DB.Set().Delete |
移除集合中一个或多个成员 |
SSCAN | DB.Set().Scanner |
迭代集合中的元素 |
SUNION | DB.Set().Union |
返回所有给定集合的并集 |
SUNIONSTORE | DB.Set().UnionStore |
将并集存储在指定的集合中 |
暂不支持的命令:SINTERCARD、SMISMEMBER。
命令 | Go API | 描述 |
---|---|---|
HDEL | DB.Hash().Delete |
删除哈希表中的一个或多个字段 |
HEXISTS | DB.Hash().Exists |
判断哈希表中指定字段是否存在 |
HGET | DB.Hash().Get |
获取哈希表中指定字段的值 |
HGETALL | DB.Hash().Items |
获取哈希表中的所有字段和值 |
HINCRBY | DB.Hash().Incr |
为哈希表中的字段值加上指定增量 |
HINCRBYFLOAT | DB.Hash().IncrFloat |
为哈希表中的字段值加上指定浮点数增量 |
HKEYS | DB.Hash().Keys |
获取哈希表中的所有字段 |
HLEN | DB.Hash().Len |
获取哈希表的字段数量 |
HMGET | DB.Hash().GetMany |
获取哈希表中多个字段的值 |
HMSET | DB.Hash().SetMany |
设置哈希表中多个字段的值 |
HSCAN | DB.Hash().Scanner |
迭代哈希表中的键值对 |
HSET | DB.Hash().SetMany |
设置哈希表中一个或多个字段的值 |
HSETNX | DB.Hash().SetNotExists |
仅当字段不存在时,设置字段的值 |
HVALS | DB.Hash().Values |
获取哈希表中的所有值 |
暂不支持的命令:HRANDFIELD、HSTRLEN。
命令 | Go API | 描述 |
---|---|---|
ZADD | DB.ZSet().AddMany |
添加一个或多个成员到有序集合,或更新已存在成员的分数 |
ZCARD | DB.ZSet().Len |
获取有序集合的成员数 |
ZCOUNT | DB.ZSet().Count |
计算指定分数区间内的成员数 |
ZINCRBY | DB.ZSet().Incr |
有序集合中对指定成员的分数加上增量 |
ZINTER | DB.ZSet().InterWith |
计算给定的有序集合的交集 |
ZINTERSTORE | DB.ZSet().InterWith |
将交集结果存储在新的有序集合中 |
ZRANGE | DB.ZSet().RangeWith |
返回指定区间内的成员 |
ZRANGEBYSCORE | DB.ZSet().RangeWith |
返回指定分数区间内的成员 |
ZRANK | DB.ZSet().GetRank |
返回成员在有序集合中的排名(按分数升序) |
ZREM | DB.ZSet().Delete |
移除有序集合中的一个或多个成员 |
ZREMRANGEBYRANK | DB.ZSet().DeleteWith |
移除指定排名区间内的所有成员 |
ZREMRANGEBYSCORE | DB.ZSet().DeleteWith |
移除指定分数区间内的所有成员 |
ZREVRANGE | DB.ZSet().RangeWith |
返回指定区间内的成员(按分数降序) |
ZREVRANGEBYSCORE | DB.ZSet().RangeWith |
返回指定分数区间内的成员(按分数降序) |
ZREVRANK | DB.ZSet().GetRankRev |
返回成员在有序集合中的排名(按分数降序) |
ZSCAN | DB.ZSet().Scan |
迭代有序集合中的元素(成员及分数) |
ZSCORE | DB.ZSet().GetScore |
返回有序集合中,成员的分数值 |
ZUNION | DB.ZSet().UnionWith |
计算给定的有序集合的并集 |
ZUNIONSTORE | DB.ZSet().UnionWith |
将并集结果存储在新的有序集合中 |
暂不支持的命令:BZMPOP、BZPOPMAX、BZPOPMIN、ZDIFF、ZDIFFSTORE、ZINTERCARD、ZLEXCOUNT、ZMPOP、ZMSCORE、ZPOPMAX、ZPOPMIN、ZRANDMEMBER、ZRANGEBYLEX、ZRANGESTORE、ZREMRANGEBYLEX、ZREVRANGEBYLEX。
命令 | Go API | 描述 |
---|---|---|
DBSIZE | DB.Key().Len |
返回当前数据库的键数量 |
DEL | DB.Key().Delete |
删除一个或多个键 |
EXISTS | DB.Key().Count |
检查一个或多个键是否存在 |
EXPIRE | DB.Key().Expire |
设置键的过期时间(以秒为单位) |
EXPIREAT | DB.Key().ExpireAt |
设置键的过期时间戳 |
FLUSHDB | DB.Key().DeleteAll |
删除当前数据库中的所有键 |
KEYS | DB.Key().Keys |
查找所有匹配给定模式的键 |
PERSIST | DB.Key().Persist |
移除键的过期时间,使其永久有效 |
PEXPIRE | DB.Key().Expire |
设置键的过期时间(以毫秒为单位) |
PEXPIREAT | DB.Key().ExpireAt |
设置键的过期时间戳(以毫秒为单位) |
RANDOMKEY | DB.Key().Random |
从当前数据库中随机返回一个键 |
RENAME | DB.Key().Rename |
重命名键,如果新键名已存在则覆盖 |
RENAMENX | DB.Key().RenameNotExists |
当新键名不存在时,重命名键 |
SCAN | DB.Key().Scanner |
迭代当前数据库中的键 |
TTL | DB.Key().Get |
以秒为单位返回键的剩余过期时间 |
TYPE | DB.Key().Get |
返回键所存储的值的类型 |
暂不支持的命令:COPY、DUMP、EXPIRETIME、MIGRATE、MOVE、OBJECT、PEXPIRETIME、PTTL、RESTORE、SORT、SORT_RO、TOUCH、TTL、TYPE、UNLINK、WAIT、WAITAOF。
命令 | Go API | 描述 |
---|---|---|
DISCARD | DB.View / DB.Update |
放弃事务 |
EXEC | DB.View / DB.Update |
执行事务中的所有命令 |
MULTI | DB.View / DB.Update |
开始一个事务 |
与 Redis 不同,Redka 的事务完全符合 ACID 属性,在发生故障时会自动回滚。
暂不支持的命令:UNWATCH、WATCH。
命令 | Go API | 描述 |
---|---|---|
ECHO | - | 回应输入的字符串 |
PING | - | 测试连接是否仍然可用 |
Redka 可以作为独立的服务器运行,也可以在 Go 程序中以库的形式使用。
官方提供了适用于 Linux、macOS 的安装脚本,也可以使用 Docker 进行部署。
在 Go 项目中,可以通过以下命令引入 Redka:
go get github.com/nalgeon/redka
需要注意的是,Redka 依赖于 SQLite,因此还需要引入 SQLite 的 Go 驱动,例如 github.com/mattn/go-sqlite3
或 modernc.org/sqlite
。
可以使用以下命令启动 Redka 服务器:
./redka
./redka data.db
./redka -h 0.0.0.0 -p 6379 data.db
如果没有指定数据库文件,那么 Redka 将完全在内存中运行。
以下是一个基本的示例,展示如何在 Go 程序中使用 Redka:
package main
import (
"log"
_ "github.com/mattn/go-sqlite3"
"github.com/nalgeon/redka"
)
func main() {
// 打开或创建数据库文件 data.db
db, err := redka.Open("data.db", nil)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 设置键值
db.Str().Set("name", "alice")
db.Str().Set("age", 25)
// 获取键值
name, err := db.Str().Get("name")
if err != nil {
log.Fatal(err)
}
log.Println("name:", name)
}
如果希望使用内存数据库,可以这样打开:
redka.Open("file:redka?mode=memory&cache=shared")
Redka 支持完整的 ACID 事务,可以使用以下方式进行事务操作:
updCount := 0
err := db.Update(func(tx *redka.Tx) error {
err := tx.Str().Set("name", "bob")
if err != nil {
return err
}
updCount++
err = tx.Str().Set("age", 50)
if err != nil {
return err
}
updCount++
return nil
})
if err != nil {
log.Fatal(err)
}
log.Printf("Updated %d records", updCount)
作者在 Apple M1 8 核 CPU,16GB RAM 的 Mac 电脑上,对 Redka 和 Redis 进行了性能测试。使用以下命令进行基准测试:
redis-benchmark -p 6379 -q -c 10 -n 1000000 -r 10000 -t get,set
结果显示,Redka 比 Redis 慢 2~5 倍。考虑到 Redka 采用了关系型数据库作为存储引擎,这样的性能差距在预料之中。同时,在许多应用场景中,这样的性能已经足够使用。
在了解了 Redka 的基本特性和用法后,我们可以进一步探讨其内部是如何将 Redis 和 SQLite 的优势结合起来的。
Redka 的核心在于如何利用 SQLite 来模拟 Redis 的数据结构。这是通过在 SQLite 数据库中创建一系列表来实现的,每个表对应 Redis 的一种数据类型。
rkey
表是整个存储的核心,用于存储所有键的元数据。其结构如下:
CREATE TABLE rkey (
id INTEGER PRIMARY KEY,
key TEXT NOT NULL,
type INTEGER NOT NULL, -- 数据类型:1 字符串,2 列表,3 集合,4 哈希,5 有序集合
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
);
用于存储列表类型的数据:
CREATE TABLE rlist (
kid INTEGER NOT NULL, -- 外键,关联 rkey.id
pos REAL NOT NULL, -- 用于元素排序的索引
elem BLOB NOT NULL
);
用于存储集合类型的数据:
CREATE TABLE rset (
kid INTEGER NOT NULL, -- 外键,关联 rkey.id
elem BLOB NOT NULL
);
用于存储哈希类型的数据:
CREATE TABLE rhash (
kid INTEGER NOT NULL, -- 外键,关联 rkey.id
field TEXT NOT NULL,
value BLOB NOT NULL
);
用于存储有序集合类型的数据:
CREATE TABLE rzset (
kid INTEGER NOT NULL, -- 外键,关联 rkey.id
elem BLOB NOT NULL,
score REAL NOT NULL
);
Redka 使用了 redcon 库来解析 Redis 的命令和协议。Redcon 是一个高性能的 Redis 协议服务器框架,支持 RESP(Redis Serialization Protocol)。
Redka 对于每个命令,都实现了相应的处理函数。这些函数会将 Redis 命令转换为对 SQLite 的操作。例如,当客户端发送 SET key value
命令时,Redka 会:
rkey
表中,如果不存在,则插入新记录。rstring
表中插入或更新对应的值。rkey
表中的元数据。通过这种方式,Redka 能够实现与 Redis 类似的操作行为。
得益于 SQLite 的事务支持,Redka 可以提供完整的 ACID 事务。这是 Redis 本身所不具备的。在 Redis 中,事务是一种简单的命令序列,缺乏隔离性和原子性。而在 Redka 中,事务可以确保:
由于数据存储在 SQLite 中,开发者可以直接使用标准的 SQL 语句对数据进行查询和分析。例如,要查询所有未过期的键,可以执行:
SELECT key, type, mtime FROM rkey WHERE etime IS NULL OR etime > strftime('%s','now') * 1000;
这为数据的监控、调试和报告提供了极大的便利性。
在需要持久化存储但又受限于内存资源的环境中,Redka 是一个理想的选择。它不需要将所有数据加载到内存中,降低了内存使用的压力。
由于 Redka 可以作为嵌入式库使用,适用于需要轻量级数据库解决方案的本地应用程序,例如桌面应用、移动应用或物联网设备。
在需要严格数据一致性的场景下,Redka 的 ACID 事务支持可以提供数据可靠性保障,例如金融交易系统、库存管理等应用。
借助 SQLite 的 SQL 支持,开发者可以方便地对数据进行复杂的查询和分析,这对于需要实时统计和报表的系统非常有用。
由于 Redka 基于 SQLite,我们有机会对其进行定制和扩展。例如: