写在前面
用go开发web项目时,经常会用到 redis ,推荐使用redigo包,目前有4800多个star,基本不会有太大的坑。
聊聊redis的i/o并发
redis是单线程的,但又是处实现并发的呢?参照地址:
redis是使用事件循环来实现并发的,事件是原子性的,还没有额外的锁开销。设计非常精妙。
我们在读写redis时,大多都是网络传输层的开销,redis计算是非常快的。所以我们尽量用多个连接去读写redis,相当于并发做网络传递,排队等着redis计算,不能让redis计算引擎闲下来。
使用连接池
在使用redigo的时候,强烈建议使用连接池,不然每次都得 tcp 建链,不嫌麻烦吗。而使用连接池的话,只管去get其它都给交类库去处理。示例代码如下:
var redisClient *redis.Pool
func init() {
maxIdle := MaxIdle
if v, ok := conf["MaxIdle"]; ok {
maxIdle = int(v.(int64))
}
maxActive := MaxActive
if v, ok := conf["MaxActive"]; ok {
maxActive = int(v.(int64))
}
// 建立连接池
redisClient = &redis.Pool{
MaxIdle: maxIdle,
MaxActive: maxActive,
IdleTimeout: MaxIdleTimeout * time.Second,
Wait: true,
Dial: func() (redis.Conn, error) {
con, err := redis.Dial("tcp", conf["Host"].(string),
redis.DialPassword(conf["Password"].(string)),
redis.DialDatabase(int(conf["Db"].(int64))),
redis.DialConnectTimeout(timeout*time.Second),
redis.DialReadTimeout(timeout*time.Second),
redis.DialWriteTimeout(timeout*time.Second))
if err != nil {
return nil, err
}
return con, nil
},
}
}
// 从池里获取连接
rc := RedisClient.Get()
// 用完后将连接放回连接池
defer rc.Close()
// 错误判断
if conn.Err() != nil {
//TODO
}
其它有几个需要注意的地方:
- MaxActive 最大连接数,即最多的tcp连接数,一般建议往大的配置,但不要超过操作系统 文件句柄 个数( centos 下可以ulimit -n查看)。
- MaxIdle 最大空闲连接数,即会有这么多个连接提前等待着,但过了超时时间也会关闭。
- IdleTimeout 空闲连接超时时间,但应该设置比redis服务器超时时间短。否则服务端超时了,客户端保持着连接也没用。
- Wait 这是个很有用的配置。好多东抄抄本抄抄的文章都没有提。如果超过最大连接,是报错,还是等待。
常见报错
连接池消耗殆尽
redigo: connection pool exhausted
redigo常常会有这样的报错。我们来从redigo源码上来分析这个问题。
// Handle limit for p.Wait == false. if !p.Wait && p.MaxActive > 0 && p.active >= p.MaxActive { p.mu.Unlock() return nil, ErrPoolExhausted }
当Wait==false,并且当前有效连接>=最大连接数里就报这个错了。要解决这个问题的话,可以修改这个参数:
- MaxActive 可以把MaxActive调大(一般设置为500,1000问题都不大。)但如果redis服务器负载已经很高了(可以看redis-server CPU占用),去调大MaxActive就没多大意义。还是需要根据实际情况来权衡。
- Wait 可以把Wait设置为true。wait的话必然会加大响应,如果对响应时间要求较高的话,还得从别的途径来解决。
- 还可以加从库通过 读写分离 来解决。特别是PHP,没有常驻的redis连接池,建议增加多个从库。