Java'da Redis için Redis belgelerinin önerdiği iki kütüphane vardır: Jedis ve Lettuce. Kısa cevap şudur: yalnızca senkron çağrılara ihtiyacınız varsa ve basit bir API istiyorsanız Jedis, asenkron ya da reaktif (Project Reactor) programlama yapıyorsanız Lettuce. Belgeler Jedis'in Redis'in tam özellik setini desteklediğini ama yalnızca senkron işlemlerle sınırlı olduğunu; Lettuce'un senkron, asenkron ve reaktif bağlantıları desteklediğini, buna karşılık API'sinin daha karmaşık olduğunu ve şu an bütün Redis özelliklerini, özellikle time series ve olasılıksal veri yapılarını desteklemediğini söyler.

Bu yazıda iki kütüphanenin kurulumunu, bağlantıyı, havuz ve çoğullama farkını, pipeline ve transaction kullanımını, istisna hiyerarşilerini ve üretim önerilerini belgelerdeki örneklerle yan yana anlatıyoruz. Yazı Java dilini değil, Redis kütüphanelerini konu alıyor; dil tarafı için Java nedir yazısına bakabilirsiniz. Bağlantı URL'lerinin dile bağlı olmayan kuralları Redis bağlantı URL ve connection pool yazısında, serinin geneli Redis öğrenme rehberi sayfasında.

Jedis ve Lettuce karşılaştırması

Jedis ve Lettuce karşılaştırması: Programlama modeli, API karmaşıklığı, Özellik kapsamı, Bağlantı yönetimi
ÖzellikJedisLettuce
Programlama modeliYalnızca senkronSenkron, asenkron ve reaktif
API karmaşıklığıBasit ve doğrudanDaha karmaşık
Özellik kapsamıRedis özelliklerinin tamamıTime series ve olasılıksal yapılar şu an yok
Bağlantı yönetimiBağlantı havuzu (RedisClient içinde)Çoğullanan bağlantı, isteğe bağlı havuz
Pipelinepipelined() ve sync()setAutoFlushCommands(false) ve flushCommands()
Temel istisna sınıfıJedisExceptionRedisException

Belgelerdeki özellik deposu örneği seçimi pratik bir dille özetler: asenkron veya reaktif bileşim gerekiyorsa ya da zaten Spring WebFlux veya Project Reactor gibi reaktif bir yığındaysanız Lettuce; engelleyen komutlar sık kullanılıyorsa ya da açık, çağrı başına bağlantı ömrüne sahip basit bir senkron API istiyorsanız Jedis. Bunun nedeni bağlantı modelidir: çoğullanan tek bir bağlantıda BLPOP gibi engelleyen komutlar herkesi bekletir. Havuz ile çoğullamanın farkını bağlantı yazısında ayrıntılı anlatıyoruz.

Jedis kurulumu ve yeni bağlantı API'si

Belgelere göre Jedis 7.2.0 yeni bir bağlantı API'si getirdi: tek sunucu bağlantısı ve havuz için RedisClient, cluster için RedisClusterClient, Sentinel için RedisSentinelClient. Bunlar sırasıyla UnifiedJedis, JedisPool, JedisPooled, JedisCluster ve JedisSentinelPool sınıflarının yerini alır. Jedis 8.0 ise JedisPooled sınıfını ve UnifiedJedis'in herkese açık yapıcılarını kaldırdı, diğer eski sınıflar kullanımdan kaldırılmış (deprecated) durumda. Eski bir öğreticideki JedisPool kodunu kopyalıyorsanız bunu hesaba katın. Maven bağımlılığı şöyledir:

text
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>8.0.2</version>
</dependency>

Temel bağlantı bir URL ile kurulur; set() ve get() belgelerdeki örnekte şu sonuçları verir:

java
import redis.clients.jedis.RedisClient;

public class Main {
    public static void main(String[] args) {
        RedisClient jedis = RedisClient.create("redis://localhost:6379");

        String res1 = jedis.set("bike:1", "Deimos");
        System.out.println(res1); // OK

        String res2 = jedis.get("bike:1");
        System.out.println(res2); // Deimos

        jedis.close();
    }
}

Jedis URL'lerinde port zorunludur, tek sorgu parametresi protocol'dür (2 veya 3) ve db dahil diğer parametreler yok sayılır. Kullanıcı adı olmadan şifre vermek için redis://:şifre@sunucu:port biçimi kullanılır; iki nokta üst üste olmadan yazılan redis://şifre@sunucu:port hatadır.

