Spring Boot 2系でRedisクラスタおよびシングルノードを統合する方法

Spring Boot 2.3.0.RELEASE をベースに、Redisのシングルノードまたはクラスタモードを統合する手順を示します。

Maven依存設定(pom.xml)

<!-- Redisサポート -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- JSONシリアライズ用 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.9</version>
</dependency>

アプリケーション設定(application.yml)

spring:
  redis:
    timeout: 6000ms
    password: ''
    # シングルノードの場合
    # host: 127.0.0.1
    # port: 6379
    # クラスタモードの場合
    cluster:
      nodes:
        - 127.0.0.1:6379
        - 127.0.0.1:6380
        - 127.0.0.1:6381
    lettuce:
      pool:
        max-idle: 8
        min-idle: 0
        max-active: 8
        max-wait: -1ms

Lettuceは非同期・ノンブロッキングな接続方式で、Nettyを基盤としており、現在推奨されています。Jedisは同期ブロッキング方式です。

Redis設定クラス

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisSetup {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connFactory) {
        var keySerializer = new StringRedisSerializer();
        var valueSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
        valueSerializer.setObjectMapper(mapper);

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connFactory);
        template.setKeySerializer(keySerializer);
        template.setValueSerializer(valueSerializer);
        template.setHashKeySerializer(keySerializer);
        template.setHashValueSerializer(valueSerializer);
        template.afterPropertiesSet();

        return template;
    }
}

Redis操作ユーティリティ

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public class RedisHelper {

    @Autowired
    private RedisTemplate<String, Object> rt;

    public boolean setExpiry(String k, long sec) {
        try {
            if (sec > 0) rt.expire(k, sec, TimeUnit.SECONDS);
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
            return false;
        }
    }

    public long getTTL(String k) {
        return rt.getExpire(k, TimeUnit.SECONDS);
    }

    public boolean exists(String k) {
        try { return rt.hasKey(k); } 
        catch (Exception ex) { ex.printStackTrace(); return false; }
    }

    public void remove(String... keys) {
        if (keys != null && keys.length > 0) {
            if (keys.length == 1) rt.delete(keys[0]);
            else rt.delete(java.util.Arrays.asList(keys));
        }
    }

    // --- 基本値操作 ---
    public Object fetch(String k) { return k == null ? null : rt.opsForValue().get(k); }

    public boolean store(String k, Object v) {
        try { rt.opsForValue().set(k, v); return true; } 
        catch (Exception ex) { ex.printStackTrace(); return false; }
    }

    public boolean storeWithExpiry(String k, Object v, long sec) {
        try {
            if (sec > 0) rt.opsForValue().set(k, v, sec, TimeUnit.SECONDS);
            else store(k, v);
            return true;
        } catch (Exception ex) { ex.printStackTrace(); return false; }
    }

    public long increment(String k, long delta) {
        if (delta < 0) throw new RuntimeException("増分は正数である必要があります");
        return rt.opsForValue().increment(k, delta);
    }

    public long decrement(String k, long delta) {
        if (delta < 0) throw new RuntimeException("減分は正数である必要があります");
        return rt.opsForValue().increment(k, -delta);
    }

    // --- ハッシュ操作 ---
    public Object hashGet(String k, String field) { return rt.opsForHash().get(k, field); }

    public Map<Object, Object> hashFetchAll(String k) { return rt.opsForHash().entries(k); }

    public boolean hashStore(String k, Map<String, Object> data) {
        try { rt.opsForHash().putAll(k, data); return true; } 
        catch (Exception ex) { ex.printStackTrace(); return false; }
    }

    public boolean hashStoreWithExpiry(String k, Map<String, Object> data, long sec) {
        try {
            rt.opsForHash().putAll(k, data);
            if (sec > 0) setExpiry(k, sec);
            return true;
        } catch (Exception ex) { ex.printStackTrace(); return false; }
    }

    public boolean hashPut(String k, String field, Object v) {
        try { rt.opsForHash().put(k, field, v); return true; } 
        catch (Exception ex) { ex.printStackTrace(); return false; }
    }

    public boolean hashPutWithExpiry(String k, String field, Object v, long sec) {
        try {
            rt.opsForHash().put(k, field, v);
            if (sec > 0) setExpiry(k, sec);
            return true;
        } catch (Exception ex) { ex.printStackTrace(); return false; }
    }

    public void hashRemove(String k, Object... fields) { rt.opsForHash().delete(k, fields); }

    public boolean hashContains(String k, String field) { return rt.opsForHash().hasKey(k, field); }

    public double hashIncrement(String k, String field, double by) { return rt.opsForHash().increment(k, field, by); }

    public double hashDecrement(String k, String field, double by) { return rt.opsForHash().increment(k, field, -by); }

    // --- セット操作 ---
    public Set<Object> setMembers(String k) { return rt.opsForSet().members(k); }

    public boolean setContains(String k, Object val) { return rt.opsForSet().isMember(k, val); }

    public long setAdd(String k, Object... vals) { return rt.opsForSet().add(k, vals); }

    public long setAddWithExpiry(String k, long sec, Object... vals) {
        Long added = rt.opsForSet().add(k, vals);
        if (sec > 0) setExpiry(k, sec);
        return added != null ? added : 0L;
    }

    public long setSize(String k) { return rt.opsForSet().size(k); }

    public long setRemove(String k, Object... vals) { return rt.opsForSet().remove(k, vals); }

    // --- リスト操作 ---
    public List<Object> listRange(String k, long start, long end) { return rt.opsForList().range(k, start, end); }

    public long listSize(String k) { return rt.opsForList().size(k); }

    public Object listIndex(String k, long idx) { return rt.opsForList().index(k, idx); }

    public boolean listPush(String k, Object val) {
        try { rt.opsForList().rightPush(k, val); return true; } 
        catch (Exception ex) { ex.printStackTrace(); return false; }
    }

    public boolean listPushWithExpiry(String k, Object val, long sec) {
        try {
            rt.opsForList().rightPush(k, val);
            if (sec > 0) setExpiry(k, sec);
            return true;
        } catch (Exception ex) { ex.printStackTrace(); return false; }
    }

    public boolean listPushAll(String k, List<Object> vals) {
        try { rt.opsForList().rightPushAll(k, vals); return true; } 
        catch (Exception ex) { ex.printStackTrace(); return false; }
    }

    public boolean listPushAllWithExpiry(String k, List<Object> vals, long sec) {
        try {
            rt.opsForList().rightPushAll(k, vals);
            if (sec > 0) setExpiry(k, sec);
            return true;
        } catch (Exception ex) { ex.printStackTrace(); return false; }
    }

    public boolean listUpdate(String k, long idx, Object val) {
        try { rt.opsForList().set(k, idx, val); return true; } 
        catch (Exception ex) { ex.printStackTrace(); return false; }
    }

    public long listRemove(String k, long count, Object val) { return rt.opsForList().remove(k, count, val); }
}

RedisTemplateの直接利用

ユーティリティクラスを使わずに、直接RedisTemplateをインジェクトして操作することも可能です:

@Autowired
private RedisTemplate<String, Object> redisTemplate;

テスト例

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class RedisIntegrationTest {

    @Resource
    private RedisHelper redisHelper;

    @Test
    public void verifyStorage() {
        boolean success = redisHelper.store("sample-key", "Hello Redis");

        if (success) {
            System.out.println(redisHelper.fetch("sample-key"));
        } else {
            System.out.println("保存に失敗しました");
        }
    }
}

タグ: SpringBoot redis Lettuce

7月22日 20:47 投稿