来源

https://www.liwenzhou.com/posts/Go/go_redis/
https://pkg.go.dev/github.com/go-redis/redis

  • cache缓存
  • 简单的队列
  • 排行榜

    介绍

    Redis是一个开源的内存数据库,Redis提供了多种不同类型的数据结构,很多业务场景下的问题都可以很自然地映射到这些数据结构上。除此之外,通过复制、持久化和客户端分片等特性,我们可以很方便地将Redis扩展成一个能够包含数百GB数据、每秒处理上百万次请求的系统。

    Redis支持的数据结构

    Redis支持诸如字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、带范围查询的排序集合(sorted sets)、位图(bitmaps)、hyperloglogs、带半径查询和流的地理空间索引等数据结构(geospatial indexes)。

    Redis应用场景

  • 缓存系统,减轻主数据库(MySQL)的压力。

  • 计数场景,比如微博、抖音中的关注数和粉丝数。
  • 热门排行榜,需要排序的场景特别适合使用ZSET。
  • 利用LIST可以实现队列的功能。

    Redis与Memcached比较

    Memcached的值只支持简单的字符串,Redis支持更丰富的数据结构,Redis的性能比Memcached好很多,Redis支持RDB持久化和AOF持久化,Redis支持master/slave模式。

    准备Redis环境

    这里直接使用Docker启动一个redis环境,方便学习使用。docker启动一个名为redis507的5.0.7版本的 redis server示例:
    docker run --name redis507 -p 6379:6379 -d redis:5.0.7
    注意:此处的版本、容器名和端口号请根据自己需要设置。启动一个redis-cli连接上面的redis server:
    docker run -it --network host --rm redis:5.0.7 redis-cli

    go-redis库 安装

    区别于另一个比较常用的Go语言redis client库:redigo,我们这里采用https://github.com/go-redis/redis连接Redis数据库并进行操作,因为go-redis支持连接哨兵及集群模式的Redis。
    使用以下命令下载并安装:
    go get -u github.com/go-redis/redis

    连接

    普通连接

    ```go // 声明一个全局的rdb变量 var rdb *redis.Client

// 初始化连接 func initClient() (err error) { rdb = redis.NewClient(&redis.Options{ Addr: “localhost:6379”, Password: “”, // no password set DB: 0, // use default DB })

  1. _, err = rdb.Ping().Result()
  2. if err != nil {
  3. return err
  4. }
  5. return nil

}

  1. <a name="GMG8U"></a>
  2. ### 连接Redis哨兵模式
  3. ```go
  4. func initClient()(err error){
  5. rdb := redis.NewFailoverClient(&redis.FailoverOptions{
  6. MasterName: "master",
  7. SentinelAddrs: []string{"x.x.x.x:26379", "xx.xx.xx.xx:26379", "xxx.xxx.xxx.xxx:26379"},
  8. })
  9. _, err = rdb.Ping().Result()
  10. if err != nil {
  11. return err
  12. }
  13. return nil
  14. }

连接Redis集群

  1. func initClient()(err error){
  2. rdb := redis.NewClusterClient(&redis.ClusterOptions{
  3. Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"},
  4. })
  5. _, err = rdb.Ping().Result()
  6. if err != nil {
  7. return err
  8. }
  9. return nil
  10. }

基本使用

set/get示例

  1. func redisExample() {
  2. err := rdb.Set("score", 100, 0).Err()
  3. if err != nil {
  4. fmt.Printf("set score failed, err:%v\n", err)
  5. return
  6. }
  7. val, err := rdb.Get("score").Result()
  8. if err != nil {
  9. fmt.Printf("get score failed, err:%v\n", err)
  10. return
  11. }
  12. fmt.Println("score", val)
  13. val2, err := rdb.Get("name").Result()
  14. if err == redis.Nil {
  15. fmt.Println("name does not exist")
  16. } else if err != nil {
  17. fmt.Printf("get name failed, err:%v\n", err)
  18. return
  19. } else {
  20. fmt.Println("name", val2)
  21. }
  22. }

zset示例

  1. func redisExample2() {
  2. zsetKey := "language_rank"
  3. languages := []redis.Z{
  4. redis.Z{Score: 90.0, Member: "Golang"},
  5. redis.Z{Score: 98.0, Member: "Java"},
  6. redis.Z{Score: 95.0, Member: "Python"},
  7. redis.Z{Score: 97.0, Member: "JavaScript"},
  8. redis.Z{Score: 99.0, Member: "C/C++"},
  9. }
  10. // ZADD
  11. num, err := rdb.ZAdd(zsetKey, languages...).Result()
  12. if err != nil {
  13. fmt.Printf("zadd failed, err:%v\n", err)
  14. return
  15. }
  16. fmt.Printf("zadd %d succ.\n", num)
  17. // 把Golang的分数加10
  18. newScore, err := rdb.ZIncrBy(zsetKey, 10.0, "Golang").Result()
  19. if err != nil {
  20. fmt.Printf("zincrby failed, err:%v\n", err)
  21. return
  22. }
  23. fmt.Printf("Golang's score is %f now.\n", newScore)
  24. // 取分数最高的3个
  25. ret, err := rdb.ZRevRangeWithScores(zsetKey, 0, 2).Result()
  26. if err != nil {
  27. fmt.Printf("zrevrange failed, err:%v\n", err)
  28. return
  29. }
  30. for _, z := range ret {
  31. fmt.Println(z.Member, z.Score)
  32. }
  33. // 取95~100分的
  34. op := redis.ZRangeBy{
  35. Min: "95",
  36. Max: "100",
  37. }
  38. ret, err = rdb.ZRangeByScoreWithScores(zsetKey, op).Result()
  39. if err != nil {
  40. fmt.Printf("zrangebyscore failed, err:%v\n", err)
  41. return
  42. }
  43. for _, z := range ret {
  44. fmt.Println(z.Member, z.Score)
  45. }
  46. }

