github.com/mdaxf/iac@v0.0.0-20240519030858-58a061660378/framework/cache/redis/redis.go (about)

     1  // The package is migrated from beego, you can get from following link:
     2  // import(
     3  //   "github.com/beego/beego/v2/client/cache"
     4  // )
     5  // Copyright 2023. All Rights Reserved.
     6  //
     7  // Licensed under the Apache License, Version 2.0 (the "License");
     8  // you may not use this file except in compliance with the License.
     9  // You may obtain a copy of the License at
    10  //
    11  //      http://www.apache.org/licenses/LICENSE-2.0
    12  //
    13  // Unless required by applicable law or agreed to in writing, software
    14  // distributed under the License is distributed on an "AS IS" BASIS,
    15  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  // See the License for the specific language governing permissions and
    17  // limitations under the License.
    18  
    19  // Package redis for cache provider
    20  //
    21  // depend on github.com/gomodule/redigo/redis
    22  //
    23  // go install github.com/gomodule/redigo/redis
    24  //
    25  // Usage:
    26  // import(
    27  //
    28  //	_ "github.com/beego/beego/v2/client/cache/redis"
    29  //	"github.com/beego/beego/v2/client/cache"
    30  //
    31  // )
    32  //
    33  //	bm, err := cache.NewCache("redis", `{"conn":"127.0.0.1:11211"}`)
    34  package redis
    35  
    36  import (
    37  	"context"
    38  	"encoding/json"
    39  	"fmt"
    40  	"strconv"
    41  	"strings"
    42  	"time"
    43  
    44  	"github.com/gomodule/redigo/redis"
    45  
    46  	"github.com/mdaxf/iac/framework/berror"
    47  	"github.com/mdaxf/iac/framework/cache"
    48  )
    49  
    50  const (
    51  	// DefaultKey defines the collection name of redis for the cache adapter.
    52  	DefaultKey = "beecacheRedis"
    53  	// defaultMaxIdle defines the default max idle connection number.
    54  	defaultMaxIdle = 3
    55  	// defaultTimeout defines the default timeout .
    56  	defaultTimeout = time.Second * 180
    57  )
    58  
    59  // Cache is Redis cache adapter.
    60  type Cache struct {
    61  	p        *redis.Pool // redis connection pool
    62  	conninfo string
    63  	dbNum    int
    64  	// key actually is prefix.
    65  	key      string
    66  	password string
    67  	maxIdle  int
    68  
    69  	// skipEmptyPrefix for backward compatible,
    70  	// check function associate
    71  	// see https://github.com/beego/beego/issues/5248
    72  	skipEmptyPrefix bool
    73  
    74  	// Timeout value (less than the redis server's timeout value).
    75  	// Timeout used for idle connection
    76  	timeout time.Duration
    77  }
    78  
    79  // NewRedisCache creates a new redis cache with default collection name.
    80  func NewRedisCache() cache.Cache {
    81  	return &Cache{key: DefaultKey}
    82  }
    83  
    84  // Execute the redis commands. args[0] must be the key name
    85  func (rc *Cache) do(commandName string, args ...interface{}) (interface{}, error) {
    86  	args[0] = rc.associate(args[0])
    87  	c := rc.p.Get()
    88  	defer func() {
    89  		_ = c.Close()
    90  	}()
    91  
    92  	reply, err := c.Do(commandName, args...)
    93  	if err != nil {
    94  		return nil, berror.Wrapf(err, cache.RedisCacheCurdFailed,
    95  			"could not execute this command: %s", commandName)
    96  	}
    97  
    98  	return reply, nil
    99  }
   100  
   101  // associate with config key.
   102  func (rc *Cache) associate(originKey interface{}) string {
   103  	if rc.key == "" && rc.skipEmptyPrefix {
   104  		return fmt.Sprintf("%s", originKey)
   105  	}
   106  	return fmt.Sprintf("%s:%s", rc.key, originKey)
   107  }
   108  
   109  // Get cache from redis.
   110  func (rc *Cache) Get(ctx context.Context, key string) (interface{}, error) {
   111  	if v, err := rc.do("GET", key); err == nil {
   112  		return v, nil
   113  	} else {
   114  		return nil, err
   115  	}
   116  }
   117  
   118  // GetMulti gets cache from redis.
   119  func (rc *Cache) GetMulti(ctx context.Context, keys []string) ([]interface{}, error) {
   120  	c := rc.p.Get()
   121  	defer func() {
   122  		_ = c.Close()
   123  	}()
   124  	var args []interface{}
   125  	for _, key := range keys {
   126  		args = append(args, rc.associate(key))
   127  	}
   128  	return redis.Values(c.Do("MGET", args...))
   129  }
   130  
   131  // Put puts cache into redis.
   132  func (rc *Cache) Put(ctx context.Context, key string, val interface{}, timeout time.Duration) error {
   133  	_, err := rc.do("SETEX", key, int64(timeout/time.Second), val)
   134  	return err
   135  }
   136  
   137  // Delete deletes a key's cache in redis.
   138  func (rc *Cache) Delete(ctx context.Context, key string) error {
   139  	_, err := rc.do("DEL", key)
   140  	return err
   141  }
   142  
   143  // IsExist checks cache's existence in redis.
   144  func (rc *Cache) IsExist(ctx context.Context, key string) (bool, error) {
   145  	v, err := redis.Bool(rc.do("EXISTS", key))
   146  	if err != nil {
   147  		return false, err
   148  	}
   149  	return v, nil
   150  }
   151  
   152  // Incr increases a key's counter in redis.
   153  func (rc *Cache) Incr(ctx context.Context, key string) error {
   154  	_, err := redis.Bool(rc.do("INCRBY", key, 1))
   155  	return err
   156  }
   157  
   158  // Decr decreases a key's counter in redis.
   159  func (rc *Cache) Decr(ctx context.Context, key string) error {
   160  	_, err := redis.Bool(rc.do("INCRBY", key, -1))
   161  	return err
   162  }
   163  
   164  // ClearAll deletes all cache in the redis collection
   165  // Be careful about this method, because it scans all keys and the delete them one by one
   166  func (rc *Cache) ClearAll(context.Context) error {
   167  	cachedKeys, err := rc.Scan(rc.key + ":*")
   168  	if err != nil {
   169  		return err
   170  	}
   171  	c := rc.p.Get()
   172  	defer func() {
   173  		_ = c.Close()
   174  	}()
   175  	for _, str := range cachedKeys {
   176  		if _, err = c.Do("DEL", str); err != nil {
   177  			return err
   178  		}
   179  	}
   180  	return err
   181  }
   182  
   183  // Scan scans all keys matching a given pattern.
   184  func (rc *Cache) Scan(pattern string) (keys []string, err error) {
   185  	c := rc.p.Get()
   186  	defer func() {
   187  		_ = c.Close()
   188  	}()
   189  	var (
   190  		cursor uint64 = 0 // start
   191  		result []interface{}
   192  		list   []string
   193  	)
   194  	for {
   195  		result, err = redis.Values(c.Do("SCAN", cursor, "MATCH", pattern, "COUNT", 1024))
   196  		if err != nil {
   197  			return
   198  		}
   199  		list, err = redis.Strings(result[1], nil)
   200  		if err != nil {
   201  			return
   202  		}
   203  		keys = append(keys, list...)
   204  		cursor, err = redis.Uint64(result[0], nil)
   205  		if err != nil {
   206  			return
   207  		}
   208  		if cursor == 0 { // over
   209  			return
   210  		}
   211  	}
   212  }
   213  
   214  // StartAndGC starts the redis cache adapter.
   215  // config: must be in this format {"key":"collection key","conn":"connection info","dbNum":"0", "skipEmptyPrefix":"true"}
   216  // Cached items in redis are stored forever, no garbage collection happens
   217  func (rc *Cache) StartAndGC(config string) error {
   218  	err := rc.parseConf(config)
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	rc.connectInit()
   224  
   225  	c := rc.p.Get()
   226  	defer func() {
   227  		_ = c.Close()
   228  	}()
   229  
   230  	// test connection
   231  	if err = c.Err(); err != nil {
   232  		return berror.Wrapf(err, cache.InvalidConnection,
   233  			"can not connect to remote redis server, please check the connection info and network state: %s", config)
   234  	}
   235  	return nil
   236  }
   237  
   238  func (rc *Cache) parseConf(config string) error {
   239  	var cf redisConfig
   240  	err := json.Unmarshal([]byte(config), &cf)
   241  	if err != nil {
   242  		return berror.Wrapf(err, cache.InvalidRedisCacheCfg, "could not unmarshal the config: %s", config)
   243  	}
   244  
   245  	err = cf.parse()
   246  	if err != nil {
   247  		return err
   248  	}
   249  
   250  	rc.dbNum = cf.dbNum
   251  	rc.key = cf.Key
   252  	rc.conninfo = cf.Conn
   253  	rc.password = cf.password
   254  	rc.maxIdle = cf.maxIdle
   255  	rc.timeout = cf.timeout
   256  	rc.skipEmptyPrefix = cf.skipEmptyPrefix
   257  
   258  	return nil
   259  }
   260  
   261  type redisConfig struct {
   262  	DbNum           string `json:"dbNum"`
   263  	SkipEmptyPrefix string `json:"skipEmptyPrefix"`
   264  	Key             string `json:"key"`
   265  	// Format redis://<password>@<host>:<port>
   266  	Conn       string `json:"conn"`
   267  	MaxIdle    string `json:"maxIdle"`
   268  	TimeoutStr string `json:"timeout"`
   269  
   270  	dbNum           int
   271  	skipEmptyPrefix bool
   272  	maxIdle         int
   273  	// parse from Conn
   274  	password string
   275  	// timeout used for idle connection, default is 180 seconds.
   276  	timeout time.Duration
   277  }
   278  
   279  // parse parses the config.
   280  // If the necessary settings have not been set, it will return an error.
   281  // It will fill the default values if some fields are missing.
   282  func (cf *redisConfig) parse() error {
   283  	if cf.Conn == "" {
   284  		return berror.Error(cache.InvalidRedisCacheCfg, "config missing conn field")
   285  	}
   286  
   287  	// Format redis://<password>@<host>:<port>
   288  	cf.Conn = strings.Replace(cf.Conn, "redis://", "", 1)
   289  	if i := strings.Index(cf.Conn, "@"); i > -1 {
   290  		cf.password = cf.Conn[0:i]
   291  		cf.Conn = cf.Conn[i+1:]
   292  	}
   293  
   294  	if cf.Key == "" {
   295  		cf.Key = DefaultKey
   296  	}
   297  
   298  	if cf.DbNum != "" {
   299  		cf.dbNum, _ = strconv.Atoi(cf.DbNum)
   300  	}
   301  
   302  	if cf.SkipEmptyPrefix != "" {
   303  		cf.skipEmptyPrefix, _ = strconv.ParseBool(cf.SkipEmptyPrefix)
   304  	}
   305  
   306  	if cf.MaxIdle == "" {
   307  		cf.maxIdle = defaultMaxIdle
   308  	} else {
   309  		cf.maxIdle, _ = strconv.Atoi(cf.MaxIdle)
   310  	}
   311  
   312  	if v, err := time.ParseDuration(cf.TimeoutStr); err == nil {
   313  		cf.timeout = v
   314  	} else {
   315  		cf.timeout = defaultTimeout
   316  	}
   317  
   318  	return nil
   319  }
   320  
   321  // connect to redis.
   322  func (rc *Cache) connectInit() {
   323  	dialFunc := func() (c redis.Conn, err error) {
   324  		c, err = redis.Dial("tcp", rc.conninfo)
   325  		if err != nil {
   326  			return nil, berror.Wrapf(err, cache.DialFailed,
   327  				"could not dial to remote server: %s ", rc.conninfo)
   328  		}
   329  
   330  		if rc.password != "" {
   331  			if _, err = c.Do("AUTH", rc.password); err != nil {
   332  				_ = c.Close()
   333  				return nil, err
   334  			}
   335  		}
   336  
   337  		_, selecterr := c.Do("SELECT", rc.dbNum)
   338  		if selecterr != nil {
   339  			_ = c.Close()
   340  			return nil, selecterr
   341  		}
   342  		return
   343  	}
   344  	// initialize a new pool
   345  	rc.p = &redis.Pool{
   346  		MaxIdle:     rc.maxIdle,
   347  		IdleTimeout: rc.timeout,
   348  		Dial:        dialFunc,
   349  	}
   350  }
   351  
   352  func init() {
   353  	cache.Register("redis", NewRedisCache)
   354  }