6. Gin集成redis

文章目录

代码地址: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启动rediswindowsdocker的相关操作可参考: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两个语句的命令之间没有其他客户端正在执行命令。

在这种场景我们需要使用TxPipelineTxPipeline总体上类似于上面的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
	}
}
相关推荐
Tapdata6 分钟前
实时物化视图的新路径:从传统 Join 到跨源实时查询
数据库
optimistic_chen9 分钟前
【Java EE进阶 --- SpringBoot】Mybatis - plus 操作数据库
数据库·spring boot·笔记·java-ee·mybatis·mybatis-plus
FJW02081439 分钟前
关系型数据库大王Mysql——DDL语句操作示例
数据库·mysql
言之。40 分钟前
Chroma 开源的 AI 应用搜索与检索数据库(即向量数据库)
数据库·人工智能·开源
来旺1 小时前
互联网大厂Java面试全解析及三轮问答专项
java·数据库·spring boot·安全·缓存·微服务·面试
摇滚侠1 小时前
Spring Boot 3零基础教程,yml文件中配置和类的属性绑定,笔记15
spring boot·redis·笔记
cr7xin1 小时前
基于Session和Redis实现短信验证码登录
数据库·redis·缓存
乌暮2 小时前
数据库--视图、索引
数据库
Web3&Basketball2 小时前
达梦数据库性能调优总结
数据库·oracle
-Xie-2 小时前
Mysql杂志(三十三)——锁
数据库·mysql