golang之Redis

Redis 是一个基于内存的非关系型数据库,在项目开发中使用非常广泛,Go 语言操作 Redis 需要使用三方包,我们选择支持 Redis 集群和 Redis 哨兵的 go-redis 包来讲述 Go 语言如何操作 Redis。

 

go-redis 包需要使用支持 Modules 的 Go 版本,并且使用导入版本控制。所以需要确保初始化 go module,命令如下所示。

go mod init package_name

 

安装go-redis

go get github.com/go-redis/redis/v8

 

 

 

Redis 单机连接

方式 1:

rdb := redis.NewClient(&redis.Options{
    Addr:     "localhost:6379",
    Password: "", // no password set
    DB:       0,  // use default DB
})
_, err = rdb.Ping().Result()
	if err != nil {
		return err
	}

go-redis 包提供 NewClient 函数,传入一个指定 Redis 服务器信息的结构体类型的参数,返回一个指向该 Redis 服务器的客户端  *Client。

 

查看传入参数的结构体完整字段:

type Options struct {
    Network            string
    Addr               string
    Dialer             func(ctx context.Context, network string, addr string) (net.Conn, error)
    OnConnect          func(ctx context.Context, cn *Conn) error
    Username           string
    Password           string
    DB                 int
    MaxRetries         int
    MinRetryBackoff    time.Duration
    MaxRetryBackoff    time.Duration
    DialTimeout        time.Duration
    ReadTimeout        time.Duration
    WriteTimeout       time.Duration
    PoolSize           int
    MinIdleConns       int
    MaxConnAge         time.Duration
    PoolTimeout        time.Duration
    IdleTimeout        time.Duration
    IdleCheckFrequency time.Duration
    readOnly           bool
    TLSConfig          *tls.Config
    Limiter            Limiter
}

方式 2:

opt, err := redis.ParseURL("redis://localhost:6379/<db>")
if err != nil {
    panic(err)
}

rdb := redis.NewClient(opt)

go-redis 包提供 ParseURL 函数,传入参数为字符串类型的连接字符串,返回一个 NewClient 函数接收的参数 *Options。

 

 


Redis 集群连接

rdb := redis.NewClusterClient(&redis.ClusterOptions{
    Addrs: []string{":7000", ":7001", ":7002", ":7003", ":7004", ":7005"},

    // To route commands by latency or randomly, enable one of the following.
    //RouteByLatency: true,
    //RouteRandomly: true,
})

go-redis 包提供 NewClusterClient 函数,传入一个指定 Redis 集群服务器信息的结构体类型的参数,返回一个 Redis 集群的客户端 *ClusterClient。

 

查看传入参数结构体的完整字段:

type ClusterOptions struct {
    Addrs              []string
    NewClient          func(opt *Options) *Client
    MaxRedirects       int
    ReadOnly           bool
    RouteByLatency     bool
    RouteRandomly      bool
    ClusterSlots       func(context.Context) ([]ClusterSlot, error)
    Dialer             func(ctx context.Context, network string, addr string) (net.Conn, error)
    OnConnect          func(ctx context.Context, cn *Conn) error
    Username           string
    Password           string
    MaxRetries         int
    MinRetryBackoff    time.Duration
    MaxRetryBackoff    time.Duration
    DialTimeout        time.Duration
    ReadTimeout        time.Duration
    WriteTimeout       time.Duration
    PoolSize           int
    MinIdleConns       int
    MaxConnAge         time.Duration
    PoolTimeout        time.Duration
    IdleTimeout        time.Duration
    IdleCheckFrequency time.Duration
    TLSConfig          *tls.Config
}

 

Redis 哨兵模式连接

rdb := redis.NewFailoverClient(&redis.FailoverOptions{
    MasterName:    "master-name",
    SentinelAddrs: []string{":9126", ":9127", ":9128"},
})

 


使用 NewFailoverClusterClient 函数可以将只读命令路由到从 Redis 服务器。

rdb := redis.NewFailoverClusterClient(&redis.FailoverOptions{
    MasterName:    "master-name",
    SentinelAddrs: []string{":9126", ":9127", ":9128"},

    // To route commands by latency or randomly, enable one of the following.
    //RouteByLatency: true,
    //RouteRandomly: true,
})

 

