Redis连接池Wireshark抓包分析


Redis连接池Wireshark抓包分析

1.文章背景

​ 因线上redis连接池出现i/o timeout,遂想通过抓包分析redis连接池的各配置参数。

​ 连接相对于其他对象,创建成本较高,资源也有限。如果没有连接池,在高并发场景下,连接关闭又新建,很快就会因为过多的TIME_WAIT(连接主动关闭方)导致无法创建更多连接了,程序被压死。

​ 本文主要确认:

​ 1.redis连接池是否复用

​ 2.redis连接池超时分析

​ 3.redis连接池直接替换(加锁)

​ 4.redis连接池直接替换(不加锁公司现有模式)

​ 5.redis连接池客户端空闲超时

​ 6.redis服务器端空闲超时

​ 7.redis连接池推荐使用方式

2.实验说明:

​ redis服务器:

​ 超时时间: 60s

​ 工具版本:

golang版本: 1.17
go-redis版本: github.com/go-redis/redis v6.15.9+incompatible
wireshark版本: Version 3.4.6 (v3.4.6-0-g6357ac1405b8)

​ redis客户端参数

type Options struct {
    // The network type, either tcp or unix.
    // Default is tcp.
    Network string
    // host:port address.
    Addr string
    // Dialer creates new network connection and has priority over
    // Network and Addr options.
    Dialer func() (net.Conn, error)
    // Hook that is called when new connection is established.
    OnConnect func(*Conn) error
    // Optional password. Must match the password specified in the
    // requirepass server configuration option.
    Password string
    // Database to be selected after connecting to the server.
    DB int
    // 命令执行失败时,最多重试多少次,默认为0即不重试
    // Maximum number of retries before giving up.
    // Default is to not retry failed commands.
    MaxRetries int
    //每次计算重试间隔时间的下限,默认8毫秒,-1表示取消间隔
    // Minimum backoff between each retry.
    // Default is 8 milliseconds; -1 disables backoff.
    MinRetryBackoff time.Duration
    //每次计算重试间隔时间的上限,默认512毫秒,-1表示取消间隔
    // Maximum backoff between each retry.
    // Default is 512 milliseconds; -1 disables backoff.
    MaxRetryBackoff time.Duration

    //连接建立超时时间,默认5秒。
    // Dial timeout for establishing new connections.
    // Default is 5 seconds.
    DialTimeout time.Duration
    //读超时,默认3秒, -1表示取消读超时
    // Timeout for socket reads. If reached, commands will fail
    // with a timeout instead of blocking. Use value -1 for no timeout and 0 for default.
    // Default is 3 seconds.
    ReadTimeout time.Duration
    //写超时,默认等于读超时
    // Timeout for socket writes. If reached, commands will fail
    // with a timeout instead of blocking.
    // Default is ReadTimeout.
    WriteTimeout time.Duration

    //默认连接池最大socket连接数,默认为4倍CPU数, 4 * runtime.NumCPU
    // Maximum number of socket connections.
    // Default is 10 connections per every CPU as reported by runtime.NumCPU.
    PoolSize int
    // Minimum number of idle connections which is useful when establishing
    // new connection is slow.
    //在启动阶段创建指定数量的Idle连接,并长期维持idle状态的连接数不少于指定数量;
    MinIdleConns int
    //连接存活时长,从创建开始计时,超过指定时长则关闭连接,默认为0,即不关闭存活时长较长的连接
    // Connection age at which client retires (closes) the connection.
    // Default is to not close aged connections.
    MaxConnAge time.Duration
    //当所有连接都处在繁忙状态时,客户端等待可用连接的最大等待时长,默认为读超时+1秒
    // Amount of time client waits for connection if all connections
    // are busy before returning an error.
    // Default is ReadTimeout + 1 second.
    PoolTimeout time.Duration
    //闲置超时,默认5分钟,-1表示取消闲置超时检查
    // Amount of time after which client closes idle connections.
    // Should be less than server's timeout.
    // Default is 5 minutes. -1 disables idle timeout check.
    IdleTimeout time.Duration
    //闲置连接检查的周期,默认为1分钟,-1表示不做周期性检查,只在客户端获取连接时对闲置连接进行处理。
    // Frequency of idle checks made by idle connections reaper.
    // Default is 1 minute. -1 disables idle connections reaper,
    // but idle connections are still discarded by the client
    // if IdleTimeout is set.
    IdleCheckFrequency time.Duration

    // Enables read only queries on slave nodes.
    readOnly bool
    // TLS Config to use. When set TLS will be negotiated.
    TLSConfig *tls.Config
}

