সরাসরি পর্বে যান
পর্ব ৫৮ · ৬৭-এর মধ্যে · মডিউল ৯

Redis — in-memory key-value আর cache

Redis — In-Memory Data Structures, TTL, Caching Patterns, Stampedes, Eviction, Rate Limiting, Locks, Persistence and Cluster

Taught in Bangla, this lesson covers Redis: an in-memory data structure server, key naming, TTL, strings and hashes, lists, sets and sorted sets for queues and leaderboards, streams, pub/sub, bitmaps and HyperLogLog, caching patterns from cache-aside to write-behind, cache stampedes and eviction, rate limiting and distributed locks, persistence, replication, Sentinel and Cluster, and licensing with Valkey and Memcached.

এই পর্বে যা শিখবেন

  • Redis কী, কেন এত দ্রুত, আর single-threaded মডেলের সুবিধা ও বিপদ
  • ছয়টি মূল data type-এর কমান্ড আর ব্যবহার — session, queue, unique visitor, leaderboard
  • Cache-aside, read-through, write-through আর write-behind; invalidation, stampede আর eviction
  • Rate limiting (fixed আর sliding window) আর distributed lock-এর সঠিক রূপ ও সীমাবদ্ধতা
  • RDB আর AOF, replication, Sentinel, Cluster-এর ১৬,৩৮৪টি hash slot, লাইসেন্স বিতর্ক আর নিরাপত্তা

১ · Redis কী — মেমরিতে রাখা data structure server

পর্ব ৫৬-এ NoSQL-এর চার পরিবার দেখেছেন; তার সবচেয়ে সরল পরিবার key-value store. Redis সেই পরিবারের সবচেয়ে পরিচিত সদস্য, তবে সাধারণ key-value-এর চেয়ে বেশি: মান শুধু একটি লেখা নয়, একটি পূর্ণ data structure — তালিকা, সেট, ক্রম-সাজানো সেট। তাই Redis নিজেকে বলে “data structure server”. নামটি এসেছে REmote DIctionary Server থেকে। ইতালির প্রোগ্রামার Salvatore Sanfilippo (antirez) ২০০৯ সালে একটি real-time ওয়েব-বিশ্লেষণ প্রকল্পের প্রয়োজনে এটি লেখা শুরু করেন।

পাড়ার মুদি দোকানের কথা ভাবুন। চাল, ডাল, তেল — সবচেয়ে বেশি বিক্রি হওয়া জিনিস দোকানদারের হাতের কাছের তাকে; বাকি সব পেছনের গুদামে। ক্রেতা চাল চাইলে হাত বাড়ালেই হলো, গুদামে যেতে হয় না। কিন্তু তাক ছোট — সব জিনিস রাখা যায় না, আর রাতে চুরি হলে তাকের মাল হারায়, গুদামের খাতা থাকে। Redis হলো সেই তাক (দ্রুত, ছোট, মেমরিতে), আর PostgreSQL বা MySQL হলো গুদাম আর খাতা (বড়, টেকসই, সত্যের উৎস)।

কেন এত দ্রুত

  • সব ডেটা RAM-এ। পর্ব ৩৯-এ দেখেছেন, মেমরি থেকে পড়া মোটামুটি ১০০ ন্যানোসেকেন্ডের ঘরে, SSD থেকে কয়েক দশ মাইক্রোসেকেন্ড বা বেশি। Redis-এ ডিস্ক শুধু টিকিয়ে রাখার জন্য (অংশ ৮), পড়ার পথে নয়।
  • সরল কাজ। SQL পার্স, optimizer, join, plan — কিছুই নেই। GET key মানে একটি hash table-এ খোঁজ, গড়ে O(1).
  • একটি থ্রেডে কমান্ড চালানো। Redis-এর মূল থ্রেড একটি event loop: হাজারো client-এর connection একসাথে খোলা রাখে (Linux-এ epoll), আর যে client-এর কমান্ড তৈরি, সেটি চালায় — একবারে একটি। lock লাগে না, থ্রেডে থ্রেডে ঝগড়াও নেই।

এই single-threaded মডেল নিয়ে একটু সাবধানে বলা দরকার। Redis 6.0 (২০২০) থেকে ঐচ্ছিক I/O থ্রেড আছে, যা network থেকে পড়া আর উত্তর পাঠানোর কাজ ভাগ করে নেয়; পরের সংস্করণগুলোতে (Redis 8 আর Valkey 8 সহ) এই অংশে আরও উন্নতির কথা বলা হয়েছে। মেমরি ফেরত দেওয়া বা AOF-এর fsync-এর মতো কিছু কাজও পেছনের থ্রেডে চলে। কিন্তু ডেটা বদলানো কমান্ডগুলো এখনো ক্রমানুসারে, একটি একটি করে চলে — এটাই মূল কথা। এর দুটি ফল:

সুবিধা: প্রতিটি কমান্ড atomic

INCR চলার মাঝে অন্য কারও কমান্ড ঢুকতে পারে না। পর্ব ৪৬-এর lost update এখানে নিজে থেকেই নেই — একসাথে ১,০০০ client INCR পাঠালে ফল ঠিক ১,০০০ বাড়বে।

বিপদ: একটি ধীর কমান্ড সবাইকে থামায়

KEYS * কোটি key ঘাঁটে — O(N) — আর সেই সময় অন্য সব client অপেক্ষায়। চালু সিস্টেমে এর বদলে SCAN (একটু একটু করে)। বিশাল একটি key মোছাও ধীর; তার জন্য UNLINK (মেমরি ফেরত পেছনের থ্রেডে)।

Redis এখানে চালানো যায় না, তাই এই পর্বের Redis কমান্ডগুলো redis-cli-এর ছবি — redis>-এর পরে কমান্ড, নিচের লাইনে সার্ভারের উত্তর। নিজের কম্পিউটারে চালাতে Docker-এ docker run -p 127.0.0.1:6379:6379 redis (শুধু নিজের কম্পিউটার থেকে ঢোকা যাবে; নিরাপত্তা অংশ ৯-এ), তারপর redis-cli. প্রথম পরিচয়:

redis> PING
PONG
redis> SET greeting "Assalamu Alaikum"
OK
redis> GET greeting
"Assalamu Alaikum"
redis> EXISTS greeting missing:key
(integer) 1
redis> DEL greeting
(integer) 1
redis> GET greeting
(nil)

(nil) মানে key নেই — SQL-এর NULL-এর মতো “অজানা” নয়, শুধু “এখানে কিছু রাখা হয়নি”. EXISTS দুটি key-এর মধ্যে কয়টি আছে তা গোনে।

২ · Key-এর নাম, TTL, string আর hash

Key-এর নামকরণ — app:entity:id

Redis-এ টেবিল বা schema নেই; সব key একটি বিশাল সমতল অভিধানে থাকে। শৃঙ্খলা আসে নামের রীতি থেকে: কোলন দিয়ে ভাগ করা অংশ, বড় থেকে ছোট — shop:product:101, app:session:a91f, rl:otp:01711000001. এই key naming convention তিনটি কাজ করে: এক নজরে বোঝা যায় key কার, ভিন্ন অ্যাপের key একে অপরকে মুছে ফেলে না, আর একটি অংশের সব key SCAN 0 MATCH shop:product:* দিয়ে খোঁজা যায়। নাম পাঠযোগ্য রাখুন, কিন্তু অপ্রয়োজনে লম্বা নয় — কোটি key-এ প্রতিটি বাড়তি বাইট মেমরি।

String — সবচেয়ে সরল মান

Redis-এর string আসলে বাইটের ক্রম: লেখা, সংখ্যা, JSON, এমনকি ছবি — একটি মান সর্বোচ্চ ৫১২ MB (বাস্তবে ছোট রাখুন)। সংখ্যার মতো দেখতে string-এ INCR atomic যোগ করে:

redis> SET shop:product:101:price 15000
OK
redis> INCR stats:views:2025-10-10
(integer) 1
redis> INCRBY stats:views:2025-10-10 9
(integer) 10
redis> SET otp:01711000001 482913 EX 120     # ১২০ সেকেন্ডের মেয়াদ
OK
redis> TTL otp:01711000001
(integer) 120
redis> SET otp:01711000001 771204 EX 120 NX  # NX: না থাকলেই লেখো
(nil)
redis> SET flash:sale:banner on PX 1500       # PX: মিলিসেকেন্ডে
OK

NX (Not eXists) লেখার আগে দেখে key আছে কি না — আছে, তাই (nil), পুরোনো OTP বদলায়নি। এই এক শব্দই পরে lock বানাবে (অংশ ৭)। উল্টো XX: থাকলেই লেখে।

TTL আর মেয়াদ শেষ হওয়া

TTL (time to live) হলো key-এর বাকি আয়ু। EXPIRE key 60 মেয়াদ বসায়, PERSIST সরায়, TTL বাকি সেকেন্ড বলে: -1 মানে key আছে কিন্তু মেয়াদ নেই, -2 মানে key-ই নেই। দুটি সূক্ষ্ম নিয়ম, যা ভুলের বড় উৎস:

  • নতুন করে SET করলে মেয়াদ মুছে যায় (যদি না KEEPTTL বা নতুন EX দেন)। কিন্তু INCR, HSET, LPUSH-এর মতো মান-বদলানো কমান্ড মেয়াদ রেখে দেয়।
  • মেয়াদ শেষ মানে সাথে সাথে মেমরি খালি নয়। Redis দুই পথে মোছে: কেউ পড়তে এলে তখন দেখে মুছে দেয় (passive), আর পেছনে নিয়মিত কিছু মেয়াদি key-এর নমুনা নিয়ে মেয়াদোত্তীর্ণগুলো মোছে (active). পাঠক কখনো মেয়াদোত্তীর্ণ মান পান না, তবে মেমরি একটু দেরিতে ফেরত আসে।

Hash — একটি বস্তুর কয়েকটি ঘর: user session

Hash একটি key-এর ভেতরে field → value-এর ছোট অভিধান — relational সারির মতো, তবে schema ছাড়া। ব্যবহারকারী লগইন করলে তাঁর session রাখার আদর্শ জায়গা:

redis> HSET app:session:a91f user_id 1 name "Rahim Uddin" cart_items 2
(integer) 3
redis> EXPIRE app:session:a91f 1800          # ৩০ মিনিট নিষ্ক্রিয় থাকলে লগআউট
(integer) 1
redis> HINCRBY app:session:a91f cart_items 1
(integer) 3
redis> HGET app:session:a91f name
"Rahim Uddin"
redis> HGETALL app:session:a91f
1) "user_id"
2) "1"
3) "name"
4) "Rahim Uddin"
5) "cart_items"
6) "3"

প্রতিটি অনুরোধে EXPIRE আবার চালালে মেয়াদ সরে সরে যায় — sliding expiry: শেষ কাজের ৩০ মিনিট পর লগআউট। মেয়াদ পুরো key-এর, আলাদা field-এর নয়; Redis 7.4-এ field-ভিত্তিক মেয়াদের কমান্ড (HEXPIRE) যোগ হয়েছে বলে ঘোষণা আছে, তবে পুরোনো সংস্করণে নেই।

একই কাজ relational ডেটাবেসে কেমন? নিচের sessions টেবিলে চারটি session: session_id, user_id, cart_items আর expires_at (মেয়াদ শেষের সময়)। লাইভ এডিটরে “এখন” হলো '2025-10-10 12:00:00' — হাতে লেখা, যাতে ফল প্রতিবার একই হয়:

-- প্রতিটি অনুরোধে: session বৈধ কি না
SELECT user_id, cart_items FROM sessions
WHERE session_id = 'c55d' AND expires_at > '2025-10-10 12:00:00';
-- মেয়াদ বাড়ানো (Redis-এ EXPIRE)
UPDATE sessions SET expires_at = datetime('2025-10-10 12:00:00', '+30 minutes')
WHERE session_id = 'c55d';
-- আলাদা পরিষ্কারের job: মেয়াদোত্তীর্ণ সারি মোছা
DELETE FROM sessions WHERE expires_at <= '2025-10-10 12:00:00';
SELECT session_id, user_id, expires_at FROM sessions ORDER BY expires_at;

