Redis Client 之 Jedis与Lettuce

6,054 阅读4分钟

前言

Jedis

JedisRedis 官方推荐的 Java 连接开发工具,要在Java开发中使用好 Redis中间件,必须对Jedis十分了解才行。

基本使用

Jedis 的使用非常简单,只需要创建 Jedis 对象的时候指定 host,port,password 即可。

Jedis jedis = new Jedis("ip",post);
......
jedis.close();

创建完 Jedis 对象,Jedis底层会打开一条 Socket 通道和 Redis 进行连接,所以在使用完 Jedis 对象后,需要 jedis.close() 关闭连接,不然会占用系统资源。如果每次使用都手动创建和销毁 Jedis 对象,对应用的性能有很大影响,毕竟创建 socket 的过程是很耗时的。所以我们使用连接池的方式减少 socket 对象的创建和销毁过程。

连接池使用

Jedis连接池是 org.apache.commons.pool2实现的,在构建连接池对象时,需要提供连接池对象的配置对象,我们可以通过这个配置对象对连接池进行相关参数的配置,比如:最大空闲连接数,最大连接总数。

JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(8);
jedisPoolConfig.setMaxTotal(18);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, "ip", 6379, 2000);
Jedis jedis = jedisPool.getResource();
......
jedis.close();
jedisPool.close();

使用 Jedis连接池后,每次用完连接对象都会把连接归还给连接池。Jedisclose()getResource() 方法的实现。

//Jedis的close方法
@Override
public void close() { 
    if (dataSource != null) {
        if (client.isBroken()) {
            this.dataSource.returnBrokenResource(this);
        } else {
            this.dataSource.returnResource(this);
        }
    } else {
        client.close();
    }
}

// JedisPool.getResource()方法
// 从对象池中获取Jedis连接时,将会对dataSource进行设置
public Jedis getResource() {
    Jedis jedis = super.getResource();   
    jedis.setDataSource(this);
    return jedis;
}

高可用连接

我们知道,连接池可以大大提高应用访问Reids服务的性能,减去大量的 Socket 的创建和销毁过程。但是 Redis 为了保障高可用,服务一般都是 Sentinel部署方式。当 Redis 服务中的主服务挂掉之后,会仲裁出另外一台 Slaves 服务充当 Master。这个时候,我们的应用即使使用了Jedis连接池,Master服务挂了,还是无法连接新的 Master服务。为了解决这个问题,Jedis也提供了相应的 Sentinel 实现,能够在 Redis Sentinel 主从切换时候,通知我们的应用,把我们的应用连接到新的 Master 服务。

Set<String> sentinels = new HashSet<>();
sentinels.add("ip1");
sentinels.add("ip2");
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(8);
jedisPoolConfig.setMaxTotal(18);
JedisSentinelPool jedisPool = new JedisSentinelPool("mymaster", sentinels, jedisPoolConfig);
Jedis jedis = jedisPool.getResource();
......
jedis.close();
jedisPool.close();

JedisSentinelPool 的使用很简单,添加了设置服务器ip的 set 集合和 masterName参数,Jedis Sentinel底层基于 Redis订阅实现 Redis主从服务的切换通知,当 Redis发生主从切换时,Sentinel 会发送通知主动通知 Jedis 进行连接的切换。JedisSentinelPool 在每次从连接池中获取连接对象的时候,都要对连接对象进行检测,如果此链接和 SentinelMaster 服务连接参数不一致,则会关闭此连接,重新获取新的 Jedis 连接对象。

@Override
public Jedis getResource() {
  while (true) {
    Jedis jedis = super.getResource();
    jedis.setDataSource(this);

    // get a reference because it can change concurrently
    final HostAndPort master = currentHostMaster;
    final HostAndPort connection = new HostAndPort(jedis.getClient().getHost(), jedis.getClient()
        .getPort());

    if (master.equals(connection)) {
      // connected to the correct master
      return jedis;
    } else {
      returnBrokenResource(jedis);
    }
  }
}

使用 Spring 时可以引入 spring-data-redis 包,使用 SpringBoot 时可以直接引用 spring-boot-starter-redis包,其内部都整合了 Jedis

当然,也可以单独引用 Jedis 包。

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

Lettuce

Lettuce 是基于 Netty框架(NIO)的事件驱动的通信,支持同步和异步调用的,可扩展的 redis client,多个线程可以共享一个 RedisConnection,线程安全。

基本使用

1、引 lettuce 依赖

<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
</dependency>

2、lettuce操作Redis

// 同步操作
@Test
public void test() {
    RedisURI redisURI = RedisURI.builder().withHost("ip").withPort(6379).withTimeout(Duration.of(10, ChronoUnit.SECONDS)).build();
    RedisClient redisClient = RedisClient.create(redisURI);
    StatefulRedisConnection<String, String> connection = redisClient.connect();
    RedisCommands<String, String> syncCommands = connection.sync();
    String set = syncCommands.set("k1", "v1");
    System.out.println(set);
    connection.close();
    redisClient.shutdown();
}

