# README
gopkg Redis Package
gopkg/redis is an componentized redis package.
It provides:
- An easy way to configre and manage redis client.
- Lock handler.
- Object fetcher.
Based on gomodule/redigo.
Install
go get github.com/wwwangxc/gopkg/redis
Quick Start
Client Proxy
package main
import (
"context"
"fmt"
// gopkg/redis will automatically read configuration
// files (./app.yaml) when package loaded
"github.com/wwwangxc/gopkg/redis"
)
func main() {
cli := redis.NewClientProxy("client_name",
redis.WithDSN("dsn"), // set dsn, default use database.client.dsn
redis.WithMaxIdle(20), // set max idel. default 2048
redis.WithMaxActive(100), // set max active. default 0
redis.WithIdleTimeout(180000), // set idle timeout. unit millisecond, default 180000
redis.WithTimeout(1000), // set command timeout. unit millisecond, default 1000
redis.WithMaxConnLifetime(10000), // set max conn life time, default 0
redis.WithWait(true), // set wait
)
// Exec GET command
cli.Do(context.Background(), "GET", "foo")
// Pipeline
// get a redis connection
c := cli.Conn()
defer c.Close()
c.Send("SET", "foo", "bar")
c.Send("GET", "foo")
c.Flush()
c.Receive() // reply from SET
v, err := c.Receive() // reply from GET
fmt.Sprintf("reply: %s", v)
fmt.Sprintf("error: %v", err)
}
Locker Proxy
package main
import (
"context"
"fmt"
// gopkg/redis will automatically read configuration
// files (./app.yaml) when package loaded
"github.com/wwwangxc/gopkg/redis"
)
func main() {
// cli := redis.NewClientProxy("client_name").Locker()
cli := redis.NewLockerProxy("client_name",
redis.WithDSN("dsn"), // set dsn, default use database.client.dsn
redis.WithMaxIdle(20), // set max idel. default 2048
redis.WithMaxActive(100), // set max active. default 0
redis.WithIdleTimeout(180000), // set idle timeout. unit millisecond, default 180000
redis.WithTimeout(1000), // set command timeout. unit millisecond, default 1000
redis.WithMaxConnLifetime(10000), // set max conn life time, default 0
redis.WithWait(true), // set wait
)
// try lock
// not block the current goroutine.
// return uuid when the lock is acquired
// return error when lock fail or lock not acquired
// support reentrant unlock
// support automatically renewal
uuid, err := l.TryLock(context.Background(), "locker_key",
redis.WithLockExpire(1000*time.Millisecond),
redis.WithLockHeartbeat(500*time.Millisecond))
if err != nil {
// return ErrLockNotAcquired when lock not acquired
if redis.IsLockNotAcquired(err) {
fmt.Printf("lock not acquired\n")
return
}
fmt.Printf("try lock fail. error: %v\n", err)
return
}
defer func() {
// return ErrLockNotExist if the key does not exist
// return ErrNotOwnerOfKey if the uuid invalid
// support reentrant unlock
if err := l.Unlock(context.Background(), "locker_key", uuid); err != nil {
fmt.Printf("unlock fail. error: %v\n", err)
}
}()
// reentrant lock when uuid not empty
// will block the current goroutine until lock is acquired when not reentrant lock
_, err = l.Lock(context.Background(), "locker_key",
redis.WithLockUUID(uuid),
redis.WithLockExpire(1000*time.Millisecond),
redis.WithLockHeartbeat(500*time.Millisecond))
if err != nil {
fmt.Printf("lock fail. error: %v\n", err)
return
}
f := func() error {
// do something...
return nil
}
// try get lock first and call f() when lock acquired. Unlock will be performed
// regardless of whether the f reports an error or not.
if err := l.LockAndCall(context.Background(), "locker_key", f); err != nil {
fmt.Printf("lock and call fail. error: %v\n", err)
return
}
}
Fetcher Proxy
package main
import (
"context"
"fmt"
// gopkg/redis will automatically read configuration
// files (./app.yaml) when package loaded
"github.com/wwwangxc/gopkg/redis"
)
func main() {
// f := redis.NewClientProxy("client_name").Fetcher()
f := redis.NewFetcherProxy("client_name",
redis.WithDSN("dsn"), // set dsn, default use database.client.dsn
redis.WithMaxIdle(20), // set max idel. default 2048
redis.WithMaxActive(100), // set max active. default 0
redis.WithIdleTimeout(180000), // set idle timeout. unit millisecond, default 180000
redis.WithTimeout(1000), // set command timeout. unit millisecond, default 1000
redis.WithMaxConnLifetime(10000), // set max conn life time, default 0
redis.WithWait(true), // set wait
)
obj := struct {
FieldA string `json:"field_a"`
FieldB int `json:"field_b"`
}{}
callback := func() (interface{}, error) {
// do something...
return nil, nil
}
// fetch object
//
// The callback function will be called if the key does not exist.
// Will cache the callback results into the key and set timeout.
// Default do nothing.
//
// The marshal function will be called before cache.
//
// Default callback do nothing, use json.Marshal and json.Unmarshal
err := f.Fetch(context.Background(), "fetcher_key", &obj,
redis.WithFetchCallback(callback, 1000*time.Millisecond),
redis.WithFetchUnmarshal(json.Unmarshal),
redis.WithFetchMarshal(json.Marshal))
if err != nil {
fmt.Printf("fetch fail. error: %v\n", err)
return
}
}
Config
client:
redis:
max_idle: 20
max_active: 100
max_conn_lifetime: 1000
idle_timeout: 180000
timeout: 1000
wait: true
service:
- name: redis_1
dsn: redis://username:[email protected]:6379/1?timeout=1000ms
- name: redis_2
dsn: redis://username:[email protected]:6379/2?timeout=1000ms
max_idle: 22
max_active: 111
max_conn_lifetime: 2000
idle_timeout: 200000
timeout: 2000
How To Mock
Client Proxy
package tests
import (
"testing"
"github.com/agiledragon/gomonkey"
"github.com/golang/mock/gomock"
"github.com/wwwangxc/gopkg/redis"
"github.com/wwwangxc/gopkg/redis/mockredis"
)
func TestMockClientProxy(t *testing.T){
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Mock redis client
mockConn := mockredis.NewMockConn(ctrl)
mockConn.EXPECT().Close().Return(nil).AnyTimes()
mockConn.EXPECT().Send(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockConn.EXPECT().Flush().Return(nil).AnyTimes()
mockConn.EXPECT().Receive().Return(nil, nil).AnyTimes()
// Mock locker
mockLocker := mockredis.NewMockLocker(ctrl)
mockLocker.EXPECT().TryLock(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).AnyTimes()
mockLocker.EXPECT().Lock(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).AnyTimes()
mockLocker.EXPECT().Unlock(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
// Mock fetcher
mockFetcher := mockredis.NewMockFetcher(ctrl)
mockFetcher.EXPECT().Fetch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
// Mock client proxy
mockCli := mockredis.NewMockClientProxy(ctrl)
mockCli.EXPECT().Do(gomock.Any(), gomock.Any(), gomock.Any()).Return("reply", nil).AnyTimes() // Do
mockCli.EXPECT().Conn().Return(mockConn).AnyTimes() // Conn
mockCli.EXPECT().Locker().Return(mockLocker).AnyTimes() // Locker
mockCli.EXPECT().Fetcher().Return(mockFetcher).AnyTimes() // Fetcher
patches := gomonkey.ApplyFunc(redis.NewClientProxy,
func(string, ...redis.ClientOption) redis.ClientProxy {
return mockCli
})
defer patches.Reset()
// do something...
}
Locker Proxy
package tests
import (
"testing"
"github.com/agiledragon/gomonkey"
"github.com/golang/mock/gomock"
"github.com/wwwangxc/gopkg/redis"
"github.com/wwwangxc/gopkg/redis/mockredis"
)
func TestMockLockerProxy(t *testing.T){
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Mock locker
mockLocker := mockredis.NewMockLocker(ctrl)
mockLocker.EXPECT().TryLock(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).AnyTimes()
mockLocker.EXPECT().Lock(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).AnyTimes()
mockLocker.EXPECT().Unlock(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
mockLocker.EXPECT().LockAndCall(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
patches := gomonkey.ApplyFunc(redis.NewLockerProxy,
func(string, ...redis.ClientOption) redis.Locker {
return mockLocker
})
defer patches.Reset()
// do something...
}
Fetcher Proxy
package tests
import (
"testing"
"github.com/agiledragon/gomonkey"
"github.com/golang/mock/gomock"
"github.com/wwwangxc/gopkg/redis"
"github.com/wwwangxc/gopkg/redis/mockredis"
)
func TestMockFetcherProxy(t *testing.T){
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Mock fetcher
mockFetcher := mockredis.NewMockFetcher(ctrl)
mockFetcher.EXPECT().Fetch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
patches := gomonkey.ApplyFunc(redis.NewFetcherProxy,
func(string, ...redis.ClientOption) redis.Fetcher {
return mockFetcher
})
defer patches.Reset()
// do something...
}
# Functions
Bool is a helper that converts a command reply to a boolean.
Bytes is a helper that converts a command reply to a slice of bytes.
ByteSlices is a helper that converts an array command reply to a [][]byte.
Float64 is a helper that converts a command reply to 64 bit float.
Float64s is a helper that converts an array command reply to a []float64.
Int is a helper that converts a command reply to an integer.
Int64 is a helper that converts a command reply to 64 bit integer.
Int64Map is a helper that converts an array of strings (alternating key, value) into a map[string]int64.
Int64s is a helper that converts an array command reply to a []int64.
IntMap is a helper that converts an array of strings (alternating key, value) into a map[string]int.
Ints is a helper that converts an array command reply to a []int.
IsErrNotOwnerOfLock is not owner of lock.
IsKeyNotExist is key not exist error.
IsLockNotAcquired is lock not acquired error.
IsLockNotExist is lock not exist error.
IsTimeout is timeout error.
NewClientProxy new redis client proxy.
NewFetcherProxy new object fetcher proxy.
NewLockerProxy new locker proxy.
Positions is a helper that converts an array of positions (lat, long) into a [][2]float64.
Scan copies from src to the values pointed at by dest.
ScanSlice scans src to the slice pointed to by dest.
ScanStruct scans alternating names and values from src to a struct.
SlowLogs is a helper that parse the SLOWLOG GET command output and return the array of SlowLog
See: https://github.com/gomodule/redigo/blob/master/redis/reply.go.
String is a helper that converts a command reply to a string.
StringMap is a helper that converts an array of strings (alternating key, value) into a map[string]string.
Strings is a helper that converts an array command reply to a []string.
Uint64 is a helper that converts a command reply to 64 bit unsigned integer.
Uint64Map is a helper that converts an array of strings (alternating key, value) into a map[string]uint64.
Uint64s is a helper that converts an array command reply to a []uint64.
Values is a helper that converts an array command reply to a []interface{}.
WithClientDSN set dsn.
WithClientIdleTimeout set idle timeout
Close connections after remaining idle for this duration.
WithClientMaxActive set max active
Maximum number of connections allocated by the pool at a given time.
WithClientMaxConnLifetime set max conn lifetime
Close connections older than this duration.
WithClientMaxIdle set max idle
Maximum number of connections in the idle connection pool.
WithClientTimeout set timeout
Write, read and connect timeout Unit millisecond, default 1000.
WithClientWait set wait
If Wait is true and the pool is at the MaxActive limit, then Get() waits for a connection to be returned to the pool before returning.
WithFetchCallback set fetch callback & expire option
The callback function will be called if the key does not exist.
WithFetchMarshal set mashal function to fetcher
The marshal function will be called before cache.
WithFetchUnmarshal set unmarshal function to fetcher
Default use json.Unmarshal.
WithLockExpire set lock expire
default 1000 millisecond.
WithLockHeartbeat set heartbeat
Heartbeat indicates the time interval for automatically renewal.
WithLockRetry set retry Retry indicates the time interval for retrying the acquire lock.
WithLockUUID set uuid of the distributed lock
A non-null UUID indicates a reentrant lock.
WithSingleflight use singleflight for fetcher.
# Variables
ErrKeyNotExist key not exist.
ErrLockNotAcquired lock not acquired.
ErrLockNotExist lock dose not exist.
ErrNotOwnerOfLock not the owner of the key.
ErrTimeout.
# Interfaces
ClientProxy Redis client proxygo:generate mockgen -source=client.go -destination=mockredis/client_mock.go -package=mockredis.
FetcherProxy object fetcher
go:generate mockgen -source=fetcher.go -destination=mockredis/fetcher_mock.go -package=mockredis.
LockerProxy distributed lock providergo:generate mockgen -source=locker.go -destination=mockredis/locker_mock.go -package=mockredis.
# Type aliases
ClientOption redis client proxy option.
FetchOption fetch option.
LockOption distributed lock option.