প্রথম কোয়েরি 8 | 5 — session বৈধ। UPDATE-এ ১টি সারি বদলায়, DELETE-এ ২টি (b27c আর d03e-এর মেয়াদ পেরিয়েছে)। শেষে থাকে a91f | 1 | 2025-10-10 12:20:00 আর c55d | 8 | 2025-10-10 12:30:00. কাজটি চলে, কিন্তু তিনটি দাম আছে: প্রতিটি অনুরোধে মেয়াদের শর্ত লিখতে মনে রাখা, মোছার জন্য আলাদা নির্ধারিত job, আর প্রতিটি পাতা খোলায় একটি ডিস্ক-ভিত্তিক UPDATE (PostgreSQL-এ যা dead tuple বানায় — পর্ব ৫৩)। Redis-এ এর সবটা একটি EXPIRE.

৩ · List, set আর sorted set — queue, unique visitor আর leaderboard

List — সরল queue

List হলো দুই মাথা-খোলা ক্রম (linked list-এর মতো): LPUSH বাঁ দিকে ঢোকায়, RPOP ডান দিক থেকে বের করে — দুটিই O(1). এক দিকে ঢোকানো, অন্য দিকে বের করা মানে FIFO queue: একটি অ্যাপ SMS পাঠানোর কাজ জমা রাখে, পেছনের worker একটি একটি করে নিয়ে পাঠায়।

redis> LPUSH queue:sms "otp:01711000001" "otp:01811000002"
(integer) 2
redis> RPOP queue:sms                        # আগে যে এসেছে, সে আগে
"otp:01711000001"
redis> BRPOP queue:sms 5                     # খালি হলে ৫ সেকেন্ড অপেক্ষা
1) "queue:sms"
2) "otp:01811000002"
redis> LMOVE queue:sms queue:sms:working RIGHT LEFT
(nil)

BRPOP (blocking) worker-কে বারবার জিজ্ঞেস করা থেকে বাঁচায়। কিন্তু RPOP-এর পর worker crash করলে কাজটি হারায় — বের করা হয়ে গেছে, পাঠানো হয়নি। নিরাপদ রূপ LMOVE (Redis 6.2+): কাজটি একটি “চলছে” তালিকায় সরানো, পাঠানো শেষে সেখান থেকে মোছা; এখানে queue খালি, তাই (nil). এর চেয়ে গুরুতর প্রয়োজনে stream (অংশ ৪)।

Set — অনন্য সদস্য: unique visitor

Set-এ প্রতিটি সদস্য একবারই থাকে। একই ভিজিটর দশবার এলেও একবার গোনা — SADD জানায় কয়টি নতুন যোগ হলো, SISMEMBER আর SCARD O(1):

redis> SADD visitors:2025-10-01 u1 u2 u3 u1 u2
(integer) 3
redis> SADD visitors:2025-10-02 u2 u4 u2 u5 u1
(integer) 4
redis> SCARD visitors:2025-10-02
(integer) 4
redis> SISMEMBER visitors:2025-10-01 u4
(integer) 0
redis> SINTER visitors:2025-10-01 visitors:2025-10-02
1) "u1"
2) "u2"

set-এ ক্রম নির্দিষ্ট নয়, তাই SINTER-এর ফল অন্য ক্রমেও আসতে পারে। একই প্রশ্ন SQL-এ — page_views টেবিলে প্রতিটি পাতা দেখার একটি সারি (visitor_id, day):

SELECT day, COUNT(*) AS views, COUNT(DISTINCT visitor_id) AS unique_visitors
FROM page_views GROUP BY day;
-- দুই দিনেই এসেছেন (Redis-এ SINTER)
SELECT visitor_id FROM page_views WHERE day = '2025-10-01'
INTERSECT
SELECT visitor_id FROM page_views WHERE day = '2025-10-02';
SELECT COUNT(DISTINCT visitor_id) AS either_day FROM page_views;

ফল: 2025-10-01 | 5 | 3 আর 2025-10-02 | 5 | 4; দুই দিনেই u1, u2; মোট অনন্য 5 (Redis-এ SUNION-এর আকার)। পার্থক্য কোথায়? SQL প্রতিবার সব সারি থেকে নতুন করে হিসাব করে — দিনে কোটি page view-এ COUNT(DISTINCT) ভারী, কারণ সব visitor_id সাজাতে বা hash করতে হয়। Redis-এর set প্রতিটি ভিজিটর আসার মুহূর্তে আপডেট হয়, আর SCARD শুধু জমা সংখ্যাটি পড়ে। তবে কাঁচা page view-এর ইতিহাস রাখে SQL; Redis রাখে শুধু উত্তরটি।

Sorted set — leaderboard-এর জন্য বানানো

Sorted set (ZSET) হলো এমন set, যার প্রতিটি সদস্যের একটি সংখ্যা — score — আছে, আর সদস্যরা সবসময় score-এর ক্রমে সাজানো। ভেতরে দুটি গঠন একসাথে: সদস্য থেকে score খোঁজার hash table (O(1)), আর ক্রম রাখার skip list. skip list-এর প্রতিটি লিংক জানে সে কয়টি সদস্য ডিঙিয়ে যায় (span); ওপর থেকে নামার পথে সেগুলো যোগ করলেই অবস্থান — তাই ZADD, ZINCRBY, ZREVRANK প্রত্যাশিত O(log N), আর প্রথম M জন আনা O(log N + M). একটি অনলাইন কুইজ প্রতিযোগিতার মাসিক leaderboard, ধাপে ধাপে:

key: lb:quiz:2025-10লাল = এই ধাপের ফল

            
অবস্থা
কমান্ড
Tanim-এর score
Tanim-এর rank
খরচ
  1. ZADD. একটি কমান্ডে পাঁচজনের score; উত্তর ৫ মানে পাঁচজনই নতুন সদস্য। প্রতিটি ঢোকানো skip list-এ সঠিক জায়গায় — O(log N). বড় score ওপরে দেখাতে আমরা উল্টো ক্রমে পড়ব।
  2. প্রথম তিন। REV মানে বড় থেকে ছোট (Redis 6.2+; পুরোনো সংস্করণে ZREVRANGE). Nadia, Mitu, Rafi. skip list-এ শুরুর বিন্দু খুঁজে তিনটি সদস্য হেঁটে যাওয়া — বাকি দুজনকে ছোঁয়াই লাগে না।
  3. একজনের অবস্থান। Tanim-এর rank ৪ — শূন্য থেকে গোনা, তাই পঞ্চম স্থান। “আপনি এখন ৪৮,৩১২তম” দেখাতে লাখো খেলোয়াড় গুনতে হয় না; skip list-এর span যোগ করলেই হয়।
  4. ZINCRBY. Tanim একটি কুইজে ৪০০ পেলেন: score ৯৮০ থেকে ১,৩৮০. Redis তাঁকে পুরোনো জায়গা থেকে তুলে নতুন জায়গায় বসায় — O(log N); একটিমাত্র atomic কমান্ড, তাই একসাথে দুটি যোগে কোনো হালনাগাদ হারায় না।
  5. আবার পড়া। প্রথম তিনে এখন Nadia, Tanim, Mitu; Tanim-এর rank ১, অর্থাৎ দ্বিতীয়। কোনো sort, কোনো আলাদা হিসাবের job চলেনি — ক্রম প্রতিটি লেখার সাথেই ঠিক থাকে।

একই leaderboard SQL-এ — scores টেবিলে player আর score, একই পাঁচজন:

UPDATE scores SET score = score + 400 WHERE player = 'Tanim';   -- ZINCRBY
SELECT player, score, RANK() OVER (ORDER BY score DESC) AS pos
FROM scores ORDER BY score DESC LIMIT 3;
-- একজনের অবস্থান: তাঁর চেয়ে বেশি কয়জন, যোগ ১
SELECT COUNT(*) + 1 AS tanim_pos FROM scores
WHERE score > (SELECT score FROM scores WHERE player = 'Tanim');
EXPLAIN QUERY PLAN SELECT player, score FROM scores ORDER BY score DESC LIMIT 3;
CREATE INDEX scores_by_score ON scores(score);
EXPLAIN QUERY PLAN SELECT player, score FROM scores ORDER BY score DESC LIMIT 3;

প্রথম তিন: Nadia | 1450 | 1, Tanim | 1380 | 2, Mitu | 1320 | 3; Tanim-এর অবস্থান 2 — Redis-এর সাথে হুবহু মেলে (SQL-এ অবস্থান ১ থেকে, Redis-এ rank ০ থেকে)। index ছাড়া plan SCAN scores আর USE TEMP B-TREE FOR ORDER BY — প্রতিবার পুরো টেবিল পড়ে sort. index-এর পর SCAN scores USING INDEX scores_by_score — B+ tree-এর ক্রমে উল্টো দিক থেকে তিনটি পড়েই থামে, তাই top-N SQL-এও দ্রুত। কিন্তু “আমার অবস্থান” প্রশ্নে পার্থক্য থেকে যায়: সাধারণ B+ tree প্রতিটি শাখায় কয়টি সারি আছে তা রাখে না, তাই COUNT(*)-কে ওপরের k জনকে এক এক করে গুনতে হয় — O(log N + k); লাখতম খেলোয়াড়ের জন্য লাখটি entry. skip list-এর span সেটিকে O(log N) করে। সমান score-এ Redis সদস্যের নামের অক্ষরক্রম দিয়ে ক্রম ঠিক করে, আর SQL-এর RANK দুজনকে একই অবস্থান দেয় — দুই জগতের “tie” আলাদা, মেলাতে হলে নিয়মটি লিখে রাখুন।

৪ · Stream, pub/sub, bitmap আর HyperLogLog

Stream (Redis 5.0, ২০১৮) হলো শুধু-যোগ-করা একটি log — প্রতিটি entry-র সময়ভিত্তিক id আর কয়েকটি field. list-এর queue-এর চেয়ে শক্ত: entry পড়লেও মোছে না, আর একটি consumer group-এর কয়েকজন worker কাজ ভাগ করে নেয়; প্রত্যেকে কাজ শেষে XACK দিয়ে জানায়, না জানালে কাজটি “অপেক্ষমাণ” তালিকায় থাকে, পরে অন্য কেউ তুলে নিতে পারে:

redis> XADD orders:events * order_id 1011 status paid
"1760090400000-0"
redis> XGROUP CREATE orders:events mailer $ MKSTREAM
OK
redis> XREADGROUP GROUP mailer w1 COUNT 10 BLOCK 5000 STREAMS orders:events >
(nil)
redis> XADD orders:events * order_id 1012 status paid
"1760090401500-0"
redis> XREADGROUP GROUP mailer w1 COUNT 10 STREAMS orders:events >
1) 1) "orders:events"
   2) 1) 1) "1760090401500-0"
         2) 1) "order_id"
            2) "1012"
            3) "status"
            4) "paid"
redis> XACK orders:events mailer 1760090401500-0
(integer) 1

* মানে id Redis বানাবে (মিলিসেকেন্ডের সময়-ক্রমিক সংখ্যা; এখানকার সংখ্যাগুলো উদাহরণ)। group তৈরির সময় $ মানে “এখন থেকে নতুনগুলো”, তাই 1011 group পায়নি, আর প্রথম XREADGROUP ৫ সেকেন্ড অপেক্ষা করে খালি হাতে ফেরে; > মানে “কাউকে এখনো দেওয়া হয়নি এমন entry”. বড় মাপের event streaming-এ Kafka-র মতো ব্যবস্থা বেশি প্রচলিত; Redis stream মাঝারি প্রয়োজনে সরল বিকল্প।

Pub/Sub. SUBSCRIBE chat:room:7 করে রাখা সবাই PUBLISH chat:room:7 "hello"-এর বার্তা সাথে সাথে পান। কিন্তু Redis বার্তা জমিয়ে রাখে না: যিনি সেই মুহূর্তে সংযুক্ত নেই, তিনি পাবেন না — বড়জোর একবার (at-most-once). লাইভ notification, চ্যাটের “টাইপ করছেন…”, বা কয়েকটি সার্ভারকে “cache মুছে দাও” বলার জন্য ভালো; হারানো চলবে না এমন কাজে stream.

