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
	}
}
相关推荐
林的快手23 分钟前
209.长度最小的子数组
java·数据结构·数据库·python·算法·leetcode
weisian1511 小时前
Redis篇--常见问题篇8--缓存一致性3(注解式缓存Spring Cache)
redis·spring·缓存
HEU_firejef1 小时前
Redis——缓存预热+缓存雪崩+缓存击穿+缓存穿透
数据库·redis·缓存
KELLENSHAW2 小时前
MySQL45讲 第三十七讲 什么时候会使用内部临时表?——阅读总结
数据库·mysql
SelectDB2 小时前
飞轮科技荣获中国电信星海大数据最佳合作伙伴奖!
大数据·数据库·数据分析
weisian1512 小时前
Redis篇--常见问题篇7--缓存一致性2(分布式事务框架Seata)
redis·分布式·缓存
白云coy2 小时前
Redis 安装部署[主从、哨兵、集群](linux版)
linux·redis
Logintern092 小时前
Linux如何设置redis可以外网访问—执行使用指定配置文件启动redis
linux·运维·redis
小刘鸭!3 小时前
Hbase的特点、特性
大数据·数据库·hbase
凡人的AI工具箱3 小时前
每天40分玩转Django:Django表单集
开发语言·数据库·后端·python·缓存·django