Spring Data Redis の ZSetOperations.remove(K key, Object… values) を使っても、値を「更新した後」に削除しようとすると 該当メンバーが見つからず削除できない という現象が発生した。本記事では再現手順と回避策を示す。
事象の概要
- 新規レコードを追加(
addData) - 一部フィールドを変更して再追加(
updateData) - 更新後の値を指定して削除(
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 に分離することで、確実な削除が可能になる。