Django 面試準備 09-1:Redis vs Memcached

深入理解兩大緩存系統的差異與選擇策略

09-1. Redis vs Memcached

📌 為什麼需要緩存?

在 Web 應用中,數據庫往往是性能瓶頸:

用戶請求 → Django → 數據庫查詢(慢!)→ 返回數據

引入緩存後:

用戶請求 → Django → 緩存(快!)→ 返回數據
                  ↓ 未命中
                 數據庫 → 更新緩存 → 返回數據

緩存的價值:

  • ⚡ 減少數據庫壓力(從 100ms 降到 1ms)
  • 🚀 提升響應速度
  • 💰 降低服務器成本

🔍 Redis vs Memcached 核心差異

快速對比表

特性RedisMemcached
數據類型String, Hash, List, Set, Sorted Set僅支持 String
持久化✅ 支持(RDB、AOF)❌ 不支持
分布式✅ 內建(Redis Cluster)⚠️ 需要客戶端實現
單線程/多線程單線程(6.0+ 支持多線程 I/O)多線程
內存管理更複雜(多種數據結構)簡單(Slab 分配)
最大值大小512MB1MB
事務支持✅ 支持❌ 不支持
發布訂閱✅ 支持❌ 不支持
腳本支持✅ Lua 腳本❌ 不支持

🎯 Redis 詳解

1. 豐富的數據類型

import redis

r = redis.Redis(host='localhost', port=6379, db=0)

# String:簡單鍵值對
r.set('user:1000:name', 'Alice')
r.get('user:1000:name')  # b'Alice'

# Hash:用戶對象
r.hset('user:1000', mapping={
    'name': 'Alice',
    'email': 'alice@example.com',
    'age': '25'
})
r.hgetall('user:1000')
# {b'name': b'Alice', b'email': b'alice@example.com', b'age': b'25'}

# List:消息隊列
r.lpush('task_queue', 'task1', 'task2', 'task3')
r.rpop('task_queue')  # b'task1'

# Set:標籤系統
r.sadd('user:1000:tags', 'python', 'django', 'redis')
r.smembers('user:1000:tags')
# {b'python', b'django', b'redis'}

# Sorted Set:排行榜
r.zadd('leaderboard', {'player1': 100, 'player2': 200, 'player3': 150})
r.zrevrange('leaderboard', 0, 2, withscores=True)
# [(b'player2', 200.0), (b'player3', 150.0), (b'player1', 100.0)]

2. 數據持久化

RDB(快照):

# redis.conf
save 900 1      # 900 秒內至少 1 次修改
save 300 10     # 300 秒內至少 10 次修改
save 60 10000   # 60 秒內至少 10000 次修改

AOF(追加日誌):

# redis.conf
appendonly yes
appendfsync everysec  # 每秒同步一次

對比:

  • RDB:快照,恢復快,但可能丟失數據
  • AOF:日誌,數據更安全,但文件更大

3. 主從複製與高可用

# 主節點
redis-server --port 6379

# 從節點
redis-server --port 6380 --replicaof 127.0.0.1 6379

Redis Sentinel(哨兵):

from redis.sentinel import Sentinel

sentinel = Sentinel([('localhost', 26379)], socket_timeout=0.1)
master = sentinel.master_for('mymaster', socket_timeout=0.1)
slave = sentinel.slave_for('mymaster', socket_timeout=0.1)

# 自動故障轉移
master.set('foo', 'bar')

🎯 Memcached 詳解

1. 簡單高效

import memcache

mc = memcache.Client(['127.0.0.1:11211'])

# 設置值
mc.set('key', 'value')
mc.set('user:1000', {'name': 'Alice', 'age': 25})

# 獲取值
mc.get('key')  # 'value'
mc.get('user:1000')  # {'name': 'Alice', 'age': 25}

# 批量操作
mc.set_multi({
    'key1': 'value1',
    'key2': 'value2',
    'key3': 'value3'
})
mc.get_multi(['key1', 'key2', 'key3'])

2. 多線程優勢

Memcached 使用多線程處理請求:

線程 1 ←─┐
線程 2 ←─┤ 處理多個並發請求
線程 3 ←─┤
線程 4 ←─┘

適合場景:

  • 大量小對象緩存
  • 高並發讀取
  • 簡單的鍵值存儲

3. 一致性哈希分布

from pymemcache.client.hash import HashClient

# 多節點部署
client = HashClient([
    ('127.0.0.1', 11211),
    ('127.0.0.1', 11212),
    ('127.0.0.1', 11213)
])

# 自動分片
client.set('key1', 'value1')  # 可能存在節點 1
client.set('key2', 'value2')  # 可能存在節點 2
client.set('key3', 'value3')  # 可能存在節點 3

📊 性能對比

基準測試

import time
import redis
import memcache

def benchmark_redis():
    r = redis.Redis()
    start = time.time()
    for i in range(10000):
        r.set(f'key:{i}', f'value:{i}')
    print(f"Redis SET: {time.time() - start:.2f}s")

    start = time.time()
    for i in range(10000):
        r.get(f'key:{i}')
    print(f"Redis GET: {time.time() - start:.2f}s")

def benchmark_memcached():
    mc = memcache.Client(['127.0.0.1:11211'])
    start = time.time()
    for i in range(10000):
        mc.set(f'key:{i}', f'value:{i}')
    print(f"Memcached SET: {time.time() - start:.2f}s")

    start = time.time()
    for i in range(10000):
        mc.get(f'key:{i}')
    print(f"Memcached GET: {time.time() - start:.2f}s")

