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
相关推荐
烂漫心空2 小时前
Windows 系统如何使用 Mysql 服务
数据库·mysql
不屈的铝合金3 小时前
MySQL 数据库服务多实例部署指南
运维·数据库·mysql·多实例部署·维度隔离
哈里谢顿4 小时前
redis实现排行榜功能
redis
天然玩家5 小时前
【数据库知识】MySQL演进/迭代5.x 8.0 9.5
数据库·mysql
降临-max5 小时前
JavaWeb企业级开发---MySQL
java·开发语言·数据库·笔记·后端·mysql
代码不停5 小时前
MySQL索引和视图
数据库·mysql
Ahtacca5 小时前
Redis 五大常用数据类型详解及 Java 客户端(RedisTemplate)操作实战
java·数据库·redis·学习·缓存
一只旭宝7 小时前
Linux专题十二:mysql数据库以及redis数据库
linux·数据库·mysql
萧曵 丶7 小时前
MySQL B+树详解
数据库·b树·mysql