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 }