3.wireshark抓包设置

设置只抓redis包

host  10.1.1.245 and port 6979

image.png

4.抓包分析-连接池复用

4.1 只设置一个连接池并运行
package main

import (
    "fmt"
    "github.com/go-redis/redis"
    "sync"
    "time"
)

var db = 0
var addr = "10.1.1.245:6979"
var pwd = "xxxxxxxxxxxxxx"
var wait sync.WaitGroup
var redisHandle *redis.Client

var scriptStr = `
    local a = 0
    while (a < 100000000 ) do
        a = a + 1
    end
    return KEYS[1]
    `
//初始化一个连接池
func init() {
    redisHandle = GetRedis()
}

func GetRedis() *redis.Client {
    //如果存在全局连接池就进行复用
    //如果全局连接池,复用超时,则新创建连接池进行覆盖
    //比如只存在一个连接池,而且被长期占用
    if redisHandle != nil {
        _, err := redisHandle.Ping().Result()
        //如果没有报错,直接复用
        if err == nil {
            return redisHandle
        }
        fmt.Println("==========重新创建全局连接池===============", err)
    }
    //创建连接时计时开始,用于计算超时时间对比
    t := time.Now()
    redisHandle = redis.NewClient(&redis.Options{
        Addr:         addr,
        Password:     pwd,
        DB:           db,
        DialTimeout:  3 * time.Second,  //设置3秒超时
        PoolSize:     1,                //连接池最大为1
        MinIdleConns: 1,                //最小空闲连接池为1
        IdleTimeout:  20 * time.Second, //客户端空闲超时为20s
    })

    //重新测试新的连接池是否可用
    //如果不可用则直接panic
    _, err := redisHandle.Ping().Result()
    stats := redisHandle.PoolStats()
    if err != nil {
        //获取当前连接池状态
        fmt.Println("redis connections:  TotalConns= ", stats.TotalConns, "  Timeouts= ", stats.Timeouts, " IdleConns= ", stats.IdleConns)
        fmt.Println("不能ping通", time.Now(), time.Since(t), err.Error())
        panic(err)
    }
    return redisHandle
}
func main() {
    for i := 0; i < 10; i++ {
        getRedis := GetRedis()
        getRedis.Set("name", "hello01", -1)
        //time.Sleep(3 * time.Second)
        s := getRedis.Get("name").String()
        fmt.Println(" 获取redis的key: ", s)
    }
}
4.2 查看进程运行状态

image.png

4.3 查看wireshark抓包情况

详见包redisonepool.pcapng

image.png

4.4 抓包分析

总共会建立两个连接:

1.客户端初始的时候会建立两个连接 (端口51588)和(端口51587)

(其中一个包是ping生成的,一个是建立连接使用的)

2.因poolsize为1,所以会关闭一个连接。

3.后续的连接会复用一个连接(端口515887)

5.抓包分析-连接池超时

5.1 只设置一个连接池并运行
package main

import (
    "fmt"
    "github.com/go-redis/redis"
    "github.com/spf13/cast"
    "math/rand"
    "sync"
    "time"
)

var db = 0
var addr = "10.1.1.245:6979"
var pwd = "xxxxxxxxxxxxxx"
var wait sync.WaitGroup
var redisHandle *redis.Client
var scriptStr = `
    local a = 0
    while (a < 100000000 ) do
        a = a + 1
    end
    return KEYS[1]
    `
//初始化一个连接池
func init() {
    redisHandle = GetRedis()
}

