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 Manager(RDM) | 老牌工具,跨平台,功能全 | 日常连本机和测试环境 |
| Another Redis Desktop Manager | 开源免费,比RDM轻量,更新活跃 | 团队协作,连多套环境 |
| Redis Insight | Redis官方出品,附带分析面板 | 查看内存分析、慢查询 |
连接的时候如果看着一堆\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里的Map<String, String>,特别适合存对象。比如用户信息,我可以一次性把id, name, age都塞进一个key里,不用每次都序列化整个对象。
常用命令:
HSET key field value [field value ...]HGET key fieldHGETALL keyHMGET key field1 field2HINCRBY key field incrementHDEL key field
Hash比String有个隐藏优势:字段级过期没法做,但字段级更新很方便,你不用为了改一个字段把整个对象取出来再写回去。反例是购物车这类场景,用户加购一个商品,如果用的是String存整个JSON,就得读出来、改JSON、再写回去,并发时还容易覆盖;换成Hash,每个商品ID当field,数量当value,HINCRBY 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就是用户ID,ZREVRANGE page 0 9把前三名取出来,毫秒级。还有一个典型玩法是延时队列:member放任务内容,score放执行时间戳,起一个定时任务用ZRANGEBYSCORE key -inf now把到期任务拉出来执行,执行完再ZREM。这个写法在任务量不大的场景下比引入消息中间件轻得多。
需要提醒的是:ZSet会占两份空间,一份存元素,一份存有序链表,元素数量几十万以上内存增长很快。能用普通Set解决的别硬上ZSet,免得为不需要的排序功能多付内存。
4. 持久化机制:RDB、AOF 与混合持久化选型
4.1 RDB快照:恢复快但丢数据
RDB是Redis默认开启的持久化方式,它把某一时刻的内存快照压缩写入磁盘,文件是二进制的.rdb。触发方式有SAVE(同步阻塞)和BGSAVE(fork子进程后台写),生产上一般通过配置触发:
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日志:每笔写操作都记录,丢数据窗口小
AOF(Append 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对不上。
RedisTemplate<String, 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的利器--bigkeys:
redis-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实例,不妨现在就试试这三条命令,排查问题的思路会清晰很多。