Bitmap. string-কে bit-এর সারি হিসেবে ব্যবহার: SETBIT active:2025-10-10 4217 1 মানে ৪,২১৭ নম্বর ব্যবহারকারী আজ সক্রিয়, BITCOUNT active:2025-10-10 মোট সক্রিয়। ১ কোটি ব্যবহারকারীর জন্য ১ কোটি bit = ১২,৫০,০০০ বাইট, প্রায় ১.২৫ MB প্রতিদিন — set-এ একই তথ্য এর বহু গুণ জায়গা নিত। শর্ত: id-গুলো ছোট, ঘন সংখ্যা হতে হবে।

HyperLogLog. কোটি অনন্য ভিজিটর গুনতে সব id মনে রাখার দরকার নেই, যদি সামান্য ভুল চলে। PFADD আর PFCOUNT একটি সম্ভাবনাভিত্তিক গঠনে প্রতি key সর্বোচ্চ প্রায় ১২ KB নেয়, আর Redis-এর নথি অনুযায়ী আদর্শ ত্রুটি প্রায় ০.৮১%. দুই দিনের HLL মেলানোও যায় (PFMERGE), কিন্তু “u4 এসেছিলেন কি না” প্রশ্নের উত্তর দেয় না — সদস্য মনে রাখে না।

Typeমূল কমান্ডপ্রচলিত ব্যবহার
StringSET, GET, INCR, SET … EX / NXcache-এর মান, counter, OTP, lock
HashHSET, HGET, HINCRBY, HGETALLsession, ছোট বস্তু
ListLPUSH, RPOP, BRPOP, LMOVEসরল queue, সাম্প্রতিক N টি ঘটনা
SetSADD, SISMEMBER, SCARD, SINTERঅনন্য ভিজিটর, tag, বন্ধুর তালিকা
Sorted setZADD, ZINCRBY, ZRANGE, ZREVRANKleaderboard, সময়-ক্রমে কাজ, sliding window
StreamXADD, XREADGROUP, XACKঘটনার log, নির্ভরযোগ্য কাজের queue
Bitmap / HLLSETBIT, BITCOUNT / PFADD, PFCOUNTদৈনিক সক্রিয়, আনুমানিক অনন্য গণনা

৫ · Caching pattern — cache-aside থেকে write-behind

Redis-এর সবচেয়ে বড় ব্যবহার cache: ধীর বা দামি কোনো উত্তরের কপি হাতের কাছে রাখা। পর্ব ৫৩-এ cache-এর স্তরগুলো দেখেছেন; এখন অ্যাপের ভেতরের cache-এর নকশা। প্রথমে দেখুন cache কোন ধরনের কোয়েরিকে রক্ষা করে — দোকানের হোম পেজে “সবচেয়ে বেশি বিক্রি” তালিকা, তিনটি টেবিলের join আর aggregation (db-shop: orders-এ অর্ডারের status, order_items-এ প্রতিটি অর্ডারের পণ্য, পরিমাণ আর বিক্রয়মূল্য, products-এ পণ্যের নাম):

SELECT p.product_id, p.name, SUM(oi.quantity) AS units,
       SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi
JOIN orders o   ON o.order_id = oi.order_id
JOIN products p ON p.product_id = oi.product_id
WHERE o.status <> 'cancelled'
GROUP BY p.product_id, p.name
ORDER BY revenue DESC LIMIT 3;
EXPLAIN QUERY PLAN
SELECT p.product_id, SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi JOIN orders o ON o.order_id = oi.order_id
JOIN products p ON p.product_id = oi.product_id
WHERE o.status <> 'cancelled' GROUP BY p.product_id ORDER BY revenue DESC LIMIT 3;

ফল: 101 | Mobile Phone | 2 | 29500, 110 | Smart Watch | 2 | 7000, 106 | Panjabi | 2 | 4400. plan-এ SCAN oi — প্রতিটি বিক্রির সারি পড়া — তারপর প্রতিটির জন্য orders আর products-এ primary key দিয়ে খোঁজ, আর GROUP BY আর ORDER BY-এর জন্য দুটি USE TEMP B-TREE. এখানে ১৬টি সারি, কিন্তু আসল দোকানে কোটি order_items হলে প্রতিটি হোম পেজ খোলায় এই কাজ চালানো অপচয় — অথচ উত্তর পাঁচ মিনিটে প্রায় বদলায় না। এমন “ধীর, ঘন ঘন পড়া, কদাচিৎ বদলানো” উত্তরই cache-এর আদর্শ প্রার্থী: ফলটি JSON বানিয়ে SET shop:report:top3 '…' EX 300.

Cache-aside — অ্যাপ নিজে cache সামলায়

Cache-aside (lazy loading নামেও পরিচিত) সবচেয়ে প্রচলিত নকশা। Redis ডেটাবেসের কথা কিছু জানে না; অ্যাপ দুজনের সাথে আলাদা কথা বলে:

  1. অ্যাপপণ্যের পাতা
  2. Redis~1 ms
  3. ডেটাবেস~50 ms
পণ্য 101, TTL 300 sলাল = এই ধাপের মূল ঘটনা

            
হিসাব
অনুরোধ
cache hit
DB কোয়েরি
cache-এ দাম
DB-তে দাম
  1. প্রথম পড়া — miss. অ্যাপ নিয়মমাফিক key বানায় (shop:product:101) আর আগে Redis-কে জিজ্ঞেস করে। কিছু নেই: cache miss. এখনো কোনো DB কোয়েরি হয়নি।
  2. ডেটাবেসে যাওয়া। miss হলে অ্যাপ নিজেই ডেটাবেস থেকে আনে — সত্যের উৎস সবসময় ডেটাবেস। এই অনুরোধটি cache না থাকার মতোই ধীর, বরং একটি বাড়তি round trip বেশি।
  3. Cache-এ রাখা। উত্তরটি TTL সহ রাখা হলো। TTL একটি নিরাপত্তা-জাল: invalidation কোনো কারণে ব্যর্থ হলেও পুরোনো মান বড়জোর ৩০০ সেকেন্ড টিকবে।
  4. দ্বিতীয় পড়া — hit. পরের ৩০০ সেকেন্ডে যত অনুরোধ, সবাই Redis থেকে পায়: cache hit. ডেটাবেস বিশ্রামে। hit-এর অনুপাত যত বেশি, cache তত কাজের।
  5. লেখা — DB আগে, তারপর DEL. দাম বদলালে আগে ডেটাবেসে commit, তারপর key মোছা। নতুন মান লিখে দেওয়ার (SET) বদলে মোছা নিরাপদ: দুটি একসাথে আসা লেখার ক্রম উল্টে গেলে SET পুরোনো মান বসিয়ে দিতে পারে, DEL-এ সেই ভয় কম।
  6. আবার miss — তাজা মান। key নেই, তাই আবার ডেটাবেস — এবার ১৪,৫০০. তিন অনুরোধে DB মাত্র দুবার। আসল সিস্টেমে হাজারো অনুরোধের পর একবার।

এবার একই নকশা Python-এ, একটি SQLite “ডেটাবেস” আর একটি dict-কে Redis ধরে। সময় আসল ঘড়ি নয়, একটি সংখ্যা (now), তাই ফল প্রতিবার একই। ১০ মিনিট ধরে প্রতি ১০ সেকেন্ডে একটি অনুরোধ — কয়বার ডেটাবেসে যেতে হলো? তারপর দাম বদলের সময় key না মুছলে কী হয়?

import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price INTEGER)")
db.execute("INSERT INTO products VALUES (101, 'Mobile Phone', 15000)")
cache, stats = {}, {"cache_hit": 0, "db_query": 0}   # key -> (value, expires_at)
def get_product(pid, now, ttl=300):
    key = f"shop:product:{pid}"
    if (entry := cache.get(key)) and entry[1] > now:          # GET key
        stats["cache_hit"] += 1; return entry[0]
    stats["db_query"] += 1                                   # miss: ask the database
    row = db.execute("SELECT name, price FROM products WHERE id = ?", (pid,)).fetchone()
    cache[key] = (row, now + ttl); return row                # SET key value EX ttl
def update_price(pid, price, delete_key):
    db.execute("UPDATE products SET price = ? WHERE id = ?", (price, pid))
    if delete_key: cache.pop(f"shop:product:{pid}", None)    # DEL key
for t in range(0, 600, 10): get_product(101, t)              # a request every 10 s for 10 min
print("10 minutes  :", stats)
update_price(101, 14500, delete_key=False); print("t=595 no DEL:", get_product(101, 595))
update_price(101, 14000, delete_key=True);  print("t=596 DEL   :", get_product(101, 596), stats)

আউটপুট:

10 minutes  : {'cache_hit': 58, 'db_query': 2}
t=595 no DEL: ('Mobile Phone', 15000)
t=596 DEL   : ('Mobile Phone', 14000) {'cache_hit': 59, 'db_query': 3}

৬০টি অনুরোধে ডেটাবেসে মাত্র ২ বার — সময় ০ আর ৩০০-তে, যখন TTL শেষ (শর্তটি >, তাই ঠিক ৩০০ সেকেন্ডে মেয়াদোত্তীর্ণ)। hit ratio ৫৮/৬০ ≈ ৯৭%. তারপর শিক্ষাটি: দাম ১৪,৫০০ হলো কিন্তু key মোছা হয়নি, তাই ৫৯৫ সেকেন্ডে গ্রাহক এখনো ১৫,০০০ দেখছেন — stale data, যা TTL শেষ না হওয়া পর্যন্ত চলবে। key মুছলে পরের পড়াতেই ১৪,০০০. পুরোনো দাম দেখানো বিরক্তিকর; পুরোনো স্টক দেখিয়ে শেষ পিস দুজনকে বিক্রি করা আরও খারাপ।

আরও তিনটি নকশা

Read-through

অ্যাপ শুধু cache-কে জিজ্ঞেস করে; miss হলে cache-স্তর নিজেই ডেটাবেস থেকে আনে। কোড পরিষ্কার, কিন্তু cache-স্তরকে ডেটাবেস চিনতে হয় — সাধারণত একটি library বা proxy এটি করে। এটিই read-through.

Write-through

প্রতিটি লেখা একসাথে cache আর ডেটাবেসে যায়, দুটি শেষ হলে তবেই সফল। cache সবসময় তাজা, কিন্তু প্রতিটি লেখা দুই জায়গায় — ধীর, আর কখনো পড়া হবে না এমন ডেটাও cache ভরায়। এটি write-through.

Write-behind

লেখা আগে শুধু cache-এ; ডেটাবেসে পরে, দল বেঁধে। লেখা খুব দ্রুত, কিন্তু মাঝের সময়ে cache গেলে ডেটা হারায় — write-behind শুধু সেখানে, যেখানে কিছু হারানো সহ্য হয় (যেমন পাতা দেখার গণনা)।

Invalidation — কে, কখন key মুছবে

পর্ব ৫৩-এ দেখেছেন, cache invalidation কঠিন। cache-aside-এ দুটি ফাঁদ বিশেষভাবে পরিচিত। এক, “DB আপডেট হলো, কিন্তু DEL-এর আগে অ্যাপ crash” — key থেকে যায়, TTL-ই ভরসা। দুই, একটি দৌড় (race condition): পাঠক A miss পেয়ে পুরোনো মান পড়লেন, এর মধ্যে লেখক B DB বদলে DEL করলেন, তারপর A পুরোনো মানটি SET করলেন — cache-এ আবার পুরোনো। তাই কেউ কেউ DEL কয়েকশো মিলিসেকেন্ড পর আরেকবার করেন (delayed double delete), কেউ মানের সাথে version রাখেন। সবচেয়ে শক্ত পথ: invalidation-কে লেখার একই transaction-এর অংশ বানানো — পর্ব ২৯-এর trigger দিয়ে একটি outbox টেবিলে “কোন key মুছতে হবে” লিখে রাখা, আর আলাদা একটি worker সেটি পড়ে DEL করে:

CREATE TABLE cache_invalidations (id INTEGER PRIMARY KEY, cache_key TEXT NOT NULL);
CREATE TRIGGER product_cache_bust AFTER UPDATE OF price, stock ON products
BEGIN
  INSERT INTO cache_invalidations (cache_key)
  VALUES ('shop:product:' || NEW.product_id);
END;
UPDATE products SET price = 1100 WHERE product_id = 102;
UPDATE products SET stock = stock - 1 WHERE category_id = 1 AND stock > 0;
UPDATE products SET name = 'DSA Book (2nd ed.)' WHERE product_id = 104;
SELECT id, cache_key FROM cache_invalidations;

তিনটি UPDATE-এ ১, ৩ আর ১টি সারি বদলায়। outbox-এ চারটি সারি: 1 | shop:product:102, 2 | shop:product:101, 3 | shop:product:102, 4 | shop:product:110. দ্বিতীয় UPDATE-এ Electronics-এর যে পণ্যগুলোর স্টক শূন্যের বেশি (101, 102, 110) — Power Bank (103) বাদ, স্টক আগেই শূন্য। তৃতীয়টি নাম বদলেছে, যা UPDATE OF price, stock-এর তালিকায় নেই, তাই কোনো সারি যোগ হয়নি — পণ্যের পাতা নাম দেখালে তালিকায় name-ও রাখতে হতো; ভুলে যাওয়া কলামই stale data-র সাধারণ উৎস। লাভটি হলো পারমাণবিকতা: UPDATE rollback হলে outbox-এর সারিও যায়, commit হলে থাকে — crash-এ “DEL ভুলে যাওয়া” আর হয় না (পর্ব ৩৮-এর outbox pattern). worker একই key দুবার মুছলেও ক্ষতি নেই — DEL idempotent. বড় সিস্টেমে trigger-এর বদলে প্রায়ই change data capture (WAL বা binlog পড়া) ব্যবহার হয়।

৬ · Cache stampede আর eviction — মেমরি ভরলে বা key ফুরোলে

Stampede — একসাথে হাজারো miss

হোম পেজের key-টি প্রতি সেকেন্ডে ৫,০০০ বার পড়া হয়, আর এর পেছনের কোয়েরি ৪০০ ms নেয়। key-এর মেয়াদ শেষ হলো। পরের ৪০০ ms-এ যত অনুরোধ আসে, প্রত্যেকে miss পায় আর প্রত্যেকে একই কোয়েরি ডেটাবেসে পাঠায় — পর্ব ৫৩-এর Little's law-এ প্রায় ৫,০০০ × ০.৪ = ২,০০০টি একই কোয়েরি একসাথে। ডেটাবেস ধীর হয়, কোয়েরি আরও লম্বা হয়, আরও অনুরোধ miss পায়। একে বলে cache stampede বা thundering herd. প্রতিকার তিনটি, প্রায়ই একসাথে:

  • একজনই বানাবে (lock): miss পেলে SET lock:home 1 NX PX 5000; যে lock পেল সে ডেটাবেসে যায়, বাকিরা একটু অপেক্ষা করে আবার cache পড়ে — বা পুরোনো মানটি দেখায়।
  • মেয়াদে jitter: deploy-এর পর হাজারো পণ্যের key একসাথে EX 3600 পেলে এক ঘণ্টা পর একসাথে মরবে। TTL-এ এলোমেলো ভাগ যোগ করুন — ৩,৬০০ ± ১০% মানে মৃত্যু ছড়িয়ে যায় ৭২০ সেকেন্ডে।
  • আগেই নবায়ন: মেয়াদ শেষের কিছু আগে, পেছনে একজন কাজটি নতুন করে বানায় — ব্যবহারকারী কখনো miss দেখেন না। একটি পরিচিত পদ্ধতি সম্ভাবনাভিত্তিক: মেয়াদ যত কাছে, প্রতিটি পাঠকের আগেভাগে নবায়ন করার সম্ভাবনা তত বেশি; ২০১৫ সালের একটি গবেষণাপত্রে (Vattani ও সহলেখক, VLDB) এর গাণিতিক রূপ “XFetch” নামে প্রস্তাব করা হয়।
redis> GET shop:home:top3
(nil)
redis> SET lock:shop:home:top3 w17 NX PX 5000   # worker w17 পেল
OK
redis> SET lock:shop:home:top3 w42 NX PX 5000   # w42 পেল না: অপেক্ষা
(nil)
redis> SET shop:home:top3 "[...]" EX 3437       # 3600 - jitter
OK
redis> DEL lock:shop:home:top3
(integer) 1

maxmemory আর eviction policy

RAM সীমিত। maxmemory 2gb সীমা বসায়; সীমায় পৌঁছালে eviction policy ঠিক করে কী হবে:

Policyকাকে সরায়কখন মানানসই
noeviction (ডিফল্ট)কাউকে না — নতুন লেখা error দেয়Redis যখন cache নয়, ডেটা রাখার জায়গা (queue, lock)
allkeys-lruযেকোনো key, সবচেয়ে কম সাম্প্রতিকবিশুদ্ধ cache — সবচেয়ে প্রচলিত বাছাই
allkeys-lfuযেকোনো key, সবচেয়ে কম ব্যবহৃত (Redis 4.0+)কিছু key স্থায়ীভাবে গরম, আর মাঝে মাঝে বিশাল scan
volatile-lru / volatile-lfuশুধু TTL-যুক্ত key থেকেএক Redis-এ cache আর স্থায়ী ডেটা মেশানো
volatile-ttlTTL-যুক্ত, যার মেয়াদ সবচেয়ে কাছেযেটি শিগগির মরবেই, সেটি আগে
allkeys-random / volatile-randomএলোমেলোসব key প্রায় সমান গুরুত্বের

Redis-এর LRU নিখুঁত নয়: সব key-এর একটি ক্রমিক তালিকা রাখলে প্রতিটি key-এ বাড়তি মেমরি লাগত। তাই প্রতিবার কয়েকটি key-এর নমুনা নেয় (maxmemory-samples, ডিফল্ট ৫) আর তাদের মধ্যে সবচেয়ে পুরোনোটি সরায় — আনুমানিক, কিন্তু সস্তা। LFU-তে প্রতিটি key-এর একটি ছোট, সময়ের সাথে ক্ষয় হওয়া ব্যবহার-গণক থাকে। পর্ব ৩৯-এর buffer pool-এর মতো এখানেও প্রশ্ন একই: একবার-পড়া scan কি গরম ডেটা ভাসিয়ে দেবে? নিচে ৩টি key-এর জায়গায় নিখুঁত LRU আর LFU-র তুলনা — একটি crawler (bot) মাঝখানে চারটি পাতা একবার করে পড়ে:

from collections import OrderedDict
def simulate(requests, maxkeys, policy):
    cache, hits, evicted = OrderedDict(), 0, []           # key -> access count; order = recency
    for key in requests:
        if key in cache:
            hits += 1; cache[key] += 1; cache.move_to_end(key)   # touched: most recent now
            continue
        if len(cache) == maxkeys:                             # memory full: evict one key
            if policy == "lru": victim = next(iter(cache))    # least recently used
            else: victim = min(cache, key=cache.get)          # least frequently used
            del cache[victim]; evicted.append(victim)
        cache[key] = 1
    return hits, evicted
hot = "home p1 home p2 home p1 home p1"
bot = "x1 x2 x3 x4"                                           # a crawler reads 4 pages once
reqs = (hot + " " + bot + " " + hot).split()
print(len(reqs), "requests")
for policy in ("lru", "lfu"):
    hits, ev = simulate(reqs, 3, policy)
    print(f"{policy}: hits {hits:2}, evicted {' '.join(ev)}")

আউটপুট:

20 requests
lru: hits 10, evicted p2 home p1 x1 x2 x3 x4
lfu: hits 12, evicted p2 x1 x2 x3 x4

LRU-তে bot-এর চারটি পাতা p2, home আর p1 — তিনটি গরম key-ই — সরিয়ে দেয়; bot চলে যাওয়ার পর home আর p1 আবার miss. LFU-তে home (৪ বার) আর p1 (৩ বার) টিকে থাকে, bot-এর পাতাগুলো একে অপরকে সরায়; দুটি বাড়তি hit. LFU-র দাম: একসময় জনপ্রিয় কিন্তু এখন মৃত key-ও গণকের জোরে টিকে থাকতে পারে — তাই Redis গণককে সময়ের সাথে কমায় (lfu-decay-time). কোন policy ভালো, তা আপনার অনুরোধের ধরনই বলবে; INFO stats-এর keyspace_hits, keyspace_misses আর evicted_keys মেপে ঠিক করুন।

৭ · Rate limiting আর distributed lock

Fixed window — INCR আর EXPIRE

OTP পাঠাতে প্রতিটি SMS-এর খরচ আছে, আর কেউ একটি নম্বরে হাজারবার OTP চাইলে সেটি অপব্যবহার — কারও ফোনে বিরক্তি, প্রতিষ্ঠানের বিল। Rate limiting মানে একটি সময়ের মধ্যে অনুরোধের সংখ্যা সীমিত রাখা। সবচেয়ে সরল রূপ fixed window: প্রতিটি নম্বরের জন্য একটি গণক, প্রথম অনুরোধে ৬০ সেকেন্ডের মেয়াদ, সীমা ৩:

key: rl:otp:01711000001লাল = সিদ্ধান্ত

            
অবস্থা
গণক
TTL
সিদ্ধান্ত
  1. প্রথম অনুরোধ। INCR না-থাকা key-কে ০ ধরে ১ বানায়। ফল ১ মানে window-টি এইমাত্র শুরু, তাই অ্যাপ মেয়াদ বসায় — ৬০ সেকেন্ড। সীমার মধ্যে, OTP যায়।
  2. দ্বিতীয় অনুরোধ। গণক ২; TTL ৪০ — window শুরুর ২০ সেকেন্ড পর। INCR মেয়াদ বদলায় না, তাই window সরে না।
  3. তৃতীয় অনুরোধ। গণক ৩, এখনো সীমার মধ্যে। একসাথে দুটি অনুরোধ এলেও দুজন আলাদা সংখ্যা পাবে (৩ আর ৪) — INCR atomic, “পড়ে, যোগ করে, লেখা”-র ফাঁক নেই।
  4. চতুর্থ অনুরোধ — না। গণক ৪, সীমা ৩. অ্যাপ OTP পাঠায় না, আর TTL দেখে বলে কত সেকেন্ড পর চেষ্টা করা যাবে। গণক বাড়তে থাকলেও ক্ষতি নেই — মেয়াদে সব মুছবে।
  5. নতুন window. ৬০ সেকেন্ড পেরিয়ে key মুছে গেছে; INCR আবার ১ দেয়, নতুন মেয়াদ। পুরো ব্যবস্থায় প্রতি নম্বরে একটি ছোট key, প্রতি অনুরোধে O(1).
  6. লুকানো ফাঁক। INCR আর EXPIRE দুটি আলাদা কমান্ড। মাঝে অ্যাপ crash করলে key-এর মেয়াদ থাকে না, আর নম্বরটি চিরকাল আটকে যায়। সমাধান: আগে SET … EX 60 NX (মেয়াদসহ তৈরি), অথবা দুটিকে MULTI/EXEC বা Lua script-এ একসাথে।

Fixed window-এর একটি জানা দুর্বলতা আছে: window-এর সীমানায় ভিড়। নিচের otp_requests টেবিলে প্রতিটি অনুরোধের নম্বর (phone) আর সময় t (১০:০০:০০ থেকে সেকেন্ডে)। প্রথম নম্বরটি ৫৮ থেকে ৬২ সেকেন্ডের মধ্যে ছয়বার চেয়েছে। এখানে window ঘড়ির মিনিটে বাঁধা (t / 60) — key-এর নামে মিনিট রেখে (rl:otp:…:16) Redis-এও এভাবে করা যায়:

-- fixed window: প্রতিটি ৬০ সেকেন্ডের খোপে কয়টি
SELECT phone, t / 60 AS win, COUNT(*) AS reqs, COUNT(*) > 3 AS blocked_some
FROM otp_requests GROUP BY phone, win ORDER BY phone, win;
-- sliding window: প্রতিটি অনুরোধের আগের ৬০ সেকেন্ডে কয়টি
SELECT phone, t,
       COUNT(*) OVER (PARTITION BY phone ORDER BY t
                      RANGE BETWEEN 59 PRECEDING AND CURRENT ROW) AS last_60s
FROM otp_requests ORDER BY phone, t;

Fixed window-এ প্রথম নম্বর win 0 | 3 আর win 1 | 3 — দুই খোপেই সীমার মধ্যে, তাই ছয়টিই পাস, অথচ সব মাত্র ৫ সেকেন্ডে! দ্বিতীয় নম্বর 0 | 4 | 1 — চতুর্থটি আটকাবে। দ্বিতীয় কোয়েরি পর্ব ২৬-এর window function দিয়ে sliding window মাপে: t = ৬০-এ 4, ৬১-এ 5, ৬২-এ 6 — sliding নিয়মে এই তিনটিই আটকাত। (t = ৫৯-এর দুটি সারি দুটিতেই 3, কারণ RANGE-এ সমান t-এর সারিগুলো একসাথে গোনা হয়।) অর্থাৎ fixed window সবচেয়ে খারাপ ক্ষেত্রে অল্প সময়ে সীমার প্রায় দ্বিগুণ ছাড়তে পারে।

Redis-এ sliding window-এর প্রচলিত রূপ sorted set: প্রতিটি অনুরোধ একটি সদস্য, score তার সময়। প্রতিবার ৬০ সেকেন্ডের পুরোনোগুলো মুছে বাকিগুলো গোনা — নির্ভুল, কিন্তু প্রতি নম্বরে অনুরোধের সমান সদস্য, তাই মেমরি বেশি। মাঝামাঝি পথ: আগের আর এখনকার window-এর দুটি গণককে ভার দিয়ে মেলানো (sliding window counter). চারটি কমান্ড একসাথে পাঠাতে MULTI/EXEC:

redis> MULTI
OK
redis> ZREMRANGEBYSCORE rl:sw:01711000001 0 1760090340000
QUEUED
redis> ZADD rl:sw:01711000001 1760090400000 req-8f2a
QUEUED
redis> ZCARD rl:sw:01711000001
QUEUED
redis> EXPIRE rl:sw:01711000001 60
QUEUED
redis> EXEC
1) (integer) 0
2) (integer) 1
3) (integer) 1
4) (integer) 1

এখানে এখনকার সময় ১৭৬০০৯০৪০০০০০ ms; তার ৬০,০০০ ms আগে পর্যন্ত সব মোছা, নতুনটি যোগ, তারপর গোনা — ফল ১, সীমার মধ্যে। সদস্যের নাম অনন্য রাখুন (এখানে একটি অনুরোধ-id), নইলে একই মিলিসেকেন্ডের দুটি অনুরোধ একটি হয়ে যায়। Redis-এর MULTI/EXEC সবগুলো কমান্ড একসাথে, মাঝে কাউকে না ঢুকিয়ে চালায় — কিন্তু relational transaction-এর মতো rollback নেই: একটি কমান্ড ব্যর্থ হলেও বাকিগুলো চলে।

Distributed lock — SET NX PX, আর তার সীমা

কয়েকটি সার্ভারে চলা একটি অ্যাপে “রাতের হিসাব-মেলানোর job একসাথে একজনই চালাবে” — এর জন্য distributed lock. সঠিক রূপের তিনটি অংশ: অনন্য একটি token (কে lock নিল), NX (না থাকলেই), আর PX মেয়াদ (lock-ধারী মরে গেলেও lock একসময় ছাড়বে)। ছাড়ার সময় শুধু নিজের lock মুছতে হবে — তাই token মিলিয়ে মোছা, একটি atomic Lua script-এ:

redis> SET lock:nightly-recon 7f3c9a NX PX 30000
OK
redis> SET lock:nightly-recon b81e44 NX PX 30000     # অন্য সার্ভার
(nil)
redis> EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:nightly-recon 7f3c9a
(integer) 1

শুধু DEL কেন নয়? ধরুন সার্ভার A-র কাজ ৩০ সেকেন্ডের বেশি লাগল; lock মেয়াদে ছেড়ে গেল, B নিল; তারপর A শেষ করে DEL করলে B-র lock মুছে যায়। token মিলিয়ে মুছলে A শুধু নিজেরটি মুছতে পারে। তবু মৌলিক সীমা থেকে যায়: A যদি garbage collection বা network-এর কারণে ৩০ সেকেন্ড থেমে থাকে, সে জানবেই না তার lock গেছে, আর দুজন একসাথে কাজ করবে। এজন্য গুরুত্বপূর্ণ কাজে fencing token পরামর্শ দেওয়া হয়: প্রতিটি lock-এর সাথে একটি বাড়তে থাকা সংখ্যা, আর ডেটাবেস পুরোনো সংখ্যার লেখা প্রত্যাখ্যান করে। বহু Redis node-এ একসাথে lock নেওয়ার Redlock অ্যালগরিদম নিয়ে ২০১৬ সালে প্রকাশ্য বিতর্ক হয়েছিল — Martin Kleppmann ঘড়ি আর বিরতির অনুমান নিয়ে সমালোচনা করেন, antirez জবাব দেন (ভাবনার প্রশ্ন ৩)। ব্যবহারিক নিয়ম: lock যদি শুধু দক্ষতার জন্য হয় (একই কাজ দুবার হলে একটু অপচয়), একটি Redis-এর SET NX PX যথেষ্ট; শুদ্ধতা নির্ভর করলে (দুবার হলে টাকা দুবার কাটে) ডেটাবেসের নিজের constraint বা lock (পর্ব ৪৬), অথবা consensus-ভিত্তিক ব্যবস্থা।

৮ · Persistence, replication, Sentinel আর Cluster

মেমরির ডেটা বিদ্যুৎ গেলেই শেষ — লোডশেডিংয়ে কম্পিউটার বন্ধ হলে খোলা Word ফাইলের মতো। Redis দুইভাবে ডিস্কে টিকিয়ে রাখে:

দিকRDB snapshotAOF (append-only file)
কী লেখেনির্দিষ্ট সময়ে পুরো ডেটার ছবিপ্রতিটি লেখার কমান্ড, ক্রমানুসারে
কীভাবেfork() করে child প্রক্রিয়া লেখে; copy-on-writeফাইলের শেষে যোগ; মাঝে মাঝে ছোট করে নতুন করে লেখা (rewrite)
crash-এ হারায়শেষ snapshot-এর পরের সব (মিনিটও হতে পারে)appendfsync everysec-এ মোটামুটি শেষ ১ সেকেন্ড; always-এ প্রায় কিছুই না, কিন্তু অনেক ধীর
চালু হতেদ্রুত — ছবিটি লোডধীর — কমান্ডগুলো আবার চালানো

RDB-এ fork-এর পর child আর parent একই মেমরির page ভাগ করে; parent কোনো page বদলালে তবেই সেটির কপি হয় (copy-on-write) — তাই লেখা-ভারী সময়ে snapshot-এর সময় মেমরি অনেক বাড়তে পারে, সার্ভারে বাড়তি RAM রাখুন। AOF পর্ব ৪৮-এর WAL-এর আত্মীয়, তবে একটি পার্থক্য গুরুত্বপূর্ণ: PostgreSQL commit-এর আগে log ডিস্কে নিশ্চিত করে, আর Redis-এর ডিফল্ট everysec উত্তর আগে দেয়, fsync পরে। নতুন সংস্করণে দুটি মেশানো যায় (AOF-এর শুরুতে RDB). ডিফল্ট কনফিগে সাধারণত RDB চালু আর AOF বন্ধ থাকে — সংস্করণ আর প্যাকেজভেদে দেখে নিন।

Replication. একটি primary, এক বা একাধিক replica; replica প্রথমে একটি snapshot নেয়, তারপর কমান্ডের ধারা। এটি asynchronous: primary client-কে OK বলে আগে, replica-য় পাঠায় পরে। primary মরে গিয়ে replica উঠে এলে শেষ কয়েকটি স্বীকৃত লেখা হারাতে পারে (পর্ব ৫২)। WAIT 1 100 অন্তত একটি replica-র স্বীকৃতির জন্য অপেক্ষা করে, কিন্তু Redis-এর নথিও বলে এটি Redis-কে strongly consistent বানায় না।

Sentinel. কয়েকটি আলাদা Sentinel প্রক্রিয়া (সাধারণত অন্তত তিনটি) primary-কে নজরে রাখে; যথেষ্টসংখ্যক (quorum) একমত হলে একটি replica-কে primary বানায় আর client-দের নতুন ঠিকানা জানায় — স্বয়ংক্রিয় failover, sharding ছাড়া।

Redis Cluster. ডেটা এক মেশিনের RAM-এ না ধরলে sharding (পর্ব ৫৪)। Cluster key-জগৎকে ঠিক ১৬,৩৮৪টি hash slot-এ ভাগ করে: slot = CRC16(key) mod 16384, আর প্রতিটি primary node কিছু slot-এর মালিক। ভুল node-এ কমান্ড গেলে সে MOVED উত্তর দিয়ে সঠিক node দেখায়; client সেটি মনে রাখে। node যোগ করলে কিছু slot সরানো হয় — key ধরে নয়, slot ধরে। একটি নিয়ম মনে রাখতেই হবে: এক কমান্ডে কয়েকটি key (MSET, SINTER, MULTI, Lua) তখনই চলে, যখন সবাই একই slot-এ — নইলে CROSSSLOT error. hash tag দিয়ে সেটি নিশ্চিত করা যায়: {user:42}:cart আর {user:42}:profile-এ শুধু বন্ধনীর ভেতরের অংশ hash হয়, তাই দুটি একই slot-এ।

redis> CLUSTER KEYSLOT user:42:cart
(integer) 12984
redis> CLUSTER KEYSLOT {user:42}:cart
(integer) 15880
redis> CLUSTER KEYSLOT {user:42}:profile
(integer) 15880

hash tag ছাড়া user:42:cart যায় slot ১২,৯৮৪-এ; tag দিলে দুটি key-ই শুধু user:42 থেকে hash হয়, তাই দুটিই ১৫,৮৮০-এ। সাবধান: খুব বেশি key একই tag-এ রাখলে একটি slot, তাই একটি node, অতিরিক্ত ভারী হয়।

আরও গভীরে: slot কেন ১৬,৩৮৪ (২১৪)? antirez-এর ব্যাখ্যা অনুযায়ী, Cluster-এর node-রা নিয়মিত heartbeat বার্তায় নিজের slot-গুলোর একটি bitmap পাঠায়; ১৬,৩৮৪ bit = ২ KB, যা প্রতি বার্তায় সহনীয়, অথচ ৬৫,৫৩৬ হলে ৮ KB হতো। আর Cluster-কে মোটামুটি হাজার node-এর মধ্যে রাখার নকশা ধরলে প্রতি node-এ যথেষ্ট slot থাকে, যাতে ভার সমানভাবে ভাগ করা যায়। এটি consistent hashing-এর চেয়ে সরল একটি বিকল্প: key → slot নির্দিষ্ট, শুধু slot → node বদলায়।

৯ · লাইসেন্স ও Valkey, Memcached আর নিরাপত্তা

২০২৪-এর লাইসেন্স বদল আর Valkey