func GetRedis() *redis.Client {
    //如果存在全局连接池就进行复用
    //如果全局连接池,复用超时,则新创建连接池进行覆盖
    //比如只存在一个连接池,而且被长期占用
    if redisHandle != nil {
        _, err := redisHandle.Ping().Result()
        //如果没有报错,直接复用
        if err == nil {
            return redisHandle
        }
        fmt.Println("==========重新创建全局连接池===============", err)
    }
    //创建连接时计时开始,用于计算超时时间对比
    t := time.Now()
    redisHandle = redis.NewClient(&redis.Options{
        Addr:         addr,
        Password:     pwd,
        DB:           db,
        DialTimeout:  3 * time.Second,  //设置3秒超时
        PoolSize:     1,                //连接池最大为1
        MinIdleConns: 1,                //最小空闲连接池为1
        IdleTimeout:  20 * time.Second, //客户端空闲超时为20s
    })

    //重新测试新的连接池是否可用
    //如果不可用则直接panic
    _, err := redisHandle.Ping().Result()
    stats := redisHandle.PoolStats()
    if err != nil {
        //获取当前连接池状态
        fmt.Println("redis connections:  TotalConns= ", stats.TotalConns, "  Timeouts= ", stats.Timeouts, " IdleConns= ", stats.IdleConns)
        fmt.Println("不能ping通", time.Now(), time.Since(t), err.Error())
        panic(err)
    }
    return redisHandle
}
func main() {
    //设置随机种子
    rand.Seed(time.Now().Unix())
    //设置超时
    //查看连接数
    for i := 0; i < 10; i++ {
        wait.Add(1)
        vt := i
        go func() {
            defer wait.Done()
            //随机延迟,需要时为了让go并发争用
            s :=rand.Intn(15)
            time.Sleep(time.Second*time.Duration(s))
            //getRedis := GetRedis()
            strings := []string{cast.ToString(vt)}
            //设置lua脚本主要是为了使连接执行很长时间,占用连接,从而显示连接池超时
            t2 := time.Now()
            eval := redisHandle.Eval(scriptStr, strings, len(strings))
            result, err := eval.Result()
            fmt.Printf("%v, 结果:%v,随机时间是:%v,lua执行耗时:%v ,err: %v\n",time.Now(),result, s,time.Since(t2), err)
        }()
    }
    wait.Wait()
}
5.2 查看进程运行状态

gOicP.png

5.3 查看wireshark抓包情况

详见包redispooltimeout.pcapng

image-20220919114658509

5.4 抓包分析

总共会建立两个连接:

1.客户端初始的时候会建立两个连接 (端口61445和61446)

2.其中一个用于连接池(端口61445),因为poolsize为1,另一个连接销毁(端口61446)。

3.后续的连接会复用这个连接池(端口61445)

4.因为连接没有设置重试,所以直接超时就报错,此处为连接池错误。

此处引申一下:

此处使用lua脚本是为了让redis处理时间占用很长时间,此时连接池维持的一个连接被占用,而且由于没有设置超时重试,所以会直接报错

6.抓包分析-连接池覆盖(此处加锁)

6.1 只设置一个连接池并运行
package main

import (
    "fmt"
    "github.com/go-redis/redis"
    "github.com/spf13/cast"
    "math/rand"
    "sync"
    "time"
)

var db = 0
var addr = "10.1.1.245:6979"
var pwd = "xxxxxxxxxxxxxx"
var wait sync.WaitGroup
var  lock sync.Mutex
var redisHandle *redis.Client
var scriptStr = `
    local a = 0
    while (a < 100000000 ) do
        a = a + 1
    end
    return KEYS[1]
    `
//初始化一个连接池
func init() {
    redisHandle = GetRedis()
}

