MySQL的缓存策略

一、MySQL缓存方案用来解决什么

  • 缓存用户定义的热点数据,用户直接从缓存获取热点数据,降低数据库的读写压力
  • 场景分析:
    1. 内存访问速度是磁盘访问速度 10 万倍(数量级)
    2. 读的需求远远大于写的需求
    3. mysql 自身缓冲层跟业务无关
    4. mysql 作为项目主要数据库,便于统计分析
    5. 缓存数据库作为辅助数据库,存放热点数据

二、还有哪些方式提升 MySQL 访问性能

  1. 读写分离
    • 是什么?:设置多个从数据库(可能分布在多个机器),写操作依然在主数据库,主数据库提供数据的主要依据。
    • 解决了什么问题?:从数据库主要解决读压力。
    • 原理是什么?:基于主从原理,采用异步复制,具有最终一致性(主从之间数据会有差异)。如果读操作有一致性要求,需读主数据库。
  2. 连接池
    • 是什么?:在服务端创建多个与数据库的连接。
    • 解决了什么问题?:提升数据库访问并发性能,同时复用连接资源,避免连接建立、断开及安全验证的开销。
    • 原理是什么?:基于 mysql 的网络模型(select、阻塞 IO 模型)。特别地,若发送一个事务(包含多个 SQL 语句),该事务必须在一个连接中执行,即事务的对象是连接。
  3. 异步连接
    • 是什么?:在服务端创建一个连接,针对这个连接采用非阻塞 IO。
    • 解决了什么问题?:节省网络传输时间。
    • 原理是什么?:使用了非阻塞 IO。

三、缓存和 MySQL 一致性状态分析

  1. MySQL 有,Redis 无
    • 此时需将 MySQL 数据同步至 Redis,否则读取时可能多一次查询 MySQL 的操作,影响效率。
  2. MySQL 无,Redis 有
    • 这属于脏数据情况,是不允许存在的。因为数据源头(MySQL)无此数据,而缓存(Redis)有,会导致后续读取出现错误数据,破坏数据一致性。
  3. 都有,数据不一致
    • 典型场景如 MySQL 数据已更新,但 Redis 未同步更新。此时读取 Redis 会获取到旧数据,导致业务逻辑使用错误数据,影响系统正确性。
  4. 都有,数据一致
    • 这是理想状态,缓存(Redis)与数据库(MySQL)数据完全一致,无论读缓存还是数据库,都能获取到正确且最新的数据,保证业务逻辑的准确性。
  5. 都没有
    • 属于正常初始状态。首次读取时会从 MySQL 查询数据(若 MySQL 后续有写入),再将数据写入缓存,为后续读取提供加速。

四、制定用户定义热点数据的读写策略

  • 读策略
    先读缓存,若缓存存在则直接返回数据;若缓存不存在,访问 MySQL 获取数据,然后将数据写入 Redis 缓存,以便后续读取直接走缓存,提升效率。
  • 写策略
    • 以安全为主
      先删除 Redis 中的数据,再写 MySQL,最后将 MySQL 数据同步到 Redis。
      • 问题:缓存方案主要目标是提升效率,此策略为确保数据安全(避免脏数据),先删除缓存,导致下次读取需重新查询 MySQL 并同步缓存,降低了效率。
    • 以效率为主
      先写缓存并设置过期时间,再写 MySQL,等待 MySQL 同步到 Redis。
      • 过期时间选择:需综合考虑与 MySQL 的网络传输时间、MySQL 处理时间、MySQL 同步到 Redis 的时间,确保在过期时间内,MySQL 能完成数据同步,减少脏数据存在窗口。
      • 安全问题:在过期时间内(如 200ms 时间窗口),可能读到脏数据。但此策略优先保证写操作的效率,适用于对效率要求较高、对短时间脏数据容忍度较高的场景。

五、 mysql的主从复制

