MySQL高可用与Redis——让数据永不掉线
生活类比:如果单台MySQL是一个厨师,那主从复制就是"师傅带徒弟"(师傅炒菜、徒弟跟着学),高可用就是"备班厨师随时待命",Redis则是"传菜窗口"——最快的菜先放窗口,不用每次都去厨房(数据库)端。
一、MySQL用户管理——给数据库"发门禁卡"
1.1 问题引入
场景:你的MySQL只有一个root账号,开发、测试、运维都用它。某天实习生用root执行了
DROP DATABASE production……
解决思路:为每个应用和人员创建独立账号,按"最小权限原则"授权。
1.2 CREATE USER创建用户
-- 创建用户(指定允许登录的主机)
CREATE USER 'app_user'@'192.168.1.%' IDENTIFIED BY 'AppPass@2024';
CREATE USER 'readonly'@'%' IDENTIFIED BY 'ReadOnly@2024'; -- %表示任意主机
CREATE USER 'dba_admin'@'localhost' IDENTIFIED BY 'Admin@2024'; -- 只允许本地登录
CREATE USER 'backup_user'@'127.0.0.1' IDENTIFIED BY 'Backup@2024';
-- 查看所有用户
SELECT user, host FROM mysql.user;
-- 查看用户权限
SHOW GRANTS FOR 'app_user'@'192.168.1.%';
1.3 密码策略与安全
-- 查看密码策略(MySQL 8.0+)
SHOW VARIABLES LIKE 'validate_password%';
-- 修改密码策略(测试环境可降低要求)
SET GLOBAL validate_password.policy = LOW; -- LOW/MEDIUM/STRONG
SET GLOBAL validate_password.length = 6;
-- 修改用户密码
ALTER USER 'app_user'@'192.168.1.%' IDENTIFIED BY 'NewPass@2024';
-- 密码过期策略
ALTER USER 'app_user'@'192.168.1.%' PASSWORD EXPIRE INTERVAL 90 DAY;
-- 锁定/解锁用户
ALTER USER 'app_user'@'192.168.1.%' ACCOUNT LOCK;
ALTER USER 'app_user'@'192.168.1.%' ACCOUNT UNLOCK;
1.4 权限分配实战
-- 运维常用权限分配方案
-- 1. 应用用户(只给增删改查)
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'192.168.1.%';
-- 2. 只读用户(报表、分析用)
GRANT SELECT ON app_db.* TO 'readonly'@'%';
-- 3. DBA管理员(全部权限)
GRANT ALL PRIVILEGES ON *.* TO 'dba_admin'@'localhost' WITH GRANT OPTION;
-- 4. 备份用户(需要锁表和复制权限)
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER, RELOAD, REPLICATION CLIENT, REPLICATION SLAVE
ON *.* TO 'backup_user'@'127.0.0.1';
-- 5. 监控用户(只读状态信息)
GRANT PROCESS, REPLICATION CLIENT ON *.* TO 'monitor'@'192.168.1.%';
-- 刷新权限
FLUSH PRIVILEGES;
-- 撤销权限
REVOKE INSERT, UPDATE, DELETE ON app_db.* FROM 'readonly'@'%';
-- 删除用户
DROP USER 'temp_user'@'%';
互动提问:
'user'@'%'和'user'@'192.168.1.%'哪个更安全?答案:
'user'@'192.168.1.%'更安全,因为它限制了只能从 192.168.1.x 网段登录。'%'允许从任何IP登录,如果密码泄露,攻击者可以从外网直接连进来。
二、备份与恢复——数据的"后悔药"
2.1 问题引入
场景:生产数据库被误删了一张核心表,或者磁盘损坏……没有备份就等于"数据火葬场"。
解决思路:定期自动备份 + 定期验证备份可恢复性。备份不验证 = 没备份。
2.2 mysqldump逻辑备份
# ===== mysqldump常用命令 =====
# 备份单个数据库
mysqldump -u root -p --single-transaction --routines --triggers \
school_db > /backup/school_db_$(date +%Y%m%d).sql
# 备份单张表
mysqldump -u root -p --single-transaction school_db students \
> /backup/students_$(date +%Y%m%d).sql
# 备份所有数据库
mysqldump -u root -p --single-transaction --all-databases \
> /backup/full_$(date +%Y%m%d).sql
# 备份并压缩(生产推荐)
mysqldump -u root -p --single-transaction school_db | gzip \
> /backup/school_db_$(date +%Y%m%d).sql.gz
# 只备份表结构(不含数据)
mysqldump -u root -p --no-data school_db > /backup/schema_only.sql
# 只备份数据(不含建表语句)
mysqldump -u root -p --no-create-info school_db students > /backup/data_only.sql
关键参数说明:
| 参数 | 作用 |
|---|---|
--single-transaction | InnoDB一致性备份(不锁表) |
--master-data=2 | 在备份中记录binlog位置(注释形式,主从用) |
--routines | 导出存储过程和函数 |
--triggers | 导出触发器 |
--events | 导出事件调度器 |
--set-gtid-purged=OFF | 不包含GTID信息(避免恢复冲突) |
2.3 恢复数据
# 恢复备份
mysql -u root -p school_db < /backup/school_db_20240115.sql
# 恢复压缩的备份
gunzip < /backup/school_db_20240115.sql.gz | mysql -u root -p school_db
# 恢复后验证
mysql -u root -p -e "USE school_db; SELECT COUNT(*) FROM students;"
2.4 xtrabackup物理备份
# 安装xtrabackup(Percona出品,支持热备份)
yum install -y percona-xtrabackup-80
# 全量备份
xtrabackup --backup --user=root --password=xxx \
--target-dir=/backup/full_20240115
# 增量备份(基于全量)
xtrabackup --backup --user=root --password=xxx \
--target-dir=/backup/incr_20240116 \
--incremental-basedir=/backup/full_20240115
# 恢复步骤
# 1. 准备备份(apply redo log)
xtrabackup --prepare --target-dir=/backup/full_20240115
# 2. 停止MySQL
systemctl stop mysqld
# 3. 清空数据目录
rm -rf /var/lib/mysql/*
# 4. 恢复
xtrabackup --copy-back --target-dir=/backup/full_20240115
# 5. 修复权限
chown -R mysql:mysql /var/lib/mysql
# 6. 启动
systemctl start mysqld
类比:
mysqldump= 把仓库里每件货物拍照登记(逻辑记录),恢复时重新录入xtrabackup= 把整个仓库复制一份(物理复制),恢复时直接搬回来- mysqldump适合小库(<10G),xtrabackup适合大库
2.5 定时备份脚本
#!/bin/bash
# /opt/scripts/mysql_backup.sh - MySQL自动备份脚本
# ===== 配置区 =====
DB_HOST="localhost"
DB_USER="backup_user"
DB_PASS="Backup@2024"
BACKUP_DIR="/data/backup/mysql"
KEEP_DAYS=7
LOG_FILE="/var/log/mysql_backup.log"
DATE=$(date +%Y%m%d_%H%M%S)
# ===== 创建目录 =====
mkdir -p ${BACKUP_DIR}
# ===== 记录日志函数 =====
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a ${LOG_FILE}
}
# ===== 执行备份 =====
log "开始备份..."
# 获取所有数据库(排除系统库)
DATABASES=$(mysql -h${DB_HOST} -u${DB_USER} -p${DB_PASS} -N -e \
"SHOW DATABASES;" | grep -Ev 'information_schema|performance_schema|sys|mysql')
TOTAL=0
FAILED=0
for DB in ${DATABASES}; do
BACKUP_FILE="${BACKUP_DIR}/${DB}_${DATE}.sql.gz"
if mysqldump -h${DB_HOST} -u${DB_USER} -p${DB_PASS} \
--single-transaction --routines --triggers \
--set-gtid-purged=OFF ${DB} 2>/dev/null | gzip > ${BACKUP_FILE}; then
SIZE=$(du -sh ${BACKUP_FILE} | awk '{print $1}')
log " [成功] ${DB} -> ${BACKUP_FILE} (${SIZE})"
TOTAL=$((TOTAL + 1))
else
log " [失败] ${DB} 备份失败!"
FAILED=$((FAILED + 1))
fi
done
# ===== 清理过期备份 =====
DELETED=$(find ${BACKUP_DIR} -name "*.sql.gz" -mtime +${KEEP_DAYS} -delete -print | wc -l)
log "清理了 ${DELETED} 个超过 ${KEEP_DAYS} 天的旧备份"
# ===== 汇总 =====
log "备份完成: 成功${TOTAL}个, 失败${FAILED}个"
# ===== 添加定时任务(每天凌晨2点执行) =====
# crontab -e
# 0 2 * * * /opt/scripts/mysql_backup.sh >> /var/log/mysql_backup.log 2>&1
互动提问:备份脚本中为什么要用
--single-transaction而不是--lock-all-tables?答案:
--single-transaction利用InnoDB的MVCC机制实现一致性快照备份,不需要锁表,备份期间应用还能正常读写。--lock-all-tables会锁住所有表,备份期间应用无法写入,影响业务。
三、主从复制——"师傅带徒弟"
3.1 问题引入
场景:单台MySQL承担所有读写请求,高峰期响应变慢。而且如果这台机器坏了,整个业务就停了。
解决思路:搭建主从复制,主库负责写入,从库负责读取,实现读写分离和故障冗余。
3.2 主从复制原理
┌──────────┐
│ 主库 │
│ (Master) │
│ │
写入 ──→ │ binlog │ ──── binlog记录所有变更操作
└────┬─────┘
│ 网络传输
▼
┌──────────┐
│ 从库 │
│ (Slave) │
│ │
读取 ←── │ relay log │ ←── IO线程拉取binlog写入relay log
│ │ SQL线程读取relay log并重放执行
└──────────┘
三个关键线程:
- 主库Binlog Dump线程:发送binlog事件给从库
- 从库IO线程:从主库拉取binlog,写入relay log
- 从库SQL线程:读取relay log,重放SQL语句
3.3 配置主从复制
主库配置(Master)
# 主库my.cnf
[mysqld]
server-id = 1 # 唯一ID
log-bin = mysql-bin # 开启binlog
binlog_format = ROW # 行格式(最安全)
sync_binlog = 1 # 每次事务同步
innodb_flush_log_at_trx_commit = 1
-- 创建复制专用账号
CREATE USER 'repl'@'192.168.1.%' IDENTIFIED BY 'ReplPass@2024';
GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'repl'@'192.168.1.%';
FLUSH PRIVILEGES;
-- 查看主库binlog位置
SHOW MASTER STATUS;
-- 记录 File 和 Position 的值,从库配置要用
-- 例如:File=mysql-bin.000003, Position=856
从库配置(Slave)
# 从库my.cnf
[mysqld]
server-id = 2 # 唯一ID(与主库不同!)
relay-log = relay-bin
read_only = ON # 从库设为只读
log_slave_updates = ON # 从库的变更也记录binlog(级联复制用)
-- 配置主库信息(传统方式)
CHANGE MASTER TO
MASTER_HOST='192.168.1.10',
MASTER_USER='repl',
MASTER_PASSWORD='ReplPass@2024',
MASTER_LOG_FILE='mysql-bin.000003',
MASTER_LOG_POS=856;
-- 启动复制
START SLAVE;
-- 检查复制状态
SHOW SLAVE STATUS\G
-- 重点关注:
-- Slave_IO_Running: Yes ← IO线程正常
-- Slave_SQL_Running: Yes ← SQL线程正常
-- Seconds_Behind_Master: 0 ← 延迟秒数(0表示同步)
互动提问:如果
Slave_SQL_Running: No怎么办?答案:通常是主从数据不一致导致的。可以:1)查看
Last_Error找到具体错误;2)跳过这个错误SET GLOBAL sql_slave_skip_counter = 1; START SLAVE;;3)严重时需要重新同步(从主库重新备份恢复到从库)。
3.4 半同步复制
-- 异步复制的风险:主库写完binlog就返回成功,如果此时主库宕机,从库可能还没收到
-- 半同步:主库至少等一个从库确认收到binlog才返回成功
-- 主库安装半同步插件
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 3000; -- 超时3秒回退为异步
-- 从库安装
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
-- 重启从库IO线程使其生效
STOP SLAVE IO_THREAD;
START SLAVE IO_THREAD;
3.5 GTID复制(推荐)
GTID(全局事务标识符)= server_uuid:transaction_number
例如:3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5
# 主从库都需要配置
[mysqld]
gtid_mode = ON
enforce_gtid_consistency = ON
-- GTID方式配置从库(不需要指定binlog文件和位置!)
CHANGE MASTER TO
MASTER_HOST='192.168.1.10',
MASTER_USER='repl',
MASTER_PASSWORD='ReplPass@2024',
MASTER_AUTO_POSITION=1; -- 自动定位GTID
START SLAVE;
SHOW SLAVE STATUS\G
-- 查看:Auto_Position: 1
GTID的优势:
- 不需要手动记录binlog文件和位置
- 故障切换更方便——从库自动找到正确的复制位置
- 可以精确判断主从数据一致性
四、Mycat中间件——读写分离与分库分表
4.1 问题引入
场景:应用代码里要区分"写操作走主库、读操作走从库",代码变得很复杂。而且当单库数据量超过千万行时,查询越来越慢。
解决思路:在应用和数据库之间加一个中间件,让中间件来处理路由。
4.2 读写分离概念
┌────────────┐
应用 ── 写SQL ──→ │ │ ──→ 主库(Master)
│ Mycat │
应用 ── 读SQL ──→ │ (中间件) │ ──→ 从库(Slave)
└────────────┘
Mycat工作原理:
- 拦截应用的SQL语句
- 分析SQL类型(SELECT走从库,INSERT/UPDATE/DELETE走主库)
- 将SQL路由到对应的数据库节点
- 汇总结果返回给应用
类比:Mycat就像公司的前台——来访者(SQL)不需要知道该找哪个部门(哪台数据库),前台帮你分配。
4.3 分库分表概念
分库分表的动机:
单表数据量过大(>1000万行)→ 查询变慢
单库写入瓶颈 → 并发不够
水平分表:按规则将一张表的数据分散到多张表
user_0, user_1, user_2, ... user_9
规则:user_id % 10 = 表编号
垂直分表:将一张宽表的列拆分到多张表
user_base(id, name, age)
user_detail(id, address, bio, avatar)
水平分库:按规则将数据分散到多个数据库
db_0 存 user_id 0-999999
db_1 存 user_id 1000000-1999999
注意:分库分表会增加系统复杂度(跨库JOIN、分布式事务等),优先考虑:
- 优化SQL和索引
- 加缓存(Redis)
- 读写分离
- 最后才考虑分库分表
五、MHA高可用——MySQL的"自动备班系统"
5.1 问题引入
场景:主库突然宕机,运维人员半夜被电话叫醒,手动切换主从、修改应用配置、通知前端切换VIP……耗时30分钟,业务中断30分钟。
解决思路:用MHA(Master High Availability)实现自动故障检测和切换。
5.2 MHA架构与Failover原理
┌──────────────────────────────────────────────────┐
│ MHA Manager │
│ (监控节点,不负责数据) │
│ │
│ 功能: │
│ 1. 监控主库是否存活 │
│ 2. 主库故障时选择最新从库提升为新主库 │
│ 3. 补齐其他从库的数据差异 │
│ 4. VIP漂移到新主库 │
└──────────────┬──────────────┬────────────────────┘
│ │
┌──────▼──────┐ ┌────▼──────────┐
│ 主库 │ │ 从库1(候选) │
│ Master │ │ Slave1 │
│ VIP在此 │ │ 最可能成为 │
│ │ │ 新主库 │
└─────────────┘ └───────────────┘
Failover流程:
- MHA Manager检测到主库不可达(多次重试确认)
- 从所有从库中选择binlog最新的作为候选主库
- 从候选主库和其他从库中收集差异binlog
- 将差异binlog应用到所有从库,确保数据一致
- 将候选从库提升为新主库
- VIP漂移到新主库
- 其他从库指向新主库
- 发送告警通知运维人员
5.3 VIP漂移
# VIP漂移脚本示例(MHA自动调用)
#!/bin/bash
# /opt/scripts/master_ip_failover
VIP="192.168.1.100"
INTERFACE="eth0"
case $1 in
start)
# 在新主库上绑定VIP
/sbin/ip addr add ${VIP}/24 dev ${INTERFACE}
/sbin/arping -I ${INTERFACE} -c 3 -s ${VIP} ${VIP}
echo "VIP ${VIP} 已绑定到 ${INTERFACE}"
;;
stop)
# 从旧主库上解绑VIP
/sbin/ip addr del ${VIP}/24 dev ${INTERFACE}
echo "VIP ${VIP} 已从 ${INTERFACE} 解绑"
;;
esac
类比:VIP就像一个"流动的服务台电话"——不管哪个厨师当班,客户打的都是同一个号码(VIP),电话自动转到当前当班厨师那里。
互动提问:MHA和MySQL官方的MGR(组复制)有什么区别?
答案:MHA是基于主从复制的外部工具,需要额外安装,使用Perl编写;MGR是MySQL 5.7.17+内置的多主/单主复制方案,基于Paxos协议,数据一致性更强但资源开销更大。小团队推荐MHA(成熟稳定),大团队可考虑MGR或Orchestrator。
六、Redis——高速缓存之王
6.1 问题引入
场景:网站首页每秒被访问10000次,每次都去MySQL查询热门商品数据,数据库CPU 100%,响应越来越慢。
解决思路:在MySQL前面加一层Redis缓存,热门数据直接从内存读取,速度提升100倍。
6.2 Key-Value概念
Redis vs Memcached对比:
| 特性 | Redis | Memcached |
|-------------|---------------------|---------------------|
| 数据结构 | 5种(String等) | 只有String |
| 持久化 | RDB + AOF | 不支持 |
| 集群 | 原生支持Cluster | 客户端分片 |
| 单线程 | 是(6.0后部分多线程) | 多线程 |
| 值大小 | 最大512MB | 最大1MB |
| 发布订阅 | 支持 | 不支持 |
| 原子操作 | 丰富 | 有限 |
结论:Redis功能远超Memcached,新项目首选Redis。Memcached只在纯简单缓存场景还有优势(多线程读写String更快)。
6.3 Redis安装
# ===== CentOS/RHEL安装Redis =====
# 方式1:通过EPEL仓库(版本可能较旧)
yum install -y epel-release
yum install -y redis
systemctl start redis
systemctl enable redis
# 方式2:从Remi仓库安装最新版
yum install -y https://rpms.remirepo.net/enterprise/remi-release-7.rpm
yum --enablerepo=remi install -y redis
# 方式3:源码编译(推荐,可指定版本)
cd /usr/local/src
wget https://download.redis.io/releases/redis-7.2.4.tar.gz
tar xzf redis-7.2.4.tar.gz
cd redis-7.2.4
make -j$(nproc)
make install PREFIX=/usr/local/redis
# 配置文件
cp redis.conf /etc/redis.conf
# 创建systemd服务并启动
systemctl start redis
systemctl enable redis
# ===== Ubuntu/Debian安装Redis =====
sudo apt update
sudo apt install -y redis-server
sudo systemctl start redis-server
sudo systemctl enable redis-server
Redis关键配置(/etc/redis.conf):
bind 127.0.0.1 192.168.1.10 # 绑定IP(生产不要用0.0.0.0)
port 6379 # 默认端口
requirepass YourStrongPass # 设置密码(生产必须!)
maxmemory 2gb # 最大内存
maxmemory-policy allkeys-lru # 内存满时的淘汰策略
daemonize yes # 后台运行
6.4 五大数据类型
String(字符串)
# 最基本类型,可以存字符串、数字、JSON等
SET name "张三"
GET name # "张三"
# 计数器(原子操作)
SET page_views 0
INCR page_views # 1
INCRBY page_views 10 # 11
# 设置过期时间
SET session_token "abc123" EX 3600 # 1小时后过期
SET cache_key "value" PX 5000 # 5000毫秒后过期
TTL session_token # 查看剩余时间
# 运维场景:存储服务器状态
SET server:web01:cpu "45%"
SET server:web01:mem "62%"
GET server:web01:cpu
List(列表)
# 有序列表,支持两端操作(类似队列/栈)
LPUSH task_queue "备份数据库" # 左端插入
LPUSH task_queue "清理日志"
RPUSH task_queue "发送报告" # 右端插入
LRANGE task_queue 0 -1 # 查看所有元素
# 弹出元素
LPOP task_queue # 左端弹出(先进先出=队列)
RPOP task_queue # 右端弹出(先进后出=栈)
# 运维场景:任务队列、最新日志
LPUSH recent_logs "2024-01-15 ERROR: 磁盘满"
LTRIM recent_logs 0 99 # 只保留最新100条
Hash(哈希)
# 键值对集合(类似Python字典)
HSET server:web01 ip "192.168.1.10"
HSET server:web01 cpu "45%"
HSET server:web01 mem "62%"
HGET server:web01 ip # "192.168.1.10"
HGETALL server:web01 # 获取所有字段
# 批量设置
HMSET server:web02 ip "192.168.1.11" cpu "30%" mem "55%"
# 运维场景:存储服务器信息(比多个String更省内存)
HINCRBY server:web01 request_count 1 # 字段原子递增
Set(集合)
# 无序、不重复的集合
SADD online_servers "web01" "web02" "web03" "db01"
SMEMBERS online_servers # 查看所有成员
SISMEMBER online_servers "web01" # 判断是否存在(1/0)
SCARD online_servers # 成员数量
# 集合运算
SADD group_a "web01" "web02"
SADD group_b "web02" "web03"
SINTER group_a group_b # 交集:web02
SUNION group_a group_b # 并集:web01 web02 web03
SDIFF group_a group_b # 差集:web01
# 运维场景:管理服务器分组
SADD nginx_cluster "10.0.0.1" "10.0.0.2" "10.0.0.3"
Sorted Set / ZSet(有序集合)
# 每个元素关联一个分数,按分数排序
ZADD server_load 45.2 "web01" 78.5 "web02" 12.3 "web03" 92.1 "db01"
ZRANGE server_load 0 -1 WITHSCORES # 按分数升序
ZREVRANGE server_load 0 2 WITHSCORES # 按分数降序(前3名)
ZRANGEBYSCORE server_load 50 100 # 分数范围内的元素
ZRANK server_load "web02" # 排名(从0开始)
# 运维场景:服务器负载排行榜
ZINCRBY server_load 5.0 "web01" # 增加分数
ZREM server_load "web03" # 移除元素
互动提问:什么时候用Hash,什么时候用多个String?
答案:当同一个对象有多个字段时用Hash(如服务器信息有IP、CPU、内存等),比用多个String key更省内存、更方便管理。但如果只需要存一个简单值,用String更直接。
6.5 持久化——数据不能丢
RDB(快照)
# redis.conf 中的RDB配置
# 触发条件:在指定时间内,有指定次数的写操作就触发快照
save 900 1 # 900秒内至少1次写操作
save 300 10 # 300秒内至少10次写操作
save 60 10000 # 60秒内至少10000次写操作
dbfilename dump.rdb # RDB文件名
dir /var/lib/redis # RDB文件目录
# 手动触发RDB快照
redis-cli SAVE # 阻塞式(生产不推荐)
redis-cli BGSAVE # 后台fork子进程(推荐)
AOF(追加日志)
# redis.conf 中的AOF配置
appendonly yes # 开启AOF
appendfilename "appendonly.aof" # AOF文件名
# 刷盘策略
appendfsync everysec # 每秒刷盘(推荐,平衡性能和安全)
# appendfsync always # 每次写都刷盘(最安全但最慢)
# appendfsync no # 由操作系统决定(最快但可能丢数据)
# AOF重写(压缩AOF文件)
auto-aof-rewrite-percentage 100 # AOF文件增长100%时自动重写
auto-aof-rewrite-min-size 64mb # 最小64MB才触发重写
| 对比项 | RDB | AOF |
|---|---|---|
| 原理 | 定时全量快照 | 记录每条写命令 |
| 恢复速度 | 快(直接加载) | 慢(重放命令) |
| 数据安全 | 可能丢失最后一次快照后的数据 | 取决于刷盘策略 |
| 文件大小 | 较小(压缩二进制) | 较大(文本命令) |
| 性能影响 | fork时有短暂阻塞 | 每次写有额外IO |
生产推荐:RDB + AOF 同时开启。重启时优先用AOF恢复(更完整),RDB用于快速冷备。
6.6 Redis主从复制
# 从库配置(redis-slave.conf)
replicaof 192.168.1.10 6379 # 指向主库IP和端口
masterauth MasterPassword # 主库密码(如果主库设了密码)
# 运行时动态配置
redis-cli> REPLICAOF 192.168.1.10 6379 # 设置为从库
redis-cli> REPLICAOF NO ONE # 取消主从(提升为主库)
# 查看复制状态
redis-cli> INFO replication
# master_repl_offset: 12345 ← 复制偏移量
# slave0: ip=192.168.1.11,port=6379,state=online,offset=12345
6.7 Redis哨兵(Sentinel)——自动故障转移
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Sentinel-1 │ │ Sentinel-2 │ │ Sentinel-3 │
│ (监控+投票) │ │ (监控+投票) │ │ (监控+投票) │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Redis │ │ Redis │ │ Redis │
│ Master │ │ Slave1 │ │ Slave2 │
│ (主库) │ │ (从库) │ │ (从库) │
└──────────┘ └──────────┘ └──────────┘
当Master宕机:
1. Sentinel们检测到Master不可达
2. 多数Sentinel投票确认Master故障(quorum)
3. 选举一个Sentinel为Leader
4. Leader选择一个Slave提升为新Master
5. 其他Slave指向新Master
6. 通知客户端新Master地址
# sentinel.conf 配置
port 26379 # Sentinel端口
sentinel monitor mymaster 192.168.1.10 6379 2 # 监控mymaster,至少2票确认故障
sentinel down-after-milliseconds mymaster 5000 # 5秒不可达判定为故障
sentinel failover-timeout mymaster 60000 # failover超时60秒
sentinel parallel-syncs mymaster 1 # 故障切换后,同时同步的从库数
sentinel auth-pass mymaster MasterPassword # 主库密码
# 启动哨兵
redis-sentinel /etc/redis-sentinel.conf
# 或
redis-server /etc/redis-sentinel.conf --sentinel
# 查看哨兵信息
redis-cli -p 26379 SENTINEL masters
redis-cli -p 26379 SENTINEL slaves mymaster
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
互动提问:为什么Sentinel至少要3个节点?
答案:因为Sentinel使用"多数投票"机制判断主库是否故障。2个节点时,如果1个认为故障、1个认为正常,无法达到多数。3个节点时,2票即可达成多数。节点数建议为奇数:3、5、7。
6.8 Redis Cluster集群——分布式缓存
Hash Slot(哈希槽)原理
Redis Cluster 将数据分散到 16384 个哈希槽(Hash Slot)中。
键 → CRC16(key) % 16384 → 槽号 → 对应节点
例如:
"user:1001" → CRC16 → 12345 % 16384 = 12345 → 节点B负责
"user:1002" → CRC16 → 5678 % 16384 = 5678 → 节点A负责
3主3从的典型分配:
节点A(Master): 槽 0-5460
节点B(Master): 槽 5461-10922
节点C(Master): 槽 10923-16383
节点A'(Slave): 复制节点A
节点B'(Slave): 复制节点B
节点C'(Slave): 复制节点C
# 每个节点的redis.conf需要添加
cluster-enabled yes # 开启集群模式
cluster-config-file nodes-6379.conf # 集群配置文件
cluster-node-timeout 5000 # 节点超时时间
# 创建集群(至少3主3从=6个节点)
redis-cli --cluster create \
192.168.1.10:6379 192.168.1.11:6379 192.168.1.12:6379 \
192.168.1.10:6380 192.168.1.11:6380 192.168.1.12:6380 \
--cluster-replicas 1
# 查看集群信息
redis-cli -c -h 192.168.1.10 -p 6379 CLUSTER INFO
redis-cli -c -h 192.168.1.10 -p 6379 CLUSTER NODES
# 集群操作(-c 参数开启集群模式,自动跳转节点)
redis-cli -c -h 192.168.1.10
> SET name "张三" # 自动路由到正确的节点
> GET name # 自动路由
类比:Redis Cluster就像一个快递分拣中心——16384个分拣格(哈希槽),每个包裹(数据)根据地址(key)自动分配到对应格子。如果某个格子满了或者坏了,把包裹重新分到其他格子。
七、综合实战:MySQL+Redis架构设计
7.1 缓存读写策略
# 缓存穿透:查询不存在的数据,每次都打到MySQL
# 解决:缓存空值 + 布隆过滤器
import redis
import json
r = redis.Redis(host='127.0.0.1', port=6379, db=0, password='xxx')
def get_student(student_id):
"""带缓存的学生信息查询"""
cache_key = f"student:{student_id}"
# 1. 先查缓存
cached = r.get(cache_key)
if cached:
if cached == b"NULL": # 缓存了空值,防止穿透
return None
return json.loads(cached)
# 2. 缓存没有,查数据库
import pymysql
conn = pymysql.connect(host='localhost', user='app', password='xxx', db='school_db')
with conn.cursor() as cursor:
cursor.execute("SELECT * FROM students WHERE id=%s", (student_id,))
result = cursor.fetchone()
# 3. 写入缓存
if result:
r.setex(cache_key, 3600, json.dumps(result, default=str)) # 缓存1小时
else:
r.setex(cache_key, 300, "NULL") # 缓存空值5分钟
return result
7.2 缓存常见问题
| 问题 | 描述 | 解决方案 |
|---|---|---|
| 缓存穿透 | 查不存在的数据 | 缓存空值 + 布隆过滤器 |
| 缓存击穿 | 热点key过期瞬间大量请求 | 互斥锁(SETNX)+ 永不过期 |
| 缓存雪崩 | 大量key同时过期 | 过期时间加随机值 |
| 数据不一致 | 缓存和数据库数据不同步 | 先更新DB,再删缓存 |
互动提问:为什么要"先更新数据库,再删缓存"而不是"先更新缓存,再更新数据库"?
答案:并发场景下,"先更新缓存"可能导致脏数据。比如线程A和B同时更新,A先写缓存、B后写缓存,但B先写DB、A后写DB,导致缓存和DB不一致。"先更新DB再删缓存"更安全——即使删缓存失败,下次读取时也会自动刷新。
运维常用命令精选
# ===== MySQL主从常用命令 =====
SHOW MASTER STATUS; # 主库状态
SHOW SLAVE STATUS\G # 从库状态
START SLAVE; # 启动复制
STOP SLAVE; # 停止复制
RESET SLAVE ALL; # 重置从库配置
SHOW BINARY LOGS; # 查看binlog列表
SHOW BINLOG EVENTS IN 'mysql-bin.000003'; # 查看binlog内容
# ===== mysqlbinlog工具 =====
mysqlbinlog --start-datetime="2024-01-15 10:00:00" \
--stop-datetime="2024-01-15 12:00:00" \
/var/lib/mysql/mysql-bin.000003 > /tmp/binlog_events.sql
# ===== Redis运维命令 =====
redis-cli INFO server # 服务器信息
redis-cli INFO memory # 内存信息
redis-cli INFO clients # 客户端信息
redis-cli INFO replication # 复制状态
redis-cli INFO keyspace # 键空间信息
redis-cli DBSIZE # 键数量
redis-cli SLOWLOG GET 10 # 慢查询日志
redis-cli CLIENT LIST # 客户端列表
redis-cli MONITOR # 实时监控所有命令
redis-cli --scan --pattern "user:*" # 扫描匹配的key
redis-cli MEMORY USAGE "key_name" # 查看key内存占用
redis-cli CONFIG GET maxmemory # 查看配置
redis-cli CONFIG SET maxmemory "2gb" # 动态修改配置
redis-cli BGSAVE # 手动RDB快照
redis-cli BGREWRITEAOF # 手动AOF重写
# ===== Redis集群命令 =====
redis-cli -c CLUSTER INFO # 集群信息
redis-cli -c CLUSTER NODES # 节点列表
redis-cli -c CLUSTER SLOTS # 槽分配
redis-cli --cluster check 192.168.1.10:6379 # 集群健康检查
redis-cli --cluster rebalance 192.168.1.10:6379 # 重新均衡槽
趣味命令
# Redis也能当"消息队列"——发布/订阅
# 终端1(订阅者):
redis-cli SUBSCRIBE news_channel
# 终端2(发布者):
redis-cli PUBLISH news_channel "服务器已更新完毕!"
# 用Redis实现简易限流器(滑动窗口)
# 限制每个IP每分钟最多60次请求
redis-cli EVAL "
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, window)
end
return current <= limit
" 1 "rate:192.168.1.100" 60 60
# MySQL实时查看正在执行的SQL(类似top)
watch -n 1 "mysqladmin -u root -p processlist 2>/dev/null"
# 用MySQL查询当前时间、版本、运行天数
mysql -u root -p -e "
SELECT
NOW() AS current_time,
VERSION() AS mysql_version,
FLOOR(VARIABLE_VALUE/86400) AS uptime_days
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Uptime';
"
# 查看Redis内存使用的"人类可读"报告
redis-cli INFO memory | while IFS=: read key value; do
if [[ "$key" =~ (used_memory|mem_fragmentation|maxmemory) ]] && [[ "$value" =~ ^[0-9]+$ ]]; then
echo "$key: $(echo "scale=2; $value/1024/1024" | bc) MB"
else
echo "$key: $value"
fi
done