输出的结果如下:

  1. $ ./06redis_demo
  2. zadd 0 succ.
  3. Golang's score is 100.000000 now.
  4. Golang 100
  5. C/C++ 99
  6. Java 98
  7. JavaScript 97
  8. Java 98
  9. C/C++ 99
  10. Golang 100

Pipeline

Pipeline 主要是一种网络优化。它本质上意味着客户端缓冲一堆命令并一次性将它们发送到服务器。这些命令不能保证在事务中执行。这样做的好处是节省了每个命令的网络往返时间(RTT)。
Pipeline 基本示例如下:

  1. pipe := rdb.Pipeline()
  2. incr := pipe.Incr("pipeline_counter")
  3. pipe.Expire("pipeline_counter", time.Hour)
  4. _, err := pipe.Exec()
  5. fmt.Println(incr.Val(), err)

上面的代码相当于将以下两个命令一次发给redis server端执行,与不使用Pipeline相比能减少一次RTT。

  1. INCR pipeline_counter
  2. EXPIRE pipeline_counts 3600

也可以使用Pipelined:

  1. var incr *redis.IntCmd
  2. _, err := rdb.Pipelined(func(pipe redis.Pipeliner) error {
  3. incr = pipe.Incr("pipelined_counter")
  4. pipe.Expire("pipelined_counter", time.Hour)
  5. return nil
  6. })
  7. fmt.Println(incr.Val(), err)

在某些场景下,当我们有多条命令要执行时,就可以考虑使用pipeline来优化。

事务

Redis是单线程的,因此单个命令始终是原子的,但是来自不同客户端的两个给定命令可以依次执行,例如在它们之间交替执行。但是,Multi/exec能够确保在multi/exec两个语句之间的命令之间没有其他客户端正在执行命令。
在这种场景我们需要使用TxPipeline。TxPipeline总体上类似于上面的Pipeline,但是它内部会使用MULTI/EXEC包裹排队的命令。例如:

  1. pipe := rdb.TxPipeline()
  2. incr := pipe.Incr("tx_pipeline_counter")
  3. pipe.Expire("tx_pipeline_counter", time.Hour)
  4. _, err := pipe.Exec()
  5. fmt.Println(incr.Val(), err)

上面代码相当于在一个RTT下执行了下面的redis命令:

  1. MULTI
  2. INCR pipeline_counter
  3. EXPIRE pipeline_counts 3600
  4. EXEC

还有一个与上文类似的TxPipelined方法,使用方法如下:

  1. var incr *redis.IntCmd
  2. _, err := rdb.TxPipelined(func(pipe redis.Pipeliner) error {
  3. incr = pipe.Incr("tx_pipelined_counter")
  4. pipe.Expire("tx_pipelined_counter", time.Hour)
  5. return nil
  6. })
  7. fmt.Println(incr.Val(), err)

Watch

在某些场景下,我们除了要使用MULTI/EXEC命令外,还需要配合使用WATCH命令。在用户使用WATCH命令监视某个键之后,直到该用户执行EXEC命令的这段时间里,如果有其他用户抢先对被监视的键进行了替换、更新、删除等操作,那么当用户尝试执行EXEC的时候,事务将失败并返回一个错误,用户可以根据这个错误选择重试事务或者放弃事务。

  1. Watch(fn func(*Tx) error, keys ...string) error

Watch方法接收一个函数和一个或多个key作为参数。基本使用示例如下:

  1. // 监视watch_count的值,并在值不变的前提下将其值+1
  2. key := "watch_count"
  3. err = client.Watch(func(tx *redis.Tx) error {
  4. n, err := tx.Get(key).Int()
  5. if err != nil && err != redis.Nil {
  6. return err
  7. }
  8. _, err = tx.Pipelined(func(pipe redis.Pipeliner) error {
  9. pipe.Set(key, n+1, 0)
  10. return nil
  11. })
  12. return err
  13. }, key)

最后看一个官方文档中使用GET和SET命令以事务方式递增Key的值的示例:

  1. const routineCount = 100
  2. increment := func(key string) error {
  3. txf := func(tx *redis.Tx) error {
  4. // 获得当前值或零值
  5. n, err := tx.Get(key).Int()
  6. if err != nil && err != redis.Nil {
  7. return err
  8. }
  9. // 实际操作(乐观锁定中的本地操作)
  10. n++
  11. // 仅在监视的Key保持不变的情况下运行
  12. _, err = tx.Pipelined(func(pipe redis.Pipeliner) error {
  13. // pipe 处理错误情况
  14. pipe.Set(key, n, 0)
  15. return nil
  16. })
  17. return err
  18. }
  19. for retries := routineCount; retries > 0; retries-- {
  20. err := rdb.Watch(txf, key)
  21. if err != redis.TxFailedErr {
  22. return err
  23. }
  24. // 乐观锁丢失
  25. }
  26. return errors.New("increment reached maximum number of retries")
  27. }
  28. var wg sync.WaitGroup
  29. wg.Add(routineCount)
  30. for i := 0; i < routineCount; i++ {
  31. go func() {
  32. defer wg.Done()
  33. if err := increment("counter3"); err != nil {
  34. fmt.Println("increment error:", err)
  35. }
  36. }()
  37. }
  38. wg.Wait()
  39. n, err := rdb.Get("counter3").Int()
  40. fmt.Println("ended with", n, err)