kagamihogeの日記

kagamihogeの日記です。

spring-bootのRedisTemplateでkeyなどが\xac\xed\x00\x05になる原因と解決方法

現象

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にある。詳細は省略するが、下記のとおり標準ライブラリのObjectOutputStreamByteArrayOutputStreamを使用する。

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仕様に基づきバイトバッファの先頭にヘッダーが書き込まれているのが分かる。

関連ソースコード

関連URL