Jedis bağlantı havuzu ayarları

Jedis 7.2.0'dan itibaren RedisClient bağlantı havuzunu kendiliğinden sağlar, ancak belgeler en iyi performans için havuzu doğru yapılandırmanın önemli olduğunu söyler. Havuz ayarları, Apache Commons Pool2'deki GenericObjectPoolConfig'in sarmalayıcısı olan ConnectionPoolConfig ile verilir:

java
ConnectionPoolConfig poolConfig = new ConnectionPoolConfig();
// maximum active connections in the pool, default is 8
poolConfig.setMaxTotal(8);
// maximum idle connections in the pool, default is 8
poolConfig.setMaxIdle(8);
// minimum idle connections in the pool, default 0
poolConfig.setMinIdle(0);

// Enables waiting for a connection to become available.
poolConfig.setBlockWhenExhausted(true);
// The maximum number of seconds to wait for a connection to become available
poolConfig.setMaxWait(Duration.ofSeconds(1));

// Enables sending a PING command periodically while the connection is idle.
poolConfig.setTestWhileIdle(true);
poolConfig.setTimeBetweenEvictionRuns(Duration.ofSeconds(1));

RedisClient jedis = RedisClient.builder()
        .hostAndPort("localhost", 6379)
        .poolConfig(poolConfig)
        .clientConfig(
            DefaultJedisClientConfig.builder()
                .socketTimeoutMillis(5000)
                .connectionTimeoutMillis(5000)
                .build())
        .build();

Belgelerdeki yaşam döngüsü özetine göre havuzdan bir bağlantı istendiğinde, boşta bağlantı varsa o verilir; yoksa bağlantı sayısı maxTotal'ın altındaysa yeni bağlantı açılır. Bağlantı işi bitince havuza döner, minEvictableIdleTime kadar boşta tutulur ve bağlantı sayısı minIdle'dan fazlaysa kapatılabilir hâle gelir. setTestWhileIdle(true) boştaki bağlantılara düzenli PING göndererek sağlık kontrolü yapar. Zaman aşımlarında socketTimeoutMillis bir komut sırasında okuma ya da yazma için izin verilen en uzun süreyi, connectionTimeoutMillis yeni bağlantı kurma süresini belirler; belgeler zaman aşımı verilmezse bir ağ veya sunucu hatasında kodun süresiz askıda kalabileceğini hatırlatır.

Bağlantı hatasında komutu yeniden deneme

Bir komut tamamlanmadan bağlantı koparsa komut JedisConnectionException ile başarısız olur. Havuz bağlantıları yönetse de komutu yeniden denemek için havuzdan yeni bir bağlantı istemeniz gerekir; bunu genellikle sınırlı sayıda denemeden oluşan bir döngüyle yaparsınız. Belgelerdeki döngü en fazla beş kez dener ve her denemede beklemeyi uzatır; son denemede istisnayı yeniden fırlatır.

Jedis ile pipeline ve transaction

Jedis'te pipeline pipelined() ile oluşturulur. Komutlar tamponda bekler ve sync() çağrıldığında gönderilir. Pipeline komutları doğrudan sonuç yerine Response<T> nesneleri döndürür; bu nesne ancak pipeline çalıştıktan sonra geçerli bir değer içerir ve değere get() ile erişilir. Belgelerdeki oturum örneği son erişim zamanını yazar, süreyi yeniler ve güncel hash'i aynı pipeline'da okur:

java
try (Pipeline pipeline = jedis.pipelined()) {
    pipeline.hset(key, "last_accessed_at", timestamp());
    pipeline.expire(key, sessionTtl);
    Response<Map<String, String>> refreshed = pipeline.hgetAll(key);
    pipeline.sync();

    return isValidSession(refreshed.get()) ? refreshed.get() : null;
}

Jedis'in pipelined() metodu transaction değildir; komutlar tek gidiş dönüşte gönderilir ama MULTI/EXEC içinde çalışmaz. Transaction için multi() kullanılır: komutları transaction nesnesine ekler, exec() çağırırsınız. Sonuçlara yine Response nesneleriyle erişebilirsiniz; exec() ayrıca bütün sonuçları sırasıyla içeren bir List<Object> döndürür.

WATCH ile iyimser kilit

WATCH bir bağlantıya bağlı olduğu için, havuz kullanan bir istemcide izleme ile EXEC'in aynı bağlantıda kalması gerekir. Belgelerdeki önbellek örneği bunu jedis.transaction(false) ile açılan bir transaction nesnesiyle sağlar: izleme, okuma, multi() ve exec() hep bu nesne üzerinden yapılır. İzlenen anahtar değişmişse exec() null döndürür ve döngü baştan dener:

java
public boolean updateField(String entityId, String field, String value) {
    String cacheKey = cacheKey(entityId);
    while (true) {
        // WATCH is tied to one connection, so open a transaction that
        // holds it until EXEC.
        try (AbstractTransaction tx = jedis.transaction(false)) {
            tx.watch(cacheKey);
            if (!tx.exists(cacheKey).get()) {
                tx.unwatch();
                return false;
            }
            tx.multi();
            tx.hset(cacheKey, field, value);
            tx.expire(cacheKey, ttl);
            List<Object> result = tx.exec();
            if (result == null) continue; // WATCH detected a change, retry.
            return true;
        }
    }
}

Transaction'ların sunucu tarafındaki kuralları MULTI, EXEC ve WATCH, pipelining'in genel mantığı Redis pipelining yazısında.

Jedis istisnaları

Jedis istisnaları: JedisConnectionException, JedisAccessControlException, JedisDataException, JedisException

Jedis istisnaları RuntimeException'dan türeyen JedisException altında toplanır; hepsi denetlenmeyen (unchecked) istisnalardır. Belgelerin önemli bulduğu dört sınıf şunlardır:

İstisnaNe zaman oluşurÖnerilen davranış
JedisConnectionExceptionBağlantı beklenmedik biçimde koptu veya kapandıBekleyerek yeniden dene veya yedeğe dön
JedisAccessControlExceptionKimlik doğrulama hatası veya yetki reddiKimlik bilgilerini veya yetkileri düzelt
JedisDataExceptionGönderilen ya da alınan veriyle ilgili sorunVeriyi veya komutu düzelt
JedisExceptionDiğer beklenmedik hatalarKaydet ve incele

Belgeler JedisException'ın hangi durumlarda fırlatılabileceğini de listeler: PING sağlık kontrolünden kötü yanıt, Sentinel'e bağlanılıp birincilin izlenmediği ya da tüm Sentinel'lerin kapalı olduğu durum, başarısız MULTI veya DISCARD, yeniden deneme süresinin ya da deneme sayısının aşılması ve havuzun tükenmesi bunlardan bazılarıdır. Yetki hatalarının sunucu tarafını Redis ACL yazısında anlatıyoruz.

Lettuce kurulumu ve bağlantı

Lettuce'un Maven bağımlılığı io.lettuce:lettuce-core'dur; belgelerdeki sürüm 7.9.0.RELEASE'dir ve Maven Central'dan en güncelini kontrol etmeniz önerilir. Bağlantı adımları Jedis'ten farklıdır: önce bir RedisURI ve RedisClient oluşturur, connect() ile bir StatefulRedisConnection alır, ondan da senkron, asenkron ya da reaktif komut arayüzünü istersiniz:

java
RedisURI uri = RedisURI.Builder
        .redis("localhost", 6379)
        .withAuthentication("default", "yourPassword")
        .build();
RedisClient client = RedisClient.create(uri);
StatefulRedisConnection<String, String> connection = client.connect();
RedisCommands<String, String> commands = connection.sync();

commands.set("foo", "bar");
String result = commands.get("foo");
System.out.println(result); // >>> bar

connection.close();

client.shutdown();

Asenkron arayüz connection.async() ile alınır ve her metot bir RedisFuture döndürür. Belgelerdeki örnek sonuçları get() ile bekler:

java
RedisAsyncCommands<String, String> asyncCommands = connection.async();

asyncCommands.set("foo", "bar").get();
System.out.println(asyncCommands.get("foo").get()); // prints bar

Reaktif arayüz connection.commands(RedisReactiveCommands.factory()) ile alınır ve Mono/Flux yayıncıları döndürür. Lettuce URL'leri redis, rediss, redis-socket ve redis-sentinel gibi şemaları tanır; timeout parametresi 10s gibi birim eki alabilir, ek yoksa milisaniye sayılır. Lettuce iki nokta üst üste olmadan yazılan redis://değer@sunucu biçimindeki değeri şifre olarak okur.

Çoğullama ve havuz

