Redis入门:go-redis基本操作与连接池
Redis入门:go-redis基本操作与连接池
摘要: 本篇讲解Go操作Redis的核心库go-redis v9,包括客户端连接配置、String/Hash/List/Set/ZSet五种数据类型的基本操作、连接池参数调优(PoolSize、MinIdleConns、PoolTimeout),分享连接池耗尽导致请求超时的踩坑经历,对比go-redis与redigo的优劣。
开篇故事
上周五晚上,我们一个营销活动的接口开始报超时。日志里全是redis: connection pool timeout,QPS一上来Redis就扛不住。我翻了半天代码,连接池配置是默认值,PoolSize才是10*runtime.GOMAXPROCS,高并发场景下根本不够用。
我直接把PoolSize调到200,MinIdleConns设成50,重新部署后超时消失。go-redis用起来简单,但连接池参数不调好,生产环境迟早出事。这篇我从安装配置讲起,把go-redis v9的基本操作和连接池调优一次性讲透。
一、go-redis安装与连接配置
go-redis是Redis的Go客户端,v9版本做了大量重构,API和v8有区别。
packagemainimport("context""fmt""time""github.com/redis/go-redis/v9")// 创建Redis客户端,配置连接池参数funcnewRedisClient()*redis.Client{rdb:=redis.NewClient(&redis.Options{Addr:"localhost:6379",// Redis地址Password:"",// 密码,没设就留空DB:0,// 使用的数据库编号PoolSize:200,// 连接池大小,默认10*GOMAXPROCSMinIdleConns:20,// 最小空闲连接,保持预热PoolTimeout:5*time.Second,// 从池中获取连接的超时IdleTimeout:5*time.Minute,// 空闲连接超时DialTimeout:5*time.Second,// 建立连接超时ReadTimeout:3*time.Second,// 读操作超时MaxRetries:3,// 命令失败重试次数})returnrdb}funcmain(){rdb:=newRedisClient()deferrdb.Close()// Ping测试连接ctx:=context.Background()pong,err:=rdb.Ping(ctx).Result()iferr!=nil{fmt.Println("连接失败:",err)return}fmt.Println("连接成功:",pong)// 输出PONG}二、五种数据类型基本操作
String与Hash
String存缓存值和计数器,Hash适合存对象。
// String类型操作funcstringOps(ctx context.Context,rdb*redis.Client){// 设置键值,带过期时间rdb.Set(ctx,"name","张三",10*time.Minute)// 获取值,区分键不存在和查询出错val,err:=rdb.Get(ctx,"name").Result()iferr==redis.Nil{fmt.Println("键不存在")}elseiferr!=nil{fmt.Println("查询出错:",err)}else{fmt.Println("name =",val)}// 自增操作,适合做计数器rdb.Set(ctx,"counter","0",0)rdb.Incr(ctx,"counter")// +1rdb.IncrBy(ctx,"counter",5)// +5rdb.Decr(ctx,"counter")// -1// SetNX:不存在才设置,常用于分布式锁ok,_:=rdb.SetNX(ctx,"lock:order:1","holder",10*time.Second).Result()fmt.Println("获取锁:",ok)}// Hash类型操作funchashOps(ctx context.Context,rdb*redis.Client){// 一个key下存多个字段rdb.HSet(ctx,"user:1","name","李四","age",28,"email","lisi@example.com")// 获取单个字段name,_:=rdb.HGet(ctx,"user:1","name").Result()fmt.Println("name =",name)// 获取所有字段fields,_:=rdb.HGetAll(ctx,"user:1").Result()fork,v:=rangefields{fmt.Printf("%s = %s\n",k,v)}rdb.HIncrBy(ctx,"user:1","age",1)// 字段自增rdb.HDel(ctx,"user:1","email")// 删除字段}List、Set与ZSet
List做队列,Set做标签去重,ZSet做排行榜。
// List类型:双向链表,适合消息队列funclistOps(ctx context.Context,rdb*redis.Client){rdb.LPush(ctx,"messages","msg1","msg2","msg3")// 左推入msg,_:=rdb.RPop(ctx,"messages").Result()// 右弹出,先进先出fmt.Println("弹出:",msg)// 获取范围元素,0到-1表示全部vals,_:=rdb.LRange(ctx,"messages",0,-1).Result()fmt.Println("剩余消息:",vals)// 阻塞式弹出,适合消费队列result,_:=rdb.BRPop(ctx,5*time.Second,"messages").Result()iflen(result)>0{fmt.Println("阻塞弹出:",result[1])}}// Set类型:无序集合,自动去重funcsetOps(ctx context.Context,rdb*redis.Client){rdb.SAdd(ctx,"tags:1","go","redis","backend")rdb.SAdd(ctx,"tags:2","go","database","backend")// 交集:共同标签inter,_:=rdb.SInter(ctx,"tags:1","tags:2").Result()fmt.Println("共同标签:",inter)// [go backend]// 差集:tags:1有但tags:2没有的diff,_:=rdb.SDiff(ctx,"tags:1","tags:2").Result()fmt.Println("差集:",diff)// [redis]}// ZSet类型:有序集合,带分数,适合排行榜funczsetOps(ctx context.Context,rdb*redis.Client){rdb.ZAdd(ctx,"rank:score",redis.Z{Score:100,Member:"p1"},redis.Z{Score:85,Member:"p2"},redis.Z{Score:92,Member:"p3"})// 按分数从高到低取前10名rank,_:=rdb.ZRevRangeWithScores(ctx,"rank:score",0,9).Result()fori,z:=rangerank{fmt.Printf("第%d名: %s 分数:%.0f\n",i+1,z.Member,z.Score)}// 增加分数rdb.ZIncrBy(ctx,"rank:score",10,"p2")}三、连接池参数调优
连接池是go-redis性能的关键,参数配错要么连接不够用要么资源浪费。
packagemainimport("context""fmt""sync""time""github.com/redis/go-redis/v9")funcmain(){rdb:=redis.NewClient(&redis.Options{Addr:"localhost:6379",PoolSize:200,// 连接池上限,默认10*GOMAXPROCSMinIdleConns:20,// 预热连接数,建议PoolSize的10%-25%PoolTimeout:4*time.Second,// 池满时等待时间,默认ReadTimeout+1sIdleTimeout:5*time.Minute,// 空闲连接存活时间MaxRetries:3,// 网络错误自动重试次数})deferrdb.Close()ctx:=context.Background()// 并发测试连接池varwg sync.WaitGroupfori:=0;i<100;i++{wg.Add(1)gofunc(nint){deferwg.Done()rdb.Set(ctx,fmt.Sprintf("key:%d",n),"value",10*time.Second)}(i)}wg.Wait()// 查看连接池状态stats:=rdb.PoolStats()fmt.Printf("总连接: %d, 空闲: %d, 等待次数: %d\n",stats.TotalConns,stats.IdleConns,stats.Waits)}调优看三个指标。stats.Waits多说明PoolSize太小。stats.IdleConns常年接近PoolSize说明配多了。stats.WaitDuration长说明要么加PoolSize,要么查Redis本身是否慢。
四、独家踩坑:连接池耗尽导致超时
上线后发现高峰期接口偶尔超时,报错redis: connection pool exhausted。第一反应是Redis扛不住了,去看监控CPU才20%。然后看应用日志,发现超时集中在一个批量查询接口,循环里每次调一次Redis,中间还套了time.Sleep(2 * time.Second)。
// 问题代码:循环单条查询,连接池被打满funcbadExample(ctx context.Context,rdb*redis.Client,ids[]int64){for_,id:=rangeids{// 每次Get从池中借一个连接val,_:=rdb.Get(ctx,fmt.Sprintf("user:%d",id)).Result()time.Sleep(2*time.Second)// 耗时操作期间连接被占用_=val}}真正的问题在PoolSize默认值太小。线上GOMAXPROCS是8,默认PoolSize才80,高并发下很快借光。修复分两步,PoolSize调到200,MinIdleConns设50。把循环查询改成Pipeline批量查。
// 修复后:用Pipeline批量查询,只占一个连接funcfixedExample(ctx context.Context,rdb*redis.Client,ids[]int64)map[int64]string{pipe:=rdb.Pipeline()cmds:=make(map[int64]*redis.StringCmd,len(ids))for_,id:=rangeids{cmds[id]=pipe.Get(ctx,fmt.Sprintf("user:%d",id))// 注册命令}pipe.Exec(ctx)// 一次性执行result:=make(map[int64]string,len(ids))forid,cmd:=rangecmds{ifval,err:=cmd.Result();err==nil{result[id]=val}}returnresult}调完后连接池等待次数从每天几万次降到几十次。经验就是连接池参数一定要根据实际QPS调,别用默认值。
五、对比分析
| 特性 | go-redis v9 | redigo |
|---|---|---|
| API风格 | 链式调用,类型安全 | 手动写命令字符串 |
| 连接池 | 内置,参数丰富 | 需手动配置Pool结构 |
| 集群支持 | 原生Cluster和Sentinel | 需第三方或自己实现 |
| Pipeline | 内置Pipeline和TxPipeline | 需手动管理conn.Send |
| 自动重试 | 内置MaxRetries | 需自己实现 |
| Context | 原生支持 | 后期版本才加 |
| 社区维护 | 活跃,官方推荐 | 维护趋缓 |
新项目建议直接选go-redis v9,API更现代,类型安全避免手写命令出错,集群和哨兵开箱即用。redigo轻量但连接管理和错误处理要写更多代码。
总结与预告
go-redis v9上手简单,五种数据类型的API设计清晰。连接池是生产环境关键,PoolSize和MinIdleConns一定要根据QPS调优。Pipeline能批量操作就别循环单条查,省连接省网络。
下一篇讲Redis进阶,深入Pipeline原理、事务的WATCH机制和发布订阅模式的实现。