現象
Spring Data RedisのRedisTemplate
でstreamのkeyを正しく指定しているはずなのに想定通りに動作しない。下記のようなXREADGROUP
コマンド相当を実行するとエラーになり、何故かkeyが文字化けしたエラーメッセージになる。
plugins { id 'java' id 'org.springframework.boot' version '3.5.3' id 'io.spring.dependency-management' version '1.1.7' } group = 'com.example' version = '0.0.1-SNAPSHOT' java { toolchain { languageVersion = JavaLanguageVersion.of(21) } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() }
List read = redisTemplate.opsForStream().read( Consumer.from("my_group1", "Alice") , StreamOffset.latest("mystream"));
Caused by: io.lettuce.core.RedisCommandExecutionException: NOGROUP No such key '��' or consumer group 'my_group1' in XREADGROUP with GROUP option
実際にredis-cli
で確認すると、先頭に良く分からないものが付加されたkeyが存在する。
127.0.0.1:6379> SCAN 0 TYPE stream 1) "0" 2) 1) "mystream2" 2) "\xac\xed\x00\x05t\x00\bmystream" 3) "mystream" 4) "\xac\xed\x00\x05t\x00\tmy-stream"
また、フィールドのkeyとvalueも同様な現象になる。
127.0.0.1:6379> XINFO STREAM "\xac\xed\x00\x05t\x00\x0cauto-created" 1) "length" (略) 17) "first-entry" 18) 1) "1752218756912-0" 2) 1) "\xac\xed\x00\x05t\x00\x01a" 2) "\xac\xed\x00\x05t\x00\x01b" 19) "last-entry" 20) 1) "1752222436239-0" 2) 1) "\xac\xed\x00\x05t\x00\x01a" 2) "\xac\xed\x00\x05t\x00\x01b"
解決策
https://qiita.com/taka_22/items/673bb2e6bf7d4a303447 にあるとおり、何らかの手段でRedisTemplate
のserializerを置き換えれば良い。
原因
RedisTemplate
デフォルトのシリアライザーはJdkSerializationRedisSerializer
で、これはjava object serialization仕様に沿った変換をする。stringのkeyをbyte[]
に変換したものをredisコマンドのkeyとして使用する。このとき、java object serialization仕様のバイトヘッダーが書き込まれてしまうので妙な事になってしまう。
詳細
まず、RedisTemplate
のデフォルトのシリアライザーは下記のようにJdkSerializationRedisSerializer
が使用される。
public class RedisTemplate<K, V> ... // (略) public void afterPropertiesSet() { super.afterPropertiesSet(); if (this.defaultSerializer == null) { this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader()); }
このJdkSerializationRedisSerializer
を辿るとSerializingConverter
, DefaultSerializer
を使用している。ここまではspringのクラス。
public class JdkSerializationRedisSerializer ... // (略) public JdkSerializationRedisSerializer(@Nullable ClassLoader classLoader) { this(new SerializingConverter(), new DeserializingConverter(classLoader));
public class SerializingConverter implements Converter<Object, byte[]> { private final Serializer<Object> serializer; public SerializingConverter() { this.serializer = new DefaultSerializer();
具体的なシリアライズの実装はDefaultSerializer
にある。詳細は省略するが、下記のとおり標準ライブラリのObjectOutputStream
とByteArrayOutputStream
を使用する。
public class DefaultSerializer ... @Override public void serialize(Object object, OutputStream outputStream) throws IOException { // (略) ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); objectOutputStream.writeObject(object); objectOutputStream.flush(); }
@FunctionalInterface public interface Serializer<T> { void serialize(T object, OutputStream outputStream) throws IOException; default byte[] serializeToByteArray(T object) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(1024); this.serialize(object, out); return out.toByteArray(); } }
ObjectOutputStream
のコンストラクタでバッファ先頭に"StreamHeader"と呼ばれるものを書き込んでいる。
public ObjectOutputStream(OutputStream out) throws IOException { // (略) writeStreamHeader(); protected void writeStreamHeader() throws IOException { bout.writeShort(STREAM_MAGIC); bout.writeShort(STREAM_VERSION);
"StreamHeader"の具体的な定義値は以下の通り。redis-cli
で確認した\xac\xed\x00\x05
と一致しているのが分かる。
public interface ObjectStreamConstants { /** * Magic number that is written to the stream header. */ static final short STREAM_MAGIC = (short)0xaced; /** * Version number that is written to the stream header. */ static final short STREAM_VERSION = 5;
というわけで、JavaのObject Serialization仕様に基づきバイトバッファの先頭にヘッダーが書き込まれているのが分かる。
関連ソースコード
- https://github.com/spring-projects/spring-data-redis/blob/main/src/main/java/org/springframework/data/redis/core/RedisTemplate.java
- https://github.com/spring-projects/spring-data-redis/blob/main/src/main/java/org/springframework/data/redis/serializer/JdkSerializationRedisSerializer.java
- https://github.com/spring-projects/spring-framework/blob/main/spring-core/src/main/java/org/springframework/core/serializer/support/SerializingConverter.java
- https://github.com/spring-projects/spring-framework/blob/main/spring-core/src/main/java/org/springframework/core/serializer/DefaultSerializer.java
- https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/io/ObjectOutputStream.java
- https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/io/ObjectStreamConstants.java