Redis Sorted Set 削除が効かない原因と回避策

Spring Data Redis の ZSetOperations.remove(K key, Object… values) を使っても、値を「更新した後」に削除しようとすると 該当メンバーが見つからず削除できない という現象が発生した。本記事では再現手順と回避策を示す。

事象の概要

  1. 新規レコードを追加(addData
  2. 一部フィールドを変更して再追加(updateData
  3. 更新後の値を指定して削除(removeData

3 の時点で remove の戻り値は 1 だが、実際には「更新前の古い値」が残ってしまう。

// 1. 追加
{date=[2024-04-13], rate1=[20], rate2=[20], id=[20]}

// 2. 更新
{date=[2024-04-13], rate1=[22], rate2=[22], id=[20]}

// 3. 削除後(?)に残るのは古い値
{date=[2024-04-13], rate1=[20], rate2=[20], id=[20]}

なぜ起きるか

Redis の Sorted Set は「値そのもの」をキーして削除する。Spring Data Redis はデフォルトで JdkSerializationRedisSerializer を使うため、同一 Java オブジェクトであってもフィールド値が異なるとシリアライズ結果が変わり、Redis 上では別のメンバーとして扱われる

結果として「更新後のオブジェクト」を指定して remove しても、Redis 側では「存在しない値」を探すことになり、削除できない。

回避策:更新時に必ず削除→追加

更新処理を「削除してから追加」に変更することで、同一 id を持つレコードが 2 重に残ることを防ぐ。

public boolean upsert(Integer userId, EldData oldData, EldData newData) {
    String key = ELD_DATA + userId;

    // 1. 古い値を削除
    long removed = redisTemplate.opsForZSet().remove(key, oldData);
    if (removed == 0) {
        // 既存レコードが存在しない場合は失敗
        return false;
    }

    // 2. 新しい値を追加
    return redisTemplate.opsForZSet().add(key, newData, newData.getDate().getTime());
}

これにより、同一 id のレコードは常に 1 件だけ保持される。

完全に同一性を保証するなら Redis Key を変える

上記の方法でも「oldDataの値が Redis に存在するとは限らない」という問題が残る。より確実にするには Redis のメンバー値を id だけにして、本体は Hash で管理 する設計に変更する。

// Sorted Set には id のみ格納(スコアは日付の epoch)
redisTemplate.opsForZSet().add(ELD_DATA + userId, data.getId(), data.getDate().getTime());

// 実データは Hash で保存
redisTemplate.opsForHash().put(ELD_DATA_BODY + userId, data.getId(), data);

削除時は id だけを指定すれば必ず削除できる。

public long remove(Integer userId, String id) {
    redisTemplate.opsForZSet().remove(ELD_DATA + userId, id);
    redisTemplate.opsForHash().delete(ELD_DATA_BODY + userId, id);
}

まとめ

Spring Data Redis の ZSetOperations.remove は「値の完全一致」で削除するため、フィールドが変わると削除できない。更新処理で「削除→追加」を行うか、データ構造を Sorted Set + Hash に分離することで、確実な削除が可能になる。

タグ: redis Spring Data Redis Sorted Set Java Serialization

5月11日 15:03 投稿