diff --git a/redis/readme.md b/redis/readme.md new file mode 100644 index 0000000..8757e82 --- /dev/null +++ b/redis/readme.md @@ -0,0 +1,54 @@ +# go-redis 连接选项 + +连接字符串 +redis://:@:/ + +redis.ParseURL + +```go + gClient = redis.NewClient(&redis.Options{ + //连接信息 + Network: "tcp", //网络类型,tcp or unix,默认tcp + Addr: "127.0.0.1:6379", //主机名+冒号+端口,默认localhost:6379 + Password: "", //密码 + DB: 0, // redis数据库index + + //连接池容量及闲置连接数量 + PoolSize: 15, // 连接池最大socket连接数,默认为4倍CPU数, 4 * runtime.NumCPU + MinIdleConns: 10, //在启动阶段创建指定数量的Idle连接,并长期维持idle状态的连接数不少于指定数量;。 + + //超时 + DialTimeout: 5 * time.Second, //连接建立超时时间,默认5秒。 + ReadTimeout: 3 * time.Second, //读超时,默认3秒, -1表示取消读超时 + WriteTimeout: 3 * time.Second, //写超时,默认等于读超时 + PoolTimeout: 4 * time.Second, //当所有连接都处在繁忙状态时,客户端等待可用连接的最大等待时长,默认为读超时+1秒。 + + //闲置连接检查包括IdleTimeout,MaxConnAge + IdleCheckFrequency: 60 * time.Second, //闲置连接检查的周期,默认为1分钟,-1表示不做周期性检查,只在客户端获取连接时对闲置连接进行处理。 + IdleTimeout: 5 * time.Minute, //闲置超时,默认5分钟,-1表示取消闲置超时检查 + MaxConnAge: 0 * time.Second, //连接存活时长,从创建开始计时,超过指定时长则关闭连接,默认为0,即不关闭存活时长较长的连接 + + //命令执行失败时的重试策略 + MaxRetries: 0, // 命令执行失败时,最多重试多少次,默认为0即不重试 + MinRetryBackoff: 8 * time.Millisecond, //每次计算重试间隔时间的下限,默认8毫秒,-1表示取消间隔 + MaxRetryBackoff: 512 * time.Millisecond, //每次计算重试间隔时间的上限,默认512毫秒,-1表示取消间隔 + + //可自定义连接函数 + Dialer: func() (net.Conn, error) { + netDialer := &net.Dialer{ + Timeout: 5 * time.Second, + KeepAlive: 5 * time.Minute, + } + return netDialer.Dial("tcp", "127.0.0.1:6379") + }, + + //钩子函数 + OnConnect: func(conn *redis.Conn) error { //仅当客户端执行命令时需要从连接池获取连接时,如果连接池需要新建连接时则会调用此钩子函数 + fmt.Printf("conn=%v\n", conn) + return nil + }, + + }) + defer gClient.Close() + +``` diff --git a/redis/redis.go b/redis/redis.go new file mode 100644 index 0000000..ea5fd3b --- /dev/null +++ b/redis/redis.go @@ -0,0 +1,105 @@ +package redis + +import ( + "context" + "time" + + "github.com/go-redis/redis/v8" +) + +const ( + defaultSeparator = ":" + + blockingQueryTimeout = 5 * time.Second + readWriteTimeout = 2 * time.Second + defaultSlowThreshold = time.Millisecond * 100 // 慢查询 +) + +type Option func(r *Redis) + +type Redis struct { + addr string // 服务器地址 + prefix string // 键值前缀 + separator string // 分隔符 +} + +func New(addr string, opts ...Option) *Redis { + r := &Redis{ + addr: addr, + } + + return r +} + +func (s *Redis) Set(ctx context.Context, key, value string) error { + conn, err := s.getRedis() + if err != nil { + return err + } + + return conn.Set(ctx, s.formatKey(key), value, 0).Err() +} + +func (s *Redis) Get(ctx context.Context, key string) (string, error) { + conn, err := s.getRedis() + if err != nil { + return "", err + } + + return conn.Get(ctx, s.formatKey(key)).Result() +} + +func (s *Redis) GetSet(ctx context.Context, key, value string) (string, error) { + conn, err := s.getRedis() + if err != nil { + return "", err + } + + val, err := conn.GetSet(ctx, s.formatKey(key), value).Result() + return val, err +} + +func (s *Redis) Del(ctx context.Context, key ...string) (int, error) { + conn, err := s.getRedis() + if err != nil { + return 0, err + } + + keys := s.formatKeys(key...) + v, err := conn.Del(ctx, keys...).Result() + if err != nil { + return 0, err + } + + return int(v), err +} + +func (s *Redis) getRedis() (redis.UniversalClient, error) { + client := redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: []string{s.addr}, + }) + + return client, nil +} + +func (s *Redis) formatKeys(keys ...string) []string { + // If no prefix is configured, this parameter is returned + if s.prefix == "" { + return keys + } + + ret := make([]string, 0, len(keys)) + for _, k := range keys { + ret = append(ret, s.formatKey(k)) + } + + return ret +} + +func (s *Redis) formatKey(key string) string { + if s.prefix == "" { + return key + } + + return s.prefix + s.separator + key +} diff --git a/redis/redis_test.go b/redis/redis_test.go new file mode 100644 index 0000000..bc5421d --- /dev/null +++ b/redis/redis_test.go @@ -0,0 +1,86 @@ +package redis + +import ( + "context" + "log" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" +) + +func TestGetSet(t *testing.T) { + runOnRedis(t, func(client *Redis) { + ctx := context.Background() + + val, err := client.GetSet(ctx, "hello", "world") + assert.NotNil(t, err) + assert.Equal(t, "", val) + + val, err = client.Get(ctx, "hello") + assert.Nil(t, err) + assert.Equal(t, "world", val) + + val, err = client.GetSet(ctx, "hello", "newworld") + assert.Nil(t, err) + assert.Equal(t, "world", val) + + val, err = client.Get(ctx, "hello") + assert.Nil(t, err) + assert.Equal(t, "newworld", val) + + ret, err := client.Del(ctx, "hello") + assert.Nil(t, err) + assert.Equal(t, 1, ret) + }) +} + +func TestRedis_SetGetDel(t *testing.T) { + runOnRedis(t, func(client *Redis) { + ctx := context.Background() + + err := client.Set(ctx, "hello", "world") + assert.Nil(t, err) + + val, err := client.Get(ctx, "hello") + assert.Nil(t, err) + assert.Equal(t, "world", val) + ret, err := client.Del(ctx, "hello") + assert.Nil(t, err) + assert.Equal(t, 1, ret) + }) +} + +func runOnRedis(t *testing.T, fn func(client *Redis)) { + redis, clean, err := CreateMiniRedis() + assert.Nil(t, err) + + defer clean() + + fn(redis) +} + +func CreateMiniRedis() (r *Redis, clean func(), err error) { + mr, err := miniredis.Run() + if err != nil { + return nil, nil, err + } + + addr := mr.Addr() + log.Println("mini redis run at:", addr) + + return New(addr), func() { + ch := make(chan struct{}) + + go func() { + mr.Close() + close(ch) + }() + + select { + case <-ch: + case <-time.After(time.Second): + } + }, nil +}