在 MySQL 主从复制机制中,主库的更新事件(如 update、insert、delete 等 DML 操作)通过 io - thread 写入到 binlog(二进制日志)中。从库会请求读取主库的 binlog,通过自身的 io - thread 将读取到的内容写入本地的 relay - log(中继日志)。接着,从库通过 sql - thread 读取 relay - log,并将其中的更新事件在从库中重放(replay)一遍,以实现数据同步。

其具体复制流程如下:

  1. Slave(从库)的 IO 线程连接到 Master(主库),并请求从指定日志文件的指定位置(或从最开始的日志位置)之后的日志内容。
  2. Master 接收到来自 Slave 的 IO 线程的请求后,负责复制的 IO 线程会根据请求信息读取日志指定位置之后的内容,返回给 Slave 的 IO 线程。返回信息除了包含日志内容外,还包括当前 Master 端的 binlog 文件名称及 binlog 的位置。
  3. Slave 的 IO 线程接收到信息后,将日志内容依次添加到 Slave 端的 relay - log 文件末尾,并将读取到的 Master 日志文件名和位置记录到 master - info 文件中,以便下一次读取时能明确告知 Master 从何处开始读取日志。
  4. Slave 的 Sql 进程检测到 relay - log 中有新增内容后,会立即解析 relay - log 的内容,使其成为在 Master 端真实执行时的可执行内容,并在从库自身执行这些操作,从而完成数据的同步复制。

六、读写分离(最终一致性)

为什么需要缓冲层?

前提

读多写少,单个主节点能支撑项目数据量;数据的主要依据是 mysql。

mysql

mysql 有缓冲层,它的作用也是用来缓存热点数据,这些数据包括索引、记录等;mysql 缓冲层是从自身出发,跟具体的业务无关,这里的缓冲策略主要是 lru。

mysql 数据主要存储在磁盘当中,适合大量重要数据的存储;磁盘当中的数据一般是远大于内存当中的数据;一般业务场景关系型数据库(mysql)作为主要数据库。

缓冲层

缓存数据库可以选用 redis,memcached;它们所有数据都存储在内存当中,当然也可以将内存当中的数据持久化到磁盘当中。

总结
  1. 由于 mysql 的缓冲层(buffer pool)不由用户来控制,也就是不能由用户来控制缓存具体数据;
  2. 访问磁盘的速度比较慢,尽量获取数据从内存中获取;
  3. 主要解决读的性能;因为写没必要优化,必须让数据正确的落盘;如果写性能出现问题,那么请使用横向扩展集群方式来解决;
  4. 项目中需要存储的数据应该远大于内存的容量,同时需要进行数据统计分析,所以数据存储获取的依据应该是关系型数据库;
  5. 缓存数据库可以存储用户自定义的热点数据;以下的讨论都是基于热点数据的同步问题。

为什么有同步的问题?

没有缓冲层之前,我们对数据的读写都是基于 mysql,所以不存在同步问题;这句话也不是必然,比如读写分离就存在同步问题(数据一致性问题)。

引入缓冲层后,我们对数据的获取需要分别操作缓存数据库和 mysql,那么这个时候数据可能存在几个状态?

  1. mysql 有,缓存无
  2. mysql 无,缓存有
  3. 都有,但数据不一致
  4. 都有,数据一致
  5. 都没有
    4 和 5 显然是没问题的,我们现在需要考虑 1、2 以及 3。
    首先明确一点:我们获取数据的主要依据是 mysql,只需要将 mysql 的数据正确同步到缓存数据库就可以了;同理,缓存有,mysql 没有,这比较危险,此时我们可以认为该数据为脏数据;所以我们需要在同步策略中避免该情况发生;同时可能存在 mysql 和缓存都有数据,但是数据不一致,这种也需要在同步策略中避免。
    注意:
    缓存不可用,整个系统依然要保持正常工作;
    mysql 不可用的话,系统停摆,停止对外提供服务。

解决数据同步问题