连接 Redis 哨兵服务器

sentinel := redis.NewSentinelClient(&redis.Options{
    Addr: ":9126",
})

addr, err := sentinel.GetMasterAddrByName(ctx, "master-name").Result()

 

 

执行 Redis 命令

方式 1:直接获取结果集

var ctx = context.Background()
val, err := rdb.Get(ctx, "key").Result()
if err != nil {
    if err == redis.Nil {
        fmt.Println("key does not exists")
        return
    }
    panic(err)
}
fmt.Println(val)

方式 2:单独访问 Err() 和 Val() 获取相应的值。

var ctx = context.Background()
get := rdb.Get(ctx, "key")
if err := get.Err(); err != nil {
    if err == redis.Nil {
        fmt.Println("key does not exists")
        return
    }
    panic(err)
}
fmt.Println(get.Val())

方式 3:执行任意命令

var ctx = context.Background()
get := rdb.Do(ctx, "get", "key")
if err := get.Err(); err != nil {
    if err == redis.Nil {
        fmt.Println("key does not exists")
        return
    }
    panic(err)
}
fmt.Println(get.Val().(string))

 

更多特定类型的取值方法:

// Shortcut for get.Val().(string) with error handling.
s, err := get.Text()
num, err := get.Int()
num, err := get.Int64()
num, err := get.Uint64()
num, err := get.Float32()
num, err := get.Float64()
flag, err := get.Bool()

需要注意的是,第一个参数需要传入 context.Context 类型的参数,作为传入请求的顶级上下文。

 

 

基本使用

[set/get]:

func redisExample() {
	err := rdb.Set("score", 100, 0).Err()
	if err != nil {
		fmt.Printf("set score failed, err:%v\n", err)
		return
	}

	val, err := rdb.Get("score").Result()
	if err != nil {
		fmt.Printf("get score failed, err:%v\n", err)
		return
	}
	fmt.Println("score", val)

	val2, err := rdb.Get("name").Result()
	if err == redis.Nil {
		fmt.Println("name does not exist")
	} else if err != nil {
		fmt.Printf("get name failed, err:%v\n", err)
		return
	} else {
		fmt.Println("name", val2)
	}
}



[zset]:

func redisExample2() {
	zsetKey := "language_rank"
	languages := []redis.Z{
		redis.Z{Score: 90.0, Member: "Golang"},
		redis.Z{Score: 98.0, Member: "Java"},
		redis.Z{Score: 95.0, Member: "Python"},
		redis.Z{Score: 97.0, Member: "JavaScript"},
		redis.Z{Score: 99.0, Member: "C/C++"},
	}
	// ZADD
	num, err := rdb.ZAdd(zsetKey, languages...).Result()
	if err != nil {
		fmt.Printf("zadd failed, err:%v\n", err)
		return
	}
	fmt.Printf("zadd %d succ.\n", num)

	// 把Golang的分数加10
	newScore, err := rdb.ZIncrBy(zsetKey, 10.0, "Golang").Result()
	if err != nil {
		fmt.Printf("zincrby failed, err:%v\n", err)
		return
	}
	fmt.Printf("Golang's score is %f now.\n", newScore)

	// 取分数最高的3个
	ret, err := rdb.ZRevRangeWithScores(zsetKey, 0, 2).Result()
	if err != nil {
		fmt.Printf("zrevrange failed, err:%v\n", err)
		return
	}
	for _, z := range ret {
		fmt.Println(z.Member, z.Score)
	}

	// 取95~100分的
	op := redis.ZRangeBy{
		Min: "95",
		Max: "100",
	}
	ret, err = rdb.ZRangeByScoreWithScores(zsetKey, op).Result()
	if err != nil {
		fmt.Printf("zrangebyscore failed, err:%v\n", err)
		return
	}
	for _, z := range ret {
		fmt.Println(z.Member, z.Score)
	}
}

输出:
$ ./06redis_demo 
zadd 0 succ.
Golang's score is 100.000000 now.
Golang 100
C/C++ 99
Java 98
JavaScript 97
Java 98
C/C++ 99
Golang 100


