2023-12-21 22:17:40 +08:00
|
|
|
|
// Package redisgo 基于github.com/gomodule/redigo,封装了redis缓存的实现。
|
|
|
|
|
// 使用时,可以参考 http://redisdoc.com/ 和 http://www.runoob.com/redis/ 中对redis命令及用法的讲解。
|
|
|
|
|
// 对于没有封装的命令,也可以参考这里的实现方法,直接调用 `Do` 方法直接调用redis命令。
|
|
|
|
|
package redis
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"os/signal"
|
|
|
|
|
"syscall"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
// "github.com/aiscrm/cache"
|
|
|
|
|
|
|
|
|
|
"github.com/gomodule/redigo/redis"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Client 先构建一个Client实例,然后将配置参数传入该实例的StartAndGC方法来初始化实例和程序进程退出后的清理工作。
|
|
|
|
|
type Client struct {
|
|
|
|
|
pool *redis.Pool
|
|
|
|
|
prefix string
|
|
|
|
|
marshal func(v interface{}) ([]byte, error)
|
|
|
|
|
unmarshal func(data []byte, v interface{}) error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Options redis配置参数
|
|
|
|
|
type Options struct {
|
|
|
|
|
Network string // 通讯协议,默认为 tcp
|
|
|
|
|
Addr string // redis服务的地址,默认为 127.0.0.1:6379
|
|
|
|
|
Password string // redis鉴权密码
|
|
|
|
|
Db int // 数据库
|
|
|
|
|
MaxActive int // 最大活动连接数,值为0时表示不限制
|
|
|
|
|
MaxIdle int // 最大空闲连接数
|
|
|
|
|
IdleTimeout int // 空闲连接的超时时间,超过该时间则关闭连接。单位为秒。默认值是5分钟。值为0时表示不关闭空闲连接。此值应该总是大于redis服务的超时时间。
|
|
|
|
|
Prefix string // 键名前缀
|
|
|
|
|
Marshal func(v interface{}) ([]byte, error) // 数据序列化方法,默认使用json.Marshal序列化
|
|
|
|
|
Unmarshal func(data []byte, v interface{}) error // 数据反序列化方法,默认使用json.Unmarshal序列化
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// StartAndGC 使用 Options 初始化redis,并在程序进程退出时关闭连接池。
|
|
|
|
|
func (c *Client) StartAndGC(options interface{}) error {
|
|
|
|
|
switch opts := options.(type) {
|
|
|
|
|
case Options:
|
|
|
|
|
if opts.Network == "" {
|
|
|
|
|
opts.Network = "tcp"
|
|
|
|
|
}
|
|
|
|
|
if opts.Addr == "" {
|
|
|
|
|
opts.Addr = "127.0.0.1:6379"
|
|
|
|
|
}
|
|
|
|
|
if opts.MaxIdle == 0 {
|
|
|
|
|
opts.MaxIdle = 3
|
|
|
|
|
}
|
|
|
|
|
if opts.IdleTimeout == 0 {
|
|
|
|
|
opts.IdleTimeout = 300
|
|
|
|
|
}
|
|
|
|
|
if len(opts.Prefix) > 0 {
|
|
|
|
|
c.prefix = opts.Prefix
|
|
|
|
|
}
|
|
|
|
|
if opts.Marshal == nil {
|
|
|
|
|
c.marshal = json.Marshal
|
|
|
|
|
}
|
|
|
|
|
if opts.Unmarshal == nil {
|
|
|
|
|
c.unmarshal = json.Unmarshal
|
|
|
|
|
}
|
|
|
|
|
pool := &redis.Pool{
|
|
|
|
|
MaxActive: opts.MaxActive,
|
|
|
|
|
MaxIdle: opts.MaxIdle,
|
|
|
|
|
IdleTimeout: time.Duration(opts.IdleTimeout) * time.Second,
|
|
|
|
|
|
|
|
|
|
Dial: func() (redis.Conn, error) {
|
|
|
|
|
conn, err := redis.Dial(opts.Network, opts.Addr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if opts.Password != "" {
|
|
|
|
|
if _, err := conn.Do("AUTH", opts.Password); err != nil {
|
|
|
|
|
conn.Close()
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if _, err := conn.Do("SELECT", opts.Db); err != nil {
|
|
|
|
|
conn.Close()
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return conn, err
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
TestOnBorrow: func(conn redis.Conn, t time.Time) error {
|
|
|
|
|
_, err := conn.Do("PING")
|
|
|
|
|
return err
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.pool = pool
|
|
|
|
|
c.closePool()
|
|
|
|
|
return nil
|
|
|
|
|
default:
|
|
|
|
|
return errors.New("Unsupported options")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Do 执行redis命令并返回结果。执行时从连接池获取连接并在执行完命令后关闭连接。
|
|
|
|
|
func (c *Client) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
|
|
|
|
|
conn := c.pool.Get()
|
|
|
|
|
defer conn.Close()
|
|
|
|
|
return conn.Do(commandName, args...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get 获取键值。一般不直接使用该值,而是配合下面的工具类方法获取具体类型的值,或者直接使用github.com/gomodule/redigo/redis包的工具方法。
|
|
|
|
|
func (c *Client) Get(key string) (interface{}, error) {
|
|
|
|
|
return c.Do("GET", c.getKey(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetString 获取string类型的键值
|
|
|
|
|
func (c *Client) GetString(key string) (string, error) {
|
|
|
|
|
return String(c.Get(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetInt 获取int类型的键值
|
|
|
|
|
func (c *Client) GetInt(key string) (int, error) {
|
|
|
|
|
return Int(c.Get(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetInt64 获取int64类型的键值
|
|
|
|
|
func (c *Client) GetInt64(key string) (int64, error) {
|
|
|
|
|
return Int64(c.Get(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetBool 获取bool类型的键值
|
|
|
|
|
func (c *Client) GetBool(key string) (bool, error) {
|
|
|
|
|
return Bool(c.Get(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetObject 获取非基本类型stuct的键值。在实现上,使用json的Marshal和Unmarshal做序列化存取。
|
|
|
|
|
func (c *Client) GetObject(key string, val interface{}) error {
|
|
|
|
|
reply, err := c.Get(key)
|
|
|
|
|
return c.decode(reply, err, val)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set 存并设置有效时长。时长的单位为秒。
|
|
|
|
|
// 基础类型直接保存,其他用json.Marshal后转成string保存。
|
|
|
|
|
func (c *Client) Set(key string, val interface{}, expire int64) error {
|
|
|
|
|
value, err := c.encode(val)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if expire > 0 {
|
|
|
|
|
_, err := c.Do("SETEX", c.getKey(key), expire, value)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
_, err = c.Do("SET", c.getKey(key), value)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Exists 检查键是否存在
|
|
|
|
|
func (c *Client) Exists(key string) (bool, error) {
|
|
|
|
|
return Bool(c.Do("EXISTS", c.getKey(key)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Del 删除键
|
|
|
|
|
func (c *Client) Del(key string) error {
|
|
|
|
|
_, err := c.Do("DEL", c.getKey(key))
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Flush 清空当前数据库中的所有 key,慎用!
|
|
|
|
|
func (c *Client) Flush() error {
|
|
|
|
|
_, err := c.Do("FLUSHDB")
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TTL 以秒为单位。当 key 不存在时,返回 -2 。 当 key 存在但没有设置剩余生存时间时,返回 -1
|
|
|
|
|
func (c *Client) TTL(key string) (ttl int64, err error) {
|
|
|
|
|
return Int64(c.Do("TTL", c.getKey(key)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Expire 设置键过期时间,expire的单位为秒
|
|
|
|
|
func (c *Client) Expire(key string, expire int64) error {
|
|
|
|
|
_, err := Bool(c.Do("EXPIRE", c.getKey(key), expire))
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Incr 将 key 中储存的数字值增一
|
|
|
|
|
func (c *Client) Incr(key string) (val int64, err error) {
|
|
|
|
|
return Int64(c.Do("INCR", c.getKey(key)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// IncrBy 将 key 所储存的值加上给定的增量值(increment)。
|
|
|
|
|
func (c *Client) IncrBy(key string, amount int64) (val int64, err error) {
|
|
|
|
|
return Int64(c.Do("INCRBY", c.getKey(key), amount))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Decr 将 key 中储存的数字值减一。
|
|
|
|
|
func (c *Client) Decr(key string) (val int64, err error) {
|
|
|
|
|
return Int64(c.Do("DECR", c.getKey(key)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DecrBy key 所储存的值减去给定的减量值(decrement)。
|
|
|
|
|
func (c *Client) DecrBy(key string, amount int64) (val int64, err error) {
|
|
|
|
|
return Int64(c.Do("DECRBY", c.getKey(key), amount))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HMSet 将一个map存到Redis hash,同时设置有效期,单位:秒
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// ```golang
|
|
|
|
|
// m := make(map[string]interface{})
|
|
|
|
|
// m["name"] = "corel"
|
|
|
|
|
// m["age"] = 23
|
|
|
|
|
// err := c.HMSet("user", m, 10)
|
|
|
|
|
// ```
|
|
|
|
|
func (c *Client) HMSet(key string, val interface{}, expire int) (err error) {
|
|
|
|
|
conn := c.pool.Get()
|
|
|
|
|
defer conn.Close()
|
|
|
|
|
err = conn.Send("HMSET", redis.Args{}.Add(c.getKey(key)).AddFlat(val)...)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if expire > 0 {
|
|
|
|
|
err = conn.Send("EXPIRE", c.getKey(key), int64(expire))
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
conn.Flush()
|
|
|
|
|
_, err = conn.Receive()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Redis hash 是一个string类型的field和value的映射表,hash特别适合用于存储对象。 **/
|
|
|
|
|
|
|
|
|
|
// HSet 将哈希表 key 中的字段 field 的值设为 val
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// ```golang
|
|
|
|
|
// _, err := c.HSet("user", "age", 23)
|
|
|
|
|
// ```
|
|
|
|
|
func (c *Client) HSet(key, field string, val interface{}) (interface{}, error) {
|
|
|
|
|
value, err := c.encode(val)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return c.Do("HSET", c.getKey(key), field, value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HGet 获取存储在哈希表中指定字段的值
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// ```golang
|
|
|
|
|
// val, err := c.HGet("user", "age")
|
|
|
|
|
// ```
|
|
|
|
|
func (c *Client) HGet(key, field string) (reply interface{}, err error) {
|
|
|
|
|
reply, err = c.Do("HGET", c.getKey(key), field)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HGetString HGet的工具方法,当字段值为字符串类型时使用
|
|
|
|
|
func (c *Client) HGetString(key, field string) (reply string, err error) {
|
|
|
|
|
reply, err = String(c.HGet(key, field))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HGetInt HGet的工具方法,当字段值为int类型时使用
|
|
|
|
|
func (c *Client) HGetInt(key, field string) (reply int, err error) {
|
|
|
|
|
reply, err = Int(c.HGet(key, field))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HGetInt64 HGet的工具方法,当字段值为int64类型时使用
|
|
|
|
|
func (c *Client) HGetInt64(key, field string) (reply int64, err error) {
|
|
|
|
|
reply, err = Int64(c.HGet(key, field))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HGetBool HGet的工具方法,当字段值为bool类型时使用
|
|
|
|
|
func (c *Client) HGetBool(key, field string) (reply bool, err error) {
|
|
|
|
|
reply, err = Bool(c.HGet(key, field))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HGetObject HGet的工具方法,当字段值为非基本类型的stuct时使用
|
|
|
|
|
func (c *Client) HGetObject(key, field string, val interface{}) error {
|
|
|
|
|
reply, err := c.HGet(key, field)
|
|
|
|
|
return c.decode(reply, err, val)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HGetAll HGetAll("key", &val)
|
|
|
|
|
func (c *Client) HGetAll(key string, val interface{}) error {
|
|
|
|
|
v, err := redis.Values(c.Do("HGETALL", c.getKey(key)))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := redis.ScanStruct(v, val); err != nil {
|
|
|
|
|
fmt.Println(err)
|
|
|
|
|
}
|
|
|
|
|
//fmt.Printf("%+v\n", val)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-14 07:53:25 +08:00
|
|
|
|
// HINCRBY 将保存的整型键值增加
|
|
|
|
|
func (c *Client) HIncrby(key string, field string, val int) (reply interface{}, err error) {
|
|
|
|
|
reply, err = c.Do("HINCRBY", c.getKey(key), field, val)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-21 22:17:40 +08:00
|
|
|
|
/**
|
|
|
|
|
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
|
|
|
|
|
**/
|
|
|
|
|
|
|
|
|
|
// BLPop 它是 LPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BLPOP 命令阻塞,直到等待超时或发现可弹出元素为止。
|
|
|
|
|
// 超时参数 timeout 接受一个以秒为单位的数字作为值。超时参数设为 0 表示阻塞时间可以无限期延长(block indefinitely) 。
|
|
|
|
|
func (c *Client) BLPop(key string, timeout int) (interface{}, error) {
|
|
|
|
|
values, err := redis.Values(c.Do("BLPOP", c.getKey(key), timeout))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if len(values) != 2 {
|
|
|
|
|
return nil, fmt.Errorf("redisgo: unexpected number of values, got %d", len(values))
|
|
|
|
|
}
|
|
|
|
|
return values[1], err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BLPopInt BLPop的工具方法,元素类型为int时
|
|
|
|
|
func (c *Client) BLPopInt(key string, timeout int) (int, error) {
|
|
|
|
|
return Int(c.BLPop(key, timeout))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BLPopInt64 BLPop的工具方法,元素类型为int64时
|
|
|
|
|
func (c *Client) BLPopInt64(key string, timeout int) (int64, error) {
|
|
|
|
|
return Int64(c.BLPop(key, timeout))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BLPopString BLPop的工具方法,元素类型为string时
|
|
|
|
|
func (c *Client) BLPopString(key string, timeout int) (string, error) {
|
|
|
|
|
return String(c.BLPop(key, timeout))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BLPopBool BLPop的工具方法,元素类型为bool时
|
|
|
|
|
func (c *Client) BLPopBool(key string, timeout int) (bool, error) {
|
|
|
|
|
return Bool(c.BLPop(key, timeout))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BLPopObject BLPop的工具方法,元素类型为object时
|
|
|
|
|
func (c *Client) BLPopObject(key string, timeout int, val interface{}) error {
|
|
|
|
|
reply, err := c.BLPop(key, timeout)
|
|
|
|
|
return c.decode(reply, err, val)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BRPop 它是 RPOP 命令的阻塞版本,当给定列表内没有任何元素可供弹出的时候,连接将被 BRPOP 命令阻塞,直到等待超时或发现可弹出元素为止。
|
|
|
|
|
// 超时参数 timeout 接受一个以秒为单位的数字作为值。超时参数设为 0 表示阻塞时间可以无限期延长(block indefinitely) 。
|
|
|
|
|
func (c *Client) BRPop(key string, timeout int) (interface{}, error) {
|
|
|
|
|
values, err := redis.Values(c.Do("BRPOP", c.getKey(key), timeout))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if len(values) != 2 {
|
|
|
|
|
return nil, fmt.Errorf("redisgo: unexpected number of values, got %d", len(values))
|
|
|
|
|
}
|
|
|
|
|
return values[1], err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BRPopInt BRPop的工具方法,元素类型为int时
|
|
|
|
|
func (c *Client) BRPopInt(key string, timeout int) (int, error) {
|
|
|
|
|
return Int(c.BRPop(key, timeout))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BRPopInt64 BRPop的工具方法,元素类型为int64时
|
|
|
|
|
func (c *Client) BRPopInt64(key string, timeout int) (int64, error) {
|
|
|
|
|
return Int64(c.BRPop(key, timeout))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BRPopString BRPop的工具方法,元素类型为string时
|
|
|
|
|
func (c *Client) BRPopString(key string, timeout int) (string, error) {
|
|
|
|
|
return String(c.BRPop(key, timeout))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BRPopBool BRPop的工具方法,元素类型为bool时
|
|
|
|
|
func (c *Client) BRPopBool(key string, timeout int) (bool, error) {
|
|
|
|
|
return Bool(c.BRPop(key, timeout))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BRPopObject BRPop的工具方法,元素类型为object时
|
|
|
|
|
func (c *Client) BRPopObject(key string, timeout int, val interface{}) error {
|
|
|
|
|
reply, err := c.BRPop(key, timeout)
|
|
|
|
|
return c.decode(reply, err, val)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LPop 移出并获取列表中的第一个元素(表头,左边)
|
|
|
|
|
func (c *Client) LPop(key string) (interface{}, error) {
|
|
|
|
|
return c.Do("LPOP", c.getKey(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LPopInt 移出并获取列表中的第一个元素(表头,左边),元素类型为int
|
|
|
|
|
func (c *Client) LPopInt(key string) (int, error) {
|
|
|
|
|
return Int(c.LPop(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LPopInt64 移出并获取列表中的第一个元素(表头,左边),元素类型为int64
|
|
|
|
|
func (c *Client) LPopInt64(key string) (int64, error) {
|
|
|
|
|
return Int64(c.LPop(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LPopString 移出并获取列表中的第一个元素(表头,左边),元素类型为string
|
|
|
|
|
func (c *Client) LPopString(key string) (string, error) {
|
|
|
|
|
return String(c.LPop(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LPopBool 移出并获取列表中的第一个元素(表头,左边),元素类型为bool
|
|
|
|
|
func (c *Client) LPopBool(key string) (bool, error) {
|
|
|
|
|
return Bool(c.LPop(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LPopObject 移出并获取列表中的第一个元素(表头,左边),元素类型为非基本类型的struct
|
|
|
|
|
func (c *Client) LPopObject(key string, val interface{}) error {
|
|
|
|
|
reply, err := c.LPop(key)
|
|
|
|
|
return c.decode(reply, err, val)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RPop 移出并获取列表中的最后一个元素(表尾,右边)
|
|
|
|
|
func (c *Client) RPop(key string) (interface{}, error) {
|
|
|
|
|
return c.Do("RPOP", c.getKey(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RPopInt 移出并获取列表中的最后一个元素(表尾,右边),元素类型为int
|
|
|
|
|
func (c *Client) RPopInt(key string) (int, error) {
|
|
|
|
|
return Int(c.RPop(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RPopInt64 移出并获取列表中的最后一个元素(表尾,右边),元素类型为int64
|
|
|
|
|
func (c *Client) RPopInt64(key string) (int64, error) {
|
|
|
|
|
return Int64(c.RPop(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RPopString 移出并获取列表中的最后一个元素(表尾,右边),元素类型为string
|
|
|
|
|
func (c *Client) RPopString(key string) (string, error) {
|
|
|
|
|
return String(c.RPop(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RPopBool 移出并获取列表中的最后一个元素(表尾,右边),元素类型为bool
|
|
|
|
|
func (c *Client) RPopBool(key string) (bool, error) {
|
|
|
|
|
return Bool(c.RPop(key))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RPopObject 移出并获取列表中的最后一个元素(表尾,右边),元素类型为非基本类型的struct
|
|
|
|
|
func (c *Client) RPopObject(key string, val interface{}) error {
|
|
|
|
|
reply, err := c.RPop(key)
|
|
|
|
|
return c.decode(reply, err, val)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LPush 将一个值插入到列表头部
|
|
|
|
|
func (c *Client) LPush(key string, member interface{}) error {
|
|
|
|
|
value, err := c.encode(member)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
_, err = c.Do("LPUSH", c.getKey(key), value)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RPush 将一个值插入到列表尾部
|
|
|
|
|
func (c *Client) RPush(key string, member interface{}) error {
|
|
|
|
|
value, err := c.encode(member)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
_, err = c.Do("RPUSH", c.getKey(key), value)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LREM 根据参数 count 的值,移除列表中与参数 member 相等的元素。
|
|
|
|
|
// count 的值可以是以下几种:
|
|
|
|
|
// count > 0 : 从表头开始向表尾搜索,移除与 member 相等的元素,数量为 count 。
|
|
|
|
|
// count < 0 : 从表尾开始向表头搜索,移除与 member 相等的元素,数量为 count 的绝对值。
|
|
|
|
|
// count = 0 : 移除表中所有与 member 相等的值。
|
|
|
|
|
// 返回值:被移除元素的数量。
|
|
|
|
|
func (c *Client) LREM(key string, count int, member interface{}) (int, error) {
|
|
|
|
|
return Int(c.Do("LREM", c.getKey(key), count, member))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LLen 获取列表的长度
|
|
|
|
|
func (c *Client) LLen(key string) (int64, error) {
|
2024-03-14 07:53:25 +08:00
|
|
|
|
return Int64(c.Do("LLEN", c.getKey(key)))
|
2023-12-21 22:17:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LRange 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。
|
|
|
|
|
// 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。
|
|
|
|
|
// 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
|
|
|
|
|
// 和编程语言区间函数的区别:end 下标也在 LRANGE 命令的取值范围之内(闭区间)。
|
|
|
|
|
func (c *Client) LRange(key string, start, end int) (interface{}, error) {
|
|
|
|
|
return c.Do("LRANGE", c.getKey(key), start, end)
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-14 07:53:25 +08:00
|
|
|
|
// LIndex 返回列表 key 中指定索引的元素。
|
|
|
|
|
func (c *Client) LIndexInt64(key string, index int) (int64, error) {
|
|
|
|
|
return Int64(c.Do("LINDEX", c.getKey(key), index))
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-21 22:17:40 +08:00
|
|
|
|
/**
|
|
|
|
|
Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。
|
|
|
|
|
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
|
|
|
|
|
有序集合的成员是唯一的,但分数(score)却可以重复。
|
|
|
|
|
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
|
|
|
|
|
**/
|
|
|
|
|
|
|
|
|
|
// ZAdd 将一个 member 元素及其 score 值加入到有序集 key 当中。
|
|
|
|
|
func (c *Client) ZAdd(key string, score int64, member string) (reply interface{}, err error) {
|
|
|
|
|
return c.Do("ZADD", c.getKey(key), score, member)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ZRem 移除有序集 key 中的一个成员,不存在的成员将被忽略。
|
|
|
|
|
func (c *Client) ZRem(key string, member string) (reply interface{}, err error) {
|
|
|
|
|
return c.Do("ZREM", c.getKey(key), member)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ZScore 返回有序集 key 中,成员 member 的 score 值。 如果 member 元素不是有序集 key 的成员,或 key 不存在,返回 nil 。
|
|
|
|
|
func (c *Client) ZScore(key string, member string) (int64, error) {
|
|
|
|
|
return Int64(c.Do("ZSCORE", c.getKey(key), member))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ZRank 返回有序集中指定成员的排名。其中有序集成员按分数值递增(从小到大)顺序排列。score 值最小的成员排名为 0
|
|
|
|
|
func (c *Client) ZRank(key, member string) (int64, error) {
|
|
|
|
|
return Int64(c.Do("ZRANK", c.getKey(key), member))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ZRevrank 返回有序集中成员的排名。其中有序集成员按分数值递减(从大到小)排序。分数值最大的成员排名为 0 。
|
|
|
|
|
func (c *Client) ZRevrank(key, member string) (int64, error) {
|
|
|
|
|
return Int64(c.Do("ZREVRANK", c.getKey(key), member))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ZRange 返回有序集中,指定区间内的成员。其中成员的位置按分数值递增(从小到大)来排序。具有相同分数值的成员按字典序(lexicographical order )来排列。
|
|
|
|
|
// 以 0 表示有序集第一个成员,以 1 表示有序集第二个成员,以此类推。或 以 -1 表示最后一个成员, -2 表示倒数第二个成员,以此类推。
|
|
|
|
|
func (c *Client) ZRange(key string, from, to int64) (map[string]int64, error) {
|
|
|
|
|
return redis.Int64Map(c.Do("ZRANGE", c.getKey(key), from, to, "WITHSCORES"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ZRevrange 返回有序集中,指定区间内的成员。其中成员的位置按分数值递减(从大到小)来排列。具有相同分数值的成员按字典序(lexicographical order )来排列。
|
|
|
|
|
// 以 0 表示有序集第一个成员,以 1 表示有序集第二个成员,以此类推。或 以 -1 表示最后一个成员, -2 表示倒数第二个成员,以此类推。
|
|
|
|
|
func (c *Client) ZRevrange(key string, from, to int64) (map[string]int64, error) {
|
|
|
|
|
return redis.Int64Map(c.Do("ZREVRANGE", c.getKey(key), from, to, "WITHSCORES"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ZRangeByScore 返回有序集合中指定分数区间的成员列表。有序集成员按分数值递增(从小到大)次序排列。
|
|
|
|
|
// 具有相同分数值的成员按字典序来排列
|
|
|
|
|
func (c *Client) ZRangeByScore(key string, from, to, offset int64, count int) (map[string]int64, error) {
|
|
|
|
|
return redis.Int64Map(c.Do("ZRANGEBYSCORE", c.getKey(key), from, to, "WITHSCORES", "LIMIT", offset, count))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ZRevrangeByScore 返回有序集中指定分数区间内的所有的成员。有序集成员按分数值递减(从大到小)的次序排列。
|
|
|
|
|
// 具有相同分数值的成员按字典序来排列
|
|
|
|
|
func (c *Client) ZRevrangeByScore(key string, from, to, offset int64, count int) (map[string]int64, error) {
|
|
|
|
|
return redis.Int64Map(c.Do("ZREVRANGEBYSCORE", c.getKey(key), from, to, "WITHSCORES", "LIMIT", offset, count))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
|
|
|
|
|
Redis 客户端可以订阅任意数量的频道。
|
|
|
|
|
当有新消息通过 PUBLISH 命令发送给频道 channel 时, 这个消息就会被发送给订阅它的所有客户端。
|
|
|
|
|
**/
|
|
|
|
|
|
|
|
|
|
// Publish 将信息发送到指定的频道,返回接收到信息的订阅者数量
|
|
|
|
|
func (c *Client) Publish(channel, message string) (int, error) {
|
|
|
|
|
return Int(c.Do("PUBLISH", channel, message))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Subscribe 订阅给定的一个或多个频道的信息。
|
|
|
|
|
// 支持redis服务停止或网络异常等情况时,自动重新订阅。
|
|
|
|
|
// 一般的程序都是启动后开启一些固定channel的订阅,也不会动态的取消订阅,这种场景下可以使用本方法。
|
|
|
|
|
// 复杂场景的使用可以直接参考 https://godoc.org/github.com/gomodule/redigo/redis#hdr-Publish_and_Subscribe
|
|
|
|
|
func (c *Client) Subscribe(onMessage func(channel string, data []byte) error, channels ...string) error {
|
|
|
|
|
conn := c.pool.Get()
|
|
|
|
|
psc := redis.PubSubConn{Conn: conn}
|
|
|
|
|
err := psc.Subscribe(redis.Args{}.AddFlat(channels)...)
|
|
|
|
|
// 如果订阅失败,休息1秒后重新订阅(比如当redis服务停止服务或网络异常)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Println(err)
|
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
|
return c.Subscribe(onMessage, channels...)
|
|
|
|
|
}
|
|
|
|
|
quit := make(chan int, 1)
|
|
|
|
|
|
|
|
|
|
// 处理消息
|
|
|
|
|
go func() {
|
|
|
|
|
for {
|
|
|
|
|
switch v := psc.Receive().(type) {
|
|
|
|
|
case redis.Message:
|
|
|
|
|
// fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
|
|
|
|
|
go onMessage(v.Channel, v.Data)
|
|
|
|
|
case redis.Subscription:
|
|
|
|
|
fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
|
|
|
|
|
case error:
|
|
|
|
|
quit <- 1
|
|
|
|
|
fmt.Println(err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
// 异常情况下自动重新订阅
|
|
|
|
|
go func() {
|
|
|
|
|
<-quit
|
|
|
|
|
time.Sleep(time.Second)
|
|
|
|
|
psc.Close()
|
|
|
|
|
c.Subscribe(onMessage, channels...)
|
|
|
|
|
}()
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
GEO 地理位置
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// GeoOptions 用于GEORADIUS和GEORADIUSBYMEMBER命令的参数
|
|
|
|
|
type GeoOptions struct {
|
|
|
|
|
WithCoord bool
|
|
|
|
|
WithDist bool
|
|
|
|
|
WithHash bool
|
|
|
|
|
Order string // ASC从近到远,DESC从远到近
|
|
|
|
|
Count int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GeoResult 用于GEORADIUS和GEORADIUSBYMEMBER命令的查询结果
|
|
|
|
|
type GeoResult struct {
|
|
|
|
|
Name string
|
|
|
|
|
Longitude float64
|
|
|
|
|
Latitude float64
|
|
|
|
|
Dist float64
|
|
|
|
|
Hash int64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GeoAdd 将给定的空间元素(纬度、经度、名字)添加到指定的键里面,这些数据会以有序集合的形式被储存在键里面,所以删除可以使用`ZREM`。
|
|
|
|
|
func (c *Client) GeoAdd(key string, longitude, latitude float64, member string) error {
|
|
|
|
|
_, err := redis.Int(c.Do("GEOADD", c.getKey(key), longitude, latitude, member))
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GeoPos 从键里面返回所有给定位置元素的位置(经度和纬度)。
|
|
|
|
|
func (c *Client) GeoPos(key string, members ...interface{}) ([]*[2]float64, error) {
|
|
|
|
|
args := redis.Args{}
|
|
|
|
|
args = args.Add(c.getKey(key))
|
|
|
|
|
args = args.Add(members...)
|
|
|
|
|
return redis.Positions(c.Do("GEOPOS", args...))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GeoDist 返回两个给定位置之间的距离。
|
|
|
|
|
// 如果两个位置之间的其中一个不存在, 那么命令返回空值。
|
|
|
|
|
// 指定单位的参数 unit 必须是以下单位的其中一个:
|
|
|
|
|
// m 表示单位为米。
|
|
|
|
|
// km 表示单位为千米。
|
|
|
|
|
// mi 表示单位为英里。
|
|
|
|
|
// ft 表示单位为英尺。
|
|
|
|
|
// 如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。
|
|
|
|
|
func (c *Client) GeoDist(key string, member1, member2, unit string) (float64, error) {
|
|
|
|
|
_, err := redis.Float64(c.Do("GEODIST", c.getKey(key), member1, member2, unit))
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GeoRadius 以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
|
|
|
|
|
func (c *Client) GeoRadius(key string, longitude, latitude, radius float64, unit string, options GeoOptions) ([]*GeoResult, error) {
|
|
|
|
|
args := redis.Args{}
|
|
|
|
|
args = args.Add(c.getKey(key), longitude, latitude, radius, unit)
|
|
|
|
|
if options.WithDist {
|
|
|
|
|
args = args.Add("WITHDIST")
|
|
|
|
|
}
|
|
|
|
|
if options.WithCoord {
|
|
|
|
|
args = args.Add("WITHCOORD")
|
|
|
|
|
}
|
|
|
|
|
if options.WithHash {
|
|
|
|
|
args = args.Add("WITHHASH")
|
|
|
|
|
}
|
|
|
|
|
if options.Order != "" {
|
|
|
|
|
args = args.Add(options.Order)
|
|
|
|
|
}
|
|
|
|
|
if options.Count > 0 {
|
|
|
|
|
args = args.Add("Count", options.Count)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reply, err := c.Do("GEORADIUS", args...)
|
|
|
|
|
return toGeoResult(reply, err, options)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GeoRadiusByMember 这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点。
|
|
|
|
|
func (c *Client) GeoRadiusByMember(key string, member string, radius float64, unit string, options GeoOptions) ([]*GeoResult, error) {
|
|
|
|
|
args := redis.Args{}
|
|
|
|
|
args = args.Add(c.getKey(key), member, radius, unit)
|
|
|
|
|
if options.WithDist {
|
|
|
|
|
args = args.Add("WITHDIST")
|
|
|
|
|
}
|
|
|
|
|
if options.WithCoord {
|
|
|
|
|
args = args.Add("WITHCOORD")
|
|
|
|
|
}
|
|
|
|
|
if options.WithHash {
|
|
|
|
|
args = args.Add("WITHHASH")
|
|
|
|
|
}
|
|
|
|
|
if options.Order != "" {
|
|
|
|
|
args = args.Add(options.Order)
|
|
|
|
|
}
|
|
|
|
|
if options.Count > 0 {
|
|
|
|
|
args = args.Add("Count", options.Count)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reply, err := c.Do("GEORADIUSBYMEMBER", args...)
|
|
|
|
|
return toGeoResult(reply, err, options)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GeoHash 返回一个或多个位置元素的 Geohash 表示。
|
|
|
|
|
func (c *Client) GeoHash(key string, members ...interface{}) ([]string, error) {
|
|
|
|
|
args := redis.Args{}
|
|
|
|
|
args = args.Add(c.getKey(key))
|
|
|
|
|
args = args.Add(members...)
|
|
|
|
|
return redis.Strings(c.Do("GEOHASH", args...))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func toGeoResult(reply interface{}, err error, options GeoOptions) ([]*GeoResult, error) {
|
|
|
|
|
values, err := redis.Values(reply, err)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
results := make([]*GeoResult, len(values))
|
|
|
|
|
for i := range values {
|
|
|
|
|
if values[i] == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
p, ok := values[i].([]interface{})
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("redisgo: unexpected element type for interface slice, got type %T", values[i])
|
|
|
|
|
}
|
|
|
|
|
geoResult := &GeoResult{}
|
|
|
|
|
pos := 0
|
|
|
|
|
name, err := redis.String(p[pos], nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
geoResult.Name = name
|
|
|
|
|
if options.WithDist {
|
|
|
|
|
pos = pos + 1
|
|
|
|
|
dist, err := redis.Float64(p[pos], nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
geoResult.Dist = dist
|
|
|
|
|
}
|
|
|
|
|
if options.WithHash {
|
|
|
|
|
pos = pos + 1
|
|
|
|
|
hash, err := redis.Int64(p[pos], nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
geoResult.Hash = hash
|
|
|
|
|
}
|
|
|
|
|
if options.WithCoord {
|
|
|
|
|
pos = pos + 1
|
|
|
|
|
pp, ok := p[pos].([]interface{})
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("redisgo: unexpected element type for interface slice, got type %T", p[i])
|
|
|
|
|
}
|
|
|
|
|
if len(pp) > 0 {
|
|
|
|
|
lat, err := redis.Float64(pp[0], nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
lon, err := redis.Float64(pp[1], nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
geoResult.Latitude = lat
|
|
|
|
|
geoResult.Longitude = lon
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
results[i] = geoResult
|
|
|
|
|
}
|
|
|
|
|
return results, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// getKey 将健名加上指定的前缀。
|
|
|
|
|
func (c *Client) getKey(key string) string {
|
|
|
|
|
return c.prefix + key
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encode 序列化要保存的值
|
|
|
|
|
func (c *Client) encode(val interface{}) (interface{}, error) {
|
|
|
|
|
var value interface{}
|
|
|
|
|
switch v := val.(type) {
|
|
|
|
|
case string, int, uint, int8, int16, int32, int64, float32, float64, bool:
|
|
|
|
|
value = v
|
|
|
|
|
default:
|
|
|
|
|
b, err := c.marshal(v)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
value = string(b)
|
|
|
|
|
}
|
|
|
|
|
return value, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// decode 反序列化保存的struct对象
|
|
|
|
|
func (c *Client) decode(reply interface{}, err error, val interface{}) error {
|
|
|
|
|
str, err := String(reply, err)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return c.unmarshal([]byte(str), val)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// closePool 程序进程退出时关闭连接池
|
|
|
|
|
func (c *Client) closePool() {
|
|
|
|
|
ch := make(chan os.Signal, 1)
|
|
|
|
|
signal.Notify(ch, os.Interrupt)
|
|
|
|
|
signal.Notify(ch, syscall.SIGTERM)
|
|
|
|
|
signal.Notify(ch, syscall.SIGKILL)
|
|
|
|
|
go func() {
|
|
|
|
|
<-ch
|
|
|
|
|
c.pool.Close()
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// init 注册到cache
|
|
|
|
|
// func init() {
|
|
|
|
|
// cache.Register("redis", &Client{})
|
|
|
|
|
// }
|
2024-03-14 07:53:25 +08:00
|
|
|
|
|
|
|
|
|
// 是否是空错误
|
|
|
|
|
func (c *Client) IsErrNil(err error) bool {
|
|
|
|
|
return err == redis.ErrNil
|
|
|
|
|
}
|