Redis বহু বছর BSD লাইসেন্সে ছিল — একটি permissive open source লাইসেন্স। ২০২৪ সালের মার্চে Redis Ltd. (প্রকল্পের পেছনের প্রতিষ্ঠান) ঘোষণা করে, Redis 7.4 থেকে নতুন সংস্করণ RSALv2 আর SSPL-এর দ্বৈত লাইসেন্সে আসবে; ঘোষিত উদ্দেশ্য ছিল বড় cloud প্রতিষ্ঠানগুলো যেন অবদান ছাড়া এটিকে সেবা হিসেবে বিক্রি না করে। SSPL-কে Open Source Initiative open source লাইসেন্স হিসেবে স্বীকৃতি দেয় না। প্রতিক্রিয়ায় কয়েক সপ্তাহের মধ্যে Linux Foundation-এর অধীনে শেষ BSD সংস্করণ থেকে একটি fork শুরু হয় — Valkey — যাতে AWS, Google Cloud, Oracle-সহ কয়েকটি প্রতিষ্ঠান যুক্ত হয়। পরে, ২০২৫ সালের Redis 8-এ AGPLv3 একটি তৃতীয় বিকল্প হিসেবে যোগ হয়েছে বলে Redis ঘোষণা করেছে। দুই প্রকল্প এখন আলাদা পথে চলছে, যদিও কমান্ড আর protocol এখনো ব্যাপকভাবে মেলে। নিজের প্রকল্পে কোনটি নেবেন, তা প্রতিষ্ঠানের লাইসেন্স-নীতি, cloud সেবা আর প্রয়োজনীয় বৈশিষ্ট্য দেখে ঠিক করুন — বিস্তারিত সর্বশেষ সরকারি সূত্রে যাচাই করে নিন, কারণ এই অবস্থা এখনো বদলাচ্ছে।

Redis বনাম Memcached

Memcached (২০০৩, LiveJournal-এর জন্য Brad Fitzpatrick) in-memory cache-এর পুরোনো ও পরীক্ষিত নাম:

দিকRedisMemcached
মানঅনেক data typeশুধু বাইটের string (প্রতি item-এ ডিফল্ট সীমা ১ MB)
থ্রেডকমান্ড এক থ্রেডে, I/O থ্রেড ঐচ্ছিকশুরু থেকেই multi-threaded
Persistence, replicationRDB/AOF, replica, Clusterনেই — বিশুদ্ধ cache
বাড়তিLua, stream, pub/sub, TTL প্রতি keyসরল, কম বৈশিষ্ট্য, অনুমানযোগ্য
কখনcache-এর সাথে session, queue, leaderboardশুধু সরল, বিশাল আকারের cache

নিরাপত্তা — Redis কখনো ইন্টারনেটে খোলা নয়

Redis বানানো হয়েছিল বিশ্বস্ত, ভেতরের network-এর জন্য। পাসওয়ার্ড ছাড়া খোলা Redis পেলে যে কেউ সব ডেটা পড়তে, মুছতে বা CONFIG-এর মতো কমান্ড দিয়ে সার্ভারের ক্ষতি করতে পারে; ইন্টারনেটে খোলা Redis সার্ভার দখল হওয়ার ঘটনা নিরাপত্তা গবেষকেরা বহুবার জানিয়েছেন। Redis 3.2 থেকে protected mode আছে (পাসওয়ার্ড বা bind না থাকলে বাইরের connection প্রত্যাখ্যান), কিন্তু ভরসা কনফিগে রাখুন:

bind 10.0.1.15 127.0.0.1      # শুধু private network-এর ঠিকানা, কখনো 0.0.0.0 নয়
protected-mode yes
port 6379
# ACL (Redis 6+): প্রতিটি অ্যাপের আলাদা ব্যবহারকারী, ন্যূনতম অনুমতি
user default off
user shopapp on >use-a-long-random-secret ~shop:* +@read +@write -@dangerous
# TLS (Redis 6+), যদি network বিশ্বস্ত না হয়
# tls-port 6380
# tls-cert-file /etc/redis/tls/redis.crt
# tls-key-file  /etc/redis/tls/redis.key

সাথে: firewall বা cloud-এর security group-এ শুধু অ্যাপ সার্ভারগুলোকে 6379-এ ঢুকতে দিন; পাসওয়ার্ড কোডে নয়, secrets manager-এ; -@dangerous দিয়ে FLUSHALL, CONFIG, KEYS-এর মতো কমান্ড অ্যাপের ব্যবহারকারীর জন্য বন্ধ — পর্ব ৫১-এর least privilege. cache-এ ব্যক্তিগত তথ্য (NID নম্বর, ফোন) রাখলে সেটিও ডেটাবেসের মতোই সুরক্ষার দাবি রাখে।

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • Redis এত দ্রুত কেন? — ডেটা মেমরিতে, সরল O(1)/O(log N) গঠন, এক থ্রেডে event loop-এ কমান্ড — lock বা context switch-এর খরচ নেই।
  • Cache-aside-এ লেখার সময় কী করবেন? — আগে ডেটাবেসে commit, তারপর key মোছা (DEL), সাথে TTL নিরাপত্তা-জাল হিসেবে।
  • Leaderboard-এর জন্য কোন data type? — Sorted set: ZINCRBY, ZRANGE … REV, ZREVRANK — সবই O(log N) বা O(log N + M).
  • Cache stampede কী, প্রতিকার? — জনপ্রিয় key মরলে একসাথে বহু miss ডেটাবেসে যায়; lock দিয়ে একজনের নবায়ন, TTL-এ jitter, আগেই নবায়ন।
  • Redis Cluster-এ কয়টি hash slot? — ১৬,৩৮৪; slot = CRC16(key) mod 16384, আর hash tag দিয়ে কয়েকটি key এক slot-এ রাখা যায়।

১০ · AI-এর যুগে Redis আর cache

“আমার API-তে Redis cache যোগ করো” — AI কয়েক সেকেন্ডে কোড দেয়, আর সেটি চলেও। কিন্তু cache-এর ভুল প্রায় কখনো error দেখায় না; দেখায় পুরোনো দাম, ভুল স্টক বা হঠাৎ ডেটাবেসের ওপর ঝাঁপিয়ে পড়া। সাধারণ ভুল:

AI-এর ভুলফলকীভাবে ধরবেন
TTL ছাড়া SET, বা লেখার পর invalidation নেইপুরোনো ডেটা অনির্দিষ্টকালপ্রতিটি SET-এ EX খুঁজুন; লেখার প্রতিটি পথে DEL আছে কি না
সব key-এ একই TTLএকসাথে মৃত্যু, stampedejitter আর lock আছে কি না দেখুন
অ্যাপের কোডে KEYS *চালু সার্ভার থেমে যায়SCAN ব্যবহার; ACL-এ KEYS বন্ধ
শুধু DEL দিয়ে lock ছাড়া, বা মেয়াদহীন lockঅন্যের lock মোছা, চিরকাল আটকে থাকাtoken মিলিয়ে Lua-তে ছাড়া, PX আছে কি না
INCR আর EXPIRE আলাদা, মাঝে ফাঁকমেয়াদহীন গণক, ব্যবহারকারী চিরকাল আটকেSET … NX EX, MULTI বা Lua
Redis-কে একমাত্র ভান্ডার ধরাfailover বা restart-এ ডেটা হারানোসত্যের উৎস কোথায়, তা লিখে নিশ্চিত করুন

AI-কে প্রসঙ্গ দিন, শুধু “cache যোগ করো” নয়:

আমার অ্যাপ PostgreSQL 16 আর Redis 7.2 ব্যবহার করে। পণ্যের পাতার কোয়েরি গড়ে 80 ms, প্রতি সেকেন্ডে 2,000 অনুরোধ, দাম দিনে কয়েকবার বদলায়, স্টক প্রতি মিনিটে। ১. cache-aside নকশায় key-এর নাম, TTL আর কোন কোন লেখার পথে key মুছতে হবে — তালিকা দাও। ২. দাম আর স্টক আলাদা TTL-এ রাখা উচিত কি না, যুক্তিসহ বলো; কতটা পুরোনো ডেটা চলবে তা আমাকে জিজ্ঞেস করো। ৩. stampede ঠেকাতে lock আর TTL jitter-এর কোড দাও, lock-এ token আর মেয়াদসহ। ৪. invalidation আর পাঠকের race condition কোথায় হতে পারে, দেখাও। ৫. KEYS কমান্ড ব্যবহার করবে না; কোনো টেবিল বা কলামের নাম অনুমান করবে না।

AI-এর দেওয়া cache-কোড চালুর আগে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১সার্ভারে ৩২টি CPU core, অথচ Redis কমান্ড চালায় একটি থ্রেডে। তবু এটি দ্রুততম ডেটা স্টোরগুলোর একটি। কেন, আর কখন এই নকশা সত্যিই সীমা হয়ে দাঁড়ায়?

প্রথমে দেখুন একটি সাধারণ Redis অনুরোধের সময় কোথায় যায়। hash table-এ একটি খোঁজ বা একটি সংখ্যা যোগ করা মাইক্রোসেকেন্ডেরও কম। অথচ client থেকে সার্ভারে যাওয়া-আসা, এমনকি একই data center-এ, সাধারণত কয়েকশো মাইক্রোসেকেন্ড। মানে কাজের বড় অংশ network আর সিস্টেম-কল, কমান্ড নিজে নয়। এখানে বহু থ্রেড দিলে লাভ সামান্য, কিন্তু খরচ বড়: প্রতিটি গঠনে lock, থ্রেডে থ্রেডে cache line-এর টানাটানি, আর সবচেয়ে বড় — atomicity-র সরল নিশ্চয়তা হারানো। এক থ্রেডে INCR, ZINCRBY বা একটি Lua script নিজে থেকেই অবিভাজ্য।

সীমা আসে তিন জায়গায়। এক, network-এর কাজ — হাজারো client-এর অনুরোধ পড়া আর উত্তর লেখা। Redis 6.0-এর I/O থ্রেড ঠিক এই অংশটিই ভাগ করে, কমান্ড চালানো নয়; পরের সংস্করণগুলোতে (Redis 8, Valkey 8) এটি আরও উন্নত হয়েছে বলে প্রকল্পগুলো জানায়। দুই, ধীর কমান্ড: KEYS, লাখো সদস্যের set-এ SMEMBERS, বিশাল key-এর DEL, বা লম্বা Lua script — চলার সময় সব client অপেক্ষায়, যা p99 latency-কে ধ্বংস করে। SLOWLOG GET এদের খুঁজে দেয়। তিন, একটি মেশিনের একটি core-এর মোট ক্ষমতা: চূড়ান্ত চাপে উত্তর হলো একই মেশিনে কয়েকটি Redis প্রক্রিয়া, বা Cluster.

Memcached শুরু থেকেই multi-threaded, কারণ তার কাজ আরও সরল — শুধু get আর set, কোনো জটিল গঠন নেই। পরে KeyDB, Dragonfly-র মতো কিছু প্রকল্প Redis-এর protocol রেখে বহু-থ্রেড নকশা চেষ্টা করেছে; প্রত্যেকে নিজস্ব benchmark-এ সুবিধা দাবি করে, যা নিজের workload-এ যাচাই করাই নিরাপদ।

শিক্ষাটি Redis ছাড়িয়েও খাটে: সমান্তরালতা সবসময় দ্রুততা নয়। কাজ যখন খুব ছোট আর ভাগ করা ডেটা নিয়ে, তখন সমন্বয়ের খরচ কাজের চেয়ে বড় হয়ে যায়। Node.js-এর event loop আর Nginx-এর নকশাও একই চিন্তা থেকে।

মূল উপলব্ধি: Redis-এর গতি আসে মেমরি আর সরলতা থেকে, থ্রেড থেকে নয়; একক থ্রেড atomicity উপহার দেয়, আর বিনিময়ে দাবি করে — কোনো কমান্ড যেন ধীর না হয়।

প্র ০২২০১৩ সালে Facebook-এর প্রকৌশলীরা NSDI সম্মেলনে “Scaling Memcache at Facebook” প্রবন্ধে বিশ্বের অন্যতম বড় cache-ব্যবস্থার বর্ণনা দেন। stale data আর stampede সামলাতে তাঁরা কী করেছিলেন, আর একটি ছোট প্রতিষ্ঠান সেখান থেকে কী নিতে পারে?

