The speed layer — why a second database earns its place, and how rich values turn a cache into an analytics engine
maxmemory-policy and TTL strategy| Time | Segment | Activity |
|---|---|---|
| 0–8 min | Recap & Framing | Where Module 3 left us; the dashboard question Atlas Charts answers slowly |
| 8–20 min | Polyglot Persistence | The latency budget: RAM vs SSD vs network round trip; system of record vs speed layer |
| 20–30 min | What Redis Is | REmote DIctionary Server; in-memory, single-threaded, rich values; Redis vs Memcached |
| 30–52 min | Core Structures | String, Hash, List, Set, Sorted Set — commands and ride-analytics uses |
| 52–66 min | Bitmaps & HyperLogLog | The 125 KB argument; BITOP retention; approximate cardinality |
| 66–76 min | Keys, TTL, Eviction | Namespacing as schema; EXPIRE, SCAN, maxmemory-policy |
| 76–86 min | Docker Lab | Run Redis 7, connect with redis-cli, work each structure on ride data |
| 86–90 min | Wrap-Up | Durability trade, wrong-answer cases, Lecture 18 build brief |
By the end of Lecture 16 you had a working analytics product: 15,000 ride bookings in MongoDB Atlas, an aggregation pipeline that computes revenue by vehicle type and failure rate by hour, and an Atlas Charts dashboard that renders the result. It is genuinely good work. It is also, for a specific class of question, the wrong machine.
Open that dashboard and every tile fires an aggregation pipeline. The pipeline scans, groups, sorts and returns. On 15,000 documents that is comfortably fast. On 15,000,000 — the volume a real ride-hailing firm generates in a few weeks — the same pipeline becomes a multi-second operation, and you are running it again for every analyst who refreshes the page.
Polyglot persistence is the practice of running more than one database, each chosen for the job it is best at. You met the term in Lecture 11 as a myth-buster. In Module 4 it becomes an architecture: MongoDB is the system of record — the durable, queryable, auditable truth — and Redis is the speed layer — a small, volatile, extremely fast set of pre-computed answers.
The argument for a speed layer is not aesthetic. It is arithmetic. These are the numbers every systems architect carries in their head:
| Operation | Typical Latency | Relative Cost | What It Means |
|---|---|---|---|
| Read from RAM | ~100 nanoseconds | 1× | Effectively instant — the CPU barely waits |
| Read from SSD | ~100 microseconds | ~1,000× | Fast, but a thousand RAM reads could have happened |
| Database round trip over a network | ~1–10 milliseconds | ~10,000–100,000× | Query planning, disk access, serialisation, network |
| Human perception of "instant" | ~100 milliseconds | — | Your entire page-load budget |
Read the last two rows together. If a dashboard page needs twelve metrics and each costs a 10 ms aggregation, you have spent 120 ms before a single pixel is drawn — you have blown the budget on data alone. If each metric is a single in-memory lookup at well under a millisecond, twelve of them cost less than one MongoDB query.
The business translation: Amazon's often-quoted internal finding was that every additional 100 ms of latency cost roughly 1% in sales. You do not need to believe the precise figure to accept the direction. Latency is not an engineering vanity metric — it is conversion, analyst productivity, and the difference between a dashboard people check hourly and one they stopped opening.
Holds all 15,000 booking documents, permanently and durably. Answers any question you think of later, including ones nobody anticipated.
Optimised for: completeness, flexibility, durability. Costs: milliseconds per query, and the query cost grows with the data.
Holds a few dozen pre-computed aggregates: total bookings, revenue by vehicle, top pickup points, today's unique customers.
Optimised for: read latency. Costs: you must decide in advance what to track, and losing it is survivable because it can be rebuilt from Mongo.
The safety property that makes this architecture sane: everything in Redis is derived. If the Redis process dies and the memory is lost, you have lost nothing that cannot be recomputed by replaying the rides collection. That is precisely why a volatile store is an acceptable component — and precisely why it must never be the only copy of anything.
Redis stands for REmote DIctionary Server. Salvatore Sanfilippo released it in 2009 after an analytics workload at his own startup proved too slow on MySQL — a lineage worth noting, because analytics is exactly what we are about to use it for.
Three design decisions define it.
The entire dataset is held in main memory. There is no buffer pool, no cache-miss path, no "is this page on disk?" branch. Every read is a memory read. The consequence is both halves of the trade: single-digit-microsecond operations, and a hard ceiling at the size of your RAM.
Command execution runs on one thread. Students usually hear this as a defect. It is the opposite.
A multi-threaded in-memory store must protect every shared structure with locks. Lock acquisition, contention, and the cache-line traffic that comes with it routinely cost more than the operation being protected. Redis removes the problem by removing concurrency: commands are executed one at a time, in arrival order, to completion.
The payoff is free atomicity. INCR is not "read, add one, write" with a race in the middle — it is one indivisible step. No application-level locking, no lost-update bug, no compare-and-swap retry loop. Two hundred concurrent booking events can increment the same counter and the count will be exactly right.
Modern Redis does use additional threads for I/O and for background persistence, but the command-execution model remains serial, and that is the property you reason about.
This is the distinction that separates Redis from a plain cache such as Memcached, and the one most worth carrying out of this lecture.
| Memcached (plain cache) | Redis (data-structure server) | |
|---|---|---|
| Value model | An opaque blob of bytes | A typed structure the server understands |
| To add 1 to a counter | Fetch the blob, parse it, add, serialise, write back — and race with every other client | INCR key — one atomic server-side operation |
| To keep a top-10 list | Fetch everything, sort in the application, write back | ZINCRBY then ZREVRANGE key 0 9 — the server maintains the order |
| Network cost | The whole value crosses the wire on every change | Only the delta crosses the wire |
Say this precisely: Redis is a key-value store in which the value is a rich data structure manipulated in place on the server. That one sentence explains why a "cache" can serve as the engine of the live dashboard you will build next week.
Every example below uses the course dataset — the bdma.rides collection of 15,000 Delhi-NCR Uber-style bookings from calendar 2024.
A String holds text, a number, or serialised JSON, up to 512 MB. When the content is numeric, Redis can do arithmetic on it atomically.
# A plain value
SET rides:last_loaded "2024-12-31T23:59:59Z"
GET rides:last_loaded
# A counter — atomic, no read-modify-write race
INCR rides:2024-03-14:total # -> 1, then 2, then 3 ...
INCRBY rides:2024-03-14:completed 1
INCRBYFLOAT revenue:2024-03-14 412.50 # booking_value is a decimal
# A cached JSON payload with a 60-second life
SETEX dash:summary:cache 60 '{"total":15000,"completed":9343}'
| Business use | Why a String |
|---|---|
| Bookings today, completions today, cancellations today | INCR is atomic and O(1) regardless of how many writers there are |
| Running revenue for a date | INCRBYFLOAT accumulates booking_value without re-reading the total |
Feature flags (flag:surge_pricing = on) | Read on every request; one memory lookup instead of a config query |
| Cached JSON for an expensive report | SETEX gives automatic invalidation — the cache cannot go stale past its TTL |
A Hash maps field names to values inside a single key. It is the natural home for a record with several attributes that you want to update independently.
# One booking's live state
HSET booking:CNR3000142 status "Completed" vehicle_type "Go Sedan" \
pickup "Connaught Place" booking_value 412.5
HGET booking:CNR3000142 status # -> "Completed"
HGETALL booking:CNR3000142 # -> every field and value
# Payment-method mix, as one object rather than five keys
HINCRBY dash:payment "UPI" 1
HINCRBY dash:payment "Cash" 1
HGETALL dash:payment
# -> UPI 6755, Cash 3797, Uber Wallet 1769, Credit Card 1619, Debit Card 1062
Why a Hash rather than five Strings? One key instead of five means one round trip to read the whole group (HGETALL), one key to expire, and markedly lower memory overhead — Redis stores small hashes in a compact encoding. Use a Hash whenever the fields are read together and belong to the same conceptual object.
A List is a linked list of strings. Pushing and popping at either end is O(1); reading the middle is not. That shape makes it a queue or a recent-activity feed, not a general-purpose array.
LPUSH feed:recent_bookings "CNR3000142 · Go Sedan · Connaught Place → Aerocity"
LPUSH feed:recent_bookings "CNR3000143 · Auto · Noida Sector 62 → Sector 18"
LRANGE feed:recent_bookings 0 9 # the ten most recent
LTRIM feed:recent_bookings 0 99 # keep only the latest 100 — bounded memory
# As a work queue
LPUSH queue:driver_payouts "CNR3000142"
RPOP queue:driver_payouts # a worker takes the oldest item
The pattern worth memorising: LPUSH followed by LTRIM gives you a feed that can never grow without limit. A recent-activity strip on a dashboard should always be capped this way.
An unordered collection with no duplicates. Adding a member that already exists is a no-op, which makes uniqueness free rather than something you check for.
SADD customers:2024-03-14 "CID482910"
SADD customers:2024-03-14 "CID482910" # -> 0, already present
SCARD customers:2024-03-14 # unique customers that day
SISMEMBER customers:2024-03-14 "CID482910" # O(1) membership test
SADD vehicles:used:2024-03-14 "Go Sedan" "Auto" "eBike"
SMEMBERS vehicles:used:2024-03-14
The genuinely valuable part is set algebra, executed inside the server:
# Customers who booked on BOTH days — day-over-day retention
SINTER customers:2024-03-13 customers:2024-03-14
# Everyone who booked on either day
SUNION customers:2024-03-13 customers:2024-03-14
# Churn candidates: booked on the 13th, did not return on the 14th
SDIFF customers:2024-03-13 customers:2024-03-14
The cost to watch: a Set stores every member in full. One million customer IDs of the form CID482910 is roughly 60–80 MB per day, per key. That is fine for a day or two and ruinous for a year. Section 4 shows what to use instead when you only need the count.
A Sorted Set is a Set in which every member also carries a numeric score, and the server permanently maintains the members in score order. Insertion, score update, and rank lookup are all O(log N).
# Accumulate revenue per vehicle type as bookings arrive
ZINCRBY dash:vehicle:revenue 412.50 "Go Sedan"
ZINCRBY dash:vehicle:revenue 688.00 "Premier Sedan"
# Top three revenue earners, with their totals — instantly
ZREVRANGE dash:vehicle:revenue 0 2 WITHSCORES
# 1) "Premier Sedan" 2) "536855"
# 3) "Go Sedan" 4) "491473"
# 5) "Uber XL" 6) "431927"
# Busiest pickup points
ZINCRBY dash:pickup:top 1 "Noida Sector 62"
ZREVRANGE dash:pickup:top 0 4 WITHSCORES
# Noida Sector 62 1209 · IGI Airport 1183 · New Delhi Railway Station 1179
# Gurgaon Cyber City 1178 · Connaught Place 1139
ZRANK dash:pickup:top "Connaught Place" # ascending rank
ZSCORE dash:pickup:top "IGI Airport" # -> 1183
ZRANGEBYSCORE dash:vehicle:revenue 400000 +inf
Why this one matters most. "Give me the top 10 by revenue" in SQL or a MongoDB pipeline means sorting the aggregate at query time — work proportional to the number of groups, repeated on every read. In a ZSET the ordering is maintained incrementally on write, so the read is a slice of an already-sorted structure. Leaderboards, trending lists, top-N by any accumulating measure, and rank-of-one-item queries are the cases where Redis is not merely faster but structurally better suited.
The trap inside every leaderboard. A ZSET ranks by whatever score you put in it and has no idea whether that score deserves to be trusted. Rank the 24 hours of our dataset by failure rate and 03:00 comes top at 43.48% — on 46 bookings for the whole year. That is a small-sample artifact, not a signal, and acting on it sends an operations team to investigate an hour that barely exists. The worst failure rate among the hours that actually carry volume is 17:00 at 39.92% — higher than the 18:00 peak's 37.14%, even though 18:00 takes 1,373 bookings to 17:00's 1,062.
The rule: never rank by a ratio without gating on its denominator. In MongoDB that gate is one stage — { $match: { bookings: { $gte: 1000 } } } after the $group. In Redis there is no such stage, so you must keep the denominator in a second structure and filter in application code before you display. Same discipline, and it is now your job rather than the query engine's.
| Question | Structure | Commands |
|---|---|---|
| How many bookings today? | String | INCR / GET |
| What is the full state of booking CNR3000142? | Hash | HSET / HGETALL |
| What were the last 10 bookings? | List | LPUSH / LTRIM / LRANGE |
| Which customers booked today? Who returned? | Set | SADD / SCARD / SINTER |
| Which vehicle type earns most? Top 5 pickups? | Sorted Set | ZINCRBY / ZREVRANGE … WITHSCORES |
| How many unique customers were active — millions of them? | Bitmap or HyperLogLog | SETBIT / BITCOUNT · PFADD / PFCOUNT |
This section is the hinge of Module 4. Everything you build in Lecture 18 rests on it.
A Bitmap is not a separate type. It is a String addressed one bit at a time. SETBIT key offset 1 turns on the bit at a given position, growing the underlying string as needed.
Map each customer to a stable numeric offset. Our IDs make this trivial: CID482910 → offset 482910.
# Customer CID482910 was active on 14 March
SETBIT dash:customers:2024-03-14 482910 1
# Was a specific customer active?
GETBIT dash:customers:2024-03-14 482910 # -> 1
# How many distinct customers were active that day?
BITCOUNT dash:customers:2024-03-14 # exact, server-side popcount
One bit per user. One million users is 1,000,000 bits.
1,000,000 ÷ 8 = 125,000 bytes ≈ 125 KB.
Tracking whether each of a million customers was active today costs about as much memory as a single photograph. A year of daily keys — 365 of them — is roughly 45 MB. For ten million users it is 1.25 MB per day and about 456 MB per year. These are numbers you can put in a budget.
Compare that with the Set from Section 3. The same one million customers stored as full CID… strings is of the order of 60–80 MB per day — several hundred times larger. The bitmap wins because it stores a yes/no fact about a known population rather than the identifiers themselves.
# Customers active on BOTH 13 and 14 March
BITOP AND dash:retained:0313-0314 dash:customers:2024-03-13 dash:customers:2024-03-14
BITCOUNT dash:retained:0313-0314 # -> day-over-day returning customers
# Active on EITHER day (reach)
BITOP OR dash:reach:0313-0314 dash:customers:2024-03-13 dash:customers:2024-03-14
BITCOUNT dash:reach:0313-0314
# Churned: active on the 13th but not the 14th
BITOP NOT dash:inactive:0314 dash:customers:2024-03-14
BITOP AND dash:churn:0313 dash:customers:2024-03-13 dash:inactive:0314
BITCOUNT dash:churn:0313
Why this is remarkable: BITOP AND across a million users is a bitwise AND over 125 KB of memory — the CPU processes 64 users per machine word. A retention cohort that would be a $lookup-and-$group pipeline over millions of documents in MongoDB becomes two commands and sub-millisecond work here. This is the single idea the Lecture 18 dashboard is built on.
The precondition, stated honestly: bitmaps require a dense, stable integer identity per user. CID482910 gives us one for free. Real systems with UUID keys must maintain a separate ID → offset mapping (often a Redis Hash plus an INCR counter). And the bitmap's size is set by the largest offset used, so sparse, very large IDs waste memory. Never hash a UUID into an offset — collisions silently corrupt the count.
When you cannot assign offsets, or the population is unbounded, HyperLogLog counts distinct items in a fixed 12 KB, regardless of whether you feed it a thousand items or a billion.
PFADD dash:hll:customers:2024-03-14 "CID482910" "CID100233" "CID775012"
PFCOUNT dash:hll:customers:2024-03-14 # approximate distinct count
# Weekly uniques — merge seven daily keys, no double counting
PFMERGE dash:hll:week:11 dash:hll:customers:2024-03-11 dash:hll:customers:2024-03-12 \
dash:hll:customers:2024-03-13 dash:hll:customers:2024-03-14
PFCOUNT dash:hll:week:11
| Set | Bitmap | HyperLogLog | |
|---|---|---|---|
| Memory for 1M users | ~60–80 MB | ~125 KB | 12 KB (fixed, always) |
| Accuracy | Exact | Exact | ~0.81% standard error |
| Can list members? | Yes | Yes, by scanning bits | No — never |
| Needs integer IDs? | No | Yes | No |
| Combine keys | SINTER, SUNION, SDIFF | BITOP AND/OR/XOR/NOT | PFMERGE (union only) |
| Use when | You need the actual members | Dense integer IDs; you need intersections | Huge or unbounded keyspace; a 1% error is immaterial |
The trade, in business language: HyperLogLog reports 1,000,000 unique visitors when the truth is somewhere around 991,900 to 1,008,100. For a traffic chart in a board pack, that is indistinguishable from exact. For "how many customers do we bill this month", it is unusable. Approximation is a decision about the question, not about the technology — and you should make it explicitly, in front of the person who consumes the number.
Redis has no tables, no columns, and no catalogue. Nothing stops you creating x1, temp, and revenue_final_v2_USE_THIS side by side. Six months later nobody — including you — knows what is in the database.
The universal convention is a colon-delimited hierarchy, read left to right from broadest to narrowest:
<namespace>:<entity>:<identifier>:<attribute>
rides:2024-03-14:completed # String — completed bookings on a date
dash:actives:2024-03-14 # Bitmap — daily-active customer flags
dash:vehicle:revenue # ZSET — revenue by vehicle type
dash:payment # Hash — booking count per payment method
booking:CNR3000142 # Hash — one booking's live state
session:CID482910 # Hash — a customer's app session
Four rules worth enforcing in code review:
1. Pick one separator — : — and never mix in _ or - at the hierarchy level. 2. Put the date in ISO form (2024-03-14) so keys sort lexicographically into chronological order. 3. Make the prefix identify the owning feature (dash:), so one team's keys can be found, expired, or migrated without touching another's. 4. Write the key schema down in a table — it is documentation nobody can derive from the server.
Never run KEYS * on a production server. It is O(N) over the entire keyspace, and because Redis is single-threaded it blocks every other client until it finishes. On a few million keys that is seconds of a total outage, caused by someone looking around. Use SCAN, which returns a cursor and a small batch, letting other commands interleave:
# Wrong — blocks the whole server
KEYS dash:*
# Right — incremental, non-blocking
SCAN 0 MATCH dash:customers:* COUNT 100
# -> returns a new cursor plus up to ~100 keys; repeat until the cursor is 0
EXPIRE dash:customers:2024-03-14 7776000 # keep 90 days (in seconds)
TTL dash:customers:2024-03-14 # -> seconds remaining
TTL dash:vehicle:revenue # -> -1, no expiry set
TTL missing:key # -> -2, key does not exist
PERSIST dash:customers:2024-03-14 # remove the expiry
SETEX dash:summary:cache 60 "{...}" # set value and TTL in one step
Expiry is how a speed layer stays small. Daily keys that nobody will query after a quarter should carry a 90-day TTL from the moment they are created — set it in the same transaction as the first write, not in a cleanup job you will forget to run.
Suppose memory fills anyway. Redis's behaviour is governed by maxmemory and maxmemory-policy:
| Policy | Behaviour When Full | Right For |
|---|---|---|
noeviction | Writes fail with an error; reads keep working | The default. Correct when every key matters and silent data loss is unacceptable |
allkeys-lru | Evicts the least recently used key, whether or not it has a TTL | A pure cache where anything can be rebuilt from the source of truth |
volatile-lru | Evicts the least recently used key among those with a TTL; keys without one are protected | Mixed workloads — the pattern our dashboard uses |
allkeys-lfu | Evicts the least frequently used key | Caches with a stable hot set and occasional one-off scans |
volatile-ttl | Evicts the key with the nearest expiry first | Short-lived session or token stores |
CONFIG SET maxmemory 512mb
CONFIG SET maxmemory-policy volatile-lru
INFO memory # used_memory_human, maxmemory_policy, evicted_keys
Why a cache needs an eviction policy at all: a cache without one is not a cache — it is an unbounded memory leak that eventually takes the process down. The policy is the explicit statement of which data you are willing to lose first. Choosing it is a business decision disguised as a config line: volatile-lru says "my dashboard aggregates are protected; my cached report payloads are expendable."
Redis is in-memory, not memory-only. It offers two ways to put data on disk, and they trade durability against write cost in opposite directions.
| RDB — Snapshot | AOF — Append-Only File | |
|---|---|---|
| Mechanism | Forks a child process that writes a point-in-time binary dump of the whole dataset | Appends every write command to a log, which is replayed on restart |
| Worst-case loss | Everything since the last snapshot — minutes | About one second with appendfsync everysec; near-zero with always |
| Write-path cost | Near zero between snapshots; a CPU and memory spike during the fork | Continuous, modest; always makes every write wait on fsync |
| Restart speed | Fast — load a compact binary file | Slower — replay the log (mitigated by periodic rewrites) |
| File size | Compact | Larger; needs background rewriting to stay bounded |
# Snapshot if 1 change in 900s, 10 in 300s, or 10,000 in 60s
CONFIG SET save "900 1 300 10 60 10000"
# Enable the append-only log, flushed once per second
CONFIG SET appendonly yes
CONFIG SET appendfsync everysec
BGSAVE # take a snapshot now, in the background
LASTSAVE # Unix timestamp of the last successful snapshot
The sentence to remember: Redis can lose the last few seconds of writes. For a speed layer that is irrelevant — the aggregates are derived and can be rebuilt by replaying bdma.rides. For a payment ledger it is disqualifying. The same property is acceptable or fatal depending entirely on what the data is. That judgement, not the config syntax, is what an MBA is paid for.
Bookings, payments, contracts, audit trails. Even with AOF always, you are paying a durability premium on a system designed for the opposite trade. Use PostgreSQL or MongoDB.
Redis is priced per gigabyte of memory, which costs roughly an order of magnitude more than SSD. A 2 TB dataset in Redis is an architectural error with an invoice attached.
There is no "WHERE booking_value > 500 AND vehicle_type = 'Auto'" unless you built that index yourself. Exploratory analysis belongs in the system of record.
The decision rule: use Redis when the access pattern is known in advance, the read volume is high, the latency budget is tight, and the data is derived or reconstructible. Fail any one of those and you want a different store — most often the one you already have.
Twelve minutes. Everyone gets a running server and touches every structure from Section 3.
Start Redis 7. The Alpine image is about 40 MB and starts in under a second.
docker run -d --name bdma-redis -p 6379:6379 redis:7-alpine
docker ps # confirm the container is UpOpen an interactive client inside the container and check the connection.
docker exec -it bdma-redis redis-cli
127.0.0.1:6379> PING
PONGStrings. Load the day's verified counts and a running revenue total.
SET rides:2024:total 15000
SET rides:2024:completed 9343
INCRBYFLOAT revenue:2024-03-14 412.50
INCRBYFLOAT revenue:2024-03-14 688.00
GET revenue:2024-03-14 # -> "1100.5"Hash. Build the payment-method mix, then read it back in one call.
HINCRBY dash:payment "UPI" 6755
HINCRBY dash:payment "Cash" 3797
HINCRBY dash:payment "Uber Wallet" 1769
HINCRBY dash:payment "Credit Card" 1619
HINCRBY dash:payment "Debit Card" 1062
HGETALL dash:paymentSorted Set. Revenue by vehicle type and the top pickup points — the two ZSETs you will reuse next week.
ZINCRBY dash:vehicle:revenue 536855 "Premier Sedan"
ZINCRBY dash:vehicle:revenue 491473 "Go Sedan"
ZINCRBY dash:vehicle:revenue 431927 "Uber XL"
ZREVRANGE dash:vehicle:revenue 0 2 WITHSCORES
ZINCRBY dash:pickup:top 1209 "Noida Sector 62"
ZINCRBY dash:pickup:top 1183 "IGI Airport"
ZINCRBY dash:pickup:top 1179 "New Delhi Railway Station"
ZINCRBY dash:pickup:top 1178 "Gurgaon Cyber City"
ZINCRBY dash:pickup:top 1139 "Connaught Place"
ZREVRANGE dash:pickup:top 0 4 WITHSCORES
ZRANK dash:pickup:top "Connaught Place"Set and List. Unique customers, set intersection, and a capped activity feed.
SADD customers:2024-03-13 "CID482910" "CID100233" "CID775012"
SADD customers:2024-03-14 "CID482910" "CID991044"
SCARD customers:2024-03-14 # -> 2
SINTER customers:2024-03-13 customers:2024-03-14 # -> "CID482910"
LPUSH feed:recent "CNR3000142 · Go Sedan · Connaught Place → Aerocity"
LPUSH feed:recent "CNR3000143 · Auto · Noida Sector 62 → Sector 18"
LTRIM feed:recent 0 99
LRANGE feed:recent 0 4Bitmaps. The headline result — flag two customers across two days and compute retention.
SETBIT dash:actives:2024-03-13 482910 1
SETBIT dash:actives:2024-03-13 100233 1
SETBIT dash:actives:2024-03-14 482910 1
SETBIT dash:actives:2024-03-14 991044 1
BITCOUNT dash:actives:2024-03-14 # -> 2
BITOP AND dash:retained dash:actives:2024-03-13 dash:actives:2024-03-14
BITCOUNT dash:retained # -> 1 (CID482910 returned)
MEMORY USAGE dash:actives:2024-03-14 # bytes — compare with the Set above
Note the memory figure. With offsets up to ~991,044 the key occupies roughly 124 KB — and it would occupy the same 124 KB if all one million customers were flagged.
Expiry and HyperLogLog. Then leave the container running — Lecture 18 writes into this same server.
EXPIRE dash:actives:2024-03-14 7776000
TTL dash:actives:2024-03-14
PFADD dash:hll:2024-03-14 "CID482910" "CID991044" "CID482910"
PFCOUNT dash:hll:2024-03-14 # -> 2
INFO keyspace
# To stop and remove later: docker stop bdma-redis && docker rm bdma-redisIf Docker is unavailable: Redis Cloud offers a free 30 MB tier at redis.com/try-free. Thirty megabytes is ample for this course. Connect with redis-cli -h <host> -p <port> -a <password>, and keep the credentials in a local .env file — never in a committed source file.
BITOP AND plus BITCOUNT turns a retention cohort into two commands. HyperLogLog trades exactness and member listing for a fixed 12 KB at ~0.81% error.maxmemory-policy — and SCAN, never KEYS *, on anything that matters.A build session. You will write a Node.js writer that updates Redis counters, sorted sets and bitmaps atomically on every incoming booking, an Express API that assembles the dashboard payload in a handful of O(1) reads, and a browser view that polls it. The central idea is a reversal of everything you did in Module 3: compute on write, not on read. Bring your running bdma-redis container and Node 18+.