分散システムにおける同時実行制御において、RedisのSETNXコマンドは重要な役割を果たします。特にユーザーごとの操作排他制御が必要な場面で効果を発揮します。
SETNXによる排他制御の基本実装
以下は、ユーザーが特定のアクティビティでギフトを受け取る際の排他制御の実装例です:
public String obtainReward(int eventId, int rewardId, String userId) {
if (validateEvent(eventId, rewardId)) {
if (!hasUserObtained(userId, eventId, rewardId)) {
try {
String lockKey = generateLockKey(userId, eventId, rewardId);
if (redis.setNX(lockKey, "locked")) {
boolean distributionResult = callRewardService("allocateReward");
if (distributionResult) {
updateUserRecord(userId, eventId, rewardId);
return "受け取り成功";
} else {
redis.del(lockKey);
return "受け取り失敗";
}
} else {
return "既に受け取っています";
}
} catch (Exception ex) {
logError(ex);
return "システムエラー";
}
}
}
return "アクティビティが存在しません";
}
SETNX使用時の重要な考慮事項
1. キーの有効期限設定
SETNXのみを使用する場合、キーに有効期限が設定されていないため、メモリ不足の原因となります。Redis 2.6.12以降ではSETコマンドで原子性的に設定可能です:
// 推奨される実装
String result = redis.set(lockKey, "locked", "NX", "EX", 300);
2. サービス呼び出し時の例外処理
外部サービス呼び出し中にタイムアウトが発生した場合、ロックが解放されない可能性があります。このような状況では、監視システムによる検出と手動対応が必要です。
3. データ不整合への対応
Redisとデータベース間で不整合が生じた場合、定期的な整合性チェックを実施する必要があります。ただし、全キースキャンは負荷が高いため、増分チェックを推奨します。
キャッシュ破壊対策への応用
SETNXはホットデータのキャッシュ破壊対策にも有効です。キャッシュ失效時に複数リクエストがデータベースにアクセスするのを防止できます:
public Object getHotData(String dataKey) {
Object data = redis.get(dataKey);
if (data == null) {
String lockKey = "lock:" + dataKey;
if (redis.set(lockKey, "1", "NX", "EX", 10)) {
try {
data = fetchFromDatabase(dataKey);
redis.set(dataKey, data, "EX", 3600);
} finally {
redis.del(lockKey);
}
} else {
// 他のスレッドがデータ取得中なので待機
Thread.sleep(50);
return getHotData(dataKey);
}
}
return data;
}
この実装では、データベースへのアクセスを単一スレッドに制限し、システム負荷を軽減します。ロックの有効期限設定により、デッドロックの発生も防止します。