当前位置: 首页 > 科技观察

Go语言如何使用Redis连接池

时间:2023-03-12 06:03:39 科技观察

一、关于连接池数据库服务器只有有限的资源,如果不充分利用这些资源,可以通过使用更多的连接来提高吞吐量。一旦所有资源都在使用中,您就无法通过添加更多连接来增加吞吐量。事实上,当连接负载很高时,吞吐量开始下降。通常可以通过限制数据库连接数以匹配可用资源来改善延迟和吞吐量。如果不使用连接池,那么每次传输数据时,我们都需要创建连接,收发数据,关闭连接。在并发不高的场景下,基本不会出问题。一旦并发增加,一般会遇到以下常见问题:性能一般不会提升CPU大量资源被系统消耗一旦网络动摇,就会出现大量TIME_WAIT,不得不重启定期维修或重新启动机器。服务器不稳定,QPS时高时低。为了解决这些问题,我们需要使用连接池。连接池的思想很简单。初始化的时候,创建一定数量的连接,先把所有的长连接保存起来,然后,谁需要用,就从这里取,用完马上放回去。如果请求数超过连接池的容量,就会被排队,退化为短连接或者直接丢弃。2.使用连接池遇到的坑最近在一个项目中,需要实现一个简单的WebServer,提供RedisHTTP接口,并以JSON格式返回结果。考虑在Go中实现它。首先看一下Redis官方推荐的GoRedis驱动。有两个官方Star项目:Radix.v2和Redigo。经过简单的比较,我选择了Radix.v2,它更轻量,也更优雅。Radix.v2包根据功能分为子包。每个子包都在一个独立的子目录下,结构非常清晰。我项目中会用到的子包是redis和pool。由于想把这种fork出来的过程简单一些,做一些简单的事情,所以在深入研究Radix.v2pool的实现之前,我选择了自己实现一个Redispool。(这里就不贴代码了。后来发现自己实现的Redispool和Radix.v2实现的Redispool原理是一样的。都是基于渠道实现的,遇到的问题也是一样的。)但是,在测试过程中,发现了一个奇怪的问题。请求过程中经常报EOF错误。而且,它是概率性发生的,一次会出问题,过段时间就好了。通过反复测试,发现bug是有规律的。当程序空闲一段时间,然后连续请求时,会出现3次失败,然后后面的请求都能成功,我的连接池大小设置为3。进一步分析,程序空闲for后300秒,请求将失败。发现我的Redis服务器配置了timeout300,至此,问题就清楚了。连接超时,Redis服务器主动断开连接。客户端将从超时的连接请求中收到EOF错误。然后查看了Radix.v2的pool包的源码,发现这个库本身并没有检测坏连接,并用新的连接机制取而代之。也就是说,每次从连接池中获取连接,都可能是坏连接。所以,我当时的临时解决办法是通过添加失败后自动重试来解决。然而,有了这样的解决方案,连接池的作用似乎就没有了。技术债务可以提前偿还或提前偿还。3、使用连接池的正确姿势考虑到我们的ngx_lua项目也使用了大量的redis连接池,为什么没有遇到这个问题。只看源代码。抽象分离后,在ngx_lua中使用redis连接池的代码大致是这样的:server{location/pool{content_by_lua_block{localredis=require"resty.redis"localred=redis:new()localok,err=red:connect("127.0.0.1",6379)ifnotokthenngx.say("failedtoconnect:",err)returnendok,err=red:set("hello","world")ifnotokthenreturnendred:set_keepalive(10000,100)}}}找到了set_keepalive方法,查了下官方文档,方法原型是syntax:ok,err=red:set_keepalive(max_idle_timeout,pool_size)看来参数max_idle_timeout是我们缺的,再进一步追源码看看如何保证里面的连接有效。function_M.set_keepalive(self,...)localsock=self.sockifnotsockthenreturnnil,"notinitialized"endifself.subscribedthenreturnil,"subscribedstate"endreturnsock:setkeepalive(...)end至此就清楚了使用tcp的keepalive心跳机制.所以,通过和Radix.v2的作者的一些讨论,我选择在redis层使用心跳机制来解决这个问题。4.最佳解决方案创建连接池后,启动一个goroutine,每隔idleTime向Redis服务器发送一个PING。其中idleTime比Redis服务器的超时配置略小。连接池初始化代码如下:p,err:=pool.New("tcp",u.Host,concurrency)errHndlr(err)gofunc(){for{p.Cmd("PING")time.Sleep(idelTime*time.Second)}}()使用redis传输数据部分代码如下:funcredisDo(p*pool.Pool,cmdstring,args...interface{})(reply*redis.Resp,errerror){reply=p.Cmd(cmd,args...)iferr=reply.Err;err!=nil{iferr!=io.EOF{Fatal.Println("redis",cmd,args,"erris",err)}}返回其中,Radix.v2连接池在连接池中获取和放回连接。代码如下://Cmdautomaticallygetsoneclientfromthepool,executesthegivencommand//(returningitsresult),andputstheclientbackinthepoolfunc(p*Pool)Cmd(cmdstring,args...interface{})*redis.Resp{c,err:=p.Get()iferr!=nil{returnredis.NewResp(err)}deferp.Put(c)returnc.Cmd(cmd,args...)}这样,我们有了keepalive的机制,不会出现超时连接,并且连接从redis连接池中取出的都是可用的连接。看似简单的代码完美解决了连接池中连接超时的问题。同时,即使Redis服务器重启,也可以自动重连。