一、MySQL缓存方案用来解决什么
- 缓存用户定义的热点数据,用户直接从缓存获取热点数据,降低数据库的读写压力
- 场景分析:
- 内存访问速度是磁盘访问速度 10 万倍(数量级)
- 读的需求远远大于写的需求
- mysql 自身缓冲层跟业务无关
- mysql 作为项目主要数据库,便于统计分析
- 缓存数据库作为辅助数据库,存放热点数据
二、还有哪些方式提升 MySQL 访问性能
- 读写分离
- 是什么?:设置多个从数据库(可能分布在多个机器),写操作依然在主数据库,主数据库提供数据的主要依据。
- 解决了什么问题?:从数据库主要解决读压力。
- 原理是什么?:基于主从原理,采用异步复制,具有最终一致性(主从之间数据会有差异)。如果读操作有一致性要求,需读主数据库。
- 连接池
- 是什么?:在服务端创建多个与数据库的连接。
- 解决了什么问题?:提升数据库访问并发性能,同时复用连接资源,避免连接建立、断开及安全验证的开销。
- 原理是什么?:基于 mysql 的网络模型(select、阻塞 IO 模型)。特别地,若发送一个事务(包含多个 SQL 语句),该事务必须在一个连接中执行,即事务的对象是连接。
- 异步连接
- 是什么?:在服务端创建一个连接,针对这个连接采用非阻塞 IO。
- 解决了什么问题?:节省网络传输时间。
- 原理是什么?:使用了非阻塞 IO。
三、缓存和 MySQL 一致性状态分析
- MySQL 有,Redis 无
- 此时需将 MySQL 数据同步至 Redis,否则读取时可能多一次查询 MySQL 的操作,影响效率。
- MySQL 无,Redis 有
- 这属于脏数据情况,是不允许存在的。因为数据源头(MySQL)无此数据,而缓存(Redis)有,会导致后续读取出现错误数据,破坏数据一致性。
- 都有,数据不一致
- 典型场景如 MySQL 数据已更新,但 Redis 未同步更新。此时读取 Redis 会获取到旧数据,导致业务逻辑使用错误数据,影响系统正确性。
- 都有,数据一致
- 这是理想状态,缓存(Redis)与数据库(MySQL)数据完全一致,无论读缓存还是数据库,都能获取到正确且最新的数据,保证业务逻辑的准确性。
- 都没有
- 属于正常初始状态。首次读取时会从 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)一遍,以实现数据同步。
其具体复制流程如下:
- Slave(从库)的 IO 线程连接到 Master(主库),并请求从指定日志文件的指定位置(或从最开始的日志位置)之后的日志内容。
- Master 接收到来自 Slave 的 IO 线程的请求后,负责复制的 IO 线程会根据请求信息读取日志指定位置之后的内容,返回给 Slave 的 IO 线程。返回信息除了包含日志内容外,还包括当前 Master 端的 binlog 文件名称及 binlog 的位置。
- Slave 的 IO 线程接收到信息后,将日志内容依次添加到 Slave 端的 relay - log 文件末尾,并将读取到的 Master 日志文件名和位置记录到 master - info 文件中,以便下一次读取时能明确告知 Master 从何处开始读取日志。
- Slave 的 Sql 进程检测到 relay - log 中有新增内容后,会立即解析 relay - log 的内容,使其成为在 Master 端真实执行时的可执行内容,并在从库自身执行这些操作,从而完成数据的同步复制。
六、读写分离(最终一致性)

