Redis这个词做后端的人基本都绕不开。从最开始拿它当缓存存个热点数据到后面做分布式锁、排行榜、消息队列再到缓存穿透、雪崩、主从切换这些硬仗Redis几乎贯穿了我这几年项目的每个阶段。这篇东西我不打算写成官方文档式的百科就按我实际踩坑和调配的顺序来从怎么把Redis跑起来到五种数据类型的真实使用场景再到持久化选型、缓存治理和分布式锁的坑最后把面试里高频问的那几个问题也一并聊透。适合刚准备上手的人也适合用了一阵子但总在某些边界场景上栽跟头的人。1. 上手前的关键认知Redis到底帮你解决了什么问题先说清楚一件事Redis不是数据库的替代品它是数据库和你业务层之间的一个缓冲地带。早年不少人拿Redis当主存储用结果数据一重启没了一大半然后跑回来骂Redis这其实是持久化配置和使用姿势的问题不是Redis本身不行。我一般这样理解Redis的价值它把“热”的数据从磁盘挪到内存里把“慢”的多步操作变成“快”的单指令操作把“重”的集中式压力分散到更近的节点上。一个请求过来如果每次都打到MySQL十几毫秒甚至几十毫秒的响应就出去了而走Redis缓存响应时间直接掉到个位数毫秒这对高并发接口来说是质变。对后端开发来说Redis最常见的落地场景其实是这几个缓存热点数据、分布式环境下共享计数的原子自增、临时会话保存、排行榜和延时队列以及跨服务之间的分布式锁。这些场景背后对应的是不同的数据结构和部署形态也是“基础到进阶”这条路线的主干。还有一个容易被忽略的点Redis的命令执行是单线程的。这意味着同一时刻只会有一个命令在跑天然没有竞争问题也正因为单线程它才不需要加锁性能才能达到十万级QPS。但单线程也决定了你不能在线上随便执行KEYS *这类耗时命令一执行就是全库阻塞这是后面要反复强调的底线。2. 环境搭建避坑指南Windows、Linux 与 Docker 三种落地方式2.1 Windows 下的本地安装与开机自启虽然生产环境基本是Linux但我现在写Demo、本地联调还是习惯先在Windows上跑一个实例。Windows下官方没有直接提供msi安装包最省事的是去官网下载zip压缩包版本解压后目录里就有redis-server.exe和redis-cli.exe。启动方式很简单命令行进入目录后执行redis-server.exe redis.windows.conf如果只是临时用直接双击redis-server.exe也能跑起来默认端口6379。但双击启动有个问题关闭窗口服务就没了而且配置文件没加载很多参数是默认值。所以我一般建议用下面这条命令注册成Windows服务redis-server.exe --service-install redis.windows.conf --loglevel verbose注册完去Windows服务管理器里找到Redis服务设为自动启动以后开机就在后台跑。想卸载服务就用--service-uninstall。本地验证是否起来开个新终端跑redis-cli ping返回PONG就说明通了。注意Windows这种跑法只是个开发辅助不要拿去当生产方案。Redis对文件系统的事件通知、fork子进程这些能力在Windows上支持不完整性能差距很大。本地连不上时先看三个东西bind 127.0.0.1是不是限制了只允许本机protected-mode是不是保护模式挡住了外部连接以及Windows防火墙6379端口有没有放行。大多数“明明启动了却连不上”的问题都出在这三处。2.2 Linux 与 Docker 部署生产环境的正确姿势生产环境我基本只用两种方式Linux直接装、容器化部署。如果你是Ubuntu系官方源里的Redis版本往往偏低建议直接用官方提供的安装源或者干脆上Docker版本可控、环境隔离回滚也方便。直接用包管理器安装时默认配置只允许本机访问需要手动改配置文件里的bind、requirepass和appendonly这些选项改完记得重启服务sudo apt-get install redis-server sudo systemctl restart redisDocker方式就更利落了一条命令拉起来docker run -d --name redis \ -p 6379:6379 \ -v /data/redis/conf:/usr/local/etc/redis \ -v /data/redis/data:/data \ redis:7.2 redis-server /usr/local/etc/redis/redis.conf这里我把配置文件和数据目录都挂载出来了好处是容器挂了重来配置和数据还在。很多人习惯直接把-p 6379:6379暴露出去但生产环境我建议不要这样裸暴露最好带上--requirepass设置密码必要时再加一层防火墙限制来源IP。用Docker跑的时候还要注意时区和挂载权限问题。Redis的数据目录如果挂载到宿主机目录属主跟容器内用户不一致可能出现写失败排查起来很费神我遇到过不止一次。所以在挂载:/data前建议先确认宿主机目录权限可写或者用命名卷替代bind mount。2.3 可视化客户端与序列化问题命令行用多了看数据还是得借助图形工具。社区里常用的几款我列一下工具名称特点适用场景Redis Desktop ManagerRDM老牌工具跨平台功能全日常连本机和测试环境Another Redis Desktop Manager开源免费比RDM轻量更新活跃团队协作连多套环境Redis InsightRedis官方出品附带分析面板查看内存分析、慢查询连接的时候如果看着一堆\xAC\xED\x00\x05t...之类的乱码不要慌这不是Redis出问题了是客户端写入数据时用了Java原生的JDK序列化。Redis本身只存字节StringRedisTemplate和RedisTemplate是两套东西前者存的是字符串后者默认用JDK序列化器所以会变成乱码。想解决要么统一用StringRedisTemplate加JSON要么自定义RedisTemplate的序列化器这个后面专门讲。3. 五种数据类型命令语法、内存模型与场景对照3.1 String缓存、计数与分布式锁的基础String是Redis里最基础也是用得最多的类型。它的value最大能存512MB但实际不要这么干大key会让网络传输和内存分配都变得很糟心。常用的命令就这么几个SET key value [NX|XX] [EX seconds|PX ms]GET keyINCR key/DECR key/INCRBY key incrementSETNX key valueSETEX key seconds valueGETSET key value我项目里用String最多的场景是热数据缓存和计数器。比如商品详情直接存一条JSON进去过期时间设置成随机值防止雪崩。计数器就更典型了点赞数、访问量、库存扣减我都是直接用INCR因为它是原子操作在高并发下不会出现并发覆盖的问题。SET key value NX EX 30这条命令是后面分布式锁的核心NX表示只有key不存在时才设置EX表示过期时间。三条指令合成一条原子性就保住了比先SETNX再EXPIRE两步操作安全得多。3.2 List别把它当数组它是队列List的底层是双向链表新版已改成quicklist两端的操作都是O(1)级别它天生适合做消息队列。常用命令LPUSH key value [value ...]/RPUSH key valueLPOP key/RPOP keyLRANGE key start stopBLPOP key timeoutLLEN key以前很多项目用List做简单的任务队列生产者LPUSH消费者BRPOP阻塞等待。这套方案简单可靠但有个痛点消息取走后如果处理失败消息就丢了没有ack机制。所以现在我的建议是简单的异步任务用List没问题但要能容忍消息丢失如果业务要求不能丢消息直接上Stream类型它支持消费者组和ack是从Redis 5.0开始官方推荐的做法。BLPOP key 0里的timeout参数设0表示永远阻塞。这个命令在用Redis做消息队列时很关键它能把轮询变成“有人推消息我才醒”极大降低客户端和Redis之间的无效交互。3.3 Hash存储对象字段比String省内存Hash非常像Java里的MapString, String特别适合存对象。比如用户信息我可以一次性把id, name, age都塞进一个key里不用每次都序列化整个对象。常用命令HSET key field value [field value ...]HGET key fieldHGETALL keyHMGET key field1 field2HINCRBY key field incrementHDEL key fieldHash比String有个隐藏优势字段级过期没法做但字段级更新很方便你不用为了改一个字段把整个对象取出来再写回去。反例是购物车这类场景用户加购一个商品如果用的是String存整个JSON就得读出来、改JSON、再写回去并发时还容易覆盖换成Hash每个商品ID当field数量当valueHINCRBY cart cartId 1一条命令搞定又省流量又安全。3.4 Set无序去重天生适合集合运算Set的特性是无序、唯一并且支持集合运算。常用命令有SADD、SMEMBERS、SISMEMBER、SCARD、SINTER、SUNION、SDIFF。去重是它最直觉的使用场景。签到用户、抽奖参与人、一个文章被哪些人点赞直接SADD进去重复添加同一个元素不会产生垃圾数据。SISMEMBER可以O(1)判断元素是否存在适合做“是否已读”“是否关注”这类查询。集合运算才是Set的杀手锏。比如“共同好友”两条SINTER取交集一条命令就出来了再比如“推荐关注”用SDIFF站在一个集合差集的角度拿数据不需要在业务代码里写循环比对。3.5 ZSet排行榜和延时队列都靠它ZSet在Set的基础上多了score字段元素按score排序所以它同时是集合、排序结构、还有序号的索引。常用命令ZADD key score memberZRANGE key start stop [WITHSCORES]ZREVRANGE key start stopZRANGEBYSCORE key min maxZSCORE key memberZINCRBY key increment memberZREM key member排行榜是ZSet最经典的应用score就是分数member就是用户IDZREVRANGE page 0 9把前三名取出来毫秒级。还有一个典型玩法是延时队列member放任务内容score放执行时间戳起一个定时任务用ZRANGEBYSCORE key -inf now把到期任务拉出来执行执行完再ZREM。这个写法在任务量不大的场景下比引入消息中间件轻得多。需要提醒的是ZSet会占两份空间一份存元素一份存有序链表元素数量几十万以上内存增长很快。能用普通Set解决的别硬上ZSet免得为不需要的排序功能多付内存。4. 持久化机制RDB、AOF 与混合持久化选型4.1 RDB快照恢复快但丢数据RDB是Redis默认开启的持久化方式它把某一时刻的内存快照压缩写入磁盘文件是二进制的.rdb。触发方式有SAVE同步阻塞和BGSAVEfork子进程后台写生产上一般通过配置触发save 900 1 save 300 10 save 60 10000意思是900秒内至少有1个key变化、300秒内10个key变化、60秒内10000个key变化满足任一条件就触发一次BGSAVE。RDB的优点是恢复速度快适合做冷备份和灾难恢复。缺点也很明显快照之间有窗口期这期间写进去的数据Redis一旦崩溃就会丢。这个窗口期可能长达几分钟。所以单独用RDB做持久化数据安全要求高的项目千万别这么干。4.2 AOF日志每笔写操作都记录丢数据窗口小AOFAppend Only File把每一次写命令追加到日志文件末尾相当于把操作日志重放一遍来恢复数据。配置方式appendonly yes appendfsync everysecappendfsync有三个可选值always每条命令都同步刷盘最安全但性能掉得明显everysec每秒刷一次性能和安全性的折中默认推荐no交给操作系统决定刷盘时机最快但也最容易丢。AOF文件会越来越大所以要定期重写把一堆历史命令压缩成最终结果。重写机制由两个参数控制auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb文件比上次重写时翻了一倍或者超过64MB就触发重写。重写过程会fork子进程期间主进程的写操作会被缓存起来所以即使重写也别太频繁否则会拖慢单线程的处理。4.3 混合持久化与恢复策略Redis 4.0开始支持混合持久化配置项是aof-use-rdb-preamble yes它的原理是AOF文件的头部先写一个RDB格式的快照后面再追加增量命令。这样重启恢复时先加载RDB快照速度接近纯RDB同时后续的增量命令又保证了数据鲜度。这套方案是我目前的主力配置数据安全和个人对性能的容忍度都能兼顾。关于恢复顺序记住一句话AOF优先于RDB。只要AOF文件存在Redis启动时就会优先加载AOF来恢复数据因为AOF中记录的数据通常更新。如果AOF文件损坏启动会直接失败这时候可以用redis-check-aof --fix工具修复RDB损坏则用redis-check-rdb。我遇到过几次文件写一半机器断电的情况恢复时用工具检查一下总没错。5. 进阶实践缓存治理、分布式锁与高可用选型5.1 缓存穿透、击穿、雪崩及应对方案缓存穿透是指查的数据连数据库里也没有缓存里自然更没有每次请求都直接打到MySQL上。典型场景是恶意请求用一个不存在的ID反复刷接口。应对方法我常用两种一是对查询结果为空的数据也做短时间的空值缓存时间设为几十秒避免压力全打到数据库二是用布隆过滤器在缓存前置一个过滤层不存在的key直接挡在门外。布隆过滤器的特点是判断不存在是百分百准确的判断存在则有可能误判你需要在业务上容忍这种误判。而且布隆过滤器删除困难不适合数据经常增删的场景。缓存击穿是指某个热点key过期的瞬间大量请求同时涌向数据库。最常见的解法是“互斥锁”缓存没拿到时先加锁只允许第一个线程去查数据库并回填缓存其他线程等着读缓存。也可以用“逻辑过期”方案缓存永不过期但value里存一个过期时间戳读到后发现过期异步线程去刷新数据这种方案对小团队来说实现成本略高但性能最好。缓存雪崩一般是指大量key在同一时段集体过期或者Redis节点整体挂了。应对核心思路是把过期时间打散在基础过期时间上加上一个随机值让过期时间在区间内均匀分布。其次可以做多级缓存本地进程内再放一层缓存即便Redis挂掉也能扛住一小段时间。条件允许时再加降级和熔断避免数据库被拖垮。我习惯把所有接口的过期时间统一加上5到60秒的随机偏移代码变动很小但效果显著。5.2 分布式锁的正确实现与常见陷阱分布式锁算是Redis最容易被写错的场景之一。一个正确的分布式锁至少要满足三点互斥性、防死锁、防误删。我推荐的标准写法是SET lock_key unique_value NX EX 30释放锁时要先校验value是不是自己设的那个再用Lua脚本保证“判断删除”两步的原子性if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end很多人踩的坑是这样写完业务代码后单纯DEL key结果业务执行时间超过了锁的过期时间锁已经自动释放了这时另外一个线程拿到锁写入了数据前一个线程跑完直接DEL把别人的锁删掉了。这就是典型的误删。加上unique_value的判断只能删自己创建的锁问题就解决了。还有一个更深的坑锁过期时间设置多长设短了业务没执行完锁先没了设长了万一持有锁的进程宕机其他线程长时间拿不到锁。所以成熟的做法是加“看门狗”续期机制锁快到期了自动延长过期时间。Redisson里的getLock().lock()默认就带续期功能我建议不要太纠结手写锁生产环境直接用Redisson封装好的方案。至于RedLock也就是多节点加锁的方案我不太建议在小团队里自己实现。它的正确性依赖多节点之间的时间同步假设反而引入了更多不确定性。绝大多数业务场景一个主从架构上的Redis锁配合续期机制已经够用了。5.3 哨兵模式与集群模式到底该怎么选**主从复制哨兵Sentinel**是高可用的核心。主节点挂了哨兵会自动把从节点提升为主节点业务无感知。他有三个职责监控、通知、故障转移。配置哨兵时它会不停发送每秒一次的PING主观下线后如果多个哨兵都判定主节点不可用就进入客观下线然后进行投票选举新主节点。哨兵模式适合单写多读、数据量还不需要分片的场景。比如博客、企业后台这类应用一个主节点扛写入几个从节点分担读流量运维成本低可靠性也不错。弊端是数据总量受单机内存限制。**集群模式Cluster**就不一样了。它把数据分散到多个主节点上每个主节点管一部分哈希槽总共16384个槽位。写入一个key时先做CRC16算出槽位再路由到对应节点。集群模式下扩展是近乎线性的加节点就等于增加槽位数据自动迁移。但这种分片模式让跨key的操作受限比如多key事务、Lua脚本涉及多个节点就无法保证原子性。我一般按这个标准做判断数据量在几十GB以内、高可用优先哨兵模式够用数据量达到几百GB级别或者单主节点内存压力已影响性能再上集群。不要一上来就搞Cluster复杂度和运维成本都高出不少。5.4 序列化方案与内存优化Java连接Redis序列化问题绕不开。默认的JdkSerializationRedisSerializer存进去的字节很浪费空间而且读出来肉眼不可读排查问题非常痛苦。我通常在项目里做两套配置。缓存复杂对象用GenericJackson2JsonRedisSerializer直接存JSON字符串单纯字符串就用StringRedisSerializer。配置RedisTemplate时key和hashKey必须用StringRedisSerializer不然生成的key前缀会是\xAC\xED...这种二进制乱码跟命令行直接SET的key对不上。RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet();内存优化的一个实用方向是选择高效类型。比如几十个字段的对象Hash往往比String更省记录集合成员Set比List省且天然去重排行榜功能直接用ZSet别自己造排序。另外key命名规范也要统一比如业务域:模块:ID这种格式方便排查也利于按前缀清理缓存。6. 高频面试与实战排查那些总被问倒的问题6.1 Redis为什么快单线程为什么还能扛住高并发面试官问“Redis为什么快”希望你说到这几层第一条它是内存型数据库数据大部分情况下都在内存里访问没有磁盘随机I/O的延迟。第二单线程模型避免了多线程的上下文切换和锁竞争这也是它快的一个核心原因。第三I/O多路复用Redis用epoll同时监听大量客户端连接只在可读可写时处理请求避免了阻塞等待。第四它的数据结构在设计上针对场景做了优化比如跳表、压缩列表适合读多写少的场景。有人会问为什么不用多线程其实Redis 6.0引入了多线程I/O来处理网络读写但命令执行仍然是单线程。因为Redis瓶颈通常不在CPU而在网络I/O和内存多线程执行命令反而会引入一致性问题和锁开销收益不大。6.2 过期策略与内存淘汰缓存不会无限膨胀的原因Redis的key过期删除不是“一到时间就立刻删”它用的是“惰性删除定期删除”的组合。惰性删除是每次访问key时检查是否过期过期就删但一个key如果一直没被访问就会一直占着内存。所以还有定期删除每隔一段时间随机抽取一批key检查过期情况批量删除。这套组合能基本解决内存泄漏但极端情况下还是可能积压过期key这时就要依赖内存淘汰策略兜底策略含义noeviction达到最大内存后写命令直接报错默认策略allkeys-lru在所有key中按最近最少使用淘汰volatile-lru只在设置了过期时间的key中按LRU淘汰allkeys-lfu在所有key中按访问频率淘汰volatile-random在有过期时间的key中随机淘汰线上我一般用allkeys-lru对数据能容忍的缓存场景足够。但要注意如果Redis里还存着不能丢的会话数据别开全key淘汰选volatile-*系。6.3 排查现场实录日志、慢查询与大key定位Redis没有像MySQL那样爆炸多的日志概念但有几个排查手段相当好用。第一是看redis-cli info里面分成Memory、Stats、Replication等块。比如used_memory_human看内存占用hit_rate看缓存命中率命中率过低说明缓存策略有问题。第二是SLOWLOG慢查询日志SLOWLOG GET 10 CONFIG SET slowlog-log-slower-than 10000slowlog-log-slower-than单位是微秒设成10000也就是10毫秒就记录下所有超过10毫秒的命令。单线程架构下这些都是可能阻塞全库的操作比如KEYS *、超大集合的SMEMBERS、批量HGETALL。第三是定位大key的利器--bigkeysredis-cli --bigkeys --host 127.0.0.1 --port 6379它会扫描整个Redis列出每种数据类型里最大的key帮你看清是哪几个大key占着内存或拖慢访问。这个命令在线上确实会扫库会有一定IO开销我一般安排在低峰期执行或者直接从内存分析工具比如官方自带的分析拿数据。还有一个容易被忽略的小工具MONITOR。它会实时打印出Redis收到的每一条命令调试逻辑时非常直观但线上开MONITOR会把性能拖垮只适合在小体量测试环境里排查问题线上别用。6.4 那些“看着不对”的问题incr 不准、乱码、连接失败很多新手问“INCR 不准”其实INCR本身是原子的不可能“不准”。所谓不准多半是这几个原因引起的一是业务里拿到自增值后又做了其他逻辑导致多跳几次二是前端重复提交同一请求发了两三次三是框架或网络重试机制导致同一条请求被执行多次但只展示一次。排查这类问题先去看业务调用链而不是怀疑Redis的原子性。连接失败基本集中在三个原因protected-mode yes时外部客户端直接连被拒bind 127.0.0.1只允许本机连Redis密码没配置或配置了但客户端没带。还有一种常见的报错MISCONF Redis is configured to save RDB snapshots是因为磁盘空间不足或fork失败导致RDB持久化失败Redis默认拒绝后续写命令来保护数据。解决办法是修复磁盘或用CONFIG SET stop-writes-on-bgsave-error no临时解除写保护但生产上不要轻易关这个保护。登录工具看到DENIED Redis is running in protected mode时记得把部署机器的protected-mode设为no同时配置文件里显式bind允许的网卡地址并设置强口令。裸奔在公网上的Redis一旦被扫描到极容易被勒索填充数据这类案例太多了。最后的实操心得说几个我真正踩过的教训。第一个是先看配置再动手别一上来就敲命令。很多问题和性能瓶颈在redis.conf里就已经注定了改一个save间隔、调整一个淘汰策略比你换服务器来得有效。第二个是监控先行。我后来在所有项目里都习惯性在Redis上加一层监控INFO stats定期采集命中率掉到某阈值、慢查询数量激增都自动报警。没有监控的情况下排查问题就像闭着眼睛修电路。第三个是别把所有功能都堆给Redis。真的需要可靠消息队列就上专业组件真的需要把几十GB数据存下来好好想清楚Redis是不是合适的容器。Redis好用但也不是万能的边界设清楚它才能稳定为你服务。最后分享一个小技巧我平时习惯把常用检查串成一条命令redis-cli info memory、redis-cli slowlog get 20、redis-cli --bigkeys定期过一遍大概几分钟就能掌握一个Redis实例的健康状况。你现在手头如果有个Redis实例不妨现在就试试这三条命令排查问题的思路会清晰很多。