典型結果:

Redis SET: 0.45s
Redis GET: 0.42s
Memcached SET: 0.38s
Memcached GET: 0.35s

結論:

  • 純鍵值操作:Memcached 略快
  • 複雜數據結構:Redis 更適合
  • 實際差異很小,場景選擇更重要

🤔 如何選擇?

選擇 Redis 的場景

需要複雜數據結構

# 購物車(Hash)
r.hset('cart:user123', 'product:456', 2)  # 商品 ID → 數量

# 最近瀏覽(List)
r.lpush('recent:user123', 'product:789')
r.ltrim('recent:user123', 0, 9)  # 只保留最近 10 個

# 在線用戶(Set)
r.sadd('online_users', 'user123', 'user456')

# 熱門文章排行(Sorted Set)
r.zincrby('hot_posts', 1, 'post:123')

需要數據持久化

# 用戶會話(需要重啟後恢復)
r.setex('session:abc123', 3600, user_data)

需要發布訂閱

# 實時通知
pubsub = r.pubsub()
pubsub.subscribe('notifications')
for message in pubsub.listen():
    print(message)

需要原子操作

# 秒殺庫存
r.watch('stock:product123')
pipeline = r.pipeline()
pipeline.multi()
pipeline.decr('stock:product123')
pipeline.execute()

選擇 Memcached 的場景

簡單的鍵值緩存

# 數據庫查詢結果
mc.set('query:user_list', user_list, time=300)

大規模分布式緩存

# 多個 Memcached 節點
client = HashClient([
    ('cache1.example.com', 11211),
    ('cache2.example.com', 11211),
    ('cache3.example.com', 11211),
    ('cache4.example.com', 11211)
])

內存敏感場景

  • Memcached 內存管理更高效
  • Slab 分配減少碎片

純緩存(不需要持久化)

# 臨時數據
mc.set('temp:calculation', result, time=60)

💡 Django 中的實踐

Redis 配置

# settings.py
CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
            'CONNECTION_POOL_KWARGS': {
                'max_connections': 50,
                'retry_on_timeout': True,
            },
            'PASSWORD': 'your-password',
            'SOCKET_CONNECT_TIMEOUT': 5,
            'SOCKET_TIMEOUT': 5,
        }
    }
}

# 使用
from django.core.cache import cache

cache.set('user:profile', user_data, timeout=300)
user_data = cache.get('user:profile')

Memcached 配置

# settings.py
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
        'LOCATION': [
            '127.0.0.1:11211',
            '127.0.0.1:11212',
        ],
        'OPTIONS': {
            'no_delay': True,
            'ignore_exc': True,
            'max_pool_size': 4,
            'use_pooling': True,
        }
    }
}

🎯 決策流程圖

需要緩存嗎?
    ├─ 否 → 直接訪問數據庫
    └─ 是 ↓

需要複雜數據結構?
    ├─ 是 → 選擇 Redis
    └─ 否 ↓

需要持久化?
    ├─ 是 → 選擇 Redis
    └─ 否 ↓

需要分布式?
    ├─ 是 → Redis 或 Memcached(都支持)
    └─ 否 ↓

追求極致性能?
    ├─ 是 → Memcached(簡單場景)
    └─ 否 → Redis(功能更強大)

💡 面試要點

Q1: Redis 和 Memcached 最大的區別是什麼?

答:

  1. 數據類型:Redis 支持多種數據結構,Memcached 只有字符串
  2. 持久化:Redis 支持持久化,Memcached 純內存
  3. 分布式:Redis 有 Cluster,Memcached 依賴客戶端

Q2: 為什麼 Redis 是單線程還能高性能?

答:

  1. 內存操作:主要是內存讀寫,非常快
  2. I/O 多路復用:epoll 處理並發連接
  3. 避免鎖競爭:單線程無需加鎖
  4. 優化的數據結構:針對性能優化

Q3: 什麼時候選擇 Memcached?

答:

  1. 簡單鍵值緩存:不需要複雜數據結構
  2. 大規模分布式:多節點橫向擴展
  3. 多線程優勢:CPU 密集型場景
  4. 純緩存場景:不需要持久化

Q4: Redis 的持久化會影響性能嗎?

答:

  • RDB:子進程執行,影響較小
  • AOF:everysec 模式影響可接受
  • 建議:根據場景選擇,純緩存可關閉持久化

🎓 實戰建議

1. 混合使用

# settings.py
CACHES = {
    # Redis:複雜數據、需要持久化
    'redis': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
    },
    # Memcached:簡單緩存、高並發
    'memcached': {
        'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
        'LOCATION': '127.0.0.1:11211',
    }
}

# 使用
from django.core.cache import caches

redis_cache = caches['redis']
memcached_cache = caches['memcached']

# 購物車用 Redis
redis_cache.set('cart:user123', cart_data)

# 頁面片段用 Memcached
memcached_cache.set('page:home', html_content, 60)

2. 監控與維護

# Redis 信息
r = redis.Redis()
info = r.info()
print(f"Used Memory: {info['used_memory_human']}")
print(f"Connected Clients: {info['connected_clients']}")

# Memcached 狀態
mc = memcache.Client(['127.0.0.1:11211'])
stats = mc.get_stats()
print(stats)

🔗 下一篇

在下一篇文章中,我們將深入學習 Django 緩存框架,了解如何在 Django 中優雅地使用緩存。

閱讀時間:10 分鐘

0%