最小可运行、可复现 的「MySQL + Redis 协同」示例,场景选最经典的
「缓存击穿/穿透保护 + 读写并发」:
- 用户表在 MySQL。
- 热点用户查询先走 Redis,缓存未命中再回源 MySQL 并回填 Redis,同时解决并发回源问题(简单互斥锁)。
- 用户积分更新时,先写 MySQL,成功后立即淘汰缓存(Cache-Aside 模式)。
创建初始数据
sql
-- 连上容器里的 MySQL:
-- docker exec -ti db /bin/bash
CREATE TABLE user (
id BIGINT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
score INT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
INSERT INTO user VALUES
(1, 'alice', 1000, NOW()),
(2, 'bob', 950, NOW()),
(3, 'cc', 820, NOW());
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MySQL + Redis 协同(完整版)
1. Cache-Aside + 延迟双删(写)
2. 分布式互斥回源(读)
3. 简单重试 & 日志
"""
import json
import time
import logging
import pymysql
import redis
from contextlib import contextmanager
from threading import Thread, local
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("collab")
# ---------------- 配置 ----------------
MYSQL_CFG = dict(host="127.0.0.1", port=3306, user="root",
password="DjAnGoBlOg!2!Q@W#E", database="redis_demo", charset="utf8mb4", autocommit=True)
REDIS_CFG = dict(host="127.0.0.1", port=6379, db=0, decode_responses=True)
CACHE_TTL = 60
LOCK_TTL = 5
RETRY = 3
# -------------------------------------
@contextmanager
def get_cursor():
conn = pymysql.connect(**MYSQL_CFG)
try:
with conn.cursor() as cur:
yield cur
finally:
conn.close()
class UserService:
"""业务层:屏蔽缓存细节"""
def __init__(self):
self.r = redis.Redis(**REDIS_CFG)
# ---------- 读 ----------
def get_user(self, uid: int) -> dict | None:
key = f"user:{uid}"
# ① 缓存命中
if data := self.r.get(key):
return json.loads(data)
# ② 分布式互斥(简单 SET NX)
lock_key = f"{key}:lock"
if not self.r.set(lock_key, 1, nx=True, ex=LOCK_TTL):
time.sleep(0.05) # 50 ms 后重读
return self.get_user(uid)
try:
# ③ double-check
if data := self.r.get(key):
return json.loads(data)
# ④ 回源
with get_cursor() as cur:
cur.execute("SELECT id,name,score FROM user WHERE id=%s", (uid,))
row = cur.fetchone()
user = {"id": row[0], "name": row[1], "score": row[2]} if row else None
if user:
self.r.setex(key, CACHE_TTL, json.dumps(user))
return user
finally:
self.r.delete(lock_key)
# ---------- 写 ----------
def add_score(self, uid: int, delta: int):
"""延迟双删策略"""
key = f"user:{uid}"
# ① 删缓存
self.r.delete(key)
try:
# ② 写 MySQL
with get_cursor() as cur:
cur.execute("UPDATE user SET score=score+%s WHERE id=%s", (delta, uid))
# ③ 延迟二次删(防并发读脏)
time.sleep(0.3)
self.r.delete(key)
except Exception as e:
log.exception("add_score error")
raise
# ---------------- 压测 ----------------
def reader(uid):
for _ in range(5):
log.info("get uid=%s data=%s", uid, UserService().get_user(uid))
time.sleep(0.1)
def writer(uid, delta):
for _ in range(2):
UserService().add_score(uid, delta)
log.info("updated uid=%s delta=%s", uid, delta)
time.sleep(0.4)
def main():
# 预热
svc = UserService()
for i in (1, 2, 3):
log.info("preload %s", svc.get_user(i))
# 并发
threads = []
for uid in (1, 2):
threads.append(Thread(target=reader, args=(uid,)))
threads.append(Thread(target=writer, args=(uid, 10)))
for t in threads:
t.start()
for t in threads:
t.join()
# 最终榜
with get_cursor() as cur:
cur.execute("SELECT id,name,score FROM user ORDER BY score DESC")
log.info("final mysql rank:\n" + "\n".join(f"{r[0]:>2} | {r[1]:<5} | {r[2]}" for r in cur.fetchall()))
if __name__ == "__main__":
main()
uml图可以查看
rust
@startuml
title MySQL + Redis 协同时序(Cache-Aside + 延迟双删)
actor 客户端
participant Redis
database "MySQL" as MYSQL
== 读:缓存未命中 ==
客户端 -> Redis: GET user:{uid}
Redis --> 客户端: nil
客户端 -> Redis: SETNX lock:{uid} 1 ex=5
alt 获取锁成功
客户端 -> MYSQL: SELECT ...
MYSQL --> 客户端: user 行
客户端 -> Redis: SETEX user:{uid} 60 json(user)
else 获取锁失败
客户端 -> 客户端: sleep(0.05)
客户端 -> Redis: GET user:{uid} \n重试
end
客户端 -> Redis: DEL lock:{uid}
== 写:延迟双删 ==
客户端 -> Redis: DEL user:{uid} // 第一次删
客户端 -> MYSQL: UPDATE score=...+delta
客户端 -> 客户端: sleep(0.3) // 延迟窗口
客户端 -> Redis: DEL user:{uid} // 第二次删
@enduml

类关系图
sql
@startuml
class UserService {
-r:Redis
+get_user(uid:int):dict
+add_score(uid:int,delta:int)
}
UserService --> Redis : 使用
UserService ..> MySQL : 通过 get_cursor()
note right of UserService
Cache-Aside
延迟双删
分布式互斥锁
end note
@enduml
