当前位置: 首页 > news >正文

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.cnf 或 my.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,创建用于同步的用户并授权:
    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: mysqldsn: "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: redisdriver: redisaddr: "127.0.0.1:6379"password: ""        # 若 Redis 有密码需填写db: 0               # Redis 数据库编号# 同步规则:使用 Lua 脚本处理数据rule:type: luascript: "redis_ops.lua"  # Lua 脚本路径(相对于项目根目录)

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

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

 
local ops = require("redisOps")       -- 加载 Redis 操作模块
local row = ops.rawRow()              -- 获取当前行数据(键为列名的 table)
local action = ops.rawAction()        -- 获取操作类型(insert/update/delete)-- 仅处理 INSERT 事件(可根据需求扩展 UPDATE/DELETE)
if action == "insert" thenlocal 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 位置

    mysql> SHOW MASTER STATUS;
    

    返回结果类似:

    +------------------+----------+--------------+------------------+
    | File             | Position | Binlog_Do_DB | Binlog_Ignore_DB |
    +------------------+----------+--------------+------------------+
    | mysql-bin.000003 | 154      | test         |                  |
    +------------------+----------+--------------+------------------+
    

    记录下 File 和 Position(如 mysql-bin.000003 和 154)。

  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. 创建测试表并插入数据
-- 登录 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。  
2. **权限不足**:  
- 检查同步用户是否拥有 `REPLICATION SLAVE` 权限。  
3. **连接超时**:  
- 检查 MySQL 和 Redis 的 IP/端口是否可达,防火墙是否放行。  
4. **数据不同步**:  
- 查看 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 

http://www.xdnf.cn/news/476137.html

相关文章:

  • C# 面向对象 构造函数带参无参细节解析
  • 巧记英语四级单词 Unit8-上【晓艳老师版】
  • Android minSdk从21升级24后SO库异常
  • 【Android构建系统】如何在Camera Hal的Android.bp中选择性引用某个模块
  • Springboot 异步场景 使用注解 @Async 及 自定义线程池分模块使用
  • 一分钟了解机器学习
  • 专业版降重指南:如何用Python批量替换同义词?自动化操作不香嘛?
  • STM32 ADC+DMA+TIM触发采样实战:避坑指南与源码解析
  • 宇宙中是否存在量子现象?
  • Jenkins的流水线执行shell脚本执行jar命令后项目未启动未输出日志问题处理
  • #跟着若城学鸿蒙# web篇-运动和方向传感器监测
  • 【愚公系列】《Manus极简入门》042-投资策略分析师:“投资智慧导航”
  • 武汉火影数字全息剧秀制作:科技与艺术的梦幻联动
  • RabbitMQ 消息模式实战:从简单队列到复杂路由(三)
  • 通信安全堡垒:profinet转ethernet ip主网关提升冶炼安全与连接
  • PCL PolygonMesh 与 TextureMesh 源码阅读与简单测试
  • 数据结构进阶:AVL树与红黑树
  • SRS流媒体服务器(5)源码分析之RTMP握手
  • Python中in和is关键字详解和使用
  • C语言实现简单的--队列
  • Redis解析
  • C#将1GB大图裁剪为8张图片
  • 100G QSFP28 BIDI光模块一览:100G单纤高速传输方案|易天光通信
  • 组件导航 (Navigation)+flutter项目搭建-混合开发+分栏
  • Android 中 权限分类及申请方式
  • HNU工训--计算机串口数据收发与测量
  • 安科瑞AcrelEMS3.0企业微电网智慧能源平台-安科瑞 蒋静
  • .NET Core liunx二进制文件安装
  • 22、能源监控与优化 - 数据中心模拟 - /能源管理组件/data-center-energy-monitoring
  • CSS面试题汇总