先问自己一个问题有没有遇到过这样的场景——线上接口突然变慢数据库CPU飙高用户反馈页面打不开重启服务后过几分钟又被打爆或者明明是同一个数据每次请求都要去数据库查一遍白白浪费了连接资源和磁盘IO上面说的这类问题绝大多数情况下都可以通过引入缓存来解决。而在缓存选型中Redis常年稳坐第一把交椅。它不止能当缓存还能做分布式锁、限流器、消息订阅分发、排行榜、附近的人、布隆过滤器……可以说Redis是国内后端开发、运维、架构师岗位面试和实战中的必考项目。网上Redis的教程到处都是但很多只讲命令不讲解原理教安装不教排错看完之后装是装上了一上生产环境立刻踩坑。这篇文章我不打算罗列API文档而是按照“装得上、认得清、用得好、能排错”这条线把整个流程完整走一遍。内容包括Linux环境下的源码编译安装、配置文件核心参数的逐项解释、客户端连接与基本操作、五种数据结构各自的操作套路以及一套实战级缓存读写逻辑的完整代码示例。文章后半部分还会集中放上让人头大的Redis常见故障排查方法。整个流程我已经在很多环境中验证过踩过的坑都写在了对应的小节里照着走能省下不少折腾时间。1. 安装前先搞清楚为什么是Redis以及我们到底要解决什么问题1.1 Redis是缓存但又不只是缓存Redis的全称是Remote Dictionary Server直译过来就是“远程字典服务”。它本质上是一个基于内存的键值对存储系统所以读写速度极快。官方给出过数字单机吞吐可以达到10万 QPS。这里要说明的是这个数字依赖机器配置、网络环境、数据量大小但即使打个折扣在绝大多数业务场景下每秒几万的读写处理能力也远超传统关系型数据库。在动手安装之前首先要明确一下使用场景。我总结下来Redis解决的核心痛点主要有三个第一热点数据的快速访问。比如商品详情、用户基本信息、配置项、验证码这些数据被高频读取但变化不频繁。如果每次都查数据库数据库的压力会非常大。把这些数据放到内存型的Redis里读取耗时大幅降低很多场景下延迟能下降到过毫秒级别这在数据库层面很难实现。第二高并发场景下的流量削峰。比如秒杀、抢购、投票、接口限流这种瞬间大流量会把数据库连接池直接打满导致整个业务系统雪崩。Redis可以用原子操作做库存扣减、用滑动窗口做频率限制把大部分请求挡在数据库之前。第三跨服务器之间的数据共享。在分布式架构中多台应用服务器的本地内存缓存是无法互相共享的——A服务器写入的缓存数据B服务器根本看不到。Redis将缓存内容集中存放所有应用服务器都能读取到同一份数据而且它的读写性能远高于从数据库读取是解决分布式会话共享、分布式配置管理这类问题的标准方案。1.2 不同安装方式的取舍不是所有环境都适合源码编译Redis的安装方式有好几种可以在Linux发行版的软件源里直接安装、用Docker容器运行也可以下载源码自己编译。不同维护方式对应不同场景软件源安装apt install redis-server / yum install redis的优势是方便省事适合本机学习、快速测试缺点是软件源里的版本往往比较旧比如某些长期维护版系统上默认源里还是Redis 5.x无法使用较新版本的特性同时也不方便定制编译参数。Docker方式的优势是环境隔离好、部署快、回滚容易适合容器化部署和本地体验缺点是数据持久化和网络配置需要额外注意很多初学者在Docker里把数据一重启就丢光。源码编译安装是最麻烦的但也是生产环境推荐使用的方式。只有通过源码编译才能控制安装路径、选择编译参数、按需开启特性发现问题时还能手动打上补丁重新编译。很多企业运维要求固定版本、固定目录、便于统一管理和统一升级源码编译能最大程度确保环境一致性。我接下来的教程以源码编译安装为主因为这条路径最能还原一个完整Redis从无到有的过程把每一步都亲自控制在自己手里。如果你只是想快速体验看文章中的命令后直接改成软件源方式也不难。2. 环境准备与源码编译安装2.1 前置依赖检查Redis使用C语言编写所以编译的前提是系统中有gcc编译器和make构建工具。还需要重点关注一点如果你要处理较新版本的Redis源码编译器版本不能太老否则会出现莫名其妙的语法错误。编译过程中如果报错信息涉及“undefined reference”“隐式函数声明”这些字眼多半回事出在编译器或库文件上。可以用下面的命令来检查环境# 查看系统版本 cat /etc/os-release # 查看编译工具版本 gcc --version make --version # 如果没有安装Ubuntu/Debian系执行 sudo apt update sudo apt install -y build-essential wget tar # CentOS/RHEL系执行 sudo yum install -y gcc make wget tar值得注意的一点是在较新版本的Ubuntu中默认安装的gcc很可能是更高版本。直接用官方源码编译大多数情况下没有问题如果在非常老的系统上如CentOS 6这类先单独升级一下编译器更稳妥。2.2 源码下载与编译安装流程编译Redis源码不需要管理员权限但如果你要安装到系统级目录如/usr/local/redis那需要写入相应目录的权限。我个人习惯使用专门的用户来运行Redis服务而不是直接使用root这是一种值得养成的安全习惯。# 下载Redis稳定版源码这里用7.0系列举例请以官方最新稳定版为准 # 所有版本都可在Redis官方下载页中找到对应的tar.gz链接 wget https://download.redis.io/releases/redis-7.0.14.tar.gz # 解压并进入目录 tar xzf redis-7.0.14.tar.gz cd redis-7.0.14 # 编译与安装 make sudo make installmake install默认会把redis-server、redis-cli、redis-sentinel、redis-check-aof、redis-check-rdb、redis-benchmark这几个可执行文件安装到/usr/local/bin目录下。如果执行完毕后在命令行直接输入redis-server能启动就可以跳过手动配置PATH环境变量的步骤。需要说明的是make命令的动作是编译所有目标文件但这一步并不会自动创建配置文件和数据目录。这恰恰是很多新手最容易遗漏的地方——服务装好了配置文件找不到日志目录更是不存在服务一启动就报警。2.3 目录规划与基础目录创建生产环境要有清晰的目录规划。这里分享一套经过实践检验的目录布局# 统一规划Redis相关目录 mkdir -p /opt/redis/{bin,conf,data,logs,run} # 拷贝可执行文件 cp /usr/local/bin/redis-server /usr/local/bin/redis-cli /usr/local/bin/redis-check-aof /usr/local/bin/redis-check-rdb /opt/redis/bin/ # 拷贝默认配置模板到conf目录 cp redis.conf /opt/redis/conf/redis.conf # 创建Redis用户许多生产环境要求专用运行账户 sudo useradd -r -s /sbin/nologin redis sudo chown -R redis:redis /opt/redis目录规划其实解决的是故障排查速度问题。文件到处乱放出了问题找日志老半天集中管理后一切都有迹可循。data目录用于存放RDB和AOF持久化文件logs目录用于存放运行日志run目录用于存放PID文件。如果在没有权限创建系统级目录的环境下也可以直接在当前用户目录下创建一系列类似目录建议结构保持一致。2.4 启动前的最简验证在修改任何配置之前建议先用默认配置做一次快速启动确认二进制文件没有问题。# 前台启动配好PATH后可直接这样执行 redis-server # 看到如下输出说明启动成功 # * Ready to accept connections tcp此时打开另一个终端窗口执行redis-cli ping # 控制台输出 PONG 说明客户端与服务端通信正常验证完毕后Ctrl C停掉前台进程。这里特别强调一点默认配置启动Redis是没有任何密码、任何访问限制的。如果这台服务器的端口对外开放外部扫描器很快就能发现并利用这个漏洞轻则数据被清空重则被植入恶意代码。所以默认配置只适合本机秒级验证任何需要暴露端口运行的环境必须按下一章节的配置说明加固。3. 核心配置项解析一行一行说清楚配置文件在干什么Redis源码包里自带的redis.conf包含上百项配置好在提供了大量注释。最初上手时不必全部理解但有几个配置直接关系到稳定性与数据安全需要先把它们搞明白。3.1 守护进程与日志设置daemonize控制Redis是否以守护进程方式在后台运行。开发测试时使用no比较方便终端里直接看全部日志生产环境改成yes后Redis会在后台跑将PID写入进程文件。# 以守护进程方式运行 daemonize yes # 设置PID文件路径默认就在编译目录中建议修改到统一规划目录 pidfile /opt/redis/run/redis.pid # 日志级别可选 debug/verbose/notice/warning loglevel notice # 日志文件路径留空则输出到控制台 logfile /opt/redis/logs/redis-server.log这个logfile配置非常推荐尽早设置。如果你不设置日志文件以守护进程方式运行时日志直接丢弃后期排查问题的难度会加大。我看到很多刚入门的人在遇到“Redis启动成功了但连不上”这种问题时第一反应是把配置恢复原样而不是去看日志里写了什么——养成看日志的习惯比任何调试技巧都重要。3.2 网络与安全配置安全配置是不能跳过的部分。下面这几项基本是每次生产部署必改# 监听地址默认是127.0.0.1仅允许本机访问。 # 如果要允许局域网/指定IP访问可以改成0.0.0.0所有网卡 # 或者指定具体网卡IP。注意对外开放时要配合密码。 bind 127.0.0.1 -::1 # 保护模式Redis 3.2后默认开启。 # 在没有设置密码且bind了非本机地址时Redis会拒绝外部访问。 # 生产环境不要关闭尤其针对敏感网络。 protected-mode yes # 访问密码建议固定长度强密码 requirepass 你的强密码 # 端口号默认6379如果与业务端口冲突可以更换 port 6379认真建议只要条件允许不要把Redis裸奔在可被外部访问的环境中。即使使用云服务提供的内网通信也一定要开启密码。更进一步的方案是使用独立的子网、安全组策略只允许应用服务器与Redis实例通信。值得提醒的是requirepass一旦设置redis-cli连接后所有命令都必须先通过认证才能执行。使用redis-cli时需要带-a参数但这样会在命令行中暴露密码更优雅的方式是通过环境变量REDISCLI_AUTH来传入密码或者连接后手动执行AUTH命令。3.3 内存上限与数据淘汰策略Redis作为内存型数据库最危险的事就是内存失控。如果不设置maxmemoryRedis在写入大量数据后会持续占满服务器物理内存最终触发系统的OOM Killer机制进程被杀掉所有数据丢失。所以内存上限必须主动配置。# 最大内存上线示例300MB生产环境按机器物理内存和业务需求分配 maxmemory 300mb # 内存达到上限后的淘汰策略 # noeviction内存满后不再写入直接返回错误适合不可丢失的关键数据 # allkeys-lru从所有键中按LRU算法淘汰最久未使用的键适合普通缓存业务 # volatile-lru仅从设置了过期时间的键中淘汰最久未使用的键 # allkeys-random、volatile-random、volatile-ttl 等方式也各有应用场景 maxmemory-policy allkeys-lru内存淘汰策略是比较核心的知识点 这里展开讲一下如果业务中Redis是完全当成缓存使用那么allkeys-lru基本最匹配需求——数据本身可以从数据库恢复内存不够了淘汰最久没访问的键即可如果Redis内保存了一些通过异步方式写入、丢了会很麻烦的数据那noeviction更安全宁可报错也不要丢数据而多种过期类型混合的业务可以考虑volatile-lru。3.4 数据持久化策略很多初学者以为Redis重启后数据就消失了其实不然。Redis提供了两种持久化机制RDBRedis DataBase按指定时间间隔将内存数据生成快照写入磁盘。优点是文件紧凑、恢复速度快缺点是如果在两次快照之间发生宕机这段时间内更新的数据会丢失。AOFAppend Only File将每一个写命令以日志形式追加记录下来服务重启时通过重放命令来恢复数据。AOF的实时性更高丢失数据窗口很小但文件体积较大、恢复速度比RDB慢。生产环境常用方案是两者同时开启。RDB负责快速恢复AOF负责降低数据丢失量。# RDB持久化机制。下面的配置含义是 # 900秒内如果至少有1个键变化则执行一次bgsave快照。 # 300秒内如果至少有10个键变化则执行一次快照。 # 60秒内如果至少有10000个键变化则执行一次快照。 save 900 1 save 300 10 save 60 10000 # RDB文件名会写入到dir指定的目录中 dbfilename dump.rdb # 数据文件存放目录 dir /opt/redis/data # AOF持久化开关 appendonly yes # AOF文件名 appendfilename appendonly.aof # 每写入一条命令就刷盘一次最安全但性能消耗最大 # 每秒刷盘一次性能与安全平衡默认值 appendfsync everysec关于appendfsync参数实际上有三个可取值always每个写命令都同步刷入磁盘最安全但性能最差基本很少用于高吞吐场景everysec每秒执行一次刷盘最多丢失最近不超过一秒的写命令这是一个在性能与安全之间很不错的折衷选法no由操作系统决定何时刷盘性能好但数据丢失风险较大。生产环境默认建议保持everysec。3.5 其他常用调整# 数据库数量默认16个0-15 databases 16 # TCP通信的backlog队列长度。高并发场景建议增大。 tcp-backlog 511 # 客户端空闲超时关闭时间单位秒。0表示永不关闭。 timeout 0 # 长时间运行的慢查询日志阈值单位微秒。 slowlog-log-slower-than 10000 # 慢查询日志长度 slowlog-max-len 128配置修改完毕后用下面命令检查语法并重启服务这是个好习惯# 用指定配置启动 /opt/redis/bin/redis-server /opt/redis/conf/redis.conf # 查看进程是否正常 ps -ef | grep redis-server # 检查端口监听状态 ss -tlnp | grep 6379 # 日志文件确认启动无误 tail -20 /opt/redis/logs/redis-server.log配置语法错误时Redis会在启动时直接输出日志所以如果你执行完命令没有看到“Ready to accept connections”立刻去翻日志文件——原因几乎都写在报错消息里。4. 客户端连接验证与基础数据结构实操4.1 命令行客户端连接方式Redis自带的命令行客户端是一个很强大的诊断工具# 连接本机默认端口 redis-cli # 指定端口和主机 redis-cli -h 192.168.1.10 -p 6379 # 使用密码认证适合本地测试使用 redis-cli -h 127.0.0.1 -p 6379 -a 你的密码 # 避免密码出现在终端历史记录中使用环境变量方式 REDISCLI_AUTH你的密码 redis-cli # 连接后切换数据库默认16个编号0-15 127.0.0.1:6379 SELECT 0既然是入门之外的核心内容顺便介绍一下登录后可以用INFO命令查看服务运行状态包括内存、连接数、命中率、角色、持久化状态等很多信息用CLIENT LIST查看连接者用MONITOR实时监听所有收到的命令生产环境慎用有性能消耗。4.2 字符串String类型实操字符串是最基础的键值结构几乎所有的缓存业务都能用它表达。一个键对应一个值值不仅可以是字符串还能存数字并执行原子自增。常用命令# 设置键值 SET user:1:name 张三 # 带过期时间设置单位秒 SET user:1:token token-abc EX 7200 # 访问并更新为新的值后返回旧值 GETSET user:1:name 李四 # 批量设置与批量获取 MSET k1 v1 k2 v2 k3 v3 MGET k1 k2 k3 # 数字操作 SET counter 100 INCR counter # 101 INCRBY counter 50 # 151 DECR counter # 150INCR这类原子自增操作非常重要——它可以用于生成递增ID、计数统计、防止并发环境下数值覆盖等问题。哪怕多个客户端同时执行INCRRedis的命令执行是单线程的每个命令都不会被打断因此不会出现“读旧值-加1-写回”流程里的竞态条件。这是Redis解决并发问题的基础能力。一个超典型应用就是验证码存储# 生成6位数字验证码5分钟后过期 SET phone:13800000000:code 483920 EX 300 # 校验时取出对比 GET phone:13800000000:code # 校验成功后立即删除防止暴力重试 DEL phone:13800000000:code4.3 哈希Hash类型实操哈希类型适合存储结构化对象。一个关系型数据库的行可以映射成一个哈希结构字段对应列值对应字段值。整体操作比使用字符串拼接更容易维护。常用命令# 存储用户信息 HSET user:1001 name 王五 age 28 city 上海 # 获取单个字段 HGET user:1001 name # 获取全部字段和值 HGETALL user:1001 # 修改某个字段 HSET user:1001 age 29 # 批量获取 HMGET user:1001 name age # 自增哈希中的数值字段 HINCRBY user:1001 score 100 # 字段不存在时才设置用于防止覆盖 HSETNX user:1001 email wangwuexample.com用哈希做实体的缓存时需要特别考虑一个点如果需要设置整个对象的过期时间Redis对哈希本身直接设置过期时间是支持的但对内部字段单独设置则不行。所以缓存实体类数据时如果只是把对象以哈希形式存入无法做到针对某个字段精准过期需要设计成“主键整体过期”的模式。4.4 列表List类型实操列表本质上是一个双向链表可以从两端压入和弹出元素特别适合时间线、消息队列、最新消息列表等场景。常用命令# 右边推入元素 RPUSH news:list 新闻1 新闻2 新闻3 # 左边推入元素 LPUSH news:list 置顶新闻 # 左边弹出元素 LPOP news:list # 右边弹出元素 RPOP news:list # 获取指定范围的元素 LRANGE news:list 0 -1 # 只保留前N个元素用于裁剪列表长度 LTRIM news:list 0 99用列表设计一个“最新消息列表”非常方便每次有消息就LPUSH然后LTRIM只保留前100条读取时直接LRANGE key 0 -1。这样不管消息量多大列表中被存放的数据始终有限内存也得到控制。还有一个值得一提的命令是BRPOP和BLPOP它们是阻塞式弹出如果列表为空则会一直阻塞等待直到有元素进来或超时。这可以用于实现一个简化的任务队列生产者往列表里LPUSH任务消费者使用BRPOP等待取任务。这个模式下即使消费者数量很多也不会出现多个消费者消费同一条任务的情况天然起到负载均衡的作用。4.5 集合Set与有序集合ZSet类型实操集合特点是无序、自动去重。更高效的是有序集合每个元素都关联一个分值Redis按分值排序。它们的应用场景差别很大。集合常用命令# 向集合中添加元素 SADD user:1:tags php mysql redis # 查看集合中所有元素 SMEMBERS user:1:tags # 判断某个元素是否在集合中 SISMEMBER user:1:tags redis # 求两个集合的交集与并集 SINTER key1 key2 SUNION key1 key2 # 从集合中随机弹出多个元素可用于抽奖、随机推荐 SPOP key 1去重的场景非常典型用户当天是否已签到、商品是否已经点赞、某个IP是否已经统计过都可以用SADD做操作后用SISMEMBER做判断。有序集合的场景就更丰富# 添加成员并关联分数 ZADD leaderboard 100 player1 ZADD leaderboard 95 player2 ZADD leaderboard 120 player3 # 分数从低到高排序获取前10名 ZRANGE leaderboard 0 9 WITHSCORES # 分数从高到低排序获取前10名排行榜场景 ZREVRANGE leaderboard 0 9 WITHSCORES # 获取某个成员的当前排名 ZRANK leaderboard player1 ZREVRANK leaderboard player1 # 给某个成员增加分数 ZINCRBY leaderboard 10 player1排行榜是最经典的Redis应用场景之一而且还可以和过期时间配合周榜、月榜都很容易实现。另外有序集合还能做基于分数范围的查询比如“找出分数在90到100之间的所有玩家”或者基于时间戳作为分数来实现超时队列很多延时任务的简易版都用这个思路。4.6 键的通用操作与过期时间管理# 设置键的过期时间 EXPIRE order:1001 3600 # 查看还有多久过期返回值单位秒-1表示没设过期时间-2表示键不存在 TTL order:1001 # 删除键 DEL order:1001 # 判断键是否存在 EXISTS order:1001 # 查找匹配的键生产环境慎用KEYS模式可用SCAN替代 SCAN 0 MATCH user:* COUNT 100 # 键的有效期续期 PEXPIRE order:1001 5000 # 单位毫秒这里要重点提醒一件事KEYS命令在生产环境要极其小心。因为KEYS *会遍历全部键空间如果数据量非常大会阻塞Redis服务一段时间。Redis是单线程处理命令一旦被阻塞所有业务读写都会排队会造成极其明显的服务卡顿。正确的方式是使用SCAN命令做游标遍历每次只返回一部分键分多次完成匹配。5. 结合项目场景的实战从命令到缓存业务的完整落地5.1 需求背景与缓存架构设计光会敲命令没有意义得放到真实应用里检验。这里用一个常见的Web项目“模拟项目X”作为演示它包含用户登录、商品列表、商品详情、排行榜几个模块。初始状态下的业务流程是接口直接查询数据库高峰期到来之后数据库连接池被打满接口延迟从50毫秒飙升到2000毫秒以上部分请求直接超时。改造思路是引入Redis做缓存层用户登录成功后将用户基本信息写入Redis设置过期时间30分钟后续请求只需要通过Redis判断登录态不再反复查询数据库。商品详情页的数据相对稳定首次从数据库读出后写入缓存后续请求大约几十秒内都直接读缓存。排行榜数据使用有序集合存储每次用户行为触发分数变更时同步更新分数排行榜接口直接读取Redis中排名结果不再遍历数据库完成一大堆聚合统计。根据具体业务去衡量热点程度和允许的脏读窗口这就是缓存的架构层设计。一个通用的判断准则是只有“读多写少”的数据才适合做缓存。数据每分钟变化几次、却每秒被读几千次这样的数据做好缓存能获得巨大收益反过来如果数据高频更新又要求极高的实时性那还是别费劲往Redis里放了因为每次写入都需要更新缓存收益不大、一致性问题倒是很多。5.2 通用缓存读写逻辑的写法一个标准的读缓存流程被总结成“Cache Aside Pattern”也就是先读缓存、缓存未命中再读数据库、读库后回写缓存、数据更新时先更新数据库再删缓存。用文字描述可能干燥直接看代码反而更清楚。这里用常见后端语言PHP片段表达核心逻辑?php // Redis连接参数需要根据实际环境配置 $redis new Redis(); $redis-connect(127.0.0.1, 6379); $redis-auth(你的密码); // 商品详情查询缓存逻辑 function getProductDetail($productId) { global $redis; $cacheKey product:detail: . $productId; // 第一步先尝试从缓存取数据 $cached $redis-get($cacheKey); if ($cached ! false) { return json_decode($cached, true); } // 第二步缓存未命中查询数据库这一步骤由PDO实现 // 这里是示意SQL // SELECT * FROM products WHERE id ?; $query SELECT * FROM products WHERE id . intval($productId); $result queryDatabase($query); if (empty($result)) { return null; } // 第三步将数据库结果写入Redis设置过期时间 $redis-set($cacheKey, json_encode($result), [EX 600]); return $result; } ?流程本身不算复杂但有一些设计上的细节值得琢磨缓存中保存的数据要不要设置过期时间答案是默认必须设置。缓存设置过期时间一方面可以防止长期占有内存另一方面也是一种“最终一致性”兜底手段——即使因为某些原因缓存和数据库数据不一致最迟在缓存过期后就会自动恢复一致不设置过期时间的缓存一旦因为程序Bug写入脏数据就永远修不回来只能手动清理是很棘手的问题。第二个值得注意的地方是缓存空结果。什么是空结果如果查询的商品ID在数据库里根本不存在那么第一次查询数据库返回null如果不去缓存空结果那这个不存在商品的ID每次请求都会穿透到数据库。假设有人恶意循环请求大量不存在ID数据库马上被打满。这种情况的防护手段是可以将空结果设置成“空值”放入缓存也设置过期时间时间相对短一些比如60秒这样有效挡住大量恶意流量。有更好的方案是布隆过滤器对可能存在的ID集合做概率判断不存在就挡在Redis之前。但这个过程需要在数据更新时同步维护过滤器复杂度较高是后续进阶的话题这里先不做展开。5.3 缓存穿透、缓存击穿与缓存雪崩这三个词是面试常客也是现实中高频出现的三种问题。它们共同点是“大量请求穿过缓存打到数据库”但触发起因不同应对手段也不一样。缓存穿透指的是查询一个必然不存在的数据缓存中找不到、数据库也查不到导致每一次请求都会直接打到数据库。恶意攻击时攻击者专门制造不存在的键如不断变换用户ID很容易让数据库CPU飙升。解决办法上面已经提到过缓存空值或者在接口层做参数合法性校验更彻底的方法是使用布隆过滤器提前判断。缓存击穿指的是某个热点缓存键过期的一瞬间刚好有大量请求并行到来大家同时发现缓存没数据然后同时去查数据库。解决办法有两个一是把热点数据的过期时间尽量拉长并让过期时间错开比如加一个随机数二是用互斥锁某个请求发现缓存失效后先尝试获取锁只有拿到锁的请求才允许读数据库并回写缓存其他请求短暂等待后读刚刚更新的缓存。// 使用Redis实现互斥锁防止缓存击穿 function getProductDetailWithMutex($productId) { global $redis; $cacheKey product:detail: . $productId; $lockKey lock:product:detail: . $productId; $cached $redis-get($cacheKey); if ($cached ! false) { return json_decode($cached, true); } // 尝试获取锁设置过期时间为10秒 $lockAcquired $redis-set($lockKey, 1, [NX, EX 10]); if ($lockAcquired) { try { // 查询数据库并回填缓存 $result queryDatabase($productId); $redis-set($cacheKey, json_encode($result), [EX 600]); return $result; } finally { // 释放锁 $redis-del($lockKey); } } // 没有拿到锁先短暂休眠后重试一次 usleep(50000); $cachedAfterWait $redis-get($cacheKey); if ($cachedAfterWait ! false) { return json_decode($cachedAfterWait, true); } // 极端情况下拿锁失败且仍然没有缓存时降级直查数据库 return queryDatabase($productId); } ?缓存雪崩指的是大量缓存键在同一时间段集中过期或者Redis实例本身宕机导致大量请求瞬间打到数据库。由于波及面太大互斥锁不管用了。常见的缓解方式包括设置过期时间时加上随机抖动避免同时过期做本地进程内缓存多级缓存兜底一部分请求设计缓存高可用架构主从、哨兵、集群配置数据库连接池和限流保护。另外提一个容易踩的坑很多人为了延长缓存有效期直接把过期时间设得特别大如7天表面上是减少回源次数但这样会导致问题发现延迟变长。如果业务容忍脏数据的时间窗口是5分钟却设置了7天过期时间一旦数据更新时漏删了缓存这个脏数据最长会存在7天。过期时间是业务容忍度的一部分不是说越长越好。6. 常见问题排查与避坑经验这部分内容是实操中最有价值的部分。很多问题看起来玄乎实际上背后原因非常简单只要掌握排查顺序都能在几分钟内找到症结。6.1 连接不上排查顺序要固定“连不上Redis”大概是最常见的问题常见表现有Connection refused、Connection timed out、NOAUTH Authentication required。我的排查顺序是先确认进程活着没有执行ps -ef | grep redis-server如果进程不存在直接翻Redis日志重点看启动报错 再确认端口监听情况执行ss -tlnp | grep 6379如果没有监听结果说明配置可能未生效或进程启动失败 接着确认网络连通性执行ping 目标IP和telnet 目标IP 6379如果ping通但telnet不通多半是防火墙或安全组拦截了端口 最后确认认证情况如果界面提示需要认证检查requirepass配置和客户端传入的密码是否一致。还要记住一句话如果protected-mode为yes同时进程没有设置requirepass并且bind设置成了非本机回环地址或0.0.0.0那么Redis会拒绝外部非本机连接并返回一个警告信息。这是默认安全策略不是故障原因意识到了就不会在这上面折腾太久。6.2 内存持续增长怎么办某段时间Redis占用内存不断上涨依次检查这几个点看是否设置了maxmemory。没有设置上限时Redis就会无限增长直到操作系统OOM。设置后建议关注maxmemory-policy 如果使用了noeviction内存占满后会有大量写入报错——从应用日志中能找到端倪 如果请求量本身就特别大检查有没有大体积Key 未设置过期时间的键可能有泄漏用memory usage key命令查看指定键的大小、用redis-cli --bigkeys扫描整个库中的大Key和大元素数量。# 扫描大Key找出内存占用率最多的键 redis-cli --bigkeys # 查看某个键占用的内存 redis-cli memory usage user:1001如果确诊是大Key引发问题处理思路一般是不要让单个键无限增长。List可以裁剪LTRIMHash可以拆分成多个小哈希字符串本身就大的可以用压缩或拆分方式存储。对于时效性高的数据确认设置了合理的过期时间即可对于不设过期时间且不需要长期保留的键尽早配合清理脚本删除。6.3 延迟突然飙升的典型案例延迟飙升是生产环境最让运维头疼的问题诱因非常多。根据我的实践经验按照发生概率排序绝大多数情况由下面几个原因造成第一大原因是慢查询命令阻塞服务。KEYS *、HGETALL超大哈希、LRANGE超大列表、SMEMBERS超大集合这些时间复杂度为O(N)的命令如果一次性操作海量数据会在Redis单线程中阻塞很久。排查方式是用SLOWLOG GET命令看最近慢查询记录# 查看最近执行的10条慢查询 127.0.0.1:6379 SLOWLOG GET 10 # 重置慢查询日志 127.0.0.1:6379 SLOWLOG RESET找到慢命令后优化方向要么是拆分数据、不要让单Key膨胀要么是禁止命令在生产环境使用要么是改用SCAN等游标式遍历。第二个常见原因是AOF刷盘阻塞。如果配置了appendfsync always每次写命令都会同步落盘而磁盘本身性能较差时写吞吐会被严重拖累。另外即使配了everysec如果磁盘IO出现长延迟后台刷盘线程阻塞时间也会影响主线程。解决方法包括升级磁盘、将AOF目录放在性能更好的磁盘上、或者在高吞吐场景下权衡是否关闭AOF只保留RDB。第三个原因是内存达到上限后的淘汰操作。Redis在内存满后触发淘汰策略需要扫描和删除可淘汰的Key这个过程会消耗CPU。尤其当Key的规模很大且淘汰策略需要遍历时影响时间会更明显。主动做法是给maxmemory预留一些余量并在业务侧关注用量趋势做到预警。第四个原因非常容易被忽略键集中过期。如果业务场景里大量Key在同一时间点过期Redis清理过期Key时会占用CPU和阻塞命令执行。解决方式在介绍雪崩时已经提到给EXPIRE时间增加随机偏移量让过期时间散开。这个习惯应该在一开始设计时就坚持而不是等出事了再去处理。6.4 数据持久化的信息速查关于持久化日常工作中值得关注的信息主要是触发RDB的时间条件、AOF重写状态、最近一次持久化是否成功等这些都可以通过INFO命令确认# 查看持久化相关统计 127.0.0.1:6379 INFO persistence # 输出重点摘要 # loading:0 # 是否正在加载持久化文件 # rdb_changes_since_last_save:0 # 自上次保存以来改动次数 # rdb_bgsave_in_progress:0 # RDB快照是否正在执行 # rdb_last_bgsave_status:ok # 最近一次RDB快照是否成功 # aof_enabled:1 # AOF是否开启 # aof_last_write_status:ok # 最近一次AOF写入是否成功如出现rdb_last_bgsave_status:err或aof_last_write_status:err就要立刻去查看磁盘空间和系统日志。另外一个经常遇到的问题服务器异常断电后重启Redis提示sharding detected或unrecoverable error这通常意味着AOF文件尾部有损坏。Redis提供了一个修复工具# 尝试修复AOF文件 redis-check-aof --fix /opt/redis/data/appendonly.aof # 尝试修复RDB文件 redis-check-rdb /opt/redis/data/dump.rdb修复操作不是银弹它可能会截断文件末尾写坏的部分数据但至少能让服务尽快恢复正常。养成定时备份RDB文件的好习惯始终是必须的。6.5 常规命令诊断连接已通、数据查询也没问题但仍然感觉业务卡顿需要系统性看服务器状态时重点关注INFO输出中的几项关键指标127.0.0.1:6379 INFO stats # 关键行 # total_commands_processed总共处理的命令数 # total_connections_received累计连接数 # instantaneous_ops_per_sec实时每秒执行命令数 # rejected_connections因为maxclients限制被拒绝的连接数量 127.0.0.1:6379 INFO memory # 关键行 # used_memoryRedis分配器分配的总内存 # used_memory_rss向操作系统申请的内存 # maxmemory配置的内存上限 # mem_fragmentation_ratio内存碎片率内存碎片率如果明显大于1.5甚至2说明内存碎片化严重可以考虑在业务低峰期重启一下Redis让内存重新整理如果不断接近1甚至小于1说明Redis正在大量使用虚拟内存很可能已经和系统发生内存交换swap性能会严重劣化。rejected_connections这个指标平时很少人关注一旦客户端报max number of clients reached错误就需要检查maxclients配置以及客户端本身有没有连接池和自动释放机制。一个常见场景是客户端连接使用完后没有调用关闭方法连接长期占着不释放导致Redis连接数被打满。7. 安装使用过程中的几个高频疑问汇总为什么我改了配置重启后没生效总有几个初学者修改完redis.conf后直接执行redis-server启动这个命令会使用内置默认配置而不是你的配置文件。正确方式是指定配置文件启动redis-server /opt/redis/conf/redis.conf。启动后还可以在客户端中执行CONFIG GET maxmemory之类的命令确认当前实际生效的参数值。如何在线修改配置不用重启Redis提供了CONFIG SET命令可以动态修改部分参数比如设置密码、修改内存上限、切换淘汰策略。这个命令可以避免频繁重启服务。注意两点CONFIG SET requirepass设置的新密码只对之后连接有效已连接的客户端不受影响执行CONFIG REWRITE才能把当前内存配置写回到配置文件否则重启后就恢复为原来文件中的配置。# 在线修改最大内存 CONFIG SET maxmemory 500mb # 在线切换淘汰策略 CONFIG SET maxmemory-policy volatile-lru # 将当前运行配置持久化到配置文件 CONFIG REWRITERedis是单线程的还这么快Redis的网络IO和命令执行由一个主线程负责靠事件驱动机制高效处理大量连接请求。传统服务器为每个连接分配一个线程线程的创建、切换、锁竞争消耗了大量资源Redis用单线程加多路复用避免了这些开销。它的速度快与其说是算法奇特不如说是把性能消耗大规模地节约掉了。当然现代Redis在大数据量场景下也已经引入了多线程IO处理来进一步优化网络I/O瓶颈但命令执行的主链路依然是单线程的因此单个慢命令阻塞整个服务的风险始终存在。单机的Redis存不下了怎么办业务数据量增长到单机内存放不下、或者单点网络带宽有瓶颈时需要考虑扩展方案。最简单的形式是一主多从架构主节点负责写多个从节点负责读将读流量分散出去主节点故障时哨兵机制负责自动把从节点提升为主节点。再往上是更复杂的集群架构使用分片机制将数据分散到多个节点官方还推出了多种开源生产级的缓存架构方案。强烈建议每到一个阶段做一次架构演进而不是刚开始就上特别复杂的集群方案。8. 个人经验与长期维护建议说点实际运维这么多次后的体会。日志是处置问题的第一手信息。Redis服务本身的日志不算丰富但报错和警告信息非常有价值。环境部署完成后建议花十分钟时间把INFO输出、启动日志、客户端日志的大致内容过目一遍做到心里有数。真出问题时第一反应不要是“重启试试”而应该先通过日志理解发生了什么。监控比优化更重要。容易犯的错是服务上线后就不管了直到业务方反馈才去救火。建议从第一天起就规划监控指标内存使用量、命中率、连接数、慢查询数、持久化状态、主从延迟。每周抽五分钟检查一下这些指标有没有明显异动能够提前规避掉绝大多数事故。配置文件版本管理。我建议把生产环境的redis.conf纳入版本管理库每次修改留下记录和变更说明。不同环境之间部署时用同一份基础配置通过模板生成各环境的差异配置不要靠人工记住哪个环境的配置改了哪里。踩过一次多人手工改配置改得环境不一致的坑之后你会明白这个习惯有多重要。再最后分享一个小技巧在使用redis-cli操作生产环境前先把危险命令比如FLUSHALL、FLUSHDB、CONFIG SET等戒掉。可以在配置文件中用rename-command重命名这些高危命令# 将危险命令重命名避免误操作或外部攻击者利用 rename-command FLUSHALL flushall_操作前请三思 rename-command FLUSHDB rename-command SHUTDOWN 这里要注意如果把命令重命名为空字符串该命令会被完全禁用。这样即使Redis被攻击者拿下也无法执行删除操作。这段配置虽然会让排查问题时麻烦一点但比起整个数据目录被清空的代价实在不值一提。从最开始环境里连不上Redis的抓狂到现在看日志一分钟定位问题、日常监控心里有底这中间的差距其实就是经历的积累。把这篇文章里的步骤完整走一遍再对着日志排一次错Redis这块基石就算真正踩稳了。
阅读完成 · 觉得有帮助?