What is Redis?
Definition
Redis (Remote Dictionary Server) is an open-source, in-memory data structure store that can be used as a database, cache, message broker, and streaming engine. It supports various data structures such as strings, hashes, lists, sets, sorted sets, bitmaps, hyperloglogs, geospatial indexes, and streams.
Key Concepts
Core Features
- In-Memory Storage: Data is stored in RAM for extremely fast access
- Data Structures: Rich set of data types beyond simple key-value
- Persistence: Optional disk persistence (RDB snapshots, AOF logs)
- Replication: Master-slave replication for high availability
- Clustering: Distributed Redis across multiple nodes
- Pub/Sub: Publish-subscribe messaging
- Transactions: Atomic operations on multiple keys
- Lua Scripting: Server-side scripting for complex operations
Data Types
- Strings: Simple key-value pairs
- Hashes: Field-value pairs within a key (like objects)
- Lists: Ordered collections of strings
- Sets: Unordered collections of unique strings
- Sorted Sets: Sets with scores for ordering
- Bitmaps: String operations at bit level
- HyperLogLog: Probabilistic data structure for counting
- Streams: Log-like data structures (Redis 5.0+)
- Geospatial: Store and query geographic coordinates
When to Use Redis
- Caching: Fast data access layer
- Session Storage: User session management
- Real-time Analytics: Counting, leaderboards, rate limiting
- Message Queuing: Pub/Sub, job queues
- Distributed Locking: Coordination between processes
- Counting: Real-time counters, unique visitors
- Leaderboards: Sorted sets for rankings
- Rate Limiting: Control API request rates
Basic Example
import redis
# Connect to Redis
r = redis.Redis(host='localhost', port=6379, db=0)
# Strings
r.set('key', 'value')
value = r.get('key')
# Hashes
r.hset('user:1', 'name', 'John')
r.hset('user:1', 'email', 'john@example.com')
user = r.hgetall('user:1')
# Lists
r.lpush('tasks', 'task1')
r.rpop('tasks')
# Sets
r.sadd('tags', 'python', 'redis')
tags = r.smembers('tags')
# Sorted Sets
r.zadd('leaderboard', {'player1': 100, 'player2': 200})
top_players = r.zrevrange('leaderboard', 0, 9)
Common Interview Questions and Answers
1. What are the advantages of Redis over other caching solutions?
Advantages:
-
Performance:
- In-memory storage provides sub-millisecond latency
- Single-threaded event loop (no context switching)
- Very high throughput (100,000+ ops/sec)
-
Rich Data Structures:
- Not just key-value, supports lists, sets, sorted sets, etc.
- Enables complex use cases without additional processing
-
Persistence Options:
- RDB snapshots for point-in-time recovery
- AOF for durability
- Can be used as both cache and database
-
Atomic Operations:
- All operations are atomic
- Transactions support
- Lua scripting for complex atomic operations
-
Built-in Features:
- Pub/Sub messaging
- Replication and clustering
- Expiration on keys
- LRU eviction policies
Comparison:
- vs Memcached: Redis has more data types and persistence
- vs Database: Redis is much faster but limited by memory
- vs Message Queue: Redis pub/sub is simpler but less feature-rich
2. Explain Redis persistence options (RDB vs AOF)
RDB (Redis Database Backup):
- How it works: Point-in-time snapshots of dataset
- Format: Binary, compact
- Configuration:
save 900 1 # Save if at least 1 key changed in 900 seconds save 300 10 # Save if at least 10 keys changed in 300 seconds save 60 10000 # Save if at least 10000 keys changed in 60 seconds - Pros:
- Fast to save and load
- Compact file size
- Good for backups and disaster recovery
- Cons:
- May lose data between snapshots
- Fork process can be slow for large datasets
AOF (Append Only File):
- How it works: Logs every write operation
- Format: Human-readable commands
- Configuration:
appendonly yes appendfsync everysec # Options: always, everysec, no - Pros:
- Better durability (can lose max 1 second of data)
- Human-readable log
- Can rewrite AOF to remove redundant commands
- Cons:
- Larger file size
- Slower than RDB
- May need to rewrite periodically
Best Practice: Use both RDB and AOF for maximum durability:
save 900 1
appendonly yes
appendfsync everysec
3. How does Redis handle memory management?
Redis memory management includes:
-
Max Memory Configuration:
maxmemory 2gb maxmemory-policy allkeys-lru # Eviction policy -
Eviction Policies:
noeviction: Don’t evict, return errors on writeallkeys-lru: Evict least recently used keysallkeys-lfu: Evict least frequently used keysvolatile-lru: Evict LRU among keys with expirationvolatile-lfu: Evict LFU among keys with expirationallkeys-random: Evict random keysvolatile-random: Evict random keys with expirationvolatile-ttl: Evict keys with shortest TTL
-
Memory Optimization:
# Use appropriate data types # Strings for simple values r.set('key', 'value') # Hashes for objects (more memory efficient) r.hset('user:1', mapping={'name': 'John', 'age': 30}) # Use expiration r.setex('key', 3600, 'value') # Expires in 1 hour # Use compression for large values import gzip compressed = gzip.compress(large_data) r.set('key', compressed) -
Memory Monitoring:
INFO memory MEMORY USAGE key
4. What is Redis clustering and how does it work?
Redis Cluster provides automatic sharding and high availability:
-
Sharding:
- Data is distributed across multiple nodes using hash slots (16384 slots)
- Each node handles a subset of slots
- Keys are mapped to slots using CRC16 hash
-
Architecture:
Node 1: Slots 0-5460 Node 2: Slots 5461-10922 Node 3: Slots 10923-16383 -
High Availability:
- Each master has one or more replicas
- Automatic failover if master fails
- Uses gossip protocol for node communication
-
Setup:
# Create cluster with 3 masters and 3 replicas redis-cli --cluster create \ 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \ 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \ --cluster-replicas 1 -
Client Behavior:
from redis.cluster import RedisCluster startup_nodes = [{"host": "127.0.0.1", "port": "7000"}] rc = RedisCluster(startup_nodes=startup_nodes, decode_responses=True) # Client handles redirection automatically rc.set('key', 'value')
Key Points:
- Requires at least 3 master nodes
- Supports up to 1000 nodes
- Automatic resharding when nodes are added/removed
- Multi-key operations limited to same slot (use hash tags)
5. Explain Redis pub/sub mechanism
Pub/Sub (Publish-Subscribe) enables message broadcasting:
-
Basic Usage:
import redis import threading r = redis.Redis() # Publisher r.publish('channel', 'message') # Subscriber pubsub = r.pubsub() pubsub.subscribe('channel') for message in pubsub.listen(): print(message) -
Pattern Matching:
# Subscribe to multiple channels pubsub.psubscribe('news.*') # Matches news.sports, news.tech, etc. -
Message Types:
subscribe: Confirmation of subscriptionmessage: Actual messageunsubscribe: Confirmation of unsubscription
-
Use Cases:
- Real-time notifications
- Event broadcasting
- Chat applications
- Cache invalidation
-
Limitations:
- Messages are not persisted (lost if no subscribers)
- No message queuing (fire and forget)
- No acknowledgment mechanism
Note: For persistent messaging, use Redis Streams (Redis 5.0+).
6. How to implement distributed locking with Redis?
Distributed Lock coordinates access to shared resources:
-
Basic Implementation:
import redis import time import uuid r = redis.Redis() def acquire_lock(lock_name, timeout=10): identifier = str(uuid.uuid4()) end = time.time() + timeout while time.time() < end: # Try to acquire lock with expiration if r.set(f'lock:{lock_name}', identifier, nx=True, ex=10): return identifier time.sleep(0.001) return False def release_lock(lock_name, identifier): pipe = r.pipeline(True) while True: try: pipe.watch(f'lock:{lock_name}') if pipe.get(f'lock:{lock_name}') == identifier.encode(): pipe.multi() pipe.delete(f'lock:{lock_name}') pipe.execute() return True pipe.unwatch() break except redis.WatchError: pass return False -
Using Redlock Algorithm (for multiple Redis instances):
from redis.lock import Lock # Single instance lock lock = Lock(r, 'my-lock', timeout=10) if lock.acquire(): try: # Critical section pass finally: lock.release() -
Best Practices:
- Always set expiration to prevent deadlocks
- Use unique identifiers to ensure you only release your own lock
- Use Lua scripts for atomic lock release
- Consider using Redlock for high availability
-
Lua Script for Atomic Release:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end
7. What are Redis transactions and how do they work?
Redis Transactions provide atomicity for multiple commands:
-
Basic Transaction:
pipe = r.pipeline() pipe.multi() # Start transaction pipe.set('key1', 'value1') pipe.set('key2', 'value2') pipe.execute() # Execute all commands atomically -
Watch for Optimistic Locking:
r.watch('key') value = r.get('key') # Modify value pipe = r.pipeline() pipe.multi() pipe.set('key', new_value) try: pipe.execute() # Fails if 'key' was modified except redis.WatchError: # Retry or handle conflict pass -
Transaction Properties:
- Atomicity: All commands execute or none
- Isolation: Commands are serialized
- No Rollback: Redis doesn’t support rollback
- Errors: If command fails, transaction continues (check return values)
-
Limitations:
- Commands are queued, not executed immediately
- No rollback on errors
- Can’t read values written in transaction until it completes
8. How to handle cache invalidation strategies?
Cache Invalidation strategies:
-
TTL (Time To Live):
# Set expiration when storing r.setex('user:1', 3600, user_data) # Expires in 1 hour r.expire('user:1', 3600) # Set expiration on existing key -
Explicit Invalidation:
# Delete on update def update_user(user_id, data): db.update_user(user_id, data) r.delete(f'user:{user_id}') # Invalidate cache -
Pattern-based Invalidation:
# Delete all keys matching pattern for key in r.scan_iter(match='user:*'): r.delete(key) -
Cache-Aside Pattern:
def get_user(user_id): # Try cache first cached = r.get(f'user:{user_id}') if cached: return json.loads(cached) # Cache miss - get from database user = db.get_user(user_id) # Store in cache r.setex(f'user:{user_id}', 3600, json.dumps(user)) return user -
Write-Through Pattern:
def update_user(user_id, data): # Update database db.update_user(user_id, data) # Update cache r.setex(f'user:{user_id}', 3600, json.dumps(data)) -
Pub/Sub for Invalidation:
# On data update, publish invalidation message r.publish('cache-invalidate', f'user:{user_id}') # Subscribers delete from their cache pubsub.subscribe('cache-invalidate')
9. Explain Redis data eviction policies
Eviction Policies determine which keys to remove when memory limit is reached:
-
Configuration:
maxmemory 2gb maxmemory-policy allkeys-lru -
Policy Types:
noeviction(default):- Don’t evict any keys
- Return errors on write operations when memory is full
- Use when data must not be lost
allkeys-lru:- Evict least recently used keys
- Works on all keys
- Good for general caching
allkeys-lfu(Redis 4.0+):- Evict least frequently used keys
- Better for long-term caching patterns
volatile-lru:- Evict LRU among keys with expiration set
- Keys without expiration are never evicted
volatile-lfu:- Evict LFU among keys with expiration
allkeys-random:- Evict random keys
- Less predictable
volatile-random:- Evict random keys with expiration
volatile-ttl:- Evict keys with shortest TTL (time to live)
- Prioritizes removing soon-to-expire keys
-
Choosing a Policy:
- Caching:
allkeys-lruorallkeys-lfu - Mixed use:
volatile-lru(keep permanent data safe) - Critical data:
noeviction(handle memory management in application)
- Caching:
10. How to implement rate limiting with Redis?
Rate Limiting controls request frequency:
-
Sliding Window Log:
import time def is_allowed(user_id, limit=100, window=3600): key = f'rate_limit:{user_id}' now = time.time() # Remove old entries r.zremrangebyscore(key, 0, now - window) # Count current requests count = r.zcard(key) if count < limit: # Add current request r.zadd(key, {str(now): now}) r.expire(key, window) return True return False -
Token Bucket:
def token_bucket(user_id, capacity=100, refill_rate=10): key = f'token_bucket:{user_id}' now = time.time() pipe = r.pipeline() pipe.hgetall(key) pipe.hset(key, mapping={ 'tokens': capacity, 'last_refill': now }) pipe.expire(key, 3600) result = pipe.execute() if result[0]: tokens = float(result[0].get(b'tokens', capacity)) last_refill = float(result[0].get(b'last_refill', now)) elapsed = now - last_refill tokens = min(capacity, tokens + elapsed * refill_rate) else: tokens = capacity if tokens >= 1: r.hincrbyfloat(key, 'tokens', -1) r.hset(key, 'last_refill', now) return True return False -
Fixed Window Counter:
def fixed_window(user_id, limit=100, window=3600): key = f'rate_limit:{user_id}:{int(time.time() / window)}' count = r.incr(key) r.expire(key, window) return count <= limit -
Using Redis INCR:
def simple_rate_limit(user_id, limit=100, window=60): key = f'rate_limit:{user_id}' count = r.incr(key) if count == 1: r.expire(key, window) return count <= limit
11. What is Redis Sentinel and its purpose?
Redis Sentinel provides high availability and automatic failover:
-
Purpose:
- Monitor Redis master and replica instances
- Automatic failover if master fails
- Configuration provider for clients
- Notification system
-
Architecture:
Sentinel 1 ──┐ Sentinel 2 ──┼──> Monitor Master and Replicas Sentinel 3 ──┘ -
Configuration:
# sentinel.conf sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 10000 sentinel parallel-syncs mymaster 1 -
Client Connection:
from redis.sentinel import Sentinel sentinel = Sentinel([ ('localhost', 26379), ('localhost', 26380), ('localhost', 26381) ]) master = sentinel.master_for('mymaster', socket_timeout=0.1) replica = sentinel.slave_for('mymaster', socket_timeout=0.1) master.set('key', 'value') value = replica.get('key') -
Features:
- Automatic failover
- Configuration discovery
- Notification of events
- Requires at least 3 sentinel instances for quorum
12. How to handle Redis failover scenarios?
Failover Handling strategies:
-
Using Sentinel (automatic):
from redis.sentinel import Sentinel sentinel = Sentinel([('localhost', 26379)]) master = sentinel.master_for('mymaster') # Client automatically reconnects to new master try: master.set('key', 'value') except redis.ConnectionError: # Retry logic master = sentinel.master_for('mymaster') master.set('key', 'value') -
Connection Pool with Retry:
from redis.connection import ConnectionPool pool = ConnectionPool( host='localhost', port=6379, max_connections=50, retry_on_timeout=True, socket_connect_timeout=5 ) r = redis.Redis(connection_pool=pool) -
Circuit Breaker Pattern:
class RedisCircuitBreaker: def __init__(self, failure_threshold=5, timeout=60): self.failure_threshold = failure_threshold self.timeout = timeout self.failures = 0 self.last_failure_time = None self.state = 'closed' # closed, open, half-open def call(self, func, *args, **kwargs): if self.state == 'open': if time.time() - self.last_failure_time > self.timeout: self.state = 'half-open' else: raise Exception("Circuit breaker is open") try: result = func(*args, **kwargs) if self.state == 'half-open': self.state = 'closed' self.failures = 0 return result except Exception as e: self.failures += 1 self.last_failure_time = time.time() if self.failures >= self.failure_threshold: self.state = 'open' raise -
Health Checks:
def check_redis_health(r): try: r.ping() return True except: return False # Periodic health check if not check_redis_health(r): # Switch to backup or degrade gracefully pass
13. What is the difference between Redis and Memcached?
| Feature | Redis | Memcached |
|---|---|---|
| Data Types | Strings, Hashes, Lists, Sets, Sorted Sets, etc. | Only strings |
| Persistence | RDB and AOF | No persistence |
| Replication | Built-in master-slave | No replication |
| Transactions | Supported | Not supported |
| Pub/Sub | Supported | Not supported |
| Lua Scripting | Supported | Not supported |
| Memory Usage | Higher (overhead per key) | Lower |
| Performance | Slightly slower | Slightly faster |
| Use Case | Cache + Database + Message Broker | Simple caching only |
Choose Redis when: You need rich data structures, persistence, or advanced features Choose Memcached when: You only need simple key-value caching with maximum performance
14. How does Redis handle concurrent access?
Redis handles concurrency through:
-
Single-Threaded Event Loop:
- All commands are executed sequentially
- No race conditions between commands
- Atomic operations by design
-
Atomic Operations:
# All these are atomic r.incr('counter') r.hset('hash', 'field', 'value') r.sadd('set', 'member') -
Transactions:
pipe = r.pipeline() pipe.multi() pipe.incr('counter1') pipe.incr('counter2') pipe.execute() # All executed atomically -
Lua Scripts (atomic):
-- Atomic increment with condition local current = redis.call('get', KEYS[1]) if current == ARGV[1] then return redis.call('incr', KEYS[1]) else return nil end -
Watch for Optimistic Locking:
r.watch('key') value = r.get('key') # Modify value pipe = r.pipeline() pipe.multi() pipe.set('key', new_value) pipe.execute() # Fails if key was modified
15. How to implement caching patterns with Redis?
Common Caching Patterns:
-
Cache-Aside (Lazy Loading):
def get_user(user_id): # Check cache cached = r.get(f'user:{user_id}') if cached: return json.loads(cached) # Cache miss - load from DB user = db.get_user(user_id) # Store in cache r.setex(f'user:{user_id}', 3600, json.dumps(user)) return user -
Write-Through:
def update_user(user_id, data): # Update DB db.update_user(user_id, data) # Update cache r.setex(f'user:{user_id}', 3600, json.dumps(data)) -
Write-Behind (Write-Back):
def update_user(user_id, data): # Update cache immediately r.setex(f'user:{user_id}', 3600, json.dumps(data)) # Queue DB update queue.enqueue(db.update_user, user_id, data) -
Refresh-Ahead:
def get_user_with_refresh(user_id): cached = r.get(f'user:{user_id}') ttl = r.ttl(f'user:{user_id}') if cached and ttl > 300: # More than 5 min left return json.loads(cached) # Refresh in background if TTL is low if cached and ttl < 300: async_refresh(user_id) if cached: return json.loads(cached) # Cache miss user = db.get_user(user_id) r.setex(f'user:{user_id}', 3600, json.dumps(user)) return user
16. What are Redis Streams and when to use them?
Redis Streams (Redis 5.0+) are log-like data structures for messaging:
-
Basic Usage:
# Add message to stream r.xadd('mystream', {'field1': 'value1', 'field2': 'value2'}) # Read messages messages = r.xread({'mystream': '0'}, count=10) # Read with consumer group r.xgroup_create('mystream', 'mygroup', id='0') messages = r.xreadgroup('mygroup', 'consumer1', {'mystream': '>'}) -
Features:
- Message persistence (unlike pub/sub)
- Consumer groups (like Kafka)
- Message acknowledgment
- Range queries
- Time-based queries
-
Use Cases:
- Event sourcing
- Message queues
- Activity feeds
- Log aggregation
- Real-time analytics
-
Advantages over Pub/Sub:
- Messages are persisted
- Consumer groups for load balancing
- Message acknowledgment
- Can replay messages
17. How to optimize Redis performance?
Performance Optimization:
-
Use Appropriate Data Types:
# Use hashes instead of JSON strings for objects r.hset('user:1', mapping={'name': 'John', 'age': 30}) # Instead of: r.set('user:1', json.dumps({'name': 'John', 'age': 30})) -
Pipeline Multiple Commands:
pipe = r.pipeline() for i in range(100): pipe.get(f'key:{i}') results = pipe.execute() # Single round trip -
Use Connection Pooling:
pool = ConnectionPool(host='localhost', port=6379, max_connections=50) r = redis.Redis(connection_pool=pool) -
Batch Operations:
# Use MSET instead of multiple SET r.mset({'key1': 'value1', 'key2': 'value2'}) # Use MGET instead of multiple GET r.mget(['key1', 'key2']) -
Avoid Large Values:
- Split large values into smaller chunks
- Use compression for large data
- Consider using external storage for very large values
-
Tune Configuration:
# Disable persistence if not needed (for pure caching) save "" appendonly no # Increase TCP backlog tcp-backlog 511 # Disable slow log if not needed slowlog-log-slower-than -1 -
Monitor and Profile:
# Monitor commands MONITOR # Check slow commands SLOWLOG GET 10 # Memory usage INFO memory
18. How does Redis handle replication?
Redis Replication (Master-Slave):
-
Setup:
# On replica (slave) server replicaof 192.168.1.100 6379 -
How it Works:
- Master handles writes
- Replicas replicate data from master
- Replication is asynchronous
- Replicas can handle reads (scale reads)
-
Replication Process:
- Replica connects to master
- Master sends RDB snapshot
- Master streams write commands to replica
- Replica applies commands to stay in sync
-
Configuration:
# Master requirepass masterpassword # Replica replicaof 192.168.1.100 6379 masterauth masterpassword replica-read-only yes -
Python Client:
# Connect to master for writes master = redis.Redis(host='master', port=6379) # Connect to replicas for reads replica = redis.Redis(host='replica', port=6379) master.set('key', 'value') value = replica.get('key') # Read from replica -
Failover:
- Use Redis Sentinel for automatic failover
- Or manually promote replica to master
19. What is the difference between Redis and a traditional database?
| Feature | Redis | Traditional DB (PostgreSQL, MySQL) |
|---|---|---|
| Storage | In-memory (RAM) | Disk-based |
| Speed | Very fast (sub-millisecond) | Slower (milliseconds) |
| Data Size | Limited by RAM | Limited by disk |
| Persistence | Optional | Always persistent |
| Data Types | Rich (strings, hashes, sets, etc.) | Tables with rows/columns |
| Queries | Simple key-based | Complex SQL queries |
| ACID | Limited (atomic operations) | Full ACID compliance |
| Use Case | Cache, session store, real-time | Primary data storage |
| Cost | Higher (RAM is expensive) | Lower (disk is cheaper) |
When to Use Each:
- Redis: Caching, sessions, real-time data, temporary data
- Database: Permanent storage, complex queries, relationships, transactions
20. How to implement session storage with Redis?
Session Storage with Redis:
-
Basic Implementation:
import json import uuid from datetime import timedelta class RedisSessionStore: def __init__(self, redis_client, session_ttl=3600): self.redis = redis_client self.ttl = session_ttl def create_session(self, user_id, data): session_id = str(uuid.uuid4()) key = f'session:{session_id}' session_data = { 'user_id': user_id, 'data': data, 'created_at': time.time() } self.redis.setex( key, self.ttl, json.dumps(session_data) ) return session_id def get_session(self, session_id): key = f'session:{session_id}' data = self.redis.get(key) if data: # Refresh TTL self.redis.expire(key, self.ttl) return json.loads(data) return None def delete_session(self, session_id): self.redis.delete(f'session:{session_id}') def update_session(self, session_id, data): key = f'session:{session_id}' session = self.get_session(session_id) if session: session['data'].update(data) self.redis.setex( key, self.ttl, json.dumps(session) ) -
Using Hash for Better Performance:
def create_session_hash(self, user_id, data): session_id = str(uuid.uuid4()) key = f'session:{session_id}' self.redis.hset(key, mapping={ 'user_id': user_id, 'data': json.dumps(data), 'created_at': str(time.time()) }) self.redis.expire(key, self.ttl) return session_id -
With Flask:
from flask import Flask, session from flask_session import Session app = Flask(__name__) app.config['SESSION_TYPE'] = 'redis' app.config['SESSION_REDIS'] = redis.Redis() Session(app)
Interview angle
- “Why is Redis fast?” - in-memory, single-threaded for command execution (so no lock contention), with an efficient event loop and simple data structures. Single-threaded also means one slow command -
KEYSon a large database - blocks everything. - “Which data structure for which job?” - strings for cached values and counters, hashes for objects, sorted sets for leaderboards and priority queues, lists for simple queues, sets for membership, streams for durable message logs.
- “How do you avoid a cache stampede?” - a lock or single-flight so only one caller recomputes on a miss, plus jittered TTLs so many keys don’t expire simultaneously, or serve stale while refreshing in the background. See 02_cache_stampede.md.
- “Is Redis durable?” - configurably, and not by default in the strongest sense. RDB snapshots lose recent writes; AOF with
everysecloses up to a second. Treat it as a cache unless you have deliberately configured otherwise.