Redis客户端Jedis,Lettuce和vertx的使用比较及部分源码解析

简介

Redis client可以说是有很多,不同的Client在使用方式,性能方面都有一些区别。Jedis作为老资格的redis client目前来说对redis的接口算是支持的最好的,也是使用起来最简单。Lettuce使用起来可能是三者之间最复杂的,但是也是性能最高的,特别是lettuce还支持了全异步的连接和连接池,更是加大了性能。vertx-redisClient作为全异步微服务框架vertx的一个组件,也是用在vertx框架中,也是一个全异步的redis-client,使用的话经常用在vertx框架中。

Redis client 难度 同步异步 对Redis支持完善度 性能
Jedis 同步
Lettuce 同步/异步
vertx-redisClient 异步

 

客户端版本

Jedis:redis.clients:jedis:3.3.0

Lettuce:io.lettuce:lettuce-core:6.0.0.M1

Vertx-redisClient:io.vertx:vertx-redis-client:3.8.5

在此时间段使用的都是各个client最新的版本,这些版本也是最近做了一次比较大的更新,主要是为了支持redis6.0发布的对acl的支持,关于redis acl详细介绍可以看篇博客:https://www.cnblogs.com/zhoujinyi/p/13222464.html。Redis acl Jedis3.3.0已经支持,Lettuce也是发布了一个实验版本M1对acl做了支持,目前vertx-redisClient还是不支持redis acl的。

Jedis客户端

初始化redis client

private fun creatRedisCluster() = JedisCluster(
        redisConfig.redisNodes.map { HostAndPort(it.host, it.port) }.toSet(),
        redisConfig.timeOut ?: 2000, // set connection time out, the jedis default is 2000ms
        redisConfig.timeOut ?: 2000, // set reconnection time out, the jedis default same as connection time out
        redisConfig.reconnectAttempts ?: 5, // max reconnection times
        redisConfig.username,
        redisConfig.password,
        null, // set client connection name, needn't configuration here, the jedis default is null
        JedisPoolConfig().apply {
            redisConfig.poolConfig.let {
                maxIdle = it.maxIdle
                minIdle = it.minIdle
                maxTotal = it.maxTotal
            }
        }
    )

我们直接使用的是集群模式,其他模式更加简单就不做多说,详细的请参考:https://github.com/xetorthio/jedis。如果你看过Redis Lettuce客户端异步连接池详解就应该知道,Lettuce在集群模式下我们需要给客户端设置cluster拓扑刷新机制,在集群出现问题或者变动的时候客户端能及时的刷新cluster拓扑从而防止各种异常的持续出现。可以看到我们在初始化Jedis Client的时候并没有给设置cluster拓扑刷新之类的属性,然而Jedis也没有给我们提供这样的接口或者配置,难道是Jedis不支持这样的配置么,答案肯定是否定的,之前也说了Jedis目前是对Redis支持的最好的客户端,那么怎么可能不考虑这样的问题呢,其实Jedis在底层中已经自动实现了这样的配置,我们看Jedis源码:

public T run(String key) {
    return runWithRetries(JedisClusterCRC16.getSlot(key), this.maxAttempts, false, null);
  }

在使用Jedis Client的时候,无论你使用那个API最终调的都是这个API,最终会走到runWithRetries这个接口:

private T runWithRetries(final int slot, int attempts, boolean tryRandomNode, JedisRedirectionException redirect) {
    if (attempts <= 0) {
      throw new JedisClusterMaxAttemptsException("No more cluster attempts left.");
    }

    Jedis connection = null;
    try {

      if (redirect != null) {
        connection = this.connectionHandler.getConnectionFromNode(redirect.getTargetNode());
        if (redirect instanceof JedisAskDataException) {
          // TODO: Pipeline asking with the original command to make it faster....
          connection.asking();
        }
      } else {
        if (tryRandomNode) {
          connection = connectionHandler.getConnection();
        } else {
          connection = connectionHandler.getConnectionFromSlot(slot);
        }
      }

      return execute(connection);

    } catch (JedisNoReachableClusterNodeException jnrcne) {
      throw jnrcne;
    } catch (JedisConnectionException jce) {
      // release current connection before recursion
      releaseConnection(connection);
      connection = null;

      if (attempts <= 1) {
        //We need this because if node is not reachable anymore - we need to finally initiate slots
        //renewing, or we can stuck with cluster state without one node in opposite case.
        //But now if maxAttempts = [1 or 2] we will do it too often.
        //TODO make tracking of successful/unsuccessful operations for node - do renewing only
        //if there were no successful responses from this node last few seconds
        this.connectionHandler.renewSlotCache();
      }

      return runWithRetries(slot, attempts - 1, tryRandomNode, redirect);
    } catch (JedisRedirectionException jre) {
      // if MOVED redirection occurred,
      if (jre instanceof JedisMovedDataException) {
        // it rebuilds cluster's slot cache recommended by Redis cluster specification
        this.connectionHandler.renewSlotCache(connection);
      }

      // release current connection before recursion
      releaseConnection(connection);
      connection = null;

      return runWithRetries(slot, attempts - 1, false, jre);
    } finally {
      releaseConnection(connection);
    }
  }