策略 1
  • 读流程:先读缓存,若缓存存在,直接返回;若缓存没有,读 mysql;若 mysql 有,同步到缓存,并返回;若 mysql 没有,则返回没有。
  • 写流程 :先删除缓存,再写 mysql,后续数据同步交由 go - mysql - transfer 等中间件处理(将问题 3 转化成 1)。
    • 先删除缓存,是为了避免其他服务读取旧数据,同时也告知系统这个数据已不是最新,建议从 mysql 获取数据。
    • 但对于服务 A 而言,写入 mysql 后,接着读操作必须要能读到最新的数据。
策略 2
  • 读流程:先读缓存,若缓存存在,直接返回;若缓存没有,读 mysql;若 mysql 有,同步到缓存,并返回;若 mysql 没有,则返回没有。
  • 写流程 :先写缓存,并设置过期时间(如 200ms),再写 mysql,后续数据同步交由其他中间件处理。
    • 这里设置的过期时间是预估时间,大致上是 mysql 到缓存同步的时间。
    • 在写的过程中如果 mysql 停止服务,或数据没写入 mysql,则 200ms 内提供了脏数据服务,但仅仅只有 200ms 的数据错乱。

同步方案

原理图

一、环境准备

1. 系统要求
  • 操作系统:Linux/Windows(推荐 Linux)。
  • 依赖工具
    • MySQL:5.6+(需开启 Binlog,配置 ROW 模式)。
    • Go:1.14+(用于编译源码,若使用二进制包可跳过)。
    • Redis:3.0+(作为数据接收端)。
2. MySQL 配置(关键)

修改 MySQL 配置文件(my.cnfmy.ini),开启 Binlog 并配置格式:

复制代码
[mysqld]
log-bin=mysql-bin          # 开启 Binlog
binlog-format=ROW          # 选择 ROW 模式(记录行级数据变更)
server_id=1                # MySQL 主库 ID(需唯一,不可与 go-mysql-transfer 的 slave_id 重复)
binlog_row_image=FULL      # 记录完整行数据(默认即可)

操作步骤

  1. 重启 MySQL 使配置生效。

  2. 登录 MySQL,创建用于同步的用户并授权:

    sql 复制代码
    CREATE USER 'sync_user'@'%' IDENTIFIED BY 'sync_password';
    GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'sync_user'@'%';
    FLUSH PRIVILEGES;

二、安装 go-mysql-transfer

1. 方式一:二进制包安装(推荐)
  1. Gitee 仓库 下载最新二进制包。
  2. 解压后得到可执行文件 go-mysql-transfer(Linux 为 ./go-mysql-transfer,Windows 为 go-mysql-transfer.exe)。
2. 方式二:源码编译(适合定制需求)
  1. 克隆代码:

    复制代码
    git clone https://gitee.com/mirrors/go-mysql-transfer.git
    cd go-mysql-transfer
  2. 配置 Go 模块并编译:

    复制代码
    GO111MODULE=on
    go env -w GOPROXY=https://goproxy.cn,direct  # 设置国内代理
    go build -o go-mysql-transfer