Belgelere göre Lettuce'ta tipik yaklaşım tek bir RedisClient oluşturup yeniden kullanmaktır. Bağlantılar çoğullanır, yani birden fazla komut tek ya da az sayıda bağlantı üzerinden eşzamanlı çalışır; bu da açık havuzlamayı çoğu durumda gereksiz kılar. Özellik deposu örneği tek bir StatefulRedisConnection'ın iş parçacığı güvenli olduğunu ve bütün süreç tarafından paylaşıldığını belirtir. Yine de Lettuce, asenkron bağlantılar için AsyncConnectionPoolSupport.createBoundedObjectPoolAsync() ile sınırlı bir havuz da sunar; bu yol, transaction gibi bağlantı durumuna bağlı işleri yalıtmanız gerektiğinde kullanışlıdır.

Lettuce ile pipeline ve transaction

Lettuce'ta pipeline için ayrı bir oluşturucu yoktur. Bağlantı düzeyinde otomatik gönderimi setAutoFlushCommands(false) ile kapatır, komutları normal asenkron çağrılar olarak kuyruğa alır, flushCommands() ile topluluğu gönderir ve otomatik gönderimi yeniden açarsınız. Her komut kendi RedisFuture'ını döndürür; LettuceFutures.awaitAll(...) hepsinin tamamlanmasını bekler:

java
List<RedisFuture<?>> futures = new ArrayList<>(rows.size() * 2);
conn.setAutoFlushCommands(false);
try {
    for (Map.Entry<String, Map<String, Object>> e : rows.entrySet()) {
        String key = keyFor(e.getKey());
        Map<String, String> encoded = encode(e.getValue());
        futures.add(async.hset(key, encoded));
        futures.add(async.expire(key, ttlSeconds));
    }
    conn.flushCommands();
} finally {
    conn.setAutoFlushCommands(true);
}
if (!LettuceFutures.awaitAll(BATCH_TIMEOUT, futures.toArray(new RedisFuture[0]))) {
    throw new IllegalStateException("bulkLoad: timed out after " + BATCH_TIMEOUT);
}

Belgeler bu koddaki iki noktayı özellikle vurgular. Birincisi, setAutoFlushCommands(false) asenkron komut nesnesini değil bağlantıyı etkiler; geri açılana kadar o bağlantıdan geçen her çağrı tamponda kalır. finally bloğu bir adım hata verse bile otomatik gönderimi geri açar; bunu unutmak, bağlantı paylaşıldığı için JVM'deki sonraki tüm komutları sessizce bozar. İkincisi, awaitAll bir zaman aşımıyla bekler; otomatik gönderim kapalıyken bir sorun olursa komutlar yerel tamponda süresiz kalabilir ve zaman aşımı bu durumda temiz bir hata verir.

Transaction'lar multi() ve exec() ile yapılır; ikisi arasındaki komutlar sunucuda kuyruğa alınır ve exec() tamamlandığında çalışır. exec() sonuçları sırasıyla içeren bir TransactionResult döndürür. WATCH kullanılan bir transaction'da, izlenen anahtar değiştiği için transaction'ın atılıp atılmadığını TransactionResult.wasDiscarded() ile kontrol edersiniz.

Lettuce üretim ayarları ve istisnaları

Lettuce üretim ayarları ve istisnaları: RedisConnectionException, RedisCommandTimeoutException, RedisCommandExecutionException, RedisCommandInterruptedException

Lettuce komut yürütme, SSL el sıkışması ve Sentinel keşfi gibi birçok işlem için varsayılan olarak 60 saniyelik genel bir zaman aşımı kullanır; bunu RedisURI.Builder üzerinde withTimeout(Duration.ofSeconds(30)) gibi bir çağrıyla değiştirebilirsiniz. TCP düzeyinde TCP_USER_TIMEOUT ve keepalive ayarları için Netty yerel taşıma katmanlarından birinin kurulu olması gerekir. Komut güvenilirliği tarafında Lettuce varsayılan olarak en az bir kez (at-least-once) stratejisi kullanır: bağlantı koptuğunda kendiliğinden yeniden bağlanır ve kuyruktaki komutları çalıştırmaya devam eder. ClientOptions içinde autoReconnect(false) verirseniz en fazla bir kez (at-most-once) kipine geçilir; Lettuce yeniden bağlanmaz ve komutları kuyruğa almak yerine atar. Lettuce 6.6 ve üstünde, kopmadan sonra hangi komutların yeniden çalıştırılacağını seçmek için bir tekrar filtresi (replayFilter) de tanımlanabilir; belgelerdeki örnek idempotent olmayan DECR komutunu ayırır.

