From d9d431158c3cf663f8d401b7244dc4bc909afee6 Mon Sep 17 00:00:00 2001 From: suxb201 Date: Wed, 27 Sep 2023 10:38:34 +0800 Subject: [PATCH] bugfix: Fixed minor issues --- docs/.vitepress/config.ts | 6 +- docs/src/zh/function/best_practices.md | 90 ++++++++++++++++++- docs/src/zh/function/introduction.md | 3 + docs/src/zh/guide/getting-started.md | 5 ++ .../{module-supported.md => modules.md} | 31 ++++--- docs/src/zh/reader/sync_reader.md | 8 +- docs/src/zh/writer/redis_writer.md | 12 +-- go.mod | 2 +- go.sum | 4 +- internal/reader/sync_standalone_reader.go | 22 +++-- shake.toml | 2 + 11 files changed, 149 insertions(+), 36 deletions(-) rename docs/src/zh/others/{module-supported.md => modules.md} (52%) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 24333d5..31b13dc 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -43,16 +43,16 @@ export default defineConfig({ ] }, { - text: 'function', + text: 'Function', items: [ { text: '什么是 function', link: '/zh/function/introduction' }, { text: '最佳实践', link: '/zh/function/best_practices' } ] }, { - text: 'others', + text: 'Others', items: [ - { text: 'redis modle 支持', link: '/zh/others/module-supported' }, + { text: 'Redis Modules', link: '/zh/others/modules' }, ] }, // { diff --git a/docs/src/zh/function/best_practices.md b/docs/src/zh/function/best_practices.md index 9be902f..f663956 100644 --- a/docs/src/zh/function/best_practices.md +++ b/docs/src/zh/function/best_practices.md @@ -4,8 +4,96 @@ outline: deep # 最佳实践 +## 过滤 + +### 过滤 Key + +```lua +local prefix = "user:" +local prefix_len = #prefix + +if string.sub(KEYS[1], 1, prefix_len) ~= prefix then + return +end + +shake.call(DB, ARGV) +``` + +效果是只将 key 以 `user:` 开头的源数据写入到目标端。没有考虑 `mset` 等多 key 命令的情况。 + +### 过滤 DB + +```lua +shake.log(DB) +if DB == 0 +then + return +end +shake.call(DB, ARGV) +``` + +效果是丢弃源端 `db` 0 的数据,将其他 `db` 的数据写入到目标端。 + + +### 过滤某类数据结构 + +可以通过 `GROUP` 变量来判断数据结构类型,支持的数据结构类型有:`STRING`、`LIST`、`SET`、`ZSET`、`HASH`、`SCRIPTING` 等。 + +#### 过滤 Hash 类型数据 +```lua +if GROUP == "HASH" then + return +end +shake.call(DB, ARGV) +``` + +效果是丢弃源端的 `hash` 类型数据,将其他数据写入到目标端。 + +#### 过滤 [LUA 脚本](https://redis.io/docs/interact/programmability/eval-intro/) + +```lua +if GROUP == "SCRIPTING" then + return +end +shake.call(DB, ARGV) +``` + +效果是丢弃源端的 `lua` 脚本,将其他数据写入到目标端。常见于主从同步至集群时,存在集群不支持的 LUA 脚本。 + ## 修改 ### 修改 Key 的前缀 -TODO +```lua +local prefix_old = "prefix_old_" +local prefix_new = "prefix_new_" + +shake.log("old=" .. table.concat(ARGV, " ")) + +for i, index in ipairs(KEY_INDEXES) do + local key = ARGV[index] + if string.sub(key, 1, #prefix_old) == prefix_old then + ARGV[index] = prefix_new .. string.sub(key, #prefix_old + 1) + end +end + +shake.log("new=" .. table.concat(ARGV, " ")) +shake.call(DB, ARGV) +``` +效果是将源端的 key `prefix_old_key` 写入到目标端的 key `prefix_new_key`。 + +### 交换 DB + +```lua +local db1 = 1 +local db2 = 2 + +if DB == db1 then + DB = db2 +elseif DB == db2 then + DB = db1 +end +shake.call(DB, ARGV) +``` + +效果是将源端的 `db 1` 写入到目标端的 `db 2`,将源端的 `db 2` 写入到目标端的 `db 1`, 其他 `db` 不变。 \ No newline at end of file diff --git a/docs/src/zh/function/introduction.md b/docs/src/zh/function/introduction.md index be055b7..579d0ee 100644 --- a/docs/src/zh/function/introduction.md +++ b/docs/src/zh/function/introduction.md @@ -38,6 +38,9 @@ address = "127.0.0.1:6380" ## function API ### 变量 + +因为有些命令中含有多个 key,比如 `mset` 等命令。所以,`KEYS`、`KEY_INDEXES`、`SLOTS` 这三个变量都是数组类型。如果确认命令只有一个 key,可以直接使用 `KEYS[1]`、`KEY_INDEXES[1]`、`SLOTS[1]`。 + | 变量 | 类型 | 示例 | 描述 | |-|-|-|-----| | DB | number | 1 | 命令所属的 `db` | diff --git a/docs/src/zh/guide/getting-started.md b/docs/src/zh/guide/getting-started.md index 1df9f34..d640771 100644 --- a/docs/src/zh/guide/getting-started.md +++ b/docs/src/zh/guide/getting-started.md @@ -38,3 +38,8 @@ address = "127.0.0.1:6380" ```shell ./redis-shake shake.toml ``` + +## 注意事项 + +1. 不要在同一个目录运行两个 RedisShake 进程,因为运行时产生的临时文件可能会被覆盖,导致异常行为。 +2. 不要降低 Redis 版本,比如从 6.0 降到 5.0,因为 RedisShake 每个大版本都会引入一些新的命令和新的编码方式,如果降低版本,可能会导致不兼容。 diff --git a/docs/src/zh/others/module-supported.md b/docs/src/zh/others/modules.md similarity index 52% rename from docs/src/zh/others/module-supported.md rename to docs/src/zh/others/modules.md index 3ca89bb..6309515 100644 --- a/docs/src/zh/others/module-supported.md +++ b/docs/src/zh/others/modules.md @@ -2,14 +2,23 @@ outline: deep --- +# Redis Modules -# 介绍 -可以为 RedisShake 贡献代码支持其它自定义 module 类型 +Redis Modules 是 Redis 4.0 版本引入的一个新特性,它允许开发者扩展 Redis 的功能。通过创建模块,开发者可以定义新的命令,数据类型,甚至改变 Redis 的行为。因此,Redis Modules 可以极大地增强 Redis 的灵活性和可扩展性。 +由于 Redis Modules 可以定义新的数据类型和命令,RedisShake 需要对这些新的数据类型和命令进行专门的处理,才能正确地迁移或同步这些数据。否则,如果 RedisShake 不理解这些新的数据类型和命令,它可能会无法正确地处理这些数据,或者在处理过程中出错。因此,对于使用了 Redis Modules 的 Redis 实例,一般需要 RedisShake 为其使用的 Module 提供相应的适配器,以便正确地处理这些自定义的数据类型和命令。 -# 核心流程 -相关代码在`internal\rdb`目录下,如需要支持其它 redis module 类型,可分解为以下三个步骤—— +## 已支持的 Redis Modules 列表 +- [TairHash](https://github.com/tair-opensource/TairHash):支持 field 级别设置过期和版本的 Hash 数据结构。 +- [TairString](https://github.com/tair-opensource/TairString):支持版本的 String 结构,可以实现分布式锁/乐观锁。 +- [TairZset](https://github.com/tair-opensource/TairZset):支持最多 256 维的 double 排序,可以实现多维排行榜。 + +## 如何支持新的 Redis Modules + +### 核心流程 + +相关代码在`internal\rdb`目录下,如需要支持其它 Redis Modules 类型,可分解为以下三个步骤: - 从rdb文件中正确读入 - RedisShake 中已经 对 redis module 自定的几种类型进行了封装,从 rdb 文件进行读取时,可直接借助于已经封装好的函数进行读取(`internal\rdb\structure\module2_struct.go`) - 构建一个合适的中间数据结构类型,用于存储相应数据(key + value) @@ -21,20 +30,14 @@ outline: deep ![module-supported.jpg](/public/module-supported.jpg) - -# 其它 -## 补充命令测试 +### 补充命令测试 为了确保正常,需要在` tests\helpers\commands` 里面添加对应 module 的命令,来测试相关命令可以在 rdb、sync、scan 三个模式下工作正常。测试框架具体见[pybbt](https://pypi.org/project/pybbt/),具体思想——借助于redis-py 包,对其进行封装,模拟客户端发送命令,然后比对实际的返回值与已有的返回值。 -## 补充命令列表 +### 补充命令列表 RedisShake 在针对大 key 进行传输时,会查命令表格`RedisShake\internal\commands\table.go`,检查命令的合规性,因此在添加新 module 时,需要将对应的命令加入表格,具体可参照`RedisShake\scripts`部分代码 -## 补充 ci -在 ci 测试中,需要添加对自定义 module 的编译,具体可见` ci.yml` 内容 +### 补充 ci +在 ci 测试中,需要添加对自定义 module 的编译,具体可见` ci.yml` 内容 -# 已支持的 redis module 列表 -- [TairHash](https://github.com/tair-opensource/TairHash) -- [TairString](https://github.com/tair-opensource/TairString) -- [TairZset](https://github.com/tair-opensource/TairZset) diff --git a/docs/src/zh/reader/sync_reader.md b/docs/src/zh/reader/sync_reader.md index a60001f..5b624a3 100644 --- a/docs/src/zh/reader/sync_reader.md +++ b/docs/src/zh/reader/sync_reader.md @@ -11,6 +11,8 @@ 优势:数据一致性最佳,对源库影响小,可以实现不停机的切换 +原理:RedisShake 模拟 Slave 连接到 Master 节点,Master 会向 RedisShake 发送数据,数据包含全量与增量两部分。全量是一个 RDB 文件,增量是 AOF 数据流,RedisShake 会接受全量与增量将其暂存到硬盘上。全量同步阶段:RedisShake 首先会将 RDB 文件解析为一条条的 Redis 命令,然后将这些命令发送至目的端。增量同步阶段:RedisShake 会持续将 AOF 数据流同步至目的端。 + ## 配置 ```toml @@ -20,6 +22,8 @@ address = "127.0.0.1:6379" # when cluster is true, set address to one of the clu username = "" # keep empty if not using ACL password = "" # keep empty if no authentication is required tls = false +sync_rdb = true # set to false if you don't want to sync rdb +sync_aof = true # set to false if you don't want to sync aof ``` * `cluster`:源端是否为集群 @@ -28,4 +32,6 @@ tls = false * 当源端使用 ACL 账号时,配置 `username` 和 `password` * 当源端使用传统账号时,仅配置 `password` * 当源端无鉴权时,不配置 `username` 和 `password` -* `tls`:源端是否开启 TLS/SSL,不需要配置证书因为 RedisShake 没有校验服务器证书 \ No newline at end of file +* `tls`:源端是否开启 TLS/SSL,不需要配置证书因为 RedisShake 没有校验服务器证书 +* `sync_rdb`:是否同步 RDB,设置为 false 时,RedisShake 会跳过全量同步阶段 +* `sync_aof`:是否同步 AOF,设置为 false 时,RedisShake 会跳过增量同步阶段,此时 RedisShake 会在全量同步阶段结束后退出 \ No newline at end of file diff --git a/docs/src/zh/writer/redis_writer.md b/docs/src/zh/writer/redis_writer.md index 72063c8..80e870e 100644 --- a/docs/src/zh/writer/redis_writer.md +++ b/docs/src/zh/writer/redis_writer.md @@ -15,13 +15,13 @@ password = "" # keep empty if no authentication is required tls = false ``` -* `cluster`:源端是否为集群 -* `address`:源端地址, 当源端为集群时,`address` 为集群中的任意一个节点即可 +* `cluster`:是否为集群。 +* `address`:连接地址。当目的端为集群时,`address` 填写集群中的任意一个节点即可 * 鉴权: - * 当源端使用 ACL 账号时,配置 `username` 和 `password` - * 当源端使用传统账号时,仅配置 `password` - * 当源端无鉴权时,不配置 `username` 和 `password` -* `tls`:源端是否开启 TLS/SSL,不需要配置证书因为 RedisShake 没有校验服务器证书 + * 当使用 ACL 账号体系时,配置 `username` 和 `password` + * 当使用传统账号体系时,仅配置 `password` + * 当无鉴权时,不配置 `username` 和 `password` +* `tls`:是否开启 TLS/SSL,不需要配置证书因为 RedisShake 没有校验服务器证书 注意事项: 1. 当目的端为集群时,应保证源端发过来的命令满足 [Key 的哈希值属于同一个 slot](https://redis.io/docs/reference/cluster-spec/#implemented-subset)。 diff --git a/go.mod b/go.mod index 6c90ddc..f809874 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect - golang.org/x/sys v0.5.0 // indirect + golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.7.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 2305655..4727f8b 100644 --- a/go.sum +++ b/go.sum @@ -327,8 +327,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/reader/sync_standalone_reader.go b/internal/reader/sync_standalone_reader.go index bde9a7b..8f5f840 100644 --- a/internal/reader/sync_standalone_reader.go +++ b/internal/reader/sync_standalone_reader.go @@ -25,6 +25,8 @@ type SyncReaderOptions struct { Username string `mapstructure:"username" default:""` Password string `mapstructure:"password" default:""` Tls bool `mapstructure:"tls" default:"false"` + SyncRdb bool `mapstructure:"sync_rdb" default:"true"` + SyncAof bool `mapstructure:"sync_aof" default:"true"` } type State string @@ -38,6 +40,7 @@ const ( ) type syncStandaloneReader struct { + opts *SyncReaderOptions client *client.Redis ch chan *entry.Entry @@ -72,6 +75,7 @@ type syncStandaloneReader struct { func NewSyncStandaloneReader(opts *SyncReaderOptions) Reader { r := new(syncStandaloneReader) + r.opts = opts r.client = client.NewRedisClient(opts.Address, opts.Username, opts.Password, opts.Tls) r.rd = r.client.BufioReader() r.stat.Name = "reader_" + strings.Replace(opts.Address, ":", "_", -1) @@ -91,9 +95,14 @@ func (r *syncStandaloneReader) StartRead() chan *entry.Entry { r.receiveRDB() startOffset := r.stat.AofReceivedOffset go r.receiveAOF(r.rd) - r.sendRDB() - r.stat.Status = kSyncAof - r.sendAOF(startOffset) + if r.opts.SyncRdb { + r.sendRDB() + } + if r.opts.SyncAof { + r.stat.Status = kSyncAof + r.sendAOF(startOffset) + } + close(r.ch) }() return r.ch @@ -103,12 +112,9 @@ func (r *syncStandaloneReader) sendReplconfListenPort() { // use status_port as redis-shake port argv := []string{"replconf", "listening-port", strconv.Itoa(config.Opt.Advanced.StatusPort)} r.client.Send(argv...) - reply, err := r.client.Receive() + _, err := r.client.Receive() if err != nil { - log.Warnf("[%s] send replconf command to redis server failed. reply=[%s], error=[%v]", r.stat.Name, reply, err) - } - if reply != "OK" { - log.Warnf("[%s] send replconf command to redis server failed. reply=[%s]", r.stat.Name, reply) + log.Warnf("[%s] send replconf command to redis server failed. error=[%v]", r.stat.Name, err) } } diff --git a/shake.toml b/shake.toml index 6d3c5cd..64f40c0 100644 --- a/shake.toml +++ b/shake.toml @@ -7,6 +7,8 @@ address = "127.0.0.1:6379" # when cluster is true, set address to one of the clu username = "" # keep empty if not using ACL password = "" # keep empty if no authentication is required tls = false +sync_rdb = true # set to false if you don't want to sync rdb +sync_aof = true # set to false if you don't want to sync aof # [scan_reader] # cluster = false # set to true if source is a redis cluster