一.引言
项目实现中需要连接 redis,为了防止因网络抖动或其他原因造成的客户端连接失败,一般需要增加重试机制判断 client 是否连接成功,之前写了一版重连代码发现有 bug,借此机会看下代码 bug 以及如何更好的重连 redis。
二.错误代码
def getRedisClient(host: String, port: Int, timeout: Int = 0): Jedis = { var redisClient: Jedis = null var attempt = 3 while (attempt > 0 && redisClient == null) { try { if (!timeout.equals(0)) { redisClient = new Jedis(host, port, timeout) } else { redisClient = new Jedis(host, port) } } catch { case e: Throwable => { e.printStackTrace() println(s"redis连接异常 $host 重试次数: $attempt") Thread.sleep(1000) } } finally { attempt -= 1 } } redisClient }
该代码为了实现报错重连机制,上线后发现存在下述问题 (短短几行代码这么多 bug o(╥﹏╥)o):
A.host 正确,不抛异常,client 不可用
出现该情况可能是网络抖动造成,不会进入 try catch 逻辑,但是后续使用该 client 会报错,因为redisClient = new Jedis() 方法没有返回值,所以只初始化无法判断该连接是否有效:
public Jedis(final String host, final int port, final int timeout) { super(host, port, timeout); }
B.host 错误,抛异常,client 不可用
host 错误情况下,直接报错 java.net.UnknownHostException
C.client == null 判断问题
while 循环 client == null 作为判断条件,但只在第一次生效,因为第一次初始化后,即使是错误的 host 和 ip,client 也会变为 Jedis 对象而不再是 null,从而不会执行后续重试逻辑:
编辑
三.修改版
上面A,B,C三个问题总结下来就是两个问题:
(1) Jedis 客户端连接失败时需要抛出异常进入 catch 逻辑
(2) while 循环判断时需要新的判断条件
这个时候 ping 方法满足我们的需求,该方法返回 String,client 正常时返回 "PONG"(新的判断条件),client 无效时则抛出异常(进入catch逻辑),所以满足了上述要求:
public String ping() { checkIsInMulti(); client.ping(); return client.getStatusCodeReply(); }
这里将重试次数增加为 10 次,将判断条件修改为 ping 后返回的结果,同时保证 ping 后出问题可以打出异常日志方便后续问题排查:
def getRedisClient(host: String, port: Int, timeout: Int = 0): Jedis = { var redisClient: Jedis = null var attempt = 10 var ping = "" while (attempt > 0 && ping != "PONG") { try { if (!timeout.equals(0) && attempt >= 10) { redisClient = new Jedis(host, port, timeout) } else { redisClient = new Jedis(host, port) } ping = redisClient.ping() } catch { case e: Throwable => { e.printStackTrace() println(s"redis连接异常 $host 重试次数: $attempt") Thread.sleep(1000) } } finally { attempt -= 1 } } println(s"初始化成功 ${host} ping: $ping") redisClient }
四.能不能再改进一点
redis 配置为主从结构,上述方法针对单台 redis 进行 10 次连接,一般情况下不会出问题,但是极端情况下还是会导致服务异常,所以可以在代码中增加另一台 host 的连接,从而保证 slave or master 单点故障的情况下服务仍高可用:
def getRedisClient(host: String, port: Int, timeout: Int = 0, other_host: String = ""): Jedis = { var redisClient: Jedis = null var attempt = 20 var ping = "" while (attempt > 0 && ping != "PONG") { try { if (!timeout.equals(0) && attempt >= 10) { redisClient = new Jedis(host, port, timeout) } else if (attempt >= 10) { redisClient = new Jedis(host, port) } else if (!timeout.equals(0)) { redisClient = new Jedis(other_host, port, timeout) } else { redisClient = new Jedis(other_host, port) } ping = redisClient.ping() } catch { case e: Throwable => { e.printStackTrace() if (attempt >= 10) { println(s"redis连接异常 $host 重试次数: $attempt") } else { println(s"redis连接异常 $other_host 重试次数: $attempt") } Thread.sleep(1000) } } finally { attempt -= 1 } } if (attempt >= 10) { println(s"初始化成功 ${host} ping: $ping") } else { println(s"初始化成功 ${other_host} ping: $ping") } redisClient
修改后的程序相比最一开始鲁棒性更好,服务也更加高可用,非常的奈斯 (^-^)V