我想方便地处理 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

广告
将在 10 秒后关闭
bannerAds