本稿は、高負荷なセールスフラッシュ(秒殺)シナリオにおけるパフォーマンスを向上させるためのRedisを活用したアプローチについて解説します。特に、Redisのメッセージキュー機能を利用した非同期処理を実装し、システムの応答性とスループットを改善する方法を中心に説明します。
問題の提起
伝統的なセールスフラッシュシステムでは、ユーザーが注文リクエストを送信すると、一連のデータベース操作が直列で実行されます。典型的なフローは以下の通りです。
- クーポン情報の照会
- 在庫数の確認
- 注文情報の照会
- 一人一注文のルールチェック
- 在庫の減算
- 注文の作成
これらのステップの多くはデータベースへのアクセスを伴い、直列実行されるため、システム全体の応答時間が長くなります。このボトルネックを解消するため、Redisを用いた高速なロジックチェックと、メッセージキューを利用した非同期処理の組み合わせが有効です。
Redisによる非同期処理の実装
最適化の核心は、ユーザーへの応答を速やかに行うことです。まず、Redisで在庫と注文履歴を確認し、注文が可能かどうかを判断します。条件を満たせば、即座にユーザーに成功レスポンスを返し、実際の注文処理はバックグラウンドで行います。これにより、フロントエンドの応答時間が大幅に短縮されます。
Redisでの在庫と注文チェック
在庫と注文履歴のチェックは、RedisのLuaスクリプトを用いてアトミックに実行します。これにより、競合状態を防ぎ、データの一貫性を保ちます。
Luaスクリプトの実装
-- パラメータ
local couponId = ARGV[1] -- クーポンID
local customerId = ARGV[2] -- ユーザーID
-- キー定義
local stockKey = 'seckill:stock:' .. couponId
local orderKey = 'seckill:order:' .. couponId
-- ロジック
-- 1. 在庫が十分か確認
if (tonumber(redis.call('get', stockKey)) <= 0) then
-- 在庫不足の場合、エラーコード1を返す
return 1
end
-- 2. ユーザーが既に注文したか確認
if(redis.call('sismember', orderKey, customerId) == 1) then
-- 重複注文の場合、エラーコード2を返す
return 2
end
-- 3. 在庫を減らす
redis.call('decr', stockKey)
-- 4. 注文を記録する
redis.call('sadd', orderKey, customerId)
-- 成功した場合、エラーコード0を返す
return 0
Javaでのメッセージキュー実装
Redisでのチェックに成功した注文は、バックグラウンドで処理するためにブロッキングキューに追加されます。以下は、Spring Bootを用いた実装例です。
@Service
public class SeckillOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedissonClient redissonClient;
private static final DefaultRedisScript<Long> STOCK_CHECK_SCRIPT;
static {
STOCK_CHECK_SCRIPT = new DefaultRedisScript<>();
STOCK_CHECK_SCRIPT.setLocation(new ClassPathResource("stock_check.lua"));
STOCK_CHECK_SCRIPT.setResultType(Long.class);
}
// 注文処理用のブロッキングキュー
private final BlockingQueue<OrderTask> orderQueue = new ArrayBlockingQueue<>(1024 * 1024);
// シングルスレッドの実行サービス
private static final ExecutorService ORDER_PROCESSOR_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct
public void init() {
ORDER_PROCESSOR_EXECUTOR.submit(new OrderProcessor());
}
// 注文処理タスク
private class OrderProcessor implements Runnable {
@Override
public void run() {
while (true) {
try {
// キューから注文タスクを取得
OrderTask orderTask = orderQueue.take();
// 注文を処理
processOrder(orderTask);
} catch (Exception e) {
log.error("注文処理中にエラーが発生しました", e);
}
}
}
}
private void processOrder(OrderTask orderTask) {
// 注文処理のロジックを実装
// ...
}
}