三、配置文件详解(app.yml

在项目根目录创建或修改 app.yml,配置 MySQL 源和 Redis 目标:

复制代码
# 全局配置
global:
  log_level: info       # 日志级别(debug/info/warn/error)
  slave_id: 100        # 模拟从库 ID(需与 MySQL 的 server_id 不同)
  flush_interval: 1s    # 批量写入接收端的间隔

# 数据源(MySQL)配置
source:
  driver: mysql
  dsn: "sync_user:sync_password@tcp(127.0.0.1:3306)/test?charset=utf8mb4"
  # 注意:dsn 格式为 "用户:密码@tcp(IP:端口)/数据库?参数"
  table: ["user"]       # 需同步的表名(支持正则,如 "user_*")
  binlog:
    start_file: ""      # 起始 Binlog 文件(首次全量同步留空)
    start_pos: 0        # 起始位置(首次全量同步为 0)

# 目标端(Redis)配置
target:
  - name: redis
    driver: redis
    addr: "127.0.0.1:6379"
    password: ""        # 若 Redis 有密码需填写
    db: 0               # Redis 数据库编号
    # 同步规则:使用 Lua 脚本处理数据
    rule:
      type: lua
      script: "redis_ops.lua"  # Lua 脚本路径(相对于项目根目录)

四、编写 Lua 脚本(数据处理逻辑)

在项目根目录创建 redis_ops.lua,定义如何将 MySQL 数据同步到 Redis:

Lua 复制代码
local ops = require("redisOps")       -- 加载 Redis 操作模块
local row = ops.rawRow()              -- 获取当前行数据(键为列名的 table)
local action = ops.rawAction()        -- 获取操作类型(insert/update/delete)

-- 仅处理 INSERT 事件(可根据需求扩展 UPDATE/DELETE)
if action == "insert" then
    local id = row["id"]
    local nick = row["nick"]
    local key = string.format("user:%d", id)  -- 定义 Redis 键名格式
    -- 将字段写入 Redis 哈希结构
    ops.HSET(key, "id", id)
    ops.HSET(key, "nick", nick)
    ops.HSET(key, "height", row["height"])
    ops.HSET(key, "sex", row["sex"])
    ops.HSET(key, "age", row["age"])
    ops.EXPIRE(key, 86400)  -- 设置过期时间(1 天,可选)
end

-- 处理 UPDATE 事件(示例)
-- if action == "update" then
--     local old_row = ops.oldRow()  -- 获取更新前的数据
--     -- 对比新旧数据,仅更新变化的字段
-- end

-- 处理 DELETE 事件(示例)
-- if action == "delete" then
--     local key = string.format("user:%d", row["id"])
--     ops.DEL(key)  -- 删除 Redis 中的键
-- end

五、启动同步流程

1. 全量数据初始化(首次同步)
复制代码
# Linux/macOS
./go-mysql-transfer -stock

# Windows
go-mysql-transfer.exe -stock
  • 作用:将 MySQL 现有数据全量导入 Redis,避免增量同步时漏数据。
  • 提示 :全量同步期间会锁定表(可选参数 -lock-table=false 可关闭锁表,但可能导致数据不一致)。
2. 增量数据同步(全量后执行)
  1. 获取 MySQL 当前 Binlog 位置

    sql 复制代码
    mysql> SHOW MASTER STATUS;

    返回结果类似:

    复制代码
    +------------------+----------+--------------+------------------+
    | File             | Position | Binlog_Do_DB | Binlog_Ignore_DB |
    +------------------+----------+--------------+------------------+
    | mysql-bin.000003 | 154      | test         |                  |
    +------------------+----------+--------------+------------------+

    记录下 FilePosition(如 mysql-bin.000003154)。

  2. 启动增量同步

    复制代码
    # Linux/macOS
    ./go-mysql-transfer -config app.yml -position mysql-bin.000003 154
    
    # Windows
    go-mysql-transfer.exe -config app.yml -position mysql-bin.000003 154
    • -config:指定配置文件路径(默认 app.yml)。
    • -position:指定增量同步的起始 Binlog 文件和位置。

六、演示验证

1. 创建测试表并插入数据
sql 复制代码
-- 登录 MySQL
mysql -u root -p

-- 创建数据库和表
CREATE DATABASE IF NOT EXISTS test;
USE test;

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` BIGINT PRIMARY KEY,
  `nick` VARCHAR(100),
  `height` INT8,
  `sex` VARCHAR(1),
  `age` INT8
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 插入测试数据
INSERT INTO `user` VALUES (10001, 'mark', 180, '1', 30);
UPDATE `user` SET `age` = 31 WHERE id = 10001;
2. 验证 Redis 数据同步
  1. 打开另一个终端,连接 Redis:

    复制代码
    redis-cli -h 127.0.0.1 -p 6379
  2. 查看同步后的数据:

    复制代码
    # 查看所有键
    KEYS "user:*"
    
    # 获取具体键的哈希值
    HGETALL user:10001

    预期输出:

    复制代码
    1) "id"
    2) "10001"
    3) "nick"
    4) "mark"
    5) "height"
    6) "180"
    7) "sex"
    8) "1"
    9) "age"
  3. "31"

    七、常见问题与解决

    1. Binlog 格式错误
    • 确保 MySQL 的 binlog-format=ROW,并重启 MySQL。
    1. 权限不足
    • 检查同步用户是否拥有 REPLICATION SLAVE 权限。
    1. 连接超时
    • 检查 MySQL 和 Redis 的 IP/端口是否可达,防火墙是否放行。
    1. 数据不同步
    • 查看 go-mysql-transfer 日志,确认是否有解析错误或网络问题。
    • 重新执行全量同步 ./go-mysql-transfer -stock 后再启动增量同步。

    通过以上步骤,可实现 MySQL 与 Redis 的实时增量同步,有效提升读性能并保证数据最终一致性。

七、缓存出现的问题

一、缓存穿透(Cache Penetration)
问题描述

当客户端频繁请求在缓存(Redis)和数据库(MySQL)中均不存在的数据 时,每次请求都会绕过缓存直接访问数据库。若这类请求量极大,会导致数据库压力骤增,甚至崩溃。
典型场景

  • 恶意攻击:黑客利用随机生成的非法 Key 发起大量请求,试图压垮数据库。
  • 业务逻辑漏洞:用户输入非法参数(如负数 ID),导致系统查询不存在的数据。
核心成因
  • 缓存层未对无效 Key 进行拦截,导致所有请求直达数据库。
  • 数据库无法快速识别并拒绝无效请求,需消耗资源执行查询。
解决方案
1. 缓存空值标记
  • 原理
    当数据库查询结果为 "无数据" 时,在缓存中记录该 Key 对应的空值(如null),并设置较短的过期时间(如 5 分钟)。
    • 后续请求先查缓存,若为空值标记,则直接返回 "无数据",不再查询数据库。
  • 优缺点
    • ✅ 简单直接,无需额外组件,快速拦截重复无效请求。
    • ❌ 可能存储大量无效 Key,占用缓存空间(可通过定期清理缓解)。
2. 布隆过滤器(Bloom Filter)
  • 原理
    • 提前将数据库中存在的 Key 存入一个布隆过滤器(一种概率型数据结构)。
    • 客户端请求前,先通过布隆过滤器校验 Key 是否存在:
      • 若过滤器判定 "不存在",则直接拒绝请求,不查询数据库。
      • 若判定 "存在",再访问缓存和数据库(可能存在误判,但概率极低)。
  • 优缺点
    • ✅ 高效过滤无效请求,内存占用远低于缓存空值,适合海量 Key 场景。
    • ❌ 不支持删除操作,且存在极小概率的误判(需合理设计过滤器参数)。
二、缓存击穿(Cache Breakdown)
问题描述

热点数据的缓存突然失效 (如过期时间到达),此时大量并发请求同时访问数据库,导致数据库瞬间压力激增,可能引发服务降级或崩溃。
典型场景

  • 秒杀活动:某个商品的缓存过期,数万用户同时查询库存,击穿缓存。
  • 周期性更新:如首页推荐数据每天凌晨 1 点统一过期,导致同一时间大量请求涌至数据库。
核心成因
  • 热点数据访问量极高,缓存失效瞬间失去拦截作用。
  • 数据库单节点处理能力有限,无法应对突发流量峰值。
解决方案
1. 分布式锁(Distributed Lock)
  • 原理
    • 当缓存失效时,通过分布式锁(如 Redis 的SET NX命令)确保同一时间只有一个请求能访问数据库
    • 该请求查询数据库并更新缓存后,释放锁,其他请求再重试。
  • 执行流程
    1. 请求尝试获取锁(如lock:key)。
    2. 成功获取锁的请求查询数据库,更新缓存。
    3. 释放锁,其他请求重试时发现缓存已更新,直接读取缓存。
  • 优缺点
    • ✅ 有效避免并发查询数据库,确保缓存原子性更新。
    • ❌ 引入锁机制,可能增加请求延迟,需处理锁超时和死锁问题。
2. 热点数据永不过期
  • 原理
    • 对访问量极高的热点数据不设置过期时间,避免因过期导致击穿。
    • 通过后台异步任务(如定时任务)主动更新缓存,确保数据实时性。
  • 适用场景
    • 数据更新频率较低,且允许短时间内存在不一致(如商品详情页)。
三、缓存雪崩(Cache Avalanche)
问题描述

大量缓存项在同一时间段内集中失效缓存服务整体不可用 ,导致海量请求直接涌至数据库,造成数据库负载过高,甚至引发系统级故障。
典型场景

  • 缓存集群宕机:如 Redis 单节点故障且未配置高可用,导致所有缓存失效。
  • 批量缓存过期:如电商首页 10 万商品缓存均设置为 24 小时过期,每天凌晨同时失效。
核心成因
  • 缓存层可用性不足(如单点故障)或过期时间设计不合理(集中失效)。
  • 数据库无法承受突发的流量峰值。
解决方案
1. 缓存高可用架构
  • 实现方式
    • 采用 Redis 集群(Cluster)或哨兵模式(Sentinel),确保缓存层无单点故障。
    • 即使部分节点宕机,其他节点仍可提供服务,避免全量缓存失效。
  • 效果:提升缓存层的可靠性,防止因缓存宕机导致的雪崩。
2. 随机化过期时间
  • 原理
    • 为缓存设置随机化的过期时间(如基础时间 ±30% 波动),避免大量 Key 同时失效。
    • 例:基础过期时间为 3600 秒(1 小时),实际设置为 3000~4200 秒之间的随机值。
  • 效果:将缓存失效的时间点分散到更长的时间段内,降低数据库压力峰值。
3. 缓存预热与持久化
  • 缓存预热
    • 系统启动前,提前将热数据加载到缓存中(如通过全量同步工具go-mysql-transfer)。
    • 避免启动后因缓存为空导致请求直达数据库。
  • 持久化机制
    • 开启 Redis 持久化(RDB/AOF),确保重启后能快速从磁盘恢复数据。
    • 若重启时间较短,依赖持久化文件恢复缓存;若时间较长,提前手动预热热数据。

总结:异常场景应对核心思路

问题类型 核心风险 解决方案关键点
缓存穿透 无效请求压垮数据库 拦截无效 Key(空值缓存、布隆过滤器)
缓存击穿 热点数据失效引发并发压力 控制并发访问(分布式锁)、避免失效(永不过期)
缓存雪崩 大量缓存失效或服务宕机 高可用架构、分散失效时间、预热与持久化

0voice · GitHub

相关推荐
技术宝哥4 分钟前
Redis(2):Redis + Lua为什么可以实现原子性
数据库·redis·lua
学地理的小胖砸1 小时前
【Python 操作 MySQL 数据库】
数据库·python·mysql
呦呦鹿鸣Rzh2 小时前
缓存的相关内容
缓存
dddaidai1232 小时前
Redis解析
数据库·redis·缓存
数据库幼崽2 小时前
MySQL 8.0 OCP 1Z0-908 121-130题
数据库·mysql·ocp
Amctwd2 小时前
【SQL】如何在 SQL 中统计结构化字符串的特征频率
数据库·sql
betazhou3 小时前
基于Linux环境实现Oracle goldengate远程抽取MySQL同步数据到MySQL
linux·数据库·mysql·oracle·ogg
lyrhhhhhhhh3 小时前
Spring 框架 JDBC 模板技术详解
java·数据库·spring
喝醉的小喵4 小时前
【mysql】并发 Insert 的死锁问题 第二弹
数据库·后端·mysql·死锁
付出不多5 小时前
Linux——mysql主从复制与读写分离
数据库·mysql