প্রবন্ধে বর্ণিত নকশাটি মূলত এই পর্বের cache-aside: পড়ার সময় আগে memcache, miss হলে MySQL, তারপর memcache-এ রাখা; লেখার সময় ডেটাবেস আপডেট, তারপর key মোছা — তাঁরাও SET-এর বদলে delete বেছেছিলেন, কারণ delete idempotent আর ক্রম উল্টালেও কম ক্ষতি করে।

দুটি সমস্যার জন্য তাঁরা “lease” নামে একটি কৌশল বর্ণনা করেন। miss হলে memcache client-কে একটি ছোট token দেয়; মান বসানোর সময় সেই token দেখাতে হয়। মাঝে key মুছে গেলে token বাতিল হয়ে যায়, তাই দেরিতে আসা পুরোনো মান বসতে পারে না — অংশ ৫-এর race condition-এর সমাধান। আর একই key-এর জন্য অল্প সময়ের মধ্যে একটির বেশি token দেওয়া হয় না; বাকিরা একটু অপেক্ষা করে আবার চেষ্টা করে, বা কিছুটা পুরোনো মান পায় — এটি stampede-এর বিরুদ্ধে lock-এর মতোই। প্রবন্ধে বলা হয়েছে, এতে কিছু জনপ্রিয় key-এ ডেটাবেসের চাপ বহু গুণ কমেছিল।

invalidation-এর জন্য তাঁরা অ্যাপের ওপর পুরোপুরি ভরসা করেননি। MySQL-এর commit log পড়ে একটি ব্যবস্থা (প্রবন্ধে mcsqueal নামে) বদলে যাওয়া সারির key-গুলো মুছতে পাঠাত — অর্থাৎ ডেটাবেসের নিজের log-ই সত্যের উৎস, অংশ ৫-এর outbox আর change data capture-এর একই চিন্তা। বহু অঞ্চলে ছড়ানো replica-র কারণে “নিজের সদ্য লেখা দেখা” (read-your-writes)-এর জন্যও আলাদা কৌশল ছিল।

একটি ছোট প্রতিষ্ঠানের জন্য শিক্ষা আকারে নয়, নীতিতে: (১) লেখার পর SET নয়, DEL; (২) জনপ্রিয় key-এর miss-এ একজনই ডেটাবেসে যাবে; (৩) invalidation অ্যাপের ভালো ইচ্ছার ওপর ছেড়ে না দিয়ে, যেখানে পারা যায়, commit-এর সাথে বাঁধা; (৪) কিছুটা পুরোনো ডেটা কোথায় চলবে আর কোথায় চলবে না, তা আগে ঠিক করা।

মূল উপলব্ধি: বিশাল মাপের cache-এর সমস্যাগুলো ছোট মাপের সমস্যাই — দৌড়, ভিড় আর ভুলে যাওয়া invalidation; শুধু সেখানে তারা প্রতিদিন কোটিবার ঘটে, তাই সমাধানগুলো নিখুঁত করতে হয়েছে।

প্র ০৩২০১৬ সালে Martin Kleppmann (পরে “Designing Data-Intensive Applications” বইয়ের লেখক হিসেবে পরিচিত) Redlock নিয়ে সমালোচনামূলক লেখা প্রকাশ করেন, আর antirez প্রকাশ্যে জবাব দেন। বিতর্কের মূল বিষয় কী ছিল, আর এখান থেকে lock সম্পর্কে কী শেখা যায়?

Redlock-এর ধারণা: একটি Redis-এর বদলে পাঁচটি স্বাধীন Redis node; client সবগুলোতে SET NX PX চেষ্টা করে, অধিকাংশে (অন্তত তিনটিতে) নির্দিষ্ট সময়ের মধ্যে পেলে lock তার। এতে একটি node মরে গেলেও lock-ব্যবস্থা টিকে থাকে।

Kleppmann-এর সমালোচনার দুটি স্তর ছিল। প্রথম, lock-এর উদ্দেশ্য আলাদা করা — দক্ষতা (একই কাজ দুবার না করা) নাকি শুদ্ধতা (দুজন একসাথে করলে ডেটা নষ্ট)। দক্ষতার জন্য একটি Redis-ই যথেষ্ট, পাঁচটি অপ্রয়োজনীয়। শুদ্ধতার জন্য তিনি বলেন Redlock যথেষ্ট নিরাপদ নয়, কারণ এটি সময়ের অনুমানের ওপর দাঁড়িয়ে: client দীর্ঘ garbage collection-এ থেমে থাকতে পারে, network বার্তা দেরি করতে পারে, কোনো node-এর ঘড়ি লাফ দিতে পারে — তখন lock-এর মেয়াদ শেষ হয়ে গেছে অথচ client জানে না। তাঁর প্রস্তাব fencing token: lock-সেবা প্রতিবার একটি বাড়তে থাকা সংখ্যা দেবে, আর যে স্টোরেজে লেখা হচ্ছে সে ছোট সংখ্যার লেখা ফিরিয়ে দেবে। Redlock এমন সংখ্যা দেয় না।

antirez-এর জবাব ছিল, Redlock ঘড়ির নিখুঁত মিল চায় না, শুধু চায় সময় মোটামুটি একই হারে চলুক; বিরতির সমস্যা যেকোনো মেয়াদি lock-এ আছে; আর কিছু পরিস্থিতিতে অনন্য token-ও fencing-এর কাজে লাগানো যায়। দুজনের লেখাই প্রকাশ্যে আছে, আর বিশেষজ্ঞদের মধ্যেও মতভেদ থেকে গেছে — কোনো পক্ষকে চূড়ান্ত বিজয়ী ঘোষণা করার দরকার নেই।

যা প্রায় সবাই মানেন: মেয়াদি lock একটি “ইজারা” — ধারক সেটি হারাতে পারে না জেনেই। তাই শুদ্ধতা জরুরি হলে শেষ প্রতিরক্ষা থাকা উচিত সেখানে, যেখানে ডেটা লেখা হয় — ডেটাবেসের unique constraint, version column দিয়ে optimistic locking (পর্ব ৪৬), idempotency key, বা ZooKeeper/etcd-এর মতো consensus-ভিত্তিক ব্যবস্থার fencing সুবিধা। bKash-এর মতো লেনদেনে “একই অনুরোধ দুবার এলেও টাকা একবার কাটবে” — এই নিশ্চয়তা আসে ডেটাবেসের idempotency থেকে, Redis lock থেকে নয়।

মূল উপলব্ধি: distributed lock-এর প্রশ্ন “কোন অ্যালগরিদম” নয়, “lock ব্যর্থ হলে কী ভাঙে”. দক্ষতার lock সস্তায় চলে; শুদ্ধতার নিশ্চয়তা রাখুন ডেটা যেখানে লেখা হয়।

প্র ০৪Redis ২০২৪ সালে BSD থেকে source-available লাইসেন্সে গেল, আর কয়েক সপ্তাহে Valkey জন্ম নিল। এর আগে MongoDB (২০১৮), Elasticsearch (২০২১) আর HashiCorp Terraform (২০২৩)-এর ক্ষেত্রেও কাছাকাছি ঘটনা ঘটেছিল। এই ধারার পেছনে কোন অর্থনৈতিক টানাপোড়েন, আর একজন developer-এর কী ভাবা উচিত?

টানাপোড়েনটি এমন: একটি প্রতিষ্ঠান বছরের পর বছর প্রকৌশলী রেখে একটি open source ডেটাবেস বানায়, আর আয় করে তার managed সেবা বা বাড়তি বৈশিষ্ট্য বিক্রি করে। permissive লাইসেন্সে যেকোনো বড় cloud প্রতিষ্ঠান একই সফটওয়্যার নিয়ে নিজের সেবা হিসেবে বিক্রি করতে পারে — আইনত সম্পূর্ণ বৈধ — আর তাদের বিতরণ-ক্ষমতা অনেক বেশি। প্রতিষ্ঠানগুলোর যুক্তি: মূল্য তৈরি করছি আমরা, তুলে নিচ্ছে অন্যরা। সমালোচকদের যুক্তি: হাজারো বাইরের অবদানকারী আর ব্যবহারকারী একটি open source প্রতিশ্রুতির ওপর ভিত্তি করে বিনিয়োগ করেছিলেন, যা হঠাৎ বদলে গেল।

প্রতিটি ঘটনায় প্রতিক্রিয়ার ধাঁচ মিলে যায়। MongoDB SSPL-এ যাওয়ার পর কিছু Linux distribution এটি বাদ দেয়। Elasticsearch-এর লাইসেন্স বদলের পর AWS-এর নেতৃত্বে OpenSearch fork আসে; পরে ২০২৪ সালে Elastic আবার AGPL একটি বিকল্প হিসেবে যোগ করে। Terraform-এর পর OpenTofu, Linux Foundation-এর অধীনে। Redis-এর ক্ষেত্রে Valkey একই পথে, আর পরে Redis-ও AGPL বিকল্প যোগ করেছে বলে জানিয়েছে। অর্থাৎ fork-এর সম্ভাবনা নিজেই open source-এর একটি নিয়ন্ত্রণ-ব্যবস্থা।

developer-এর জন্য ব্যবহারিক প্রশ্নগুলো: (১) আমি কি শুধু ব্যবহার করছি, নাকি এটিকে সেবা হিসেবে অন্যকে দিচ্ছি? বেশিরভাগ লাইসেন্স-বিধিনিষেধ দ্বিতীয়টির ওপর, তবু নিজের প্রতিষ্ঠানের আইনি নীতি দেখা জরুরি। (২) আমার cloud সেবা কোনটি চালায় — অনেক managed সেবা Valkey-তে সরেছে বা সরছে বলে জানিয়েছে। (৩) আমি কি এমন বৈশিষ্ট্যে নির্ভর করছি যা শুধু একটিতে আছে? নির্ভর যত কম, বদলানো তত সহজ — পর্ব ৩১-এর vendor lock-in-এর চিন্তা।

এই বিষয়ে তথ্য দ্রুত বদলায়; প্রকল্পগুলোর সরকারি ঘোষণা আর লাইসেন্সের মূল লেখা পড়ে সিদ্ধান্ত নিন, কোনো ব্লগের সারাংশ নয়।

মূল উপলব্ধি: লাইসেন্স একটি প্রযুক্তিগত নির্ভরতা — কোড যেমন, তেমনি। permissive লাইসেন্স বদলাতে পারে, আর fork তার উত্তর দিতে পারে; বিকল্প খোলা রাখাই নিরাপদ নকশা।

প্র ০৫কেউ কেউ বলেন, “Redis-এ AOF আর replica আছে, তাহলে PostgreSQL কেন? সবকিছু Redis-এ রাখি।” একটি পেমেন্ট বা অর্ডারের সিস্টেমে এই যুক্তি কোথায় দুর্বল, আর কখন Redis সত্যিই প্রধান ভান্ডার হতে পারে?

টেকসইতার দিক থেকে শুরু করুন। ডিফল্ট appendfsync everysec-এ Redis client-কে OK বলে, তারপর সেকেন্ডে একবার ডিস্কে নিশ্চিত করে — তাই crash-এ মোটামুটি শেষ এক সেকেন্ডের স্বীকৃত লেখা হারাতে পারে। always দিলে সেই ঝুঁকি প্রায় যায়, কিন্তু প্রতিটি লেখায় fsync — গতির বড় অংশ হারায়। তার ওপর replication asynchronous: primary মরার মুহূর্তে যা replica-য় পৌঁছায়নি, failover-এর পর তা নেই। ঠিক যে লেনদেনে গ্রাহক “সফল” দেখেছেন, সেটিই হয়তো নেই। পর্ব ৫২-এর ভাষায় RPO শূন্য নয়।