Lettuce istisnasıNe zaman oluşurÖnerilen davranış
RedisConnectionExceptionBağlantı kurulamadı veya koptuBekleyerek yeniden dene veya yedeğe dön
RedisCommandTimeoutExceptionKomut zaman aşımını aştıBekleyerek yeniden dene, zaman aşımı ayarını gözden geçir
RedisCommandExecutionExceptionRedis WRONGTYPE gibi bir hata yanıtı döndürdüKomutu veya veri modelini düzelt
RedisCommandInterruptedExceptionSonucu bekleyen iş parçacığı kesildiKesme durumunu geri yükle ve temiz biçimde çık

Asenkron ve reaktif API'lerde aynı istisnalar doğrudan fırlatılmaz, CompletionStage hatası ya da akış hatası olarak gelir; belgelerdeki örnek whenComplete içinde hatanın nedenini açıp RedisConnectionException olup olmadığına bakar. Cluster kullanıyorsanız Lettuce MOVED ve ASK yönlendirmelerini kendisi işler, ama belgeler uyarlanabilir topoloji yenilemesini (ClusterTopologyRefreshOptions) açmanızı ve uygulama trafik almaya başlamadan önce her birincil düğüme PING göndererek bağlantıları ısıtmanızı önerir. Bu yönlendirme hatalarının anlamı Redis Cluster ve CROSSSLOT yazısında.

İki kütüphane için ortak bir not: birden fazla uç noktası olan bir Redis kurulumuna bağlanıyorsanız belgeler JVM'nin DNS önbelleğini kapatmanızı ister, çünkü bir düğüm düştüğünde değişen IP adresine rağmen uygulama eski adresi kullanmaya devam edebilir. Bu, networkaddress.cache.ttl ve networkaddress.cache.negative.ttl güvenlik özelliklerini 0 yaparak sağlanır. İstemci tarafı önbellek Jedis'te 5.2.0 ve üstünde, RESP3 ve bir CacheConfig ile açılır; ayrıntı client-side caching yazısında.

Sık yapılan hatalar

  • Eski Jedis sınıflarıyla yeni projeye başlamak. JedisPooled Jedis 8.0'da kaldırıldı; RedisClient kullanın.
  • Jedis'te zaman aşımı vermemek. Ağ sorunu olduğunda kod süresiz bekleyebilir; socketTimeoutMillis ve connectionTimeoutMillis ayarlayın.
  • Havuzlu istemcide WATCH ile EXEC'i farklı bağlantılara düşürmek. Jedis'te transaction(false) nesnesi üzerinden çalışın.
  • Lettuce'ta otomatik gönderimi geri açmamak. setAutoFlushCommands(true) çağrısını mutlaka finally bloğuna koyun.
  • Lettuce'un çoğullanan bağlantısında engelleyen komutları sık kullanmak. Bu tür iş yüklerinde ayrı bağlantı ya da Jedis daha uygundur.
  • Bütün istisnaları JedisException veya RedisException ile yakalamak. Bağlantı ve zaman aşımı hatalarını komut hatalarından ayırın.

Sık Sorulan Sorular

Spring Boot projesinde hangisini seçmeliyim?

Belgelerdeki özellik deposu örneğine göre Spring WebFlux veya Project Reactor gibi reaktif bir yığındaysanız Lettuce doğal seçimdir. Basit senkron erişim ve engelleyen komutlar ağırlıktaysa Jedis daha basittir.

Lettuce ile bağlantı havuzu kurmam gerekir mi?

Çoğu durumda hayır. Lettuce bağlantıları çoğullar ve tek bir StatefulRedisConnection iş parçacığı güvenlidir. Bağlantı durumuna bağlı işleri yalıtmanız gerekiyorsa asenkron havuz desteğini kullanabilirsiniz.

Jedis pipeline sonucunu neden hemen okuyamıyorum?

Pipeline komutları Response<T> döndürür ve bu nesneler ancak sync() çağrısından sonra geçerli bir değer içerir. get() çağrısını sync()'ten sonra yapın.

Lettuce time series komutlarını destekliyor mu?

Belgelere göre Lettuce şu an bütün Redis özelliklerini, özellikle time series ve olasılıksal veri yapılarını desteklemiyor. Bu özelliklere ihtiyacınız varsa Jedis'i değerlendirin; konuların kendisi Redis time series ve Bloom ve Cuckoo filtreleri yazılarında.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Redis öğrenme rehberi · Redis bağlantı URL ve connection pool · MULTI, EXEC ve WATCH · Redis pipelining · Redis Cluster ve CROSSSLOT · Java nedir