MySQL + Redis 协同 示例

最小可运行、可复现 的「MySQL + Redis 协同」示例,场景选最经典的
「缓存击穿/穿透保护 + 读写并发」

  1. 用户表在 MySQL。
  2. 热点用户查询先走 Redis,缓存未命中再回源 MySQL 并回填 Redis,同时解决并发回源问题(简单互斥锁)。
  3. 用户积分更新时,先写 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
相关推荐
xcLeigh1 小时前
Python 项目实战:用 Flask 实现 MySQL 数据库增删改查 API
数据库·python·mysql·flask·教程·python3
xu_yule1 小时前
Redis存储(15)Redis的应用_分布式锁_Lua脚本/Redlock算法
数据库·redis·分布式
Fleshy数模2 小时前
MySQL 表创建全攻略:Navicat 图形化与 Xshell 命令行双模式实践
linux·mysql
Nandeska2 小时前
15、基于MySQL的组复制
数据库·mysql
AllData公司负责人3 小时前
AllData数据中台-数据同步平台【Seatunnel-Web】整库同步MySQL同步Doris能力演示
大数据·数据库·mysql·开源
醇氧3 小时前
【docker】mysql 8 的健康检查(Health Check)
mysql·docker·容器
清风拂山岗 明月照大江4 小时前
Redis笔记汇总
java·redis·缓存
lekami_兰4 小时前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
消失的旧时光-19435 小时前
第十四课:Redis 在后端到底扮演什么角色?——缓存模型全景图
java·redis·缓存
爱学英语的程序员6 小时前
面试官:你了解过哪些数据库?
java·数据库·spring boot·sql·mysql·mybatis