তারপর গঠনের দিক। relational ডেটাবেস শুধু টিকিয়ে রাখে না, নিয়মও রক্ষা করে: foreign key, CHECK, unique constraint, বহু টেবিল জুড়ে ACID transaction, আর যেকোনো নতুন প্রশ্নের জন্য ad-hoc SQL. Redis-এ এর কোনোটিই নেই — MULTI/EXEC-এ rollback নেই, constraint নেই, আর “গত মাসে চট্টগ্রামের কোন গ্রাহক ৫,০০০ টাকার বেশি কিনেছেন” জানতে আগে থেকে সেই key বানিয়ে রাখতে হয়। খরচও আছে: সব ডেটা RAM-এ, যা ডিস্কের চেয়ে বহু গুণ দামি; কয়েক বছরের অর্ডারের ইতিহাস মেমরিতে রাখা সাধারণত অর্থহীন।

তবু কিছু ক্ষেত্রে Redis-ই প্রধান ভান্ডার, আর সেটি যুক্তিসংগত: যে ডেটা হারালে আবার বানানো যায় বা হারানো সহনীয় — session (হারালে আবার লগইন), rate limit-এর গণক, leaderboard যা ডেটাবেসের score থেকে পুনর্গঠন করা যায়, ক্ষণস্থায়ী গেমের অবস্থা, real-time উপস্থিতি। Redis Ltd. নিজেও Redis-কে প্রধান ডেটাবেস হিসেবে প্রচার করে, আর কিছু প্রতিষ্ঠান কঠোর AOF আর সতর্ক failover নিয়ে তা করে — কিন্তু সেটি একটি সচেতন সিদ্ধান্ত, ডিফল্ট নয়।

একটি সহজ পরীক্ষা: “এই key-গুলো সব এই মুহূর্তে মুছে গেলে কী হবে?” উত্তর যদি হয় “একটু ধীর হবে, আবার ভরে যাবে”, Redis ঠিক জায়গা। উত্তর যদি হয় “কার কত টাকা, কেউ জানবে না”, তাহলে সেই ডেটা relational ডেটাবেসে, আর Redis তার পাশে cache.

মূল উপলব্ধি: Redis গতির জন্য বানানো, সত্যের উৎস হওয়ার জন্য নয় (ডিফল্টে)। যে ডেটা হারানো চলে না, তার ঘর relational ডেটাবেস; Redis তার দ্রুত, ক্ষণস্থায়ী ছায়া।

অনুশীলন

একটি কোয়েরি লেখা, Redis-এর উত্তর আগে বলা, একটি LRU হাতে চালানো আর একটি stampede-এর হিসাব।

  1. কোয়েরি লিখুন: otp_requests(phone, t) টেবিল থেকে (t = সেকেন্ড) সেই নম্বরগুলো বের করুন, যারা যেকোনো ৬০ সেকেন্ডের sliding window-এ ৩টির বেশি OTP চেয়েছে, সাথে সেই window-এ সর্বোচ্চ কয়টি। অংশ ৭-এর এডিটরে চালিয়ে দেখতে পারেন।
    WITH w AS (
      SELECT phone, COUNT(*) OVER (PARTITION BY phone ORDER BY t
               RANGE BETWEEN 59 PRECEDING AND CURRENT ROW) AS last_60s
      FROM otp_requests)
    SELECT phone, MAX(last_60s) AS peak FROM w
    GROUP BY phone HAVING MAX(last_60s) > 3 ORDER BY peak DESC;

    অংশ ৭-এর ডেটায় ফল 01711000001 | 6 আর 01811000002 | 4. লক্ষ্য করুন, fixed window-এর হিসাবে প্রথম নম্বরটি কখনো সীমা পেরোয়নি — sliding হিসাবেই তার আসল ভিড় দেখা যায়। window function-এর ফলের ওপর সরাসরি WHERE বা HAVING চলে না বলে CTE-তে আগে হিসাব (পর্ব ২৬)।

  2. উত্তর আগে বলুন (trace): একটি খালি Redis-এ পরপর এই কমান্ডগুলো দ্রুত চালানো হলো। প্রতিটির উত্তর কী? SET views 10 EX 100 → INCR views → TTL views → SET views 20 → TTL views → DEL views → TTL views → INCR views
    OK
    (integer) 11
    (integer) 100      ← INCR মেয়াদ রেখে দেয় (দেরি হলে 99 ইত্যাদি)
    OK
    (integer) -1       ← নতুন SET মেয়াদ মুছে দিল
    (integer) 1
    (integer) -2       ← key-ই নেই
    (integer) 1        ← না-থাকা key-কে 0 ধরে যোগ

    দুটি ফাঁদ: SET পুরোনো মেয়াদ মুছে দেয় (KEEPTTL বা নতুন EX না দিলে), আর INCR না-থাকা key নিজেই তৈরি করে — মেয়াদ ছাড়া। rate limiter-এ এই দ্বিতীয়টিই “চিরকাল আটকে থাকা” ভুলের উৎস।

  3. হাতে চালান (LRU): সর্বোচ্চ ৩টি key-এর একটি cache, নীতি allkeys-lru (নিখুঁত LRU ধরুন)। অনুরোধের ক্রম: A B C A D B E A. কয়টি hit, কোন কোন key কোন ক্রমে সরানো হলো, আর শেষে cache-এ কারা?

    বাঁ দিক = সবচেয়ে কম সাম্প্রতিক। A miss [A]; B miss [A B]; C miss [A B C]; A hit [B C A]; D miss, B সরে [C A D]; B miss, C সরে [A D B]; E miss, A সরে [D B E]; A miss, D সরে [B E A].

    hit ১টি; সরানো হলো B, C, A, D; শেষে B, E, A. A চতুর্থ অনুরোধে hit পেয়েও পরে সরে গেল, কারণ তার পরে তিনটি আলাদা key এসেছে — LRU শুধু সাম্প্রতিকতা দেখে, কতবার ব্যবহার হলো তা নয়।

  4. হিসাব করুন (stampede): একটি অফারের পাতার key প্রতি সেকেন্ডে ৩,০০০ বার পড়া হয়, পেছনের কোয়েরি ২৫০ ms নেয়। (ক) কোনো প্রতিরক্ষা ছাড়া key মরলে প্রায় কতগুলো কোয়েরি একসাথে ডেটাবেসে যাবে? (খ) lock দিলে? (গ) deploy-এর সময় ৫,০০০ পণ্যের key সবাই EX 1800 পেল — ±১০% jitter দিলে মৃত্যু কত সেকেন্ডে ছড়াবে, আর গড়ে প্রতি সেকেন্ডে কয়টি?
    • (ক) কোয়েরিটি চলতে চলতে যত অনুরোধ আসে, সবাই miss: ৩,০০০ × ০.২৫ = প্রায় ৭৫০টি একই কোয়েরি একসাথে (Little's law). চাপে কোয়েরি ধীর হলে সংখ্যা আরও বাড়ে।
    • (খ) ১টি — যে lock পেল সে-ই; বাকিরা অপেক্ষা করে আবার cache পড়ে, বা পুরোনো মান দেখায়।
    • (গ) ১,৮০০-এর ১০% = ১৮০, তাই মেয়াদ ১,৬২০ থেকে ১,৯৮০ সেকেন্ডের মধ্যে — ৩৬০ সেকেন্ডে ছড়ানো; সমানভাবে ছড়ালে ৫,০০০ / ৩৬০ ≈ প্রতি সেকেন্ডে ১৪টি miss, একসাথে ৫,০০০-এর বদলে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমCache-aside নকশায় একটি পণ্যের তথ্য পড়ার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. নিয়ম মেনে key বানানো, যেমন shop:product:101
  2. Redis-এ GET করে দেখা
  3. না পেলে (miss) ডেটাবেস থেকে কোয়েরি করে আনা
  4. ফলটি মেয়াদসহ (SET … EX) Redis-এ রাখা
  5. ব্যবহারকারীকে উত্তর ফেরত দেওয়া

একটি অনলাইন কুইজে লাখো খেলোয়াড়ের score প্রতি মুহূর্তে বদলায়, আর প্রতিটি পাতায় প্রথম ১০ জন ও “আপনার অবস্থান” দেখাতে হয়। Redis-এর কোন data type সবচেয়ে মানানসই?

  • List
  • Set
  • Sorted set
  • HyperLogLog

Cache-aside নকশায় একটি পণ্যের দাম বদলানো হচ্ছে। কোন ক্রমটি সাধারণত সবচেয়ে নিরাপদ?

  • আগে key মোছা, তারপর ডেটাবেস আপডেট
  • আগে ডেটাবেসে commit, তারপর key মোছা (DEL), সাথে TTL
  • আগে Redis-এ নতুন দাম SET, তারপর ডেটাবেস আপডেট
  • শুধু ডেটাবেস আপডেট — TTL নিজেই একসময় ঠিক করবে

একটি OTP limiter প্রতিটি নম্বরে প্রতি ঘড়ির মিনিটে (১০:০০:০০–১০:০০:৫৯, ১০:০১:০০–…) সর্বোচ্চ ৫টি অনুরোধ দেয় (fixed window). কেউ ১০:০০:৫৯-এ ৫টি আর ১০:০১:০০-এ ৫টি অনুরোধ পাঠালে কী ঘটে?

  • প্রথম ৫টি পাস, পরের ৫টি আটকায়
  • প্রথম দলের শেষটি আটকায়
  • সবগুলো আটকায়, কারণ ২ সেকেন্ডে ১০টি
  • ১০টিই পাস করে

একটি Redis-এ cache-এর key (সবার TTL আছে) আর queue-এর key (TTL নেই, হারানো চলবে না) একসাথে রাখা। মেমরি ভরে গেলে শুধু cache-এর key সরাতে এবং যার মেয়াদ সবচেয়ে কাছে তাকে আগে সরাতে কোন policy?

  • volatile-ttl
  • allkeys-lru
  • noeviction
  • allkeys-random

Redis-এ AOF চালু, appendfsync everysec. হঠাৎ বিদ্যুৎ চলে গেলে সাধারণত কতটা লেখা হারানোর ঝুঁকি থাকে?

  • কিছুই না — প্রতিটি লেখা সাথে সাথে ডিস্কে
  • মোটামুটি শেষ ১ সেকেন্ডের লেখা
  • শেষ snapshot-এর পরের সব লেখা
  • সব ডেটা, কারণ Redis মেমরিতে চলে

Redis Cluster-এ MSET user:1:cart x user:1:profile y চালালে CROSSSLOT error আসে। কারণ আর সমাধান কী?

  • Cluster-এ MSET একেবারেই নিষিদ্ধ
  • hash slot ফুরিয়ে গেছে — slot বাড়াতে হবে
  • দুটি key আলাদা hash slot-এ; hash tag দিয়ে {user:1}:cart আর {user:1}:profile লেখা
  • replica নেই — একটি replica যোগ করা

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Redis = মেমরিতে data structure: string, hash, list, set, sorted set, stream, bitmap, HLL — কমান্ড এক থ্রেডে, তাই atomic; ধীর কমান্ড (KEYS) সবাইকে থামায়।
  2. নাম আর মেয়াদ: app:entity:id; প্রতিটি cache-এর key-এ TTL, সাথে jitter. নতুন SET মেয়াদ মোছে, INCR রাখে।
  3. Cache-aside: GET → miss → DB → SET EX; লেখায় DB আগে, তারপর DEL. invalidation commit-এর সাথে বাঁধা গেলে আরও ভালো (outbox). stampede-এ lock, jitter, আগেই নবায়ন।
  4. মেমরি ভরলে: maxmemory + policy — বিশুদ্ধ cache-এ allkeys-lru বা allkeys-lfu, মেশানো হলে volatile-*. ডিফল্ট noeviction.
  5. টেকসইতা ও নিরাপত্তা: RDB বনাম AOF, asynchronous replication, Sentinel, Cluster-এর ১৬,৩৮৪ slot; Redis ডিফল্টে সত্যের উৎস নয়, আর কখনো ইন্টারনেটে খোলা নয়।

পরের পর্বে: key-value-এর পর আরও তিনটি বিশেষ জগৎ — সম্পর্কের জাল নিয়ে graph ডেটাবেস, লেখা খোঁজার search engine, আর সময়ের ধারার time-series ডেটাবেস (পর্ব ৫৯)।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান