Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Scaling Read Tracking with Redis Bitmaps

2025年09月17日に「Dev.to」が公開したITニュース「Scaling Read Tracking with Redis Bitmaps」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

ユーザーの未読/既読状態をMySQLで管理すると、大量更新でDBがボトルネックになった。そこでRedisのビットマップを活用。カテゴリごとにビットマップを作成し、ユーザーIDで既読状態を管理する。ユーザーが読んだらビットを立て、新着があればビットマップを削除する。これにより、高速でスケーラブル、メモリ消費も少ないシステムが実現した。

出典: Scaling Read Tracking with Redis Bitmaps | Dev.to公開日:

ITニュース解説

ニュース記事で取り上げられているのは、「ユーザーがカテゴリを読んだかどうかを追跡する機能」という、一見するとシンプルな機能のシステム設計において発生した課題と、その解決策に関する事例である。この事例は、システムエンジニアを目指す上で、機能要件の深掘りや適切な技術選定がいかに重要であるかを教えてくれる良い教材となる。

初期設計における課題点

あるアプリケーションが、「ユーザーが各カテゴリの最新投稿を読んだかどうか」という状態を管理する機能を実装しようとした際、最初の設計ではMySQLという一般的なリレーショナルデータベースが採用された。具体的には、category_user_readというテーブルが作成され、このテーブルにはuser_id(ユーザーを一意に識別する番号)、category_id(カテゴリを一意に識別する番号)、そしてread(ユーザーがそのカテゴリを読んだかどうかを示す真偽値)の3つの情報が格納された。user_idcategory_idの組み合わせは、テーブル内で重複しないようにする主キーとして設定され、さらにuser_idは別のusersテーブルを参照する外部キーとしても設定されていた。

この設計における機能ロジックは簡潔だった。ユーザーがカテゴリを開くと、そのユーザーとそのカテゴリに対応する行のreadカラムがtrue(既読)に更新される。また、新しい投稿が追加された際には、そのカテゴリに関連する全ての行のreadカラムがfalse(未読)に一斉に更新されるというものだった。一見するとシンプルで理にかなっているように思われたこの設計は、実際にアプリケーションの運用が始まると深刻な問題を引き起こすことになった。

問題の根源は、データベースへの過剰な負荷と、それに伴う排他制御の競合、いわゆる「ロック」の発生にあった。ユーザーがカテゴリを開くたびにデータベースへの更新処理が発生し、さらに新しい投稿が追加されるたびに、場合によっては数百万という大量の行が一斉に更新されることになった。データベース管理システム(このケースではMySQLのInnoDBストレージエンジン)は、データの整合性を保つために、更新処理中に該当する行へのアクセスを一時的に制限する「ロック」という仕組みをかける。複数の更新処理が同時に実行されようとすると、これらのロックが互いに干渉し、処理の遅延や停止を引き起こす「競合状態」が発生した。特に、user_idが外部キーとして設定されていたことも問題を悪化させた。外部キー制約がある場合、データベースは更新時に参照整合性、つまり参照先のテーブルのデータが本当に存在するかどうかを確認する必要があり、このチェックもまた追加のロックを伴い、処理のボトルネックをさらに増大させた。結果として、数千人ものユーザーが同時にアプリケーションを利用するような状況では、このcategory_user_readテーブルがシステム全体のパフォーマンスを阻害する最大の要因となってしまったのである。

要件の再定義とRedis Bitmapアプローチ

このような運用上の問題に直面し、開発チームは当初の機能要件を改めて深く掘り下げることになった。その結果、判明したのは、アプリケーションが本当に必要としていたのは、「ユーザーがカテゴリを未読の場合に、タブに赤い点(通知)を表示する」という、よりシンプルな機能であったということだ。「最後に読んだ投稿ID」のような詳細な情報は不要であり、さらに、この未読・既読状態のデータは、多少の損失があっても許容できる程度の重要度であることが明らかになった。しかし、最も重要なのは、この機能が非常に高速に動作し、数百万ユーザー規模にも対応できるスケーラビリティを持つことであった。この要件の明確化が、全く新しい設計思想へとチームを導くことになったのである。

そこで提案されたのが、MySQLのような汎用的なリレーショナルデータベースではなく、インメモリデータストアであるRedisの「Bitmap(ビットマップ)」機能を利用するアプローチだった。Bitmapとは、0と1の値が並んだ巨大な配列のようなものをイメージすると良いだろう。この配列の各「位置」が特定のユーザーIDに対応し、その位置の値が0なら未読、1なら既読といった状態を表すことができる。

このRedis Bitmapアプローチでは、カテゴリごとに1つのBitmapがRedis上に作成される。そして、各ユーザーのuser_idを、このBitmapのインデックス(位置)として直接利用する。例えば、category:{カテゴリID}:readというキー名でBitmapを作成し、ユーザーIDが123のユーザーがそのカテゴリを読んだ場合、SETBIT category:{カテゴリID}:read 123 1というコマンドを実行することで、Bitmapの123番目のビットを1に設定する。ユーザーが読んだかどうかを確認するには、GETBIT category:{カテゴリID}:read 123というコマンドで、123番目のビットが0か1かを読み取るだけで良い。

新しい投稿が追加された際の未読状態へのリセットも、非常に効率的になった。以前のMySQL設計では、該当するカテゴリの全ユーザーの行をread = falseに更新するという大規模な処理が必要だったが、Redis Bitmapでは、DEL category:{カテゴリID}:readというコマンドで、そのカテゴリのBitmapキー自体を削除するだけでよい。Bitmapが削除されれば、そのカテゴリの全てのユーザーの状態はリセットされ、未読と見なされる。もしユーザーが再びカテゴリを開けば、その時点でビットが再設定されるため、この方法で全く問題ない。

このアプローチには、ユーザーIDが連続的でなく、最大値が非常に大きい場合に、Bitmapが実際のユーザー数よりも広大な領域を確保することになり、一部無駄なメモリが発生する可能性というトレードオフも存在する。しかし、現代のシステムにおいて、この程度のメモリオーバーヘッドは、得られるシンプルさと劇的なパフォーマンス向上に比べれば、小さな問題であることが多い。

Redis Bitmapによる改善結果

Redis Bitmapを採用した結果、アプリケーションのパフォーマンスは劇的に向上した。

第一に、RedisのBitmap操作は非常に高速であり、その処理時間はデータの量やユーザー数にほとんど影響されない「定数時間」で完了する。これは、以前のようなデータベースのロック競合の問題が完全に解消されたことを意味する。

第二に、メモリ使用量が非常に効率的である。1つのビットで1人のユーザーの状態を表現できるため、例えば100万人のユーザーの既読・未読状態を追跡するのに、約122KBという極めて少ないメモリしか消費しない。数千万人のユーザーがいても、メモリ使用量はMB単位に収まるため、コスト効率も非常に高い。

第三に、スケーラビリティが大幅に向上した。Redisはインメモリデータベースであり、本来的に多数の同時アクセスに強い特性を持つ。大量のユーザーが同時にカテゴリを開いたとしても、Redisはロックによる競合なしに、それらのリクエストを高速に処理できる。これは、アプリケーションが将来的にさらに多くのユーザーを抱えることになっても、この未読・既読追跡機能がボトルネックになる心配がないことを示している。

この事例は、システム設計において、機能の実際の要件を正確に把握し、その要件に最も適した技術を選択することの重要性を明確に示している。初期設計では汎用的なデータベースで全てを解決しようとした結果、スケーラビリティの問題に直面したが、RedisのBitmapのような特定の用途に特化したデータ構造を適切に利用することで、問題を効率的かつ高速に解決できたのである。システムエンジニアを目指す上では、このように幅広い技術の特性を理解し、状況に応じて最適な選択ができるようになることが求められる。

関連コンテンツ