[keys]: 匹配查询
vals, err := rdb.Keys(ctx, "prefix*").Result()


[自定义执行命令]:
res, err := rdb.Do(ctx, "set", "key", "value").Result()


[按照通配符删除key]
当通配符匹配的key的数量不多时,可以使用Keys()得到所有的key在使用Del命令删除。 如果key的数量非常多的时候,我们可以搭配使用Scan命令和Del命令完成删除。
ctx := context.Background()
iter := rdb.Scan(ctx, 0, "prefix*", 0).Iterator()
for iter.Next(ctx) {
	err := rdb.Del(ctx, iter.Val()).Err()
	if err != nil {
		panic(err)
	}
}
if err := iter.Err(); err != nil {
	panic(err)
}


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

pipe := rdb.Pipeline()

incr := pipe.Incr("pipeline_counter")
pipe.Expire("pipeline_counter", time.Hour)

_, err := pipe.Exec()
fmt.Println(incr.Val(), err)

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

也可以使用Pipelined:
var incr *redis.IntCmd
_, err := rdb.Pipelined(func(pipe redis.Pipeliner) error {
	incr = pipe.Incr("pipelined_counter")
	pipe.Expire("pipelined_counter", time.Hour)
	return nil
})
fmt.Println(incr.Val(), err)
在某些场景下,当我们有多条命令要执行时,就可以考虑使用pipeline来优化。



[事务]
Redis是单线程的,因此单个命令始终是原子的,但是来自不同客户端的两个给定命令可以依次执行,例如在它们之间交替执行。但是,Multi/exec能够确保在multi/exec两个语句之间的命令之间没有其他客户端正在执行命令。

在这种场景我们需要使用TxPipeline。TxPipeline总体上类似于上面的Pipeline,但是它内部会使用MULTI/EXEC包裹排队的命令。例如:
pipe := rdb.TxPipeline()

incr := pipe.Incr("tx_pipeline_counter")
pipe.Expire("tx_pipeline_counter", time.Hour)

_, err := pipe.Exec()
fmt.Println(incr.Val(), err)

上面代码相当于在一个RTT下执行了下面的redis命令:
MULTI
INCR pipeline_counter
EXPIRE pipeline_counts 3600
EXEC

还有一个与上文类似的TxPipelined方法,使用方法如下:
var incr *redis.IntCmd
_, err := rdb.TxPipelined(func(pipe redis.Pipeliner) error {
	incr = pipe.Incr("tx_pipelined_counter")
	pipe.Expire("tx_pipelined_counter", time.Hour)
	return nil
})
fmt.Println(incr.Val(), err)


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


Watch方法接收一个函数和一个或多个key作为参数。基本使用示例如下:
// 监视watch_count的值,并在值不变的前提下将其值+1
key := "watch_count"
err = client.Watch(func(tx *redis.Tx) error {
	n, err := tx.Get(key).Int()
	if err != nil && err != redis.Nil {
		return err
	}
	_, err = tx.Pipelined(func(pipe redis.Pipeliner) error {
		pipe.Set(key, n+1, 0)
		return nil
	})
	return err
}, key)

  

最后看一个V8版本官方文档中使用GET和SET命令以事务方式递增Key的值的示例,仅当Key的值不发生变化时提交一个事务。

func transactionDemo() {
    var (
        maxRetries   = 1000
        routineCount = 10
    )
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // Increment 使用GET和SET命令以事务方式递增Key的值
    increment := func(key string) error {
        // 事务函数
        txf := func(tx *redis.Tx) error {
            // 获得key的当前值或零值
            n, err := tx.Get(ctx, key).Int()
            if err != nil && err != redis.Nil {
                return err
            }

            // 实际的操作代码(乐观锁定中的本地操作)
            n++

            // 操作仅在 Watch 的 Key 没发生变化的情况下提交
            _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
                pipe.Set(ctx, key, n, 0)
                return nil
            })
            return err
        }

        // 最多重试 maxRetries 次
        for i := 0; i < maxRetries; i++ {
            err := rdb.Watch(ctx, txf, key)
            if err == nil {
                // 成功
                return nil
            }
            if err == redis.TxFailedErr {
                // 乐观锁丢失 重试
                continue
            }
            // 返回其他的错误
            return err
        }

        return errors.New("increment reached maximum number of retries")
    }

    // 模拟 routineCount 个并发同时去修改 counter3 的值
    var wg sync.WaitGroup
    wg.Add(routineCount)
    for i := 0; i < routineCount; i++ {
        go func() {
            defer wg.Done()
            if err := increment("counter3"); err != nil {
                fmt.Println("increment error:", err)
            }
        }()
    }
    wg.Wait()

    n, err := rdb.Get(context.TODO(), "counter3").Int()
    fmt.Println("ended with", n, err)
}

 