可以看到当发生JedisConnectionException或者JedisRedirectionException异常的时候里面都会在某种情况下调renewSlotCache这个接口,其实这个接口实际上就是在做我们刚说的cluster的拓扑刷新,也就说说Jedis他已经实现了这样的功能,只不过是在我们使用Cluster Api的时候发生部分异常的时候会去自己刷新拓扑。至于Jedis具体是怎么刷新拓扑的有兴趣的可以继续往下跟源码,这里就不多做介绍。

API实例

因为Jedis的api都是同步的,所以我们只示例一个同步的接口

fun exists(key: String): Boolean = redisCommand.exists(key)

Lettuce客户端

初始化Redis Cleint

Lettuce Redis Client的初始化就不再这里多说了,之前的Redis Lettuce客户端异步连接池详解。

API实例

    suspend fun exists(key: String): RedisFuture =
            redisAsyncPool.redisPool.use { conn ->
                return conn.async().exists(key)
            }

这个是一个直接使用lettuce api的接口,但是我们实际调用中,可以有三种实现再去封装,以提供给不同的场景

  • 提供给kotlin的异步
  • 提供给java的异步
  • 同步
// kotlin异步使用,使用suspeng挂起函数,使用await获取Fauter的值
suspend fun isRevokedAsync(key: String): Boolean =
            exists(key).await().toInt() != 0

// java异步使用,使用java1.8提供的CompletionStage作为返回值类型
fun isRevokedFuture(key: String): CompletionStage =
    redisAsyncPool.redisPool.let { redisPool ->
        redisPool.acquire().thenCompose { conn ->
            redisPool.use(conn) {
                conn.async().exists(key)
                     .thenApply { it.toInt() != 0 }
            }
        }
   }

// 同步
fun isRevoked(key: String): Boolean =
    redisAsyncPool.redisPool.let { redisPool ->
         val conn = redisPool.acquire().get()
         redisPool.use(conn) {
             conn.sync().exists(key).toInt() != 0
         }  
     }

Vertx-redisClient客户端

初始化vertx-redisClient

private fun redisCluster(): RedisAPI {
        val endpoints = redisConfig.redisNodes.mapNotNull {
            SocketAddress.inetSocketAddress(it.port, it.host)
        }.toMutableList()

        val redisOptions = RedisOptions()
                .setType(RedisClientType.CLUSTER) // set redis client type: cluster
                .setEndpoints(endpoints)
                .setPassword(redisConfig.password)
                .setUseSlave(RedisSlaves.SHARE) // set SLAVE nodes can randomly
                .setNetClientOptions(
                        NetClientOptions()
                                .setReconnectAttempts(redisConfig.reconnectAttempts ?: 0) // set reconnection times
                                .setReconnectInterval(redisConfig.reconnectInterval
                                        ?: 1000) // set reconnection interval
                )
        val client = Redis.createClient(vertx, redisOptions)
        return RedisAPI.api(client)
    }

API实例

private fun exists(args: List, handle: (AsyncResult) -> Unit) {
        redisAPI.exists(args, handle)
    }

// 使用kotlin suspendCoroutine从回调函数中获取想要的返回值
 suspend fun isRevokedAsync(key: String) = suspendCoroutine { cont ->
        exists(listOf(key)) { result ->
            // 成功返回结果
            if (result.succeeded()) {
                cont.resume(result.result().get(0).toBoolean())
            // 否则抛一个Exception
            } else {
                cont.resumeWithException(result.cause())
            }
        }
    }

 

你可能感兴趣的:(Redis,java,kotiln)