jedis

Redis向けのシンプルで高速なJava/Kotlinクライアント。同期的なAPIと豊富なRedis機能サポートで、エンタープライズアプリケーションに最適。

GitHub概要

redis/jedis

Redis Java client

スター12,173
ウォッチ675
フォーク3,902
作成日:2010年6月11日
言語:Java
ライセンス:MIT License

トピックス

javajedisredisredis-clientredis-cluster

スター履歴

redis/jedis Star History
データ取得日時: 2025/10/22 09:55

概要

JedisはRedis向けの人気のあるJava/Kotlinクライアントライブラリです。シンプルで直感的なAPIを提供し、Redisのほぼすべての機能をサポートしています。同期的な設計により、理解しやすく使いやすいのが特徴です。エンタープライズアプリケーションやAndroidアプリケーションでの採用実績も豊富です。

主な特徴

  • 完全なRedis機能サポート: すべてのRedisコマンドに対応
  • 接続プール: JedisPoolによる効率的な接続管理
  • クラスタ対応: Redis Clusterのネイティブサポート
  • Sentinel対応: 高可用性構成のサポート
  • パイプライン: バッチ処理による高速化
  • トランザクション: MULTI/EXECによるアトミック操作

注意事項

Jedisは**同期的(ブロッキング)**なライブラリです。Kotlinコルーチンと組み合わせる場合は、必ずDispatchers.IOでラップする必要があります。非同期処理が必要な場合は、Lettuceなどの非同期クライアントの使用を検討してください。

インストール

Gradle (Kotlin DSL)

dependencies {
    implementation("redis.clients:jedis:5.1.3")
}

Android プロジェクトでの設定

<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.INTERNET" />
// build.gradle.kts
android {
    // ProGuard設定(必要に応じて)
    buildTypes {
        release {
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
}

dependencies {
    implementation("redis.clients:jedis:5.1.3")
}

基本的な使い方

接続プールの設定(推奨)

import redis.clients.jedis.JedisPool
import redis.clients.jedis.JedisPoolConfig

// シングルトンパターンで接続プールを管理
object RedisClient {
    val pool: JedisPool by lazy {
        val poolConfig = JedisPoolConfig().apply {
            maxTotal = 10      // 最大接続数
            maxIdle = 5        // 最大アイドル接続数
            minIdle = 2        // 最小アイドル接続数
            testOnBorrow = true // 接続時のヘルスチェック
            testWhileIdle = true // アイドル時のヘルスチェック
        }
        
        // JedisPool(poolConfig, host, port, timeout, password)
        JedisPool(poolConfig, "localhost", 6379, 3000, null)
    }
}

// 使用例
fun example() {
    RedisClient.pool.resource.use { jedis ->
        jedis.set("greeting", "こんにちは")
        val value = jedis.get("greeting")
        println(value) // こんにちは
    }
}

高度な使い方

Kotlinコルーチンとの統合

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import redis.clients.jedis.JedisPool

class RedisRepository(private val pool: JedisPool) {
    
    // 文字列の操作
    suspend fun setString(key: String, value: String, exSeconds: Long? = null): String = 
        withContext(Dispatchers.IO) {
            pool.resource.use { jedis ->
                if (exSeconds != null) {
                    jedis.setex(key, exSeconds, value)
                } else {
                    jedis.set(key, value)
                }
            }
        }
    
    suspend fun getString(key: String): String? = 
        withContext(Dispatchers.IO) {
            pool.resource.use { jedis ->
                jedis.get(key)
            }
        }
    
    // リストの操作
    suspend fun pushToList(key: String, vararg values: String): Long = 
        withContext(Dispatchers.IO) {
            pool.resource.use { jedis ->
                jedis.lpush(key, *values)
            }
        }
    
    suspend fun getListRange(key: String, start: Long = 0, stop: Long = -1): List<String> = 
        withContext(Dispatchers.IO) {
            pool.resource.use { jedis ->
                jedis.lrange(key, start, stop)
            }
        }
    
    // ハッシュの操作
    suspend fun setHash(key: String, field: String, value: String): Long = 
        withContext(Dispatchers.IO) {
            pool.resource.use { jedis ->
                jedis.hset(key, field, value)
            }
        }
    
    suspend fun setHashMap(key: String, map: Map<String, String>): String = 
        withContext(Dispatchers.IO) {
            pool.resource.use { jedis ->
                jedis.hset(key, map)
            }
        }
    
    suspend fun getHash(key: String): Map<String, String> = 
        withContext(Dispatchers.IO) {
            pool.resource.use { jedis ->
                jedis.hgetAll(key)
            }
        }
    
    // セットの操作
    suspend fun addToSet(key: String, vararg members: String): Long = 
        withContext(Dispatchers.IO) {
            pool.resource.use { jedis ->
                jedis.sadd(key, *members)
            }
        }
    
    suspend fun getSetMembers(key: String): Set<String> = 
        withContext(Dispatchers.IO) {
            pool.resource.use { jedis ->
                jedis.smembers(key)
            }
        }
}

Redis Clusterの設定

import redis.clients.jedis.HostAndPort
import redis.clients.jedis.JedisCluster
import redis.clients.jedis.JedisPoolConfig

object RedisClusterClient {
    val cluster: JedisCluster by lazy {
        val clusterNodes = setOf(
            HostAndPort("127.0.0.1", 7000),
            HostAndPort("127.0.0.1", 7001),
            HostAndPort("127.0.0.1", 7002)
        )
        
        val poolConfig = JedisPoolConfig().apply {
            maxTotal = 50
            maxIdle = 10
            minIdle = 5
        }
        
        JedisCluster(
            clusterNodes,
            3000,          // connectionTimeout
            3000,          // soTimeout
            5,             // maxAttempts
            "password",    // password (nullの場合は認証なし)
            poolConfig
        )
    }
}

// 使用例
suspend fun clusterExample() = withContext(Dispatchers.IO) {
    RedisClusterClient.cluster.set("{user}:1", "John")
    RedisClusterClient.cluster.set("{user}:2", "Jane")
    // {user}は同じハッシュスロットを使用することを保証
}

Redis Sentinelの設定

import redis.clients.jedis.JedisSentinelPool

object RedisSentinelClient {
    val pool: JedisSentinelPool by lazy {
        val sentinels = setOf(
            "127.0.0.1:26379",
            "127.0.0.1:26380",
            "127.0.0.1:26381"
        )
        
        val poolConfig = JedisPoolConfig().apply {
            maxTotal = 10
            maxIdle = 5
            minIdle = 2
        }
        
        JedisSentinelPool(
            "mymaster",    // masterName
            sentinels,
            poolConfig,
            3000,          // timeout
            "password"     // password
        )
    }
}

実践的な例

トランザクション処理

class BankingService(private val pool: JedisPool) {
    
    suspend fun transferMoney(
        fromAccount: String,
        toAccount: String,
        amount: Double
    ): TransferResult = withContext(Dispatchers.IO) {
        pool.resource.use { jedis ->
            val fromKey = "account:$fromAccount:balance"
            val toKey = "account:$toAccount:balance"
            
            // トランザクション開始前にキーを監視
            jedis.watch(fromKey)
            
            val currentBalance = jedis.get(fromKey)?.toDoubleOrNull() ?: 0.0
            if (currentBalance < amount) {
                jedis.unwatch()
                return@withContext TransferResult.InsufficientFunds
            }
            
            // トランザクション開始
            val transaction = jedis.multi()
            try {
                transaction.incrByFloat(fromKey, -amount)
                transaction.incrByFloat(toKey, amount)
                transaction.lpush("transactions", "$fromAccount->$toAccount:$amount")
                
                val results = transaction.exec()
                if (results == null) {
                    // WATCHしたキーが変更された
                    TransferResult.ConcurrentModification
                } else {
                    TransferResult.Success
                }
            } catch (e: Exception) {
                transaction.discard()
                TransferResult.Error(e.message ?: "Unknown error")
            }
        }
    }
}

sealed class TransferResult {
    object Success : TransferResult()
    object InsufficientFunds : TransferResult()
    object ConcurrentModification : TransferResult()
    data class Error(val message: String) : TransferResult()
}

パイプライン処理

class AnalyticsService(private val pool: JedisPool) {
    
    suspend fun recordBulkEvents(events: List<Event>) = withContext(Dispatchers.IO) {
        pool.resource.use { jedis ->
            val pipeline = jedis.pipelined()
            
            events.forEach { event ->
                // 複数のコマンドをバッチ処理
                pipeline.hincrBy("analytics:${event.type}", event.date, 1)
                pipeline.zadd("events:${event.type}", event.timestamp.toDouble(), event.id)
                pipeline.expire("events:${event.type}", 86400) // 1日で期限切れ
            }
            
            // すべてのコマンドを一度に送信
            pipeline.sync()
        }
    }
    
    suspend fun getBulkData(keys: List<String>): Map<String, String?> = 
        withContext(Dispatchers.IO) {
            pool.resource.use { jedis ->
                val pipeline = jedis.pipelined()
                val responses = keys.map { key ->
                    key to pipeline.get(key)
                }
                
                pipeline.sync()
                
                responses.associate { (key, response) ->
                    key to response.get()
                }
            }
        }
}

data class Event(
    val id: String,
    val type: String,
    val date: String,
    val timestamp: Long
)

Kotlin拡張関数

// JedisExtensions.kt

import redis.clients.jedis.Jedis
import redis.clients.jedis.params.SetParams
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.decodeFromString

/**
 * JSONオブジェクトとして保存・取得
 */
inline fun <reified T> Jedis.setJson(key: String, value: T, exSeconds: Long? = null) {
    val json = Json.encodeToString(value)
    if (exSeconds != null) {
        setex(key, exSeconds, json)
    } else {
        set(key, json)
    }
}

inline fun <reified T> Jedis.getJson(key: String): T? {
    val json = get(key) ?: return null
    return try {
        Json.decodeFromString<T>(json)
    } catch (e: Exception) {
        null
    }
}

/**
 * アトミックな条件付き設定
 */
fun Jedis.setIfAbsent(key: String, value: String, exSeconds: Long? = null): Boolean {
    val params = SetParams().nx() // SET if Not eXists
    if (exSeconds != null) {
        params.ex(exSeconds)
    }
    return set(key, value, params) == "OK"
}

/**
 * 複数キーの一括取得
 */
fun Jedis.mgetAsMap(vararg keys: String): Map<String, String> {
    if (keys.isEmpty()) return emptyMap()
    
    val values = mget(*keys)
    return keys.zip(values)
        .filter { it.second != null }
        .associate { it.first to it.second!! }
}

// 使用例
data class User(val id: Long, val name: String, val email: String)

suspend fun extensionExample(pool: JedisPool) = withContext(Dispatchers.IO) {
    pool.resource.use { jedis ->
        // JSONとして保存
        val user = User(1, "山田太郎", "[email protected]")
        jedis.setJson("user:1", user, 3600)
        
        // JSONとして取得
        val retrieved = jedis.getJson<User>("user:1")
        println(retrieved?.name)
        
        // 条件付き設定
        val wasSet = jedis.setIfAbsent("lock:resource", "locked", 30)
        if (wasSet) {
            // ロック取得成功
        }
    }
}

エラーハンドリングとリトライ

import kotlinx.coroutines.delay
import redis.clients.jedis.exceptions.JedisConnectionException
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

class ResilientRedisClient(private val pool: JedisPool) {
    
    suspend fun <T> executeWithRetry(
        maxAttempts: Int = 3,
        initialDelay: Duration = 100.milliseconds,
        maxDelay: Duration = 1000.milliseconds,
        factor: Double = 2.0,
        operation: suspend (jedis: redis.clients.jedis.Jedis) -> T
    ): T {
        var currentDelay = initialDelay
        var lastException: Exception? = null
        
        repeat(maxAttempts) { attempt ->
            try {
                return withContext(Dispatchers.IO) {
                    pool.resource.use { jedis ->
                        operation(jedis)
                    }
                }
            } catch (e: JedisConnectionException) {
                lastException = e
                if (attempt < maxAttempts - 1) {
                    delay(currentDelay)
                    currentDelay = (currentDelay.inWholeMilliseconds * factor)
                        .coerceAtMost(maxDelay.inWholeMilliseconds)
                        .milliseconds
                }
            }
        }
        
        throw lastException ?: IllegalStateException("Retry failed")
    }
    
    // 使用例
    suspend fun getSafe(key: String): String? {
        return try {
            executeWithRetry { jedis ->
                jedis.get(key)
            }
        } catch (e: Exception) {
            // ログ記録など
            null
        }
    }
}

パフォーマンスのベストプラクティス

1. 接続プールの最適化

val optimizedPoolConfig = JedisPoolConfig().apply {
    // 接続プール設定
    maxTotal = 50                // 最大接続数
    maxIdle = 30                 // 最大アイドル接続数
    minIdle = 10                 // 最小アイドル接続数
    
    // 接続検証
    testOnBorrow = true          // 借用時の検証
    testOnReturn = false         // 返却時の検証(通常不要)
    testWhileIdle = true         // アイドル時の検証
    
    // タイムアウト設定
    maxWaitMillis = 3000         // 接続取得の最大待機時間
    
    // エビクション設定
    timeBetweenEvictionRunsMillis = 30000  // 30秒ごとにチェック
    minEvictableIdleTimeMillis = 60000      // 60秒アイドルで削除対象
    numTestsPerEvictionRun = 3              // 1回のチェックで検証する接続数
}

2. バッチ処理の活用

class OptimizedRedisOperations(private val pool: JedisPool) {
    
    // パイプラインを使用した高速バッチ処理
    suspend fun batchGet(keys: List<String>): List<String?> = 
        withContext(Dispatchers.IO) {
            pool.resource.use { jedis ->
                val pipeline = jedis.pipelined()
                val responses = keys.map { pipeline.get(it) }
                pipeline.sync()
                responses.map { it.get() }
            }
        }
    
    // Luaスクリプトを使用したアトミック操作
    suspend fun incrementWithLimit(key: String, limit: Long): Long? = 
        withContext(Dispatchers.IO) {
            pool.resource.use { jedis ->
                val script = """
                    local current = redis.call('GET', KEYS[1])
                    if not current then current = 0 else current = tonumber(current) end
                    if current >= tonumber(ARGV[1]) then
                        return nil
                    else
                        return redis.call('INCR', KEYS[1])
                    end
                """.trimIndent()
                
                val result = jedis.eval(script, 1, key, limit.toString())
                result as? Long
            }
        }
}

3. Android向けの最適化

class AndroidRedisManager(context: Context) {
    private val pool: JedisPool by lazy {
        val config = JedisPoolConfig().apply {
            // Androidでは控えめな設定を使用
            maxTotal = 5
            maxIdle = 3
            minIdle = 1
            // バッテリー消費を抑えるため、頻繁なヘルスチェックは避ける
            testWhileIdle = false
        }
        
        // 通常はローカルネットワークまたはVPN経由
        JedisPool(config, BuildConfig.REDIS_HOST, BuildConfig.REDIS_PORT)
    }
    
    // アプリケーションのライフサイクルに合わせて管理
    fun onAppDestroy() {
        pool.close()
    }
}

他のRedisクライアントとの比較

Jedis vs Lettuce

特徴JedisLettuce
同期/非同期同期のみ両方対応
パフォーマンス
使いやすさ
コルーチン統合要ラッパーネイティブ対応
メモリ効率
機能の豊富さ

Jedis vs Redisson

特徴JedisRedisson
APIの複雑さシンプル高機能
分散ロック手動実装組み込み
コレクションAPI基本的高度
ライブラリサイズ

まとめ

Jedisは、そのシンプルさと完全なRedis機能サポートにより、Java/Kotlinアプリケーションで広く使用されているRedisクライアントです。同期的な設計は理解しやすく、小規模から中規模のアプリケーションに最適です。ただし、高度な非同期処理が必要な場合は、Lettuceなどの非同期クライアントの使用を検討すべきです。Kotlinコルーチンと組み合わせる際は、必ずDispatchers.IOでラップすることを忘れないでください。