setnx处理:

unc (c *cmdable) SetNX(key string, value interface{}, expiration time.Duration) *BoolCmd {
    var cmd *BoolCmd
    if expiration == 0 {
        // Use old `SETNX` to support old Redis versions.
        cmd = NewBoolCmd("setnx", key, value)
    } else {
        if usePrecise(expiration) {
            cmd = NewBoolCmd("set", key, value, "px", formatMs(expiration), "nx")
        } else {
            cmd = NewBoolCmd("set", key, value, "ex", formatSec(expiration), "nx")
        }
    }
    c.process(cmd)
    return cmd
}  

setnx的含义就是SET if Not Exists,该方法是原子的。如果key不存在,则设置当前key为value成功,返回1;如果当前key已经存在,则设置当前key失败,返回0


//判断当前订单是否已进行处理
bool := redis.RedisDB.SetNX(order, order, 24*time.Hour)
if bool.Val() { //SetNX只进行一次操作,若返回为true,则之前未处理该订单,此次已set该key
    logger.Info("The transaction order key value has been saved")
} else { //若返回false,则说明该交易订单已请求,向调用者返回报错
    logger.Error("The transaction order key value has been saved")
    apicommon.ReturnErrorResponse(ctx, 1, "Order transaction already exists, please check transaction", "")
    return
}  

 

 Hash

 

// hset
// user_1 是hash key,username 是字段名, tizi365是字段值
err := client.HSet("user_1", "username", "tizi365").Err()
if err != nil {
    panic(err)
}

// hget
// user_1 是hash key,username是字段名
username, err := client.HGet("user_1", "username").Result()
if err != nil {
    panic(err)
}
fmt.Println(username)


//hgetall
// 一次性返回key=user_1的所有hash字段和值
data, err := client.HGetAll("user_1").Result()
if err != nil {
    panic(err)
}

// data是一个map类型,这里使用使用循环迭代输出
for field, val := range data {
    fmt.Println(field,val)
}


// hincrby
// 累加count字段的值,一次性累加2, user_1为hash key
count, err := client.HIncrBy("user_1", "count", 2).Result()
if err != nil {
    panic(err)
}

fmt.Println(count)


// hkeys
// keys是一个string数组
keys, err := client.HKeys("user_1").Result()
if err != nil {
    panic(err)
}

fmt.Println(keys)


// hlen
size, err := client.HLen("key").Result()
if err != nil {
    panic(err)
}

fmt.Println(size)


// hmget
// HMGet支持多个field字段名,意思是一次返回多个字段值
vals, err := client.HMGet("key","field1", "field2").Result()
if err != nil {
    panic(err)
}

// vals是一个数组
fmt.Println(vals)


//hmset
// 初始化hash数据的多个字段值
data := make(map[string]interface{})
data["id"] = 1
data["username"] = "tizi"

// 一次性保存多个hash字段值
err := client.HMSet("key", data).Err()
if err != nil {
    panic(err)
}


// hsetnx
err := client.HSetNX("key", "id", 100).Err()
if err != nil {
    panic(err)
}



//hdel
// 删除一个字段id
client.HDel("key", "id")

// 删除多个字段
client.HDel("key", "id", "username")



//hexists
// 检测id字段是否存在
err := client.HExists("key", "id").Err()
if err != nil {
    panic(err)
}

 

更多使用可参考: https://www.tizi365.com/archives/290.html 

 

lua与事务的使用:

https://juejin.cn/post/7024307688196538375

 

 

 

相关文档:

 

posted @ 2022-08-15 20:25  X-Wolf  阅读(3715)  评论(0编辑  收藏  举报