ডিস্ট্রিবিউটেড সিস্টেমে রেট লিমিটিং ইমপ্লিমেন্ট করার জন্য Redis হচ্ছে সবচেয়ে জনপ্রিয় এবং কার্যকর টুল। আগের উদাহরণগুলোতে আমরা যে রেট লিমিটিং দেখেছিলাম, তা ছিল 'In-memory' বা মেমরি-বেইজড।
ইন-মেমরি রেট লিমিটিংয়ের সমস্যা কোথায়? ধরুন, আপনার অ্যাপ্লিকেশনের ৩টি সার্ভার (Server A, Server B, Server C) চলছে এবং সামনে একটি লোড ব্যালান্সার আছে। একজন ইউজার যদি লিমিট ক্রস করে রিকোয়েস্ট পাঠায়, লোড ব্যালান্সার হয়তো প্রথম রিকোয়েস্ট Server A-তে, দ্বিতীয়টি Server B-তে পাঠাবে। যেহেতু প্রতিটি সার্ভার তার নিজের লোকাল মেমরিতে কাউন্ট রাখছে, তাই ইউজার সহজেই রেট লিমিট বাইপাস করে ফেলতে পারবে।
এই সমস্যার সমাধান হলো একটি সেন্ট্রালাইজড ডেটাবেস ব্যবহার করা, যেখানে সব সার্ভার রিকোয়েস্টের কাউন্ট চেক করবে। এই কাজের জন্য Redis সবচেয়ে উপযুক্ত।
কেন Redis?
১. অত্যন্ত দ্রুত (In-memory Caching): ডিস্ক-বেইজড ডেটাবেসের (যেমন PostgreSQL/MySQL) তুলনায় Redis মেমরিতে কাজ করে বলে এর রেসপন্স টাইম মিলি-সেকেন্ডের চেয়েও কম। ২. অ্যাটমিক অপারেশন (Atomic Operations): Redis-এর INCR (Increment) এবং EXPIRE কমান্ডগুলো অ্যাটমিক। এর মানে হলো, একাধিক সার্ভার একই সাথে কাউন্ট বাড়ালেও ডেটা ওভারল্যাপ (Race condition) হয় না।
Redis দিয়ে Fixed Window Counter ইমপ্লিমেন্টেশন
সবচেয়ে সহজে Redis দিয়ে 'Fixed Window Counter' অ্যালগরিদম ইমপ্লিমেন্ট করা যায়।
কীভাবে কাজ করে? ১. যখনই কোনো ইউজার রিকোয়েস্ট পাঠায়, আমরা বর্তমান সময় (Time Window) এবং ইউজারের ID দিয়ে একটি ইউনিক Redis Key তৈরি করি। (যেমন: rate_limit:user_123:1696420800) ২. আমরা Redis-এর INCR কমান্ড ব্যবহার করে ওই Key-এর ভ্যালু ১ বাড়িয়ে দিই। ৩. মেমরি বাঁচানোর জন্য Key-টির একটি নির্দিষ্ট EXPIRE টাইম সেট করে দিই। ৪. যদি Key-এর ভ্যালু আমাদের লিমিটের চেয়ে বেশি হয়, তবে রিকোয়েস্ট ব্লক করে দিই।
Python কোড উদাহরণ (Redis-py ব্যবহার করে)
নিচে পাইথন এবং Redis ব্যবহার করে একটি ডিস্ট্রিবিউটেড রেট লিমিটিংয়ের উদাহরণ দেওয়া হলো:
Python
import redis
import time
# Create a connection to the Redis server
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
def is_rate_limited(user_id, limit_per_minute):
# Divide the current time into 60-second windows
current_minute = int(time.time()) // 60
# Generate a unique Redis key for the user
redis_key = f"rate_limit:{user_id}:{current_minute}"
# Use a pipeline to ensure INCR and EXPIRE execute atomically
pipeline = redis_client.pipeline()
pipeline.incr(redis_key)
# Set expiration to 65 seconds to automatically clear the key from memory
pipeline.expire(redis_key, 65)
# Execute the commands and get the result
result = pipeline.execute()
# Get the result of the INCR command (number of requests in the current window)
current_request_count = result[0]
if current_request_count > limit_per_minute:
return True # Rate limit exceeded, block the request
else:
return False # Within the limit, allow the request
# Testing
user = "user_999"
limit = 5 # Maximum 5 requests per minute
for i in range(7):
if is_rate_limited(user, limit):
print(f"Request {i+1}: Blocked (Rate Limit Exceeded)")
else:
print(f"Request {i+1}: Allowed")অ্যাডভান্সড পদ্ধতি: Redis Lua Script
উপরের পদ্ধতিতে Pipeline ব্যবহার করলেও খুব হাই-ট্রাফিক সিস্টেমে (High Concurrency) ছোটখাটো রেইস কন্ডিশন (Race Condition) হতে পারে। প্রোডাকশন গ্রেড সিস্টেমে (যেমন: API Gateway) এই সমস্যা সমাধানের জন্য Lua Script ব্যবহার করা হয়।
Redis-এর ভেতরে Lua স্ক্রিপ্ট রান করলে পুরো স্ক্রিপ্টটি একটি সিঙ্গেল অ্যাটমিক অপারেশন হিসেবে কাজ করে। ফলে ডেটা ইনকনসিস্টেন্সি হওয়ার কোনো সুযোগ থাকে না।
Sliding Window Log (ZSET ব্যবহার করে)
Fixed Window এর একটি বড় সমস্যা হলো Burst Traffic (উইন্ডোর শেষে এবং শুরুতে একসাথে অনেক রিকোয়েস্ট আসা)। এই সমস্যা এড়াতে Redis এর Sorted Sets (ZSET) ব্যবহার করে Sliding Window ইমপ্লিমেন্ট করা যায়।
এক্ষেত্রে: ১. ইউজারের ID দিয়ে একটি ZSET তৈরি করা হয়। ২. রিকোয়েস্ট আসার সাথে সাথে বর্তমান টাইমস্ট্যাম্প (Timestamp) ওই সেটে স্কোর (Score) হিসেবে সেভ করা হয়। ৩. নির্দিষ্ট উইন্ডোর (যেমন গত ৬০ সেকেন্ড) আগের সব টাইমস্ট্যাম্প রিমুভ (ZREMRANGEBYSCORE) করে দেওয়া হয়। ৪. এরপর সেটে কতগুলো এন্ট্রি আছে তা গণনা (ZCARD) করা হয়। যদি তা লিমিটের বেশি হয়, রিকোয়েস্ট ব্লক করা হয়।
বড় বড় টেক কোম্পানিগুলো তাদের এপিআই রেট লিমিটিংয়ের জন্য এভাবেই Redis-কে কোর কম্পোনেন্ট হিসেবে ব্যবহার করে।