// 异步操作
@Test
public void testAsync(){
    RedisURI redisURI = RedisURI.builder().withHost("ip").withPort(6379).withTimeout(Duration.of(10, ChronoUnit.SECONDS)).build();
    RedisClient redisClient = RedisClient.create(redisURI);
    StatefulRedisConnection<String, String> connection = redisClient.connect();
    RedisAsyncCommands<String, String> asyncCommands = connection.async();
    RedisFuture<String> set = asyncCommands.set("k2", "v2");
    System.out.println(set);
    connection.close();
    redisClient.shutdown();
}

// 响应式API
@Test
public void testReactive() {
    RedisURI redisURI = RedisURI.builder().withHost("192.168.96.173").withPort(6379).withTimeout(Duration.of(10, ChronoUnit.SECONDS)).build();
    RedisClient redisClient = RedisClient.create(redisURI);
    StatefulRedisConnection<String, String> connection = redisClient.connect();
    RedisReactiveCommands<String, String> reactiveCommands = connection.reactive();
    Mono<String> set = reactiveCommands.set("name", "feiyangyang");
    System.out.println(set.block());

    // 设置过期时间
    SetArgs args = SetArgs.Builder.nx().ex(10);
    set = reactiveCommands.set("age", "19", args);
    set.subscribe(System.out::println);

    // 开启事务,先将 count 设置为1,再自增1
    reactiveCommands.multi().doOnSuccess(r -> {
        reactiveCommands.set("count", "1").doOnNext(System.out::println).subscribe();
        reactiveCommands.incr("count").doOnNext(System.out::println).subscribe();
    }).flatMap(s -> reactiveCommands.exec())
            .doOnNext(transactionResult -> System.out.println(transactionResult.wasDiscarded())).subscribe();

}

高可用连接

// 主从
@Test
public void masterSlave() {
    RedisURI redisURI = RedisURI.builder().withHost("ip").withPort(6379).withTimeout(Duration.of(10, ChronoUnit.SECONDS)).build();
    RedisClient client = RedisClient.create(redisURI);
    StatefulRedisMasterReplicaConnection<String, String> connection = MasterReplica.connect(client, StringCodec.UTF8, redisURI);
    // 从节点读主节点数据
    connection.setReadFrom(ReadFrom.REPLICA);

    RedisCommands<String, String> commands = connection.sync();
    commands.set("name", "feiyangyang");
    System.out.println(commands.get("name"));

    connection.close();
    client.shutdown();
}

// 哨兵
@Test
public void sentinel() {
    List<RedisURI> uris = new ArrayList();
    uris.add(RedisURI.builder().withSentinel("ip1", 26379).withSentinelMasterId("mymaster").withPassword("123456").build());
    uris.add(RedisURI.builder().withSentinel("ip2", 26379).withSentinelMasterId("mymaster").withPassword("123456").build());
    uris.add(RedisURI.builder().withSentinel("ip3", 26379).withSentinelMasterId("mymaster").withPassword("123456").build());

    RedisClient client = RedisClient.create();
    StatefulRedisMasterReplicaConnection<String, String> connection = MasterReplica.connect(client, StringCodec.UTF8, uris);
    connection.setReadFrom(ReadFrom.REPLICA);
    RedisCommands<String, String> commands = connection.sync();
    commands.set("name", "feiyangyang");
    System.out.println(commands.get("name"));
    connection.close();
    client.shutdown();
}

// 集群
@Test
public void cluster() {
    Set<RedisURI> uris = new HashSet<>();
    uris.add(RedisURI.builder().withHost("ip1").withPort(7000).withPassword("123456").build());
    uris.add(RedisURI.builder().withHost("ip2").withPort(7001).withPassword("123456").build());
    uris.add(RedisURI.builder().withHost("ip3").withPort(7000).withPassword("123456").build());
    uris.add(RedisURI.builder().withHost("ip4").withPort(7001).withPassword("123456").build());
    uris.add(RedisURI.builder().withHost("ip5").withPort(7000).withPassword("123456").build());
    uris.add(RedisURI.builder().withHost("ip6").withPort(7001).withPassword("123456").build());
    RedisClusterClient client = RedisClusterClient.create(uris);
    StatefulRedisClusterConnection<String, String> connection = client.connect();
    RedisAdvancedClusterCommands<String, String> commands = connection.sync();
    commands.set("name", "feiyangyang");
    System.out.println(commands.get("name"));

    //选择从节点,只读
    NodeSelection<String, String> replicas = commands.replicas();
    NodeSelectionCommands<String, String> nodeSelectionCommands = replicas.commands();
    Executions<List<String>> keys = nodeSelectionCommands.keys("*");
    keys.forEach(key -> System.out.println(key));

    connection.close();
    client.shutdown();
}

总结

Jedis 是直连 redis server,会有线程安全问题。除非使用连接池,为每个 Jedis实例增加物理连接。

优点:

  • 简单易理解
  • 全面的Redis操作API

缺点:

  • 同步阻塞IO
  • 不支持异步
  • 线程不安全

Lettuce是基于Netty的,连接实例可以在多个线程间并发访问,Lettuce还支持异步连接方式,提高网络等待和磁盘IO效率。

优点:

  • 线程安全
  • 基于Netty框架的事件驱动通信,可异步调用
  • 适用于分布式缓存

缺点:

  • 学习成本高,上手相对复杂