MySQL 和 Redis 是现代应用架构中经典的 "持久存储 + 高速缓存" 组合。它们各自优势互补:
- MySQL:强一致性、事务支持、持久化,适合存储核心业务数据(如用户信息、订单)。
- Redis:内存数据库、微秒级响应、丰富数据结构,适合缓存热点数据、会话、排行榜等。
✅ 一、典型搭配模式
| 场景 | 策略 |
|---|---|
| 读多写少的数据(如商品详情) | 先查 Redis,命中则返回;未命中查 MySQL,再写入 Redis |
| 写操作(如更新用户资料) | 先更新 MySQL,再删除/更新 Redis 中的缓存(Cache-Aside Pattern) |
| 防止缓存穿透 | 对空结果也缓存(短 TTL),或使用布隆过滤器 |
| 防止缓存雪崩 | 设置随机过期时间 |
| 防止缓存击穿 | 对热点 key 使用互斥锁(如 Redis 分布式锁) |
📌 核心原则 :以 MySQL 为唯一真实数据源,Redis 仅为缓存
✅ 二、Python 实战示例(使用 pymysql + redis-py)
1. 安装依赖
pip install pymysql redis
2. 初始化连接
python
import redis
import pymysql
import json
import time
from contextlib import contextmanager
# Redis 连接(带连接池)
redis_client = redis.Redis(
host='localhost',
port=6379,
decode_responses=True, # 自动解码 bytes 为 str
socket_connect_timeout=2,
socket_timeout=2
)
# MySQL 连接配置
DB_CONFIG = {
'host': 'localhost',
'user': 'root',
'password': 'your_password',
'database': 'blog_db',
'charset': 'utf8mb4',
'cursorclass': pymysql.cursors.DictCursor
}
@contextmanager
def get_db_connection():
conn = pymysql.connect(**DB_CONFIG)
try:
yield conn
finally:
conn.close()
3. 缓存策略实现:Cache-Aside(旁路缓存)
📌 场景:根据用户 ID 获取用户信息
python
CACHE_PREFIX = "user:"
CACHE_TTL = 300 # 5分钟
def get_user_by_id(user_id: int):
"""从缓存或数据库获取用户"""
cache_key = f"{CACHE_PREFIX}{user_id}"
# 1. 先查 Redis
cached = redis_client.get(cache_key)
if cached is not None:
print("✅ Hit cache")
return json.loads(cached)
# 2. 缓存未命中,查 MySQL
print("❌ Cache miss, querying MySQL...")
with get_db_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT id, name, email FROM users WHERE id = %s", (user_id,))
user = cursor.fetchone()
if user:
# 3. 写入缓存(注意:序列化为 JSON)
redis_client.setex(cache_key, CACHE_TTL, json.dumps(user))
return user
else:
# 4. 防止缓存穿透:对空结果也缓存(短时间)
redis_client.setex(cache_key, 60, "") # 空值缓存 60 秒
return None
4. 更新数据时同步缓存
python
def update_user(user_id: int, name: str, email: str):
"""更新用户信息,并失效缓存"""
# 1. 先更新 MySQL
with get_db_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(
"UPDATE users SET name=%s, email=%s WHERE id=%s",
(name, email, user_id)
)
conn.commit()
# 2. 删除 Redis 缓存(推荐"删除"而非"更新",避免并发不一致)
cache_key = f"{CACHE_PREFIX}{user_id}"
redis_client.delete(cache_key)
print(f"🧹 Cache invalidated for user {user_id}")
💡 为什么删除而不是更新?
- 更新需查 DB 再写缓存,多一次查询
- 并发场景下可能写入旧数据(先更新 DB 的请求后写缓存)
- 删除更简单安全:下次读自动加载最新数据
5. 防止缓存击穿:热点 Key 加锁
当某个 key 过期瞬间,大量请求同时击穿到 DB。
python
import uuid
def get_user_safe(user_id: int):
"""带分布式锁防止缓存击穿"""
cache_key = f"{CACHE_PREFIX}{user_id}"
# 1. 查缓存
cached = redis_client.get(cache_key)
if cached is not None:
return json.loads(cached)
# 2. 尝试获取分布式锁(锁名 = cache_key + ":lock")
lock_key = cache_key + ":lock"
lock_value = str(uuid.uuid4())
lock_ttl = 5000 # 5秒
# 使用 Lua 脚本原子加锁(见前文)
lock_script = redis_client.register_script("""
if redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then
return 1
else
return 0
end
""")
if lock_script(keys=[lock_key], args=[lock_value, lock_ttl]):
try:
# 双重检查:可能其他线程已加载
cached = redis_client.get(cache_key)
if cached is not None:
return json.loads(cached)
# 查询 DB 并写缓存
with get_db_connection() as conn:
with conn.cursor() as cursor:
cursor.execute("SELECT id, name, email FROM users WHERE id = %s", (user_id,))
user = cursor.fetchone()
if user:
redis_client.setex(cache_key, CACHE_TTL, json.dumps(user))
return user
else:
redis_client.setex(cache_key, 60, "")
return None
finally:
# 释放锁(Lua 脚本确保安全)
unlock_script = redis_client.register_script("""
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
""")
unlock_script(keys=[lock_key], args=[lock_value])
else:
# 未获得锁,短暂等待后重试(或直接返回错误)
time.sleep(0.01)
return get_user_safe(user_id) # 递归重试(生产环境建议限制重试次数)
✅ 三、完整使用流程示例
ini
if __name__ == "__main__":
user_id = 123
# 首次查询:缓存未命中 → 查 DB → 写缓存
user = get_user_by_id(user_id)
print("User:", user)
# 第二次查询:命中缓存
user = get_user_by_id(user_id)
print("User (cached):", user)
# 更新用户
update_user(user_id, "New Name", "new@example.com")
# 再次查询:缓存已失效,重新加载
user = get_user_by_id(user_id)
print("Updated user:", user)
✅ 四、高级技巧与注意事项
1. 缓存空值防穿透
bash
# 如上文所示,对 DB 返回 None 的情况也缓存空字符串(TTL 较短)
2. 设置随机过期时间防雪崩
ini
import random
base_ttl = 300
random_ttl = base_ttl + random.randint(0, 60) # 300~360 秒
redis_client.setex(key, random_ttl, value)
3. 使用 Pipeline 批量操作
scss
with redis_client.pipeline() as pipe:
pipe.setex("user:1", 300, json.dumps(user1))
pipe.setex("user:2", 300, json.dumps(user2))
pipe.execute()
4. 监控缓存命中率
python
# 在 get_user 中增加指标
total_requests += 1
if cached:
cache_hits += 1
print(f"Hit rate: {cache_hits/total_requests:.2%}")
5. 数据一致性保障
- 最终一致性:接受短暂不一致(适用于大多数场景)
- 强一致性:写操作时加全局锁(性能差,慎用)
- 延迟双删(不推荐):先删缓存 → 更新 DB → 延迟再删缓存(仍有窗口期)
✅ 五、总结:最佳实践清单
| 项目 | 建议 |
|---|---|
| 读流程 | 先 Redis,后 MySQL |
| 写流程 | 先 MySQL,后删 Redis |
| 缓存键设计 | 语义清晰,如 user:{id}、product:{sku} |
| 空值处理 | 缓存空结果(短 TTL) |
| 过期时间 | 设置随机值防雪崩 |
| 热点 Key | 用分布式锁防击穿 |
| 序列化 | 用 JSON(兼容性好),避免 pickle(安全风险) |
| 连接管理 | Redis 用连接池,MySQL 用上下文管理器 |
💡 记住 :
Redis 是加速器,不是数据库。所有关键数据必须以 MySQL 为准。
通过以上范例,你可以安全、高效地在 Python 项目中结合 MySQL 和 Redis。如果需要 Django / Flask 集成示例 或 使用连接池/异步版本(aioredis) ,欢迎继续提问!