文章目录
代码地址:https://gitee.com/lymgoforIT/golang-trick/tree/master/14-go-redis
作为后端研发,Redis
是无处不在的,那么go
操作Redis
也是每位后端研发应该掌握的基本技能。
go-redis
官方文档 https://redis.uptrace.dev/guide/
一:连接Redis
首先在本地启动Redis
服务端,监听6379
端口
当然,也可以使用docker
启动redis
;windows
上docker
的相关操作可参考:56.windows docker 安装ES、Go操作ES(github.com/olivere/elastic/v7库)
注意: 此处的版本、容器名和端口号可以根据自己需要设置。
启动一个 redis-cli
连接上面的 redis server
。
go
docker run -it --network host --rm redis:5.0.7 redis-cli
执行go get github.com/go-redis/redis/v8
导入依赖包,编写代码
go
package main
import (
"context"
"fmt"
"github.com/go-redis/redis/v8"
)
var redisClient *redis.Client
var ctx = context.Background()
func init() {
config := &redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0, // 使用默认DB
PoolSize: 15,
MinIdleConns: 10, //在启动阶段创建指定数量的Idle连接,并长期维持idle状态的连接数不少于指定数量;。
//超时
//DialTimeout: 5 * time.Second, //连接建立超时时间,默认5秒。
//ReadTimeout: 3 * time.Second, //读超时,默认3秒, -1表示取消读超时
//WriteTimeout: 3 * time.Second, //写超时,默认等于读超时
//PoolTimeout: 4 * time.Second, //当所有连接都处在繁忙状态时,客户端等待可用连接的最大等待时长,默认为读超时+1秒。
}
redisClient = redis.NewClient(config)
}
func main() {
redisClient.Set(ctx, "name", "zhangsan", 0)
val, err := redisClient.Get(ctx, "name").Result()
if err != nil {
fmt.Println("读取错误", err)
}
fmt.Println(fmt.Sprintf("key:name,val:%s", val))
}
执行上述代码,可见终端输出
当然,也可以打开Redis
客户端工具,查到对应的key
二:基本使用
包括设置值、取值、设置过期时间、判断key
是否存在、key
不存在时才设置值、删除等操作
go
func Test_Base(t *testing.T) {
// 添加key
//0表示没有过期时间
redisClient.Set(ctx, "testKey", "xxx", 0)
// 获取值
val, err := redisClient.Get(ctx, "testKey").Result()
if err != nil {
fmt.Println("错误", err)
}
fmt.Println("值:", val)
// 设置key过期时间 成功true
redisClient.Expire(ctx, "testKey", time.Second*60)
// 存在返回1
redisClient.Exists(ctx, "testKey")
// key不存在时设置值
redisClient.SetNX(ctx, "unkey", "val", 0)
redisClient.Set(ctx, "testKey2", "xxx", 0)
// 删除key 可删除多个
redisClient.Del(ctx, "testKey2", "testKey")
}
三:字符串
包括设置、读取、加、减、获取过期时间、模糊查询key
,遍历模糊查询结果等
go
func Test_String(t *testing.T) {
// 设置值
redisClient.Set(ctx, "strKey", 100, 0)
redisClient.Set(ctx, "straey", 100, 0)
// key自增1
redisClient.Incr(ctx, "strKey")
// 增加 66
redisClient.IncrBy(ctx, "straey", 66)
// -1
redisClient.Decr(ctx, "straey")
// -5
redisClient.DecrBy(ctx, "straey", 5)
// 过期时间
redisClient.TTL(ctx, "strKey")
// str*ey : *为任意字符串
// str[kKac]ey : 匹配[] 内的单个字符 strkey,strKey,straey,strcey
// str?ey : ? 任意单个字符
// 扫描key
iter := redisClient.Scan(ctx, 0, "str?ey", 0).Iterator()
for iter.Next(ctx) {
fmt.Println("keys", iter.Val(), ": val", redisClient.Get(ctx, iter.Val()).Val())
}
if err := iter.Err(); err != nil {
panic(any(err))
}
}
四:列表
go
func Test_List(t *testing.T) {
// 添加
redisClient.LPush(ctx, "listKey1", 111, 222, 333, 444)
redisClient.RPush(ctx, "listKey1", 5555)
// 不存在不添加
redisClient.LPushX(ctx, "unlistKey", 111)
var intf []int
// 根据索引获取 绑定到数组
redisClient.LRange(ctx, "listKey1", 0, 10).ScanSlice(&intf)
fmt.Println(intf)
var i int
// 弹出
redisClient.LPop(ctx, "listKey1").Scan(&i)
fmt.Println(i)
//....
}
五:哈希
go
func Test_Hash(t *testing.T) {
redisClient.HMSet(ctx, "hkey1", "name", "shushan", "age", 99, "b", true)
all := redisClient.HGetAll(ctx, "hkey1")
fmt.Printf(" %v \n ", all)
}
六:Set
go
func Test_Set(t *testing.T) {
// 添加
redisClient.SAdd(ctx, "setKey1", "m1", "onlyk1")
redisClient.SAdd(ctx, "setKey2", "m2", "xca")
sl, _ := redisClient.SDiff(ctx, "setKey1", "setKey2").Result()
fmt.Println(sl)
// onlyk1,m1
//随机移除
var val string
redisClient.SPop(ctx, "setKey1").Scan(&val)
fmt.Println(val)
// .....
}
七:管道
管道即一次打包多个命令,一次性发给服务端执行,能够节省命令传输时间。比如10
个命令,不使用管道时,得发送10
次,并接收10
次响应。使用管道时,则是把10
个命令打包一次性发送,并一次性接收10
个响应。
- 使用
redis
客户端的Pipeline
方法获得管道 - 之后使用获得的管道
pipe
去编写命令 - 最后使用管道的
Exec
方法提交打包后的多个命令
go
func Test_Pipe(t *testing.T) {
pipe := redisClient.Pipeline()
incr := pipe.Set(ctx, "pip_test", "bt", 0)
pipe.Expire(ctx, "pip_test", time.Hour)
// 提交
cmds, err := pipe.Exec(ctx)
if err != nil {
fmt.Println(err)
}
for _, cmd := range cmds {
fmt.Println(cmd.String())
}
// 该值得Exec提交后有效
fmt.Println(incr.Val())
}
八、事务
MULTI/EXEC
Redis
是单线程的,因此单个命令始终是原子的,但是来自不同客户端的两个给定命令可以依次执行,例如在它们之间交替执行。但是,MULTI/EXEC
能够确保在MULTI/EXEC
两个语句的命令之间没有其他客户端正在执行命令。
在这种场景我们需要使用TxPipeline
。TxPipeline
总体上类似于上面的Pipeline
,但是它内部会使用MULTI/EXEC
包裹排队的命令。例如:
go
pipe := rdb.TxPipeline()
incr := pipe.Incr("tx_pipeline_counter")
pipe.Expire("tx_pipeline_counter", time.Hour)
_, err := pipe.Exec()
fmt.Println(incr.Val(), err)
上面代码相当于在一个RTT
(往返时间)下执行了下面的redis
命令:
bash
MULTI
INCR pipeline_counter
EXPIRE pipeline_counts 3600
EXEC
还有一个与上文类似的·TxPipelined·方法,使用方法如下:
go
var incr *redis.IntCmd
_, err := rdb.TxPipelined(func(pipe redis.Pipeliner) error {
incr = pipe.Incr("tx_pipelined_counter")
pipe.Expire("tx_pipelined_counter", time.Hour)
return nil
})
fmt.Println(incr.Val(), err)
Watch
在某些场景下,我们除了要使用MULTI/EXEC
命令外,还需要配合使用WATCH
命令。在用户使用WATCH
命令监视某个键之后,直到该用户执行EXEC
命令的这段时间里,如果有其他用户抢先对被监视的键进行了替换、更新、删除等操作,那么当用户尝试执行EXEC
的时候,事务将失败并返回一个错误,用户可以根据这个错误选择重试事务或者放弃事务。
go
Watch(fn func(*Tx) error, keys ...string) error
Watch
方法接收一个函数和一个或多个key
作为参数。基本使用示例如下:
go
// 监视watch_count的值,并在值不变的前提下将其值+1
key := "watch_count"
err = client.Watch(func(tx *redis.Tx) error {
n, err := tx.Get(key).Int()
if err != nil && err != redis.Nil {
return err
}
_, err = tx.Pipelined(func(pipe redis.Pipeliner) error {
pipe.Set(key, n+1, 0)
return nil
})
return err
}, key) // 在执行事务时,如果这个key发生了变化(如被其他客户端修改了),则上面Watch方法中的事务会执行失败
九:示例
go-redis
实现接口IP限流,IP黑名单,IP白名单的示例
go
package Middlewares
import (
"github.com/gin-gonic/gin"
"strconv"
"time"
"voteapi/pkg/app/response"
"voteapi/pkg/gredis"
"voteapi/pkg/util"
)
const IP_LIMIT_NUM_KEY = "ipLimit:ipLimitNum"
const IP_BLACK_LIST_KEY = "ipLimit:ipBlackList"
var prefix = "{gateway}"
var delaySeconds int64 = 60 // 观察时间跨度,秒
var maxAttempts int64 = 10000 // 限制请求数
var blackSeconds int64 = 0 // 封禁时长,秒,0-不封禁
func GateWayPlus() gin.HandlerFunc {
return func(c *gin.Context) {
path := c.FullPath()
clientIp := c.ClientIP()
// redis配置集群时必须
param := make(map[string]string)
param["path"] = path
param["clientIp"] = clientIp
if !main(param) {
c.Abort()
response.JsonResponseError(c, "当前IP请求过于频繁,暂时被封禁~")
}
}
}
func main(param map[string]string) bool {
// 预知的IP黑名单
var blackList []string
if util.InStringArray(param["clientIp"], blackList) {
return false
}
// 预知的IP白名单
var whiteList []string
if util.InStringArray(param["clientIp"], whiteList) {
return false
}
blackKey := prefix + ":" + IP_BLACK_LIST_KEY
limitKey := prefix + ":" + IP_LIMIT_NUM_KEY
curr := time.Now().Unix()
item := util.Md5(param["path"] + "|" + param["clientIp"])
return normal(blackKey, limitKey, item, curr)
}
// 普通模式
func normal(blackKey string, limitKey string, item string, time int64) (res bool) {
if blackSeconds > 0 {
timeout, _ := gredis.RawCommand("HGET", blackKey, item)
if timeout != nil {
to, _ := strconv.Atoi(string(timeout.([]uint8)))
if int64(to) > time {
// 未解封
return false
}
// 已解封,移除黑名单
gredis.RawCommand("HDEL", blackKey, item)
}
}
l, _ := gredis.RawCommand("HGET", limitKey, item)
if l != nil {
last, _ := strconv.Atoi(string(l.([]uint8)))
if int64(last) >= maxAttempts {
return false
}
}
num, _ := gredis.RawCommand("HINCRBY", limitKey, item, 1)
if ttl, _ := gredis.TTLKey(limitKey); ttl == int64(-1) {
gredis.Expire(limitKey, int64(delaySeconds))
}
if num.(int64) >= maxAttempts && blackSeconds > 0 {
// 加入黑名单
gredis.RawCommand("HSET", blackKey, item, time+blackSeconds)
// 删除记录
gredis.RawCommand("HDEL", limitKey, item)
}
return true
}
// LUA脚本模式
// 支持redis集群部署
func luaScript(blackKey string, limitKey string, item string, time int64) (res bool) {
script := `
local blackSeconds = tonumber(ARGV[5])
if(blackSeconds > 0)
then
local timeout = redis.call('hget', KEYS[1], ARGV[1])
if(timeout ~= false)
then
if(tonumber(timeout) > tonumber(ARGV[2]))
then
return false
end
redis.call('hdel', KEYS[1], ARGV[1])
end
end
local last = redis.call('hget', KEYS[2], ARGV[1])
if(last ~= false and tonumber(last) >= tonumber(ARGV[3]))
then
return false
end
local num = redis.call('hincrby', KEYS[2], ARGV[1], 1)
local ttl = redis.call('ttl', KEYS[2])
if(ttl == -1)
then
redis.call('expire', KEYS[2], ARGV[4])
end
if(tonumber(num) >= tonumber(ARGV[3]) and blackSeconds > 0)
then
redis.call('hset', KEYS[1], ARGV[1], ARGV[2] + ARGV[5])
redis.call('hdel', KEYS[2], ARGV[1])
end
return true
`
result, err := gredis.RawCommand("EVAL", script, 2, blackKey, limitKey, item, time, maxAttempts, delaySeconds, blackSeconds)
if err != nil {
return false
}
if result == int64(1) {
return true
} else {
return false
}
}