func GetRedis() *redis.Client {
    //如果存在全局连接池就进行复用
    //如果全局连接池,复用超时,则新创建连接池进行覆盖
    //比如只存在一个连接池,而且被长期占用
    //保证同一时刻只能拿到一个连接,如果连接池慢,则重新创建,如果连接池没有慢则直接返回
    lock.Lock()
    defer  lock.Unlock()
    if redisHandle != nil {
        _, err := redisHandle.Ping().Result()
        //如果没有报错,直接复用
        if err == nil {
            return redisHandle
        }
        fmt.Println("==========重新创建全局连接池===============", err)
    }
    //创建连接时计时开始,用于计算超时时间对比
    t := time.Now()
    redisHandle = redis.NewClient(&redis.Options{
        Addr:         addr,
        Password:     pwd,
        DB:           db,
        DialTimeout:  3 * time.Second,  //设置3秒超时
        PoolSize:     1,                //连接池最大为1
        MinIdleConns: 1,                //最小空闲连接池为1
        IdleTimeout:  20 * time.Second, //客户端空闲超时为20s
        //ReadTimeout:
        //PoolTimeout:  7 * time.Second, //获取连接池超时时间
    })

    //重新测试新的连接池是否可用
    //如果不可用则直接panic
    _, err := redisHandle.Ping().Result()
    stats := redisHandle.PoolStats()
    if err != nil {
        //获取当前连接池状态
        fmt.Println("redis connections:  TotalConns= ", stats.TotalConns, "  Timeouts= ", stats.Timeouts, " IdleConns= ", stats.IdleConns)
        fmt.Println("不能ping通", time.Now(), time.Since(t), err.Error())
        panic(err)
    }
    return redisHandle
}
func main() {
    //设置随机种子
    rand.Seed(time.Now().Unix())
    //设置超时
    //查看连接数
    for i := 0; i < 15; i++ {
        wait.Add(1)
        vt := i
        //go func() {
        go func() {
            defer wait.Done()
            //随机延迟,需要时为了保持go
            //s :=rand.Intn(5)
            s :=rand.Intn(3)
            time.Sleep(time.Second*time.Duration(s))
            getRedis := GetRedis()
            strings := []string{cast.ToString(vt)}
            //设置lua脚本主要是为了使连接执行很长时间,占用连接
            st := time.Now()
            eval := getRedis.Eval(scriptStr, strings, len(strings))
            result, err := eval.Result()
            fmt.Printf(" 结果:%v,耗时:%v ,err: %v\n", result,time.Since(st), err)
        }()
    }
    wait.Wait()
}
6.2 查看进程运行状态

image.png

6.3 查看wireshark抓包情况

详见包redispoolreplace.pcapng

image.png

6.4 抓包分析

总共会建立两次连接池,四次连接:

1.客户端初始的时候会建立两个连接 (端口52541和52542)

2.其中一个用于连接池(端口52542),因为poolsize为1,另一个连接销毁(端口52541)。

3.后续的连接会复用这个连接池(端口52542)

4.因为redis计算时间较长,导致只有一个连接池的连接迟迟不能释放,所以下一个连接检测到连接池报错,会重新建立一个新的连接池。

5.客户端初始的时候会重新建立两个连接 (端口52547和52548)

6.其中一个用于连接池(端口52547),因为poolsize为1,另一个连接销毁(端口52548)。

7.后续的连接会复用这个连接池(端口52547)

8.请注意此时。旧的连接没有旧的连接池没有被杀死,可以正常处理数据,等到程序关闭后销毁(内核管理)。

此处引申一下:

此处使用lua脚本是为了让redis处理时间占用很长时间,此时连接池维持的一个连接被占用,其他进程getredis的时候,会检测到获取线程池错误,而重新获取一个连接。

7.抓包分析-连接池覆盖(没有加锁-公司现在模式)

7.1 只设置一个连接池并运行
package main

import (
    "fmt"
    "github.com/go-redis/redis"
    "github.com/spf13/cast"
    "math/rand"
    "sync"
    "time"
)

var db = 0
var addr = "10.1.1.245:6979"
var pwd = "xxxxxxxxxxxxxx"
var wait sync.WaitGroup
var  lock sync.Mutex
var redisHandle *redis.Client
var scriptStr = `
    local a = 0
    while (a < 100000000 ) do
        a = a + 1
    end
    return KEYS[1]
    `
//初始化一个连接池
func init() {
    fmt.Println(time.Now(),"开始时间")
    redisHandle = GetRedis()

}

func GetRedis() *redis.Client {
    //如果存在全局连接池就进行复用
    //如果全局连接池,复用超时,则新创建连接池进行覆盖
    //比如只存在一个连接池,而且被长期占用
    //保证同一时刻只能拿到一个连接,如果连接池慢,则重新创建,如果连接池没有慢则直接返回
    //lock.Lock()
    //defer  lock.Unlock()
    if redisHandle != nil {
        _, err := redisHandle.Ping().Result()
        //如果没有报错,直接复用
        if err == nil {
            return redisHandle
        }
        fmt.Println(time.Now(),"==========重新创建全局连接池===============", err)
    }
    //创建连接时计时开始,用于计算超时时间对比
    t := time.Now()
    redisHandle = redis.NewClient(&redis.Options{
        Addr:         addr,
        Password:     pwd,
        DB:           db,
        DialTimeout:  3 * time.Second,  //设置3秒超时
        PoolSize:     1,                //连接池最大为1
        MinIdleConns: 1,                //最小空闲连接池为1
        IdleTimeout:  20 * time.Second, //客户端空闲超时为20s
        //ReadTimeout:
        //PoolTimeout:  7 * time.Second, //获取连接池超时时间
    })

    //重新测试新的连接池是否可用
    //如果不可用则直接panic
    _, err := redisHandle.Ping().Result()
    if err != nil {
        //获取当前连接池状态
        fmt.Println(time.Now(),"连接池状态错误", time.Since(t), time.Since(t), err.Error())
        panic(err)
    }
    return redisHandle
}
func main() {
    //设置随机种子
    rand.Seed(time.Now().Unix())
    //设置超时
    //查看连接数
    for i := 0; i < 15; i++ {
        wait.Add(1)
        vt := i
        //go func() {
        go func() {
            defer wait.Done()
            //随机延迟,需要时为了保持go
            //获取计时
            t := time.Now()
            s :=rand.Intn(3)
            time.Sleep(time.Second*time.Duration(s))
            redisHandle = GetRedis()
            strings := []string{cast.ToString(vt)}
            //设置lua脚本主要是为了使连接执行很长时间,占用连接池,从而需要重新创建连接池
            st := time.Since(t)
            //设置执行lua开始时间点
            t2 := time.Now()
            eval := redisHandle.Eval(scriptStr, strings, len(strings))
            result, err := eval.Result()
            fmt.Printf("%v, 结果:%v,随机时间是:%v,建立连接到获取结果耗时%v,lua执行耗时:%v ,err: %v\n",time.Now(),result, s,st,time.Since(t2), err)
        }()
    }
    wait.Wait()
}
7.2 查看进程运行状态

image.png

7.3 查看wireshark抓包情况

详见包redispoolreplacewithnolockclient.pcapng,redispoolreplacewithnolockserver.pcapng

image-20220919185225208

7.4 抓包分析

总共会建立7次连接池,十四次连接:

1.客户端初始的时候会建立两个连接

2.其中一个用于连接池,因为poolsize为1,另一个连接销毁(端口52541)。

重点

3.因为getredis没有加锁,等到真正执行命令时,不是原子操作,所以导致真正执行eval时,获取执行状态异常,所以报错了。

什么意思呢,就是因为没有锁redishanle可能是同时获取的,同时PoolTimeout=ReadTimeout+1 等于4s,如果不用随机数(下图),那么最多执行四到五次,而不会重新创建连接池。因为并发状态下,刚开始时没有阻塞的斜程获取的redishandle值都一样,不会报错。

gaCqY.png

4.而如果加入随机数,那么可能有一定概率是能获取到错误值的,这个值可能是redishandle执行时,或者是重新生成redishandle时。

image.png

4.最后我想说,这里触发的panic确实是客户端导致的,和服务端无关,但是我没有判断出世为什么。

此处引申一下:

此处使用lua脚本是为了让redis处理时间占用很长时间,此时连接池维持的一个连接被占用,其他进程getredis的时候,会检测到获取线程池错误,而重新获取一个连接。

8.抓包分析-客户端超时

8.1 只设置一个连接池并运行
package main

import (
    "fmt"
    "github.com/go-redis/redis"
    "math/rand"
    "sync"
    "time"
)

var db = 0
var addr = "10.1.1.245:6979"
var pwd = "xxxxxxxxxxxxxx"
var wait sync.WaitGroup
var  lock sync.Mutex
var redisHandle *redis.Client
var scriptStr = `
    local a = 0
    while (a < 100000000 ) do
        a = a + 1
    end
    return KEYS[1]
    `
//初始化一个连接池
func init() {
    fmt.Println(time.Now(),"开始时间")
    redisHandle = GetRedis()

}

func GetRedis() *redis.Client {
    //如果存在全局连接池就进行复用
    //如果全局连接池,复用超时,则新创建连接池进行覆盖
    //比如只存在一个连接池,而且被长期占用
    //保证同一时刻只能拿到一个连接,如果连接池慢,则重新创建,如果连接池没有慢则直接返回
    //lock.Lock()
    //defer  lock.Unlock()
    if redisHandle != nil {
        _, err := redisHandle.Ping().Result()
        //如果没有报错,直接复用
        if err == nil {
            return redisHandle
        }
        fmt.Println(time.Now(),"==========重新创建全局连接池===============", err)
    }
    //创建连接时计时开始,用于计算超时时间对比
    t := time.Now()
    redisHandle = redis.NewClient(&redis.Options{
        Addr:         addr,
        Password:     pwd,
        DB:           db,
        DialTimeout:  3 * time.Second,  //设置3秒超时
        PoolSize:     1,                //连接池最大为1
        MinIdleConns: 1,                //最小空闲连接池为1
        IdleTimeout:  20 * time.Second, //客户端空闲超时为20s
        //ReadTimeout:
        //PoolTimeout:  7 * time.Second, //获取连接池超时时间
    })

    //重新测试新的连接池是否可用
    //如果不可用则直接panic
    _, err := redisHandle.Ping().Result()
    if err != nil {
        //获取当前连接池状态
        fmt.Println(time.Now(),"连接池状态错误", time.Since(t), time.Since(t), err.Error())
        panic(err)
    }
    return redisHandle
}
func main() {
    //设置随机种子
    rand.Seed(time.Now().Unix())
    //设置超时
    //查看连接数
    for i := 0; i < 2; i++ {
        redisHandle.Set("name", "hello01", -1)
        //time.Sleep(3 * time.Second)
        s := redisHandle.Get("name").String()
        fmt.Println(" 获取redis的key: ", s)
        time.Sleep(25*time.Second)
    }

}
8.2 查看进程运行状态

gMxAL.png

8.3 查看wireshark抓包情况

详见包redisclientout.pcapng

gMzSi.png

8.4 抓包分析

总共会建立四个连接:

1.客户端初始的时候会建立两个连接 (端口51891)和(端口51892)

(其中一个包是ping生成的,一个是建立连接使用的)

2.因poolsize为1,所以会关闭一个连接(端口51892)

3.后续的连接客户端会超时。客户端的闲时检查事件默认时1分钟,所以此处不会自动检查

4.等25秒后,连接建立的时候会重新建立两个连接(这里我感觉是个bug)

5.等到1分钟的空闲连接检查,其中一个连接会被干掉。

6.等70十分钟后客户端主动断开连接。

9.抓包分析-服务端超时

9.1 只设置一个连接池并运行
package main

import (
    "fmt"
    "github.com/go-redis/redis"
    "math/rand"
    "sync"
    "time"
)

var db = 0
var addr = "10.1.1.245:6979"
var pwd = "xxxxxxxxxxxxxx"
var wait sync.WaitGroup
var lock sync.Mutex
var redisHandle *redis.Client
var scriptStr = `
    local a = 0
    while (a < 100000000 ) do
        a = a + 1
    end
    return KEYS[1]
    `

//初始化一个连接池
func init() {
    fmt.Println(time.Now(), "开始时间")
    redisHandle = GetRedis()

}

func GetRedis() *redis.Client {
    //如果存在全局连接池就进行复用
    //如果全局连接池,复用超时,则新创建连接池进行覆盖
    //比如只存在一个连接池,而且被长期占用
    //保证同一时刻只能拿到一个连接,如果连接池慢,则重新创建,如果连接池没有慢则直接返回
    //lock.Lock()
    //defer  lock.Unlock()
    if redisHandle != nil {
        _, err := redisHandle.Ping().Result()
        //如果没有报错,直接复用
        if err == nil {
            return redisHandle
        }
        fmt.Println(time.Now(), "==========重新创建全局连接池===============", err)
    }
    //创建连接时计时开始,用于计算超时时间对比
    t := time.Now()
    redisHandle = redis.NewClient(&redis.Options{
        Addr:         addr,
        Password:     pwd,
        DB:           db,
        DialTimeout:  3 * time.Second,  //设置3秒超时
        PoolSize:     1,                //连接池最大为1
        MinIdleConns: 1,                //最小空闲连接池为1
        IdleTimeout:  20 * time.Second, //客户端空闲超时为20s
        IdleCheckFrequency: 80*time.Second,
        //ReadTimeout:
        //PoolTimeout:  7 * time.Second, //获取连接池超时时间
    })

    //重新测试新的连接池是否可用
    //如果不可用则直接panic
    _, err := redisHandle.Ping().Result()
    if err != nil {
        //获取当前连接池状态
        fmt.Println(time.Now(), "连接池状态错误", time.Since(t), time.Since(t), err.Error())
        panic(err)
    }
    return redisHandle
}
func main() {
    //设置随机种子
    rand.Seed(time.Now().Unix())
    //设置超时
    //查看连接数

    redisHandle.Set("name", "hello01", -1)
    //time.Sleep(3 * time.Second)
    s := redisHandle.Get("name").String()
    fmt.Println(" 获取redis的key: ", s)
    time.Sleep(70 * time.Second)
    redisHandle.Set("name", "hello01", -1)
    //time.Sleep(3 * time.Second)
    s = redisHandle.Get("name").String()
    fmt.Println(" 获取redis的key: ", s)
    time.Sleep(10 * time.Second)
}
9.2 查看进程运行状态

gMK5v.png

9.3 查看wireshark抓包情况

详见包redisclientout.pcapng

gMZKq.png](https://i.imgtg.com/2022/09/19/gMZKq.png)

9.4 抓包分析

前提条件:

​ 已知服务器的超时时间为: 60s IP:10.1.1.245

总共会建立四个连接:

1.客户端初始的时候会建立两个连接 (端口50496)和(端口50495)

(其中一个包是ping生成的,一个是建立连接使用的)

2.因poolsize为1,所以会关闭一个连接(端口50495)。

3.后续的连接客户端会超时。客户端的空闲检查时间默认是1分钟,可能会导致客户端先发生超时重连,所以要设置IdleCheckFrequency > 60s

4.等60秒后,redis服务端先发生断开。

5.重新建立一个两个连接

6.查询完毕后先关闭一个连接。

6.等80十分钟后客户端结束后主动断开连接

10.抓包分析-连接池复用(综合汇总)

10.1 设置连接池并运行
package main

import (
    "fmt"
    "github.com/go-redis/redis"
    "github.com/spf13/cast"
    "math/rand"
    "sync"
    "time"
)

var db = 0
var addr = "10.1.1.245:6979"
var pwd = "xxxxxxxxxxxxxx"
var wait sync.WaitGroup
var  lock sync.Mutex
var redisHandle *redis.Client
var scriptStr = `
    local a = 0
    while (a < 10000000 ) do
        a = a + 1
    end
    return KEYS[1]
    `
//初始化一个连接池
func init() {
    fmt.Println(time.Now(),"开始时间")
    redisHandle = GetRedis()

}

func GetRedis() *redis.Client {
    redisHandle = redis.NewClient(&redis.Options{
        Addr:         addr,
        Password:     pwd,
        DB:           db,
        DialTimeout:  3 * time.Second,  //设置3秒超时
        PoolSize:     1000,                //连接池最大为1000
        MinIdleConns: 10,                //最小空闲连接池为10
        MaxRetries:  3,                  //设置连接池超时重试为3次
        IdleTimeout:  3 * time.Second, //客户端空闲超时为20s
        //ReadTimeout:
        //PoolTimeout:  7 * time.Second, //获取连接池超时时间
    })
    return redisHandle
}
func main() {
    //设置随机种子
    rand.Seed(time.Now().Unix())
    //设置超时
    //查看连接数
    for i := 0; i < 15; i++ {
        wait.Add(1)
        vt := i
        //go func() {
        go func() {
            defer wait.Done()
            //随机延迟,需要时为了保持go
            //获取计时
            s :=rand.Intn(3)
            time.Sleep(time.Second*time.Duration(s))
            strings := []string{cast.ToString(vt)}
            //设置lua脚本主要是为了使连接执行很长时间,占用连接池,从而需要重新创建连接池
            //设置执行lua开始时间点
            t2 := time.Now()
            eval := redisHandle.Eval(scriptStr, strings, len(strings))
            result, err := eval.Result()
            fmt.Printf("%v, 结果:%v,随机时间是:%v,lua执行耗时:%v ,err: %v\n",time.Now(),result, s,time.Since(t2), err)
        }()
    }
    wait.Wait()
}
10.2 查看进程运行状态
gOaZv.png](http
10.3 查看wireshark抓包情况

详见包redispoolend.pcapng

gO5Mc.png

10.4 抓包分析

1.复用连接池

2.定义重试次数MaxRetries: 3,

3.增大空闲连接数IdleTimeout: 3 * time.Second,

4.增大最大空闲连接池 PoolSize: 1000,

参考

实验说明 Golang HTTP 连接池参数

Redis连接池详解

redis模块连接池配置

redis-lua介绍

redis连接池实现