Redis 与 SQLite 的完美结合:深入探究 Redka 项目

随着数据存储和访问需求的不断增长,不同类型的数据库在各自的领域中发挥着重要的作用。Redis 以其高性能的内存数据库特性,广泛应用于需要快速响应的场景;SQLite 则以其轻量级的嵌入式关系数据库,被广泛应用于移动设备和小型应用中。那么,如果将两者的优点结合起来,会产生怎样的火花呢? Redka就是这样一个旨在利用 SQLite 重新实现 Redis 优秀部分的项目,同时保持与 Redis API 的兼容性。

一、Redka 的诞生与特点

Redka 的核心理念是在保留 Redis 便捷 API 和操作方式的前提下,利用 SQLite 实现数据的持久化和关系型特性。它的出现为开发者提供了一种新的选择,可以在不牺牲性能的情况下,享受到关系型数据库的优势。

1.1 有趣的特性

  • 数据不必完全加载在内存中:与 Redis 需要将全部数据加载到内存中的特性不同,Redka 利用了 SQLite 的磁盘存储机制,减少了内存的压力。
  • 支持 ACID 事务:借助 SQLite,Redka 可以提供完整的事务支持,确保数据操作的原子性、一致性、隔离性和持久性。
  • 使用 SQL 视图进行内省和报告:开发者可以直接使用 SQL 查询数据,方便进行数据分析和报告生成。
  • 同时提供进程内(Go API)和独立(RESP)服务器:Redka 既可以作为一个独立的服务器运行,也可以在 Go 程序中以库的形式使用。
  • 兼容 Redis 的命令和协议:这意味着现有的 Redis 客户端和工具都可以直接用于 Redka,无需额外的学习成本。

1.2 性能考量

虽然 Redka 可能无法完全达到 Redis 的极致性能,但在许多场景下,它的性能表现并不逊色。考虑到它采用了关系型数据库 SQLite 作为存储引擎,这样的性能表现已经非常出色。更重要的是,Redka 为开发者提供了一种可以同时享受 Redis 快速操作和 SQLite 持久化存储的解决方案。

二、支持的 Redis 命令

Redka 旨在最大程度上兼容 Redis 的 API。以下是目前已经支持的部分命令,按照数据类型进行分类。

2.1 字符串(String)

命令 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。

2.2 列表(List)

命令 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。

2.3 集合(Set)

命令 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。

2.4 哈希(Hash)

命令 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。

2.5 有序集合(Sorted Set)

命令 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。

2.6 键(Key)

命令 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。

2.7 事务(Transaction)

命令 Go API 描述
DISCARD DB.View / DB.Update 放弃事务
EXEC DB.View / DB.Update 执行事务中的所有命令
MULTI DB.View / DB.Update 开始一个事务

与 Redis 不同,Redka 的事务完全符合 ACID 属性,在发生故障时会自动回滚。

暂不支持的命令:UNWATCH、WATCH。

2.8 连接管理(Connection Management)

命令 Go API 描述
ECHO - 回应输入的字符串
PING - 测试连接是否仍然可用

三、安装与运行

3.1 安装

Redka 可以作为独立的服务器运行,也可以在 Go 程序中以库的形式使用。

作为独立服务器

官方提供了适用于 Linux、macOS 的安装脚本,也可以使用 Docker 进行部署。

作为 Go 库

在 Go 项目中,可以通过以下命令引入 Redka:

go get github.com/nalgeon/redka

需要注意的是,Redka 依赖于 SQLite,因此还需要引入 SQLite 的 Go 驱动,例如 github.com/mattn/go-sqlite3modernc.org/sqlite

3.2 运行

作为服务器运行

可以使用以下命令启动 Redka 服务器:

./redka
./redka data.db
./redka -h 0.0.0.0 -p 6379 data.db

如果没有指定数据库文件,那么 Redka 将完全在内存中运行。

作为库在 Go 中使用

以下是一个基本的示例,展示如何在 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")

3.3 事务示例

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
  • 10 个并发连接
  • 1,000,000 次请求
  • 10,000 个随机键
  • GET/SET 命令

结果显示,Redka 比 Redis 慢 2~5 倍。考虑到 Redka 采用了关系型数据库作为存储引擎,这样的性能差距在预料之中。同时,在许多应用场景中,这样的性能已经足够使用。

五、深入探索 Redka 的内部实现

在了解了 Redka 的基本特性和用法后,我们可以进一步探讨其内部是如何将 Redis 和 SQLite 的优势结合起来的。

5.1 数据存储结构设计

Redka 的核心在于如何利用 SQLite 来模拟 Redis 的数据结构。这是通过在 SQLite 数据库中创建一系列表来实现的,每个表对应 Redis 的一种数据类型。

5.1.1 键表(rkey)

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             -- 子元素个数
);
5.1.2 字符串表(rstring)

用于存储字符串类型的键值:

CREATE TABLE rstring (
    kid      INTEGER NOT NULL,   -- 外键,关联 rkey.id
    value    BLOB NOT NULL
);
5.1.3 列表表(rlist)

用于存储列表类型的数据:

CREATE TABLE rlist (
    kid      INTEGER NOT NULL,   -- 外键,关联 rkey.id
    pos      REAL NOT NULL,      -- 用于元素排序的索引
    elem     BLOB NOT NULL
);
5.1.4 集合表(rset)

用于存储集合类型的数据:

CREATE TABLE rset (
    kid      INTEGER NOT NULL,   -- 外键,关联 rkey.id
    elem     BLOB NOT NULL
);
5.1.5 哈希表(rhash)

用于存储哈希类型的数据:

CREATE TABLE rhash (
    kid      INTEGER NOT NULL,   -- 外键,关联 rkey.id
    field    TEXT NOT NULL,
    value    BLOB NOT NULL
);
5.1.6 有序集合表(rzset)

用于存储有序集合类型的数据:

CREATE TABLE rzset (
    kid      INTEGER NOT NULL,   -- 外键,关联 rkey.id
    elem     BLOB NOT NULL,
    score    REAL NOT NULL
);

5.2 命令解析与执行

Redka 使用了 redcon 库来解析 Redis 的命令和协议。Redcon 是一个高性能的 Redis 协议服务器框架,支持 RESP(Redis Serialization Protocol)。

Redka 对于每个命令,都实现了相应的处理函数。这些函数会将 Redis 命令转换为对 SQLite 的操作。例如,当客户端发送 SET key value 命令时,Redka 会:

  1. 检查键是否存在于 rkey 表中,如果不存在,则插入新记录。
  2. rstring 表中插入或更新对应的值。
  3. 更新 rkey 表中的元数据。

通过这种方式,Redka 能够实现与 Redis 类似的操作行为。

5.3 事务处理

得益于 SQLite 的事务支持,Redka 可以提供完整的 ACID 事务。这是 Redis 本身所不具备的。在 Redis 中,事务是一种简单的命令序列,缺乏隔离性和原子性。而在 Redka 中,事务可以确保:

  • 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败。
  • 一致性(Consistency):数据库在事务前后都保持一致性状态。
  • 隔离性(Isolation):并发事务之间不会互相影响。
  • 持久性(Durability):一旦事务提交,数据将被永久保存。

5.4 使用 SQL 视图进行内省和报告

由于数据存储在 SQLite 中,开发者可以直接使用标准的 SQL 语句对数据进行查询和分析。例如,要查询所有未过期的键,可以执行:

SELECT key, type, mtime FROM rkey WHERE etime IS NULL OR etime > strftime('%s','now') * 1000;

这为数据的监控、调试和报告提供了极大的便利性。

六、Redka 的应用场景

6.1 资源受限的环境

在需要持久化存储但又受限于内存资源的环境中,Redka 是一个理想的选择。它不需要将所有数据加载到内存中,降低了内存使用的压力。

6.2 嵌入式系统和本地应用

由于 Redka 可以作为嵌入式库使用,适用于需要轻量级数据库解决方案的本地应用程序,例如桌面应用、移动应用或物联网设备。

6.3 需要事务支持的应用

在需要严格数据一致性的场景下,Redka 的 ACID 事务支持可以提供数据可靠性保障,例如金融交易系统、库存管理等应用。

6.4 数据分析和报表

借助 SQLite 的 SQL 支持,开发者可以方便地对数据进行复杂的查询和分析,这对于需要实时统计和报表的系统非常有用。

七、与其他数据库的比较

7.1 与 Redis 的比较

  • 内存使用:Redis 需要将数据全部加载到内存中,而 Redka 不需要,适合数据量较大或内存受限的场景。
  • 事务支持:Redis 的事务不具备真正的隔离性,而 Redka 提供完整的 ACID 事务支持。
  • 性能:Redis 的性能通常比 Redka 更高,特别是在高并发和密集读写的场景下。但 Redka 的性能也在可接受的范围内。

7.2 与纯 SQLite 的比较

  • API 友好性:Redka 提供了与 Redis 一致的 API,易于使用。
  • 数据结构:Redka 实现了 Redis 丰富的数据结构,如列表、集合等,而 SQLite 则是基于表的关系型数据结构。

八、定制与扩展

由于 Redka 基于 SQLite,我们有机会对其进行定制和扩展。例如:

  • 更换底层存储引擎:可以尝试将底层数据库替换为其他支持 SQL 的数据库,例如 ClickHouse,以支持更大的数据量和更高的性能。
  • 集成其他 NoSQL 特性:可以引入如 RocksDB 等 NoSQL 数据库的特性,进一步提升数据存储和检索的效率。
  • 定制数据结构:根据具体需求,添加新的数据结构或命令,满足特定的业务场景。

你可能感兴趣的:(tech-review,redis,sqlite,后端,架构,数据库,缓存)