我想方便地处理 Redis 的有序集合,使用 Go 语言
总结一下
-
- Go で Redis を扱う redigo はシンプルな反面コード量が多くなりがち
-
- Redis を使ってやりたいことはある程度パターンがある
-
- 同じようなコードを毎回書かないために github.com/izumin5210/ro をつくった
ro を利用することで記述を最低限にしコードの見通しを良くしつつ快適に Redis を扱えるようになる
Redis(一种内存数据库)与Golang(一种编程语言)之间的常见情况。
我觉得在Golang中处理Redis的时候,redigo和go-redis都很受欢迎。我经常使用redigo,但是redigo只提供了一个最基本的API,用于向Redis发送命令并将其转换为Go的类型,只是非常基本。此外,正如下面所列举的,redigo和Redis本身都有一些特殊之处,所以有时候会遇到一些困难。
-
- キーの命名の一貫性を保つのが面倒
-
- コマンドが独特で覚えられない
ZREVRANGE foo 10 20 で何件返ってくるんだっけ?
毎回 redis-cli 等で動作確認している気がする
ボイラープレートが多い
オブジェクト全体を Hash に
ソートに使いたいスコアをそれぞれ Sorted Set に
複数アイテム保存するときに pipeline にしないとパフォーマンスが…
失敗したら DISCARD しないと…
あ,複数アイテムまとめて保存したいときはどうするんだろう…?
複数アイテム取得したいときは…?
type Post struct {
ID int64 `redis:"id"`
Title string `redis:"title"`
UserID int64 `redis:"user_id"`
CreatedAt int64 `redis:"created_at"`
}
func SavePost(p *Post) error {
conn := pool.Get()
defer conn.Close()
var err error
err = conn.Send("MULTI")
key := fmt.Sprintf("Post:%d", p.ID)
if err == nil {
err = conn.Send("HMSET", redis.Args{}.Add(key).AddFlat(p)...)
}
if err == nil {
err = conn.Send("ZADD", "Post/created_at", p.CreatedAt, key)
}
if err == nil {
err = conn.Send("ZADD", fmt.Sprintf("Post/user:%d", p.UserID), p.CreatedAt, key)
}
if err == nil {
err = conn.Do("EXEC")
} else {
conn.Do("DISCARD")
}
return err
}
「麻煩事」「每次都要查找」這樣的事情會降低生產力,所以希望解決這個問題。
例如在 ISUCON 等情況下,當時間和大腦資源有限時,不想被這種事情困擾。
github.com/izumin5210/ro的网址
为了解决上述问题,我创建了一个名为github.com/izumin5210/ro的库。
Redigo可以将Hash映射成结构体,但它不会做更多的事情。
ro提供了一组抽象化的read / write命令函数的存储对象,并提供了构建查询器来从有序集合中读取值的命令。
请说明使用方法
定义
通常のStructにro.Modelを組み込みます。
ro.Modelはいくつかの関数を持っています。
GetKeySuffix()は必ずオーバーライドする必要があります。これは、保存するハッシュのキーの後半部分を決定します。ちなみに、前半部分はデフォルトでは構造体名になります。
GetScoreMap()はオプションです。この関数が返すマップのキーは、ソート済みセットのキーの後半部分として使用され、値はスコアとしてソート済みセットに保存されます。
type Post struct {
ro.Model
ID int64 `redis:"id"`
Title string `redis:"title"`
UserID int64 `redis:"user_id"`
CreatedAt int64 `redis:"created_at"`
}
func (p *Post) GetKeySuffix() string {
return fmt.Sprint(p.ID)
}
func (p *Post) GetScoreMap() map[string]interface{} {
return map[string]interface{}{
"created_at": p.CreatedAt,
fmt.Sprintf("user:%d", p.UserID): p.CreatedAt,
}
}
初始化
给出一个能返回 redis.Conn 的函数和一个想要处理的结构体指针给 ro.New。(关于前者,由于 redsync 的实现中它不是一个函数,而是接收接口参数的,所以我觉得应该按照那种方式来适应。)
pool = &redis.Pool{
Dial: func() (redis.Conn, error) {
return redis.DialURL("redis://localhost:6379")
},
}
store := ro.New(pool.Get, &Post{})
保留
只需将结构体传递给Set()。也可以传递多个结构体。
无论传递多少个,它会自动将其转化为管道事务,因此不会出现性能问题。
(尽管在内部稍微使用了一些反射,所以从这个意义上说,相对于直接使用redigo速度较慢)
store.Set(&Post{
ID: 1,
UserID: 1,
Title: "post 1",
Body: "This is a post 1",
CreatedAt: now.UnixNano(),
})
拿出来
如果你只想先知道键并读取整个对象时,只需将其传递给 Get(),然后就会返回。这也类似于 Set(),可以批量获取多个项目。
可以通过Query()函数进行条件链式操作。将此传递给Select()可进行搜索,传递给Count()可统计数量。暂时不需要考虑ZREVRANGE和ZCOUNT。
这些处理当然也会通过流水线的方式进行。
post := &Post{ID: 1}
store.Get(post)
posts := []*Post{}
store.Select(&posts, store.Query("created_at").GtEq(now.UnixNano()).Reverse())
cnt, _ := store.Count(store.Query("user:1").Gt(now.UnixNano()).Reverse())
fmt.Println(cnt)
方便性
我创作这个项目的原因是因为经常写一些代码来将数据存入Redis中,感觉很繁琐。在ISUCON 7总决赛之前开始开发,并在总决赛中实战投入使用。实际上,我将大部分数据从MySQL迁移到Redis或内存中,迁移非常顺利,没有遇到特别困难的问题。
今年的比赛主题是使用 big.Int,所以如果将其放入 Redis 中进行编组/解组,会增加成本,最好还是放在内存中。很遗憾。
总结
我介绍了一个叫做 github.com/izumin5210/ro 的库,用于在 Golang 中优雅地处理 Redis。它可以帮助我们减少样板代码,减少代码量,并提高可读性。
如果对Redis使用方式不当,往往会在之后遭受严重后果,因此建议合理规划使用。我认为在类似ISUCON的环境中,它可以发挥作用。
参考:从Redis生产故障中学到的代码审查要点 – Qiita