backend / caching / redis / 01_what_is_redis.md

What is Redis?

4 interview angles 16 min read source

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

  1. In-Memory Storage: Data is stored in RAM for extremely fast access
  2. Data Structures: Rich set of data types beyond simple key-value
  3. Persistence: Optional disk persistence (RDB snapshots, AOF logs)
  4. Replication: Master-slave replication for high availability
  5. Clustering: Distributed Redis across multiple nodes
  6. Pub/Sub: Publish-subscribe messaging
  7. Transactions: Atomic operations on multiple keys
  8. Lua Scripting: Server-side scripting for complex operations

Data Types

  1. Strings: Simple key-value pairs
  2. Hashes: Field-value pairs within a key (like objects)
  3. Lists: Ordered collections of strings
  4. Sets: Unordered collections of unique strings
  5. Sorted Sets: Sets with scores for ordering
  6. Bitmaps: String operations at bit level
  7. HyperLogLog: Probabilistic data structure for counting
  8. Streams: Log-like data structures (Redis 5.0+)
  9. 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:

  1. Performance:

    • In-memory storage provides sub-millisecond latency
    • Single-threaded event loop (no context switching)
    • Very high throughput (100,000+ ops/sec)
  2. Rich Data Structures:

    • Not just key-value, supports lists, sets, sorted sets, etc.
    • Enables complex use cases without additional processing
  3. Persistence Options:

    • RDB snapshots for point-in-time recovery
    • AOF for durability
    • Can be used as both cache and database
  4. Atomic Operations:

    • All operations are atomic
    • Transactions support
    • Lua scripting for complex atomic operations
  5. 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:

  1. Max Memory Configuration:

    maxmemory 2gb
    maxmemory-policy allkeys-lru  # Eviction policy
  2. Eviction Policies:

    • noeviction: Don’t evict, return errors on write
    • allkeys-lru: Evict least recently used keys
    • allkeys-lfu: Evict least frequently used keys
    • volatile-lru: Evict LRU among keys with expiration
    • volatile-lfu: Evict LFU among keys with expiration
    • allkeys-random: Evict random keys
    • volatile-random: Evict random keys with expiration
    • volatile-ttl: Evict keys with shortest TTL
  3. 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)
  4. 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:

  1. 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
  2. Architecture:

    Node 1: Slots 0-5460
    Node 2: Slots 5461-10922
    Node 3: Slots 10923-16383
  3. High Availability:

    • Each master has one or more replicas
    • Automatic failover if master fails
    • Uses gossip protocol for node communication
  4. 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
  5. 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:

  1. 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)
  2. Pattern Matching:

    # Subscribe to multiple channels
    pubsub.psubscribe('news.*')  # Matches news.sports, news.tech, etc.
  3. Message Types:

    • subscribe: Confirmation of subscription
    • message: Actual message
    • unsubscribe: Confirmation of unsubscription
  4. Use Cases:

    • Real-time notifications
    • Event broadcasting
    • Chat applications
    • Cache invalidation
  5. 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:

  1. 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
  2. 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()
  3. 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
  4. 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:

  1. Basic Transaction:

    pipe = r.pipeline()
    pipe.multi()  # Start transaction
    pipe.set('key1', 'value1')
    pipe.set('key2', 'value2')
    pipe.execute()  # Execute all commands atomically
  2. 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
  3. 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)
  4. 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:

  1. 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
  2. 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
  3. Pattern-based Invalidation:

    # Delete all keys matching pattern
    for key in r.scan_iter(match='user:*'):
        r.delete(key)
  4. 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
  5. 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))
  6. 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:

  1. Configuration:

    maxmemory 2gb
    maxmemory-policy allkeys-lru
  2. 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
  3. Choosing a Policy:

    • Caching: allkeys-lru or allkeys-lfu
    • Mixed use: volatile-lru (keep permanent data safe)
    • Critical data: noeviction (handle memory management in application)

10. How to implement rate limiting with Redis?

Rate Limiting controls request frequency:

  1. 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
  2. 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
  3. 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
  4. 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:

  1. Purpose:

    • Monitor Redis master and replica instances
    • Automatic failover if master fails
    • Configuration provider for clients
    • Notification system
  2. Architecture:

    Sentinel 1 ──┐
    Sentinel 2 ──┼──> Monitor Master and Replicas
    Sentinel 3 ──┘
  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
  4. 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')
  5. 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:

  1. 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')
  2. 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)
  3. 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
  4. 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:

  1. Single-Threaded Event Loop:

    • All commands are executed sequentially
    • No race conditions between commands
    • Atomic operations by design
  2. Atomic Operations:

    # All these are atomic
    r.incr('counter')
    r.hset('hash', 'field', 'value')
    r.sadd('set', 'member')
  3. Transactions:

    pipe = r.pipeline()
    pipe.multi()
    pipe.incr('counter1')
    pipe.incr('counter2')
    pipe.execute()  # All executed atomically
  4. 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
  5. 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:

  1. 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
  2. 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))
  3. 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)
  4. 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:

  1. 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': '>'})
  2. Features:

    • Message persistence (unlike pub/sub)
    • Consumer groups (like Kafka)
    • Message acknowledgment
    • Range queries
    • Time-based queries
  3. Use Cases:

    • Event sourcing
    • Message queues
    • Activity feeds
    • Log aggregation
    • Real-time analytics
  4. 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:

  1. 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}))
  2. Pipeline Multiple Commands:

    pipe = r.pipeline()
    for i in range(100):
        pipe.get(f'key:{i}')
    results = pipe.execute()  # Single round trip
  3. Use Connection Pooling:

    pool = ConnectionPool(host='localhost', port=6379, max_connections=50)
    r = redis.Redis(connection_pool=pool)
  4. Batch Operations:

    # Use MSET instead of multiple SET
    r.mset({'key1': 'value1', 'key2': 'value2'})
    
    # Use MGET instead of multiple GET
    r.mget(['key1', 'key2'])
  5. Avoid Large Values:

    • Split large values into smaller chunks
    • Use compression for large data
    • Consider using external storage for very large values
  6. 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
  7. 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):

  1. Setup:

    # On replica (slave) server
    replicaof 192.168.1.100 6379
  2. How it Works:

    • Master handles writes
    • Replicas replicate data from master
    • Replication is asynchronous
    • Replicas can handle reads (scale reads)
  3. Replication Process:

    • Replica connects to master
    • Master sends RDB snapshot
    • Master streams write commands to replica
    • Replica applies commands to stay in sync
  4. Configuration:

    # Master
    requirepass masterpassword
    
    # Replica
    replicaof 192.168.1.100 6379
    masterauth masterpassword
    replica-read-only yes
  5. 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
  6. 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:

  1. 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)
                )
  2. 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
  3. 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 - KEYS on 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 everysec loses up to a second. Treat it as a cache unless you have deliberately configured otherwise.