当前位置:首页 > 问答 > 正文

用Java折腾Redis配置那些事儿,边写代码边调参数体验

这事儿得从我接手那个老项目说起,项目里用Redis缓存一些用户信息,本来跑得好好的,突然有一天,运营说部分用户登录后老是转圈圈,体验很差,我一查日志,好家伙,Redis连接超时的错误一堆,看来是时候好好折腾一下Redis的配置了。

我用的Java客户端是Jedis,当时图省事,配置就是最基础的那种,代码里直接new JedisPool(config, "127.0.0.1", 6379)就完事儿了,现在问题来了,肯定不能这么糙了。

第一回合:和连接超时时间(connectionTimeout)的较量

最开始报的错误就是连接超时,我寻思着,是不是网络有点波动,默认的超时时间太短了?Jedis的默认connectionTimeout是2000毫秒,也就是2秒,我试着把它调大到5000毫秒。

JedisPoolConfig poolConfig = new JedisPoolConfig();
// ... 其他池子配置先不管
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379, 5000); // 最后一个参数就是connectionTimeout,单位毫秒

改完部署上去,观察了一会儿,连接超时的错误确实少了一些,但没完全消失,而且还出现了新的情况:有时候请求的响应变得特别慢,我意识到,光调大这个参数是治标不治本,如果网络真的不稳定,等5秒才报错,虽然连接可能成功了,但用户的请求已经被阻塞了5秒,体验更差,这就像等一个迟到的人,等2分钟你可能就走了,等10分钟你可能会非常烦躁,这个值不能太小(频繁超时),也不能太大(阻塞太久),后来我把它设成了3000毫秒,算是取个折中,并且开始怀疑是不是连接池本身有问题。

用Java折腾Redis配置那些事儿,边写代码边调参数体验

第二回合:深挖连接池参数(JedisPoolConfig)

之前的配置根本没好好设置连接池,用的都是默认值,这可能是问题的根源,我打开JedisPoolConfig的文档,开始一个个参数琢磨。

  1. maxTotal(最大连接数):默认是8,这太少了!想象一下,我们系统并发一高,8个连接瞬间被用完,第9个请求就得等着,等久了不就超时了吗?我看了下服务器资源,果断先调到50,这个值不能瞎设,得根据业务量和服务器配置来。

  2. maxIdle和minIdle(最大和最小空闲连接数):默认分别是8和0,maxIdle我让它和maxTotal保持一致,也是50,避免连接频繁创建和关闭,minIdle我设成了5,意思是让池子里始终维持至少5个空闲连接,来了请求就能直接用,不用临时建立连接,速度更快。

    用Java折腾Redis配置那些事儿,边写代码边调参数体验

  3. maxWaitMillis(获取连接时的最大等待时间):默认是-1,意思是无限等待,这太危险了!万一连接池耗尽,所有线程都会卡死在这,我把它设成了2000毫秒,如果2秒内还拿不到连接,就抛个异常,然后我们代码里可以捕获这个异常,给用户返回一个“系统繁忙”之类的友好提示,总比一直傻等着强。

  4. testOnBorrow(借出连接时是否校验):这个参数我纠结了很久,如果设为true,每次从池子里拿连接都会执行一次ping命令,确保连接是好的,这能保证拿到的连接绝对有效,但每次多一次网络开销,性能有损耗,如果设为false,性能好了,但有可能拿到一个已经断开的连接(比如被Redis服务器主动踢掉了),到时候操作就会报错,我采取了折中方案,用了testWhileIdle

  5. testWhileIdle(定期校验空闲连接):这个我设成了true,然后配合另外三个参数:

    • timeBetweenEvictionRunsMillis:我设成30000毫秒(30秒),意思是每30秒运行一次空闲连接校验任务。
    • minEvictableIdleTimeMillis:设成60000毫秒(1分钟),如果一个连接空闲超过1分钟,就把它踢掉。
    • numTestsPerEvictionRun:每次校验任务最多检查5个连接。

    这样配置的好处是,既不用每次借连接都检查,又能定期清理掉无效的空闲连接,在性能和可靠性之间找了个平衡。

    用Java折腾Redis配置那些事儿,边写代码边调参数体验

折腾完这一套连接池配置,代码看起来像样多了:

JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(50);
poolConfig.setMaxIdle(50);
poolConfig.setMinIdle(5);
poolConfig.setMaxWaitMillis(2000);
poolConfig.setTestOnBorrow(false);
poolConfig.setTestWhileIdle(true);
poolConfig.setTimeBetweenEvictionRunsMillis(30000);
poolConfig.setMinEvictableIdleTimeMillis(60000);
poolConfig.setNumTestsPerEvictionRun(5);
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379, 3000);

部署之后,观察了几天,之前那种连接超时和响应慢的问题基本再没出现过。

第三回合:读写超时(soTimeout)的小插曲

安稳日子没过多久,又出了个新问题,有用户反馈,上传一个大头像之后,登录就报错,查日志发现是socket read timeout,原来,Redis的soTimeout默认也是2000毫秒,用户上传大头像,用户信息比较大,序列化后写入Redis的时间超过了2秒,导致读超时。

这个参数在Jedis里设置有点隐蔽,它在Connection类里,在创建JedisPool时,需要用一个带更多参数的构造方法:

// 注意最后一个参数 soTimeout,单位也是毫秒
JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379, 3000, null, 0, 5000);

这里我把soTimeout设成了5000毫秒,给大value的读写留足了时间,我也反思了业务,把那些特别大的对象(比如头像的Base64编码)就不要往Redis里塞了,还是应该存到文件服务器上,Redis里只存个URL。

经过这几轮的折腾,我算是明白了,Redis配置这事儿,真没有一劳永逸的“黄金参数”,都是摸着石头过河,根据自己业务的实际情况——比如网络环境、数据大小、并发量——边写代码边调整,观察效果,再调整,最重要的不是记住某个参数值,而是理解每个参数背后代表的含义,出了问题能有个清晰的排查思路。