Redisビットマップを使った月間チェックイン機能のJava実装

ポイントサービスやキャンペーンアプリでよくある「本日のチェックイン」機能を、Redis のビットマップ機能を使って高速・省メモリに実装する方法を解説します。Redis の SETBIT / BITFIELD を活用することで、1 ユーザーあたり月 31 日分のデータをわずか 4 byte で保持できます。

設計方針

  • 1 ビット = 1 日の出欠を表現
  • キーは checkin:{userId}:{yyyyMM} の形式
  • offset は「その月の 0 から始まる日数」
  • 集計は BITFIELD で一括取得し、ビットカウントで総数算出

チェックイン処理

public void recordCheckIn() {
    Long uid = AuthContext.currentUserId();
    LocalDate today = LocalDate.now();
    String key = "checkin:" + uid + ":" + today.format(DateTimeFormatter.BASIC_ISO_DATE).substring(0, 6);
    int offset = today.getDayOfMonth() - 1;

    redisTemplate.opsForValue().setBit(key, offset, true);
}

上記コードでは、setBit を呼ぶだけで該当 offset に 1 を立てます。初回アクセス時に Redis は内部的に必要なバイト列を自動生成するため、事前の初期化は不要です。

当月のチェックイン日数を取得

public int checkedDaysThisMonth() {
    Long uid = AuthContext.currentUserId();
    LocalDate today = LocalDate.now();
    String key = "checkin:" + uid + ":" + today.format(DateTimeFormatter.BASIC_ISO_DATE).substring(0, 6);
    int dayLimit = today.getDayOfMonth();

    List<Long> segment = redisTemplate.opsForValue()
            .bitField(key, BitFieldSubCommands.create()
                    .get(BitFieldSubCommands.BitFieldType.unsigned(dayLimit))
                    .valueAt(0));

    if (segment == null || segment.isEmpty()) return 0;

    long bits = segment.get(0);
    return Long.bitCount(bits);
}

Long.bitCount を使うことで、ループ不要で立っているビット数(=チェックイン済み日数)を O(1) で取得できます。

連続チェックイン日数を求める例

public int consecutiveDays() {
    Long uid = AuthContext.currentUserId();
    LocalDate today = LocalDate.now();
    String key = "checkin:" + uid + ":" + today.format(DateTimeFormatter.BASIC_ISO_DATE).substring(0, 6);
    int dayLimit = today.getDayOfMonth();

    List<Long> segment = redisTemplate.opsForValue()
            .bitField(key, BitFieldSubCommands.create()
                    .get(BitFieldSubCommands.BitFieldType.unsigned(dayLimit))
                    .valueAt(0));

    if (segment == null || segment.isEmpty()) return 0;

    long bits = segment.get(0);
    int streak = 0;
    while ((bits & 1L) == 1) {
        streak++;
        bits >>>= 1;
    }
    return streak;
}

ここでは最下位ビットから連続する 1 をカウントすることで、今日から逆算した連続チェックイン日数を得ます。

メモリ効率の目安

期間ビット数バイト数
1 ヶ月314
1 年36546
100 万ユーザー×1 年3.65 億約 44 MB

ビットマップはスパースでも内部的に intset のように圧縮されないため、あらかじめ想定ユーザーを見越したキー設計を行うと管理が楽になります。

応用例

  • 曜日別出欠集計:月跨ぎで 7 ビットごとにマスクを取れば OK
  • ミニゲームの「1 日 1 回」報告受付
  • アドベントカレンダー形式の限定コンテンツ解放

タグ: redis bitmap Java Spring Data Redis SETBIT

7月16日 17:12 投稿