为什么需要缓冲层?
前提
读多写少,单个主节点能支撑项目数据量;数据的主要依据是 mysql。
mysql
mysql 有缓冲层,它的作用也是用来缓存热点数据,这些数据包括索引、记录等;mysql 缓冲层是从自身出发,跟具体的业务无关,这里的缓冲策略主要是 lru。
mysql 数据主要存储在磁盘当中,适合大量重要数据的存储;磁盘当中的数据一般是远大于内存当中的数据;一般业务场景关系型数据库(mysql)作为主要数据库。
缓冲层
缓存数据库可以选用 redis,memcached;它们所有数据都存储在内存当中,当然也可以将内存当中的数据持久化到磁盘当中。
总结
- 由于 mysql 的缓冲层(buffer pool)不由用户来控制,也就是不能由用户来控制缓存具体数据;
- 访问磁盘的速度比较慢,尽量获取数据从内存中获取;
- 主要解决读的性能;因为写没必要优化,必须让数据正确的落盘;如果写性能出现问题,那么请使用横向扩展集群方式来解决;
- 项目中需要存储的数据应该远大于内存的容量,同时需要进行数据统计分析,所以数据存储获取的依据应该是关系型数据库;
- 缓存数据库可以存储用户自定义的热点数据;以下的讨论都是基于热点数据的同步问题。

为什么有同步的问题?
没有缓冲层之前,我们对数据的读写都是基于 mysql,所以不存在同步问题;这句话也不是必然,比如读写分离就存在同步问题(数据一致性问题)。
引入缓冲层后,我们对数据的获取需要分别操作缓存数据库和 mysql,那么这个时候数据可能存在几个状态?
- mysql 有,缓存无
- mysql 无,缓存有
- 都有,但数据不一致
- 都有,数据一致
- 都没有
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 # 记录完整行数据(默认即可)
操作步骤:
重启 MySQL 使配置生效。
登录 MySQL,创建用于同步的用户并授权:
sqlCREATE USER 'sync_user'@'%' IDENTIFIED BY 'sync_password'; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'sync_user'@'%'; FLUSH PRIVILEGES;
二、安装 go-mysql-transfer
1. 方式一:二进制包安装(推荐)
- 从 Gitee 仓库 下载最新二进制包。
- 解压后得到可执行文件
go-mysql-transfer
(Linux 为./go-mysql-transfer
,Windows 为go-mysql-transfer.exe
)。2. 方式二:源码编译(适合定制需求)
克隆代码:
git clone https://gitee.com/mirrors/go-mysql-transfer.git cd go-mysql-transfer
配置 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:
Lualocal 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. 增量数据同步(全量后执行)
获取 MySQL 当前 Binlog 位置:
sqlmysql> SHOW MASTER STATUS;
返回结果类似:
+------------------+----------+--------------+------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | +------------------+----------+--------------+------------------+ | mysql-bin.000003 | 154 | test | | +------------------+----------+--------------+------------------+
记录下
File
和Position
(如mysql-bin.000003
和154
)。启动增量同步:
# 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 数据同步
打开另一个终端,连接 Redis:
redis-cli -h 127.0.0.1 -p 6379
查看同步后的数据:
# 查看所有键 KEYS "user:*" # 获取具体键的哈希值 HGETALL user:10001
预期输出:
1) "id" 2) "10001" 3) "nick" 4) "mark" 5) "height" 6) "180" 7) "sex" 8) "1" 9) "age"
"31"
七、常见问题与解决
- Binlog 格式错误:
- 确保 MySQL 的
binlog-format=ROW
,并重启 MySQL。
- 权限不足:
- 检查同步用户是否拥有
REPLICATION SLAVE
权限。
- 连接超时:
- 检查 MySQL 和 Redis 的 IP/端口是否可达,防火墙是否放行。
- 数据不同步:
- 查看 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
命令)确保同一时间只有一个请求能访问数据库。- 该请求查询数据库并更新缓存后,释放锁,其他请求再重试。
- 执行流程 :
- 请求尝试获取锁(如
lock:key
)。- 成功获取锁的请求查询数据库,更新缓存。
- 释放锁,其他请求重试时发现缓存已更新,直接读取缓存。
- 优缺点 :
- ✅ 有效避免并发查询数据库,确保缓存原子性更新。
- ❌ 引入锁机制,可能增加请求延迟,需处理锁超时和死锁问题。
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(空值缓存、布隆过滤器) 缓存击穿 热点数据失效引发并发压力 控制并发访问(分布式锁)、避免失效(永不过期) 缓存雪崩 大量缓存失效或服务宕机 高可用架构、分散失效时间、预热与持久化