1. 先搞清楚Redis在你项目里到底扮演什么角色
三年前我第一次在生产环境用Redis,干过一件特别蠢的事:把所有用户session都存在Redis里,但忘了配持久化,重启完直接全员掉线。那一刻我才意识到,Redis这玩意儿的定位你搞不清楚,后面全是坑。
很多人学Redis第一反应是"这不就是个缓存数据库吗",其实这个认知至少耽误了你三层功力。Redis全称是Remote Dictionary Server,本质是一个基于内存的键值存储系统,它之所以能在无数项目里占据核心位置,靠的不是"快"这么简单,而是"快+数据结构丰富+原子操作+生命周期可控"这四个特性的组合拳。
我习惯把Redis在一个项目中的作用分成三个层次来理解:
第一个层次是缓存层。把热点数据从MySQL里捞出来放Redis,查询走内存,读写吞吐量直接从每秒几百飙到几万。这个层次最入门,但也是最容易出问题的——缓存穿透、击穿、雪崩三大坑全在这个层里。
第二个层次是数据结构层。Redis不止有String,还有List、Hash、Set、ZSet,这意味着你可以用它做排行榜、做去重、做队列、做计数器。这一层用好了,很多原本需要单独中间件解决的问题,一个Redis就能顶住。
第三个层次是原子操作层。Redis是单线程执行命令的,所有操作天然串行、天然原子,配合SET NX EX、INCR、Lua脚本这些能力,分布式锁、秒杀防超卖、幂等控制这些高并发场景你都能在Redis上直接搞定。
所以这篇笔记我不会按官方文档的目录顺序给你罗列命令,而是按照我自己从入门到在生产环境稳定运行的完整路径来写:先落地部署、再吃透数据结构、然后打通持久化与高可用、最后用SpringBoot把Redis真正接入业务,并在结尾把缓存治理的实战经验一并交代清楚。
2. 安装部署与客户端选型,别在第一公里就踩坑
2.1 Windows与Linux环境下的安装差异
Redis官方其实不支持Windows,这个点很多人不知道。你在Windows上装的那些Redis,基本是微软老版本的分支或者用WSL跑起来的Linux版本。如果只是本地学习,Windows版本完全够用,但如果要上生产,请老老实实用Linux。
Windows下最简单的安装方式是去Redis的GitHub Releases页面找带Windows标识的压缩包,解压后直接运行redis-server.exe,默认端口6379,不用改任何配置就能跑起来。需要注意的一点是,解压根目录下的redis.windows.conf才是实际生效的配置文件,启动时建议带上这个文件:
redis-server.exe redis.windows.conf不带配置文件启动也能跑,但你会失去密码验证、持久化开关、最大内存限制这些关键配置。
Linux下安装Redis 7.0的完整路径是这样的:
# 更新包索引并安装编译工具 sudo apt update sudo apt install -y build-essential tcl # 下载源码包 wget https://download.redis.io/releases/redis-7.0.11.tar.gz tar xzf redis-7.0.11.tar.gz cd redis-7.0.11 # 编译安装 make sudo make install # 启动前先配置好后台运行 redis-server --daemonize yes很多人卡在make那一步直接报错,往往是因为没装build-essential。另外还有一个高频坑:如果你的Linux是ARM机器(比如树莓派或者某些云主机),编译命令是同样的三件套make、sudo make install,但下载的源码包地址不变,Redis源码对ARM的支持很完备,不需要单独找ARM版本。
2.2 Docker部署主从结构:一条命令的事
如果你手头有Docker,我强烈建议学习阶段直接走容器化路线,省去一堆环境依赖的破事。拉一个镜像起来做单机实验是基本功,这里我更想展示怎么用Docker Compose一键拉一套主从结构,因为主从复制是Redis高可用的地基,你迟早要面对它。
docker-compose.yml的核心内容如下:
version: '3.8' services: redis-master: image: redis:7.0 container_name: redis-master ports: - "6379:6379" command: redis-server --appendonly yes redis-slave: image: redis:7.0 container_name: redis-slave depends_on: - redis-master ports: - "6380:6379" command: redis-server --slaveof redis-master 6379这个配置里,主节点开了AOF持久化(appendonly yes),从节点直接用--slaveof指向主节点。启动后你可以在主节点写入数据,再去从节点读,会发现数据已经同步过去了。
这里我想特别提醒一个坑:主从复制的数据同步是单向的,从节点默认是只读的,你在从节点写数据会直接报错,这是设计如此,不是配置坏了。
2.3 可视化客户端的选型思路
命令行redis-cli是基本功,但人眼看不方便。工具我推荐两个:第一个是官方的RedisInsight,功能最全,支持内存分析、慢日志查询、可视化数据浏览,就是启动稍重;第二个是Another Redis Desktop Manager,也就是搜索里的"another redis desktop manager",开源免费,跨平台,直连部署简单,日常查看Key和Value完全够用。
两个工具连接时都需要注意:如果你的Redis配置了密码,连接地址填服务器IP、端口填6379、密码填你设置的requirepass。如果连不上,先telnet一下端口通不通,大概率是服务器安全组没放行6379,跟工具本身没关系。
3. 五大数据类型:不是背命令,是理解底层数据结构
3.1 String与Hash的本质区别:内存不是白省的
String是Redis里最基础的类型,一个key对应一个value,value最大512MB。但新手最容易踩的坑是把对象序列化成JSON字符串塞进String里,比如用户信息、订单信息这种结构化数据,存了一堆类似user:10001的key,每个都是冗长的JSON串。
问题是,你更新用户昵称时就得整串读出来、改完、再整体写回去,费流量还费CPU。Hash类型才是这类场景的正解,它相当于Redis里的一个小Map:
HSET user:10001 name "张三" age 25 city "北京" HGET user:10001 name HINCRBY user:10001 age 1字段级别的读写让你不需要动整个对象,HINCRBY还能对某个字段做原子自增。我自己实测过,存储100万条用户数据,用Hash比用JSON String节省约30%的内存,原因在于Redis对Hash做了ziplist或listpack的紧凑编码优化。
3.2 List、Set、ZSet在不同业务里的定位
List是双向链表结构,典型场景是消息队列的简化版——LPUSH从左边推入,RPOP从右边弹出,天然实现了FIFO。但它有个致命弱点:消息丢失不负责,消费者宕机了队列里的数据也还在,但要是Redis本身重启且没持久化,你的队列就没了。所以List做轻量级队列可以,别指望它当Kafka用。
Set是无序去重集合,适合做"当天活跃用户""文章点赞人"这类数据,SADD和SISMEMBER两个命令就能搞定去重和判断。它还有一个隐藏用法,就是集合运算:SINTER取交集可以做"共同关注"功能,SUNION取并集可以做"好友推荐"功能,一行命令搞定原本要在业务代码里写循环的数据操作。
ZSet是Redis里最有价值的数据结构,每个元素带着一个score,Redis按score排序存储。最经典的应用就是排行榜:直播打赏榜、积分排行榜、热销商品榜。ZADD leaderboard 100 "user1",查询Top10用ZREVRANGE leaderboard 0 9,效率极高。更妙的是ZSet可以做范围查询,比如"积分在1000到2000之间的用户",一条命令返回结果,直接省掉了数据库里的慢SQL。
3.3 键的过期策略与内存淘汰机制
数据类型之外,每个key都可以设置过期时间,这是缓存系统最关键的能力:
SET token "abc123" EX 3600 EXPIRE user:10001 86400 TTL user:10001EXPIRE设置的是相对时间,SETEX可以在写入时直接指定过期秒数。但你可能不知道,Redis的过期删除不是实时扫描所有key的,而是三种策略的组合:
惰性删除:访问key时才检查是否过期,过期就直接删掉返回空。主动删除:后台任务每100ms随机抽一批设置了过期时间的key,发现过期的就删。内存淘汰:如果上面的策略没跟上,内存满了就按配置的maxmemory-policy来清数据,常见的有allkeys-lru(最近最少使用)、volatile-lru(只在设了过期时间的key里做LRU)。
生产环境我建议设maxmemory上限,策略用allkeys-lru,这样即使缓存数据激增也不会把Redis内存打爆导致OOM崩溃。
4. 持久化机制:数据安全与性能的权衡
4.1 RDB与AOF各自的脾气
Redis默认开启RDB快照持久化,它做的事是fork一个子进程,把当前内存里的全量数据写入磁盘上的dump.rdb文件。优点是恢复速度快、文件紧凑,缺点是快照之间有窗口期,如果你Redis宕机了,最后一次快照之后写入的数据全部丢失。
AOF则是追加写日志,Redis每执行一条写命令,就把这条命令追加到appendonly.aof文件末尾。优点是数据安全性更高,可以配appendfsync everysec,最多丢一秒数据;缺点是文件体积增长快,而且恢复时要重放所有命令,速度比RDB慢。
两者不冲突,生产最佳实践是同时开启:用RDB做冷备和快速恢复,用AOF做崩溃时的数据兜底。Redis 7.0引入了AOF多部分文件格式,重写时不再生成全量临时文件再切换,而是采用manifest清单管理多个AOF片段,重写期间的写入不再阻塞。
4.2 我在日志排查里最常见的持久化故障
"redis日志"是热搜词里的常客,我见过太多人去服务器上看redis.log,发现大量这样的警告:
WARNING overcommit_memory is set to 0这个警告的意思是Linux内核的内存过度分配策略被设置为0,当Redis做RDB快照fork子进程时,如果物理内存不足,子进程可能启动失败。修复方式是修改系统参数:
sysctl vm.overcommit_memory=1再往配置文件里加repl-backlog-size调大复制缓冲区。另一个让我踩过坑的是主从复制时从节点的redis.log里全是MASTER <-> REPLICA sync: Master tried to disconnect之类的报错,排查半天发现是主节点repl-backlog-size默认只有1MB,同步压力大时缓冲区直接被撑爆。把主节点的repl-backlog-size调到64MB,问题立刻消失。
4.3 备份策略的实战建议
RDB文件本质上就是你的数据快照,我建议配合定时任务每天做一次异地备份。简单的思路是写个cron任务,把dump.rdb用scp传到备份服务器,保留7天。AOF文件也可以定时拷贝,但因为它持续在写,直接拷贝可能拿到不完整的文件,建议用BGREWRITEAOF触发一次重写后再拷贝。
恢复流程也说一下:如果Redis所在机器崩溃了,重新装好Redis后,把备份的rdb文件放到配置的dir目录下,用redis-server redis.conf启动,Redis启动时会自动加载这个rdb文件。AOF恢复则是在启动时把appendonly.aof里的命令逐条重放。我个人的习惯是"RDB为主+AOF为辅",两者同时在,Redis启动时优先加载AOF,因为AOF的数据通常更新。
5. 主从、哨兵与集群:高可用到底在解决什么问题
5.1 主从复制,数据的安全副本
主从复制的第一个作用是数据冗余。主节点挂了,从节点还有完整数据,不会一夜回到解放前。第二个作用是读写分离,读操作全部打给从节点,主节点专心处理写请求,减轻单机压力。
复制过程有一个细节值得理解:全量同步与增量同步。首次同步时从节点发PSYNC命令,主节点做一次BGSAVE生成RDB全量快照发给从节点。之后主节点的写命令会持续发给从节点,这叫增量同步。如果从节点断线重连,Redis会从repl_backlog_buffer里找断线期间遗漏的命令进行补发,这个缓冲区就是我在上面提到的repl-backlog-size,它不够大就会触发一次全量同步。
5.2 哨兵模式如何实现自动故障转移
主从复制解决不了"主节点挂了之后怎么自动选主"的问题。哨兵(Sentinel)就是干这个的:它负责监控所有主从节点的存活状态,当主节点被判定为客观下线后,会从从节点中选举一个新的主节点,并通知所有客户端更新连接地址。
哨兵模式部署至少要三个哨兵节点,原因很简单:哨兵之间需要投票达成共识,只有一个哨兵会形成"主观误判"——它觉得主节点挂了,其实是网络抖动。哨兵数量为奇数,方便多数派决策。
Docker Compose部署哨兵,可以在前文主从配置的基础上增加sentinel服务,配置文件核心一行:
sentinel monitor mymaster redis-master 6379 2这里的2表示至少两个哨兵同意主节点下线,才触发故障转移。你可以在测试环境手动kill掉主节点容器,观察哨兵自动把一个从节点提升为主节点,整个过程大概10秒。
5.3 集群模式,解决的是容量问题
哨兵解决高可用,但依然是单主节点写入,内存上限受制于一台机器。Redis Cluster把数据分片存储在多个主节点上:整个Key空间被分成16384个槽位,每个节点负责一部分槽位。计算方式是对key做CRC16校验,然后对16384取模。
集群模式至少需要3个主节点,官方建议每个主节点至少配一个从节点,也就是6个节点起步。部署时用redis-cli --cluster create命令指定节点列表,Redis会自动分配槽位并建立主从关系。客户端访问某个key时,如果请求被路由到了错误的节点,节点会返回MOVED错误并把正确的节点地址告诉客户端,客户端再重新请求。
集群的另一个隐藏规则必须知道:多key操作在集群里有限制,比如MGET只能操作hash tag相同的key,否则会报CROSSSLOT错误。这个限制在你从单机迁移到集群时会卡很多团队,提前设计好key的命名规范很重要。
6. SpringBoot整合实战:序列化、increment()报错与分布式锁
6.1 RedisTemplate与StringRedisTemplate到底选谁
SpringBoot整合Redis是Java后端的高频场景。操作姿势有两种主流方式,一个是StringRedisTemplate,一个是RedisTemplate。它们的本质区别在于序列化器:
StringRedisTemplate默认用String序列化,key和value都是字符串,人类可读。RedisTemplate默认用JDK序列化,结果是二进制串,console里看是一堆乱码。
很多初学者直接用RedisTemplate存数据,发现Redis Desktop Manager里看到的是\xAC\xED\x00\x05t\x00开头的乱码,这就是JDK序列化的锅。我给你的建议非常明确:如果没有存储对象的强需求,一律用StringRedisTemplate;如果一定要存对象,手动指定Jackson序列化器,不要用默认的JdkSerializationRedisSerializer。
6.2 热搜里那个increment()报错的排查全链路
"java中redis使用redistemplate的increment()报错不是integer or out of range"这个热搜词说明这个问题不是个例。我当初也被坑过,现在完整复盘一遍排查思路。
报错信息一般是:
Caused by: org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR value is not an integer or out of range先明确一点:Redis的INCR命令只支持整型值,它会把value解析成长整型然后加一。报错只有两种可能:
第一种,key对应的value确实不是整数。常见的产生原因是序列化方式不一致——之前用RedisTemplate写入了一个对象的引用,之后用StringRedisTemplate去INCR这个key,Redis读出来的是JDK序列化后的二进制串,当然不是整数。第二种,这个key之前被设置成了字符串类型,比如你用SET user:10001 "abc"存过字符串,再对这个key执行INCR,自然报错。
排查步骤我建议这样走:
# 第一步,看这个key的type TYPE user:10001 # 第二步,看这个key的value GET user:10001如果GET出来的值带引号,比如"100",说明value不是纯整数,而是字符串。原因是你的序列化器把数字转成了带引号的字符串,Redis的INCR无法把它当整数处理。解决办法有两个:一是确保使用StringRedisTemplate或者手动配置StringRedisSerializer,让value真正以数字形态存储;二是如果是别人写入的脏数据,先删掉这个key再重新初始化:
stringRedisTemplate.delete("user:10001"); stringRedisTemplate.opsForValue().increment("user:10001");"将redis的数减一"这个搜索词也顺手说一下,对应命令是DECR,在SpringBoot里的写法是redisTemplate.opsForValue().decrement(key)。常踩的误会是不知道INCR和DECR本身就是原子操作,根本不需要先GET再SET,在并发环境下你那种先查后写的方式一定会丢数据。
6.3 分布式锁:从SET NX到Redisson的演进
分布式锁是Redis在微服务场景下最重要的应用之一。最原始的写法是用SETNX命令,只有key不存在时才能设置成功,正好用来抢锁:
SETNX lock_key my_value但这版实现有漏洞:如果拿到锁的线程崩溃了,没有执行解锁操作,这个锁就永远不会被释放,其他线程全部卡死。所以有了加过期时间的改进:
SET lock_key my_value NX EX 30NX保证只有key不存在时才设置,EX 30确保30秒后自动释放。但过期时间也有坑:如果业务执行超过了30秒,锁被自动释放了,另一个线程拿到了锁,这时第一个线程执行完手动删除锁,删掉的是别人刚拿到的锁,这就产生了并发问题。
解决方式是引入Redisson这个库,它实现了自动续期的看门狗机制:默认锁超时30秒,每10秒自动续期一次,只要业务线程没结束,锁就不会过期;同时删除锁时用Lua脚本比较value,确保只能删除自己持有的锁。这是我在生产环境验证过的最省心的方案,引入依赖后使用非常简便:
RLock lock = redissonClient.getLock("order:pay:10001"); if (lock.tryLock(3, 30, TimeUnit.SECONDS)) { try { // 业务逻辑 } finally { lock.unlock(); } }Redisson的锁底层用的就是Redis+Lua脚本,既保证了原子性又保证了正确性。如果你要在面试里聊分布式锁,Redisson的看门狗机制和RedLock算法是绕不开的两个知识点。
6.4 序列化方案对比
把这块单拎出来是因为它值得。我见过无数团队因为序列化方案没统一导致的生产事故——缓存读写不一致、AOF文件膨胀、主从复制数据错乱,源头全在序列化器上。
| 序列化器 | 可读性 | 存储效率 | 兼容性 | 适用场景 |
|---|---|---|---|---|
| StringRedisSerializer | 高 | 低 | Java原生 | 简单字符串缓存、计数器 |
| Jackson2JsonRedisSerializer | 高 | 中 | 跨语言 | 对象缓存、接口响应缓存 |
| JdkSerializationRedisSerializer | 底层二进制 | 低 | 仅Java | 不推荐,产生乱码 |
我目前的SpringBoot项目统一做法是:key一律用String序列化器,value用Jackson序列化器,并且开启activateDefaultTyping的多态支持。这样存进去的value是JSON字符串,既能在Redis Desktop Manager里看懂,跨语言消费也方便。
7. 缓存治理:穿透、击穿、雪崩的完整应对方案
7.1 缓存穿透:查询一个不存在的key
缓存穿透指的是请求查询的数据在数据库里根本不存在,导致缓存永远不命中,每次请求都直接打到数据库。攻击者可以用一个不存在的ID疯狂刷接口,数据库分分钟被压垮。
解决方案最常用的是缓存空值:就算数据库查不到,也在Redis里缓存一个空值或特殊标记,设置短过期时间,比如60秒。这样同一个不存在的key在60秒内不会再次穿透。
进阶方案是布隆过滤器,在请求进缓存之前先用布隆过滤器判断key是否存在,过滤器说不存在就一定不存在,直接返回。布隆过滤器的缺点是存在误判率,它说存在不一定真的存在,但说不存在就一定不存在,正好适合拦截非法ID。
7.2 缓存击穿:热点key突然失效
缓存击穿和穿透容易混淆,击穿指的是一个热度极高的key在过期的瞬间,大量并发请求同时打到数据库。比如秒杀商品详情页,缓存key刚好在秒杀开始前过期,几十万请求瞬间把MySQL打满。
应对手段有两个方向。第一个是互斥锁:当缓存过期时,不是所有请求都去查库,而是先尝试获取分布式锁,只有一个线程能拿到锁去查数据库并回填缓存,其他线程等锁释放后直接从缓存拿数据。第二个是逻辑过期:不给key设置物理过期时间,而是在value里存一个逻辑过期时间戳,查询时对比时间戳判断是否过期,如果过期就返回旧数据同时异步更新缓存。逻辑过期方案的优点是性能好,缺点是数据一致性弱一些,适合读多写少但数据实时性要求不高的场景。
7.3 缓存雪崩:大量key同时失效
雪崩是指大量key在同一时间段集中过期,或者Redis节点直接宕机,导致海量请求全部落到底层数据库。比起穿透和击穿,雪崩的影响范围往往更大。
应对策略分多个维度:
第一,TTL随机化。给缓存key设置过期时间时加一个随机偏移量,比如基础过期时间5分钟,再加0到60秒的随机值。这样所有key不会在同一秒集体失效。我当时的一个项目实践是把整个缓存体系的TTL分散在180到300秒之间随机分布,效果立竿见影。
第二,多级缓存。Redis之上再加一层本地缓存(比如Caffeine),Redis挂了还有本地挡一层,虽然数据一致性变差了,但至少数据库不会被瞬时流量打垮。
第三,Redis本身的高可用。用前文讲的哨兵或集群模式,主节点挂了自动切换从节点,服务不中断。
第四,熔断降级。如果Redis和数据库都扛不住,直接返回降级响应,比如商品详情页返回缓存中的旧版本或者基础兜底数据,宁可页面数据旧一点,不能把数据库打挂。
7.4 缓存一致性的核心矛盾
缓存治理到最后绕不开一个终极问题:数据库更新了,缓存怎么保证一致?
最粗暴的方案是更新数据库后删除缓存,下次查询时重新加载。这个方案在绝大多数场景下够用,因为它允许短暂的不一致窗口。另一个方案是延迟双删:先删缓存,再更新数据库,隔几百毫秒再删一次缓存,为了解决并发场景下旧数据回填缓存的问题。更严谨的方案是监听MySQL的binlog变化,用Canal把数据变更同步推送给Redis更新,这也是很多中大型团队采用的方案。
我个人的分级建议是:业务允许短暂不一致,用"更新数据库后删除缓存";业务要求秒级一致,加MQ异步重试删缓存;业务要求严格一致,别用Redis做缓存了,直接走数据库或者换专业的分布式缓存协议。一致性永远是程度问题,不是非黑即白。
8. 从学习笔记到生产实战的最后一段体会
学Redis最忌讳的是只记命令、不建体系。命令是死的,你就算把官方文档里的所有命令背下来,遇到"缓存击穿用什么锁""AOF文件膨胀怎么办""主从延迟怎么优化"照样一头雾水。
我的切身体会是,这个领域性价比最高的学习路径是:先用Docker快速拉起一主两从三哨兵的环境,把故障转移手动触发几次,亲眼看看主节点挂了系统是怎么自愈的;再把SpringBoot项目里的缓存场景全部用Redis重写一遍,Set与ZSet的实用价值会在你的代码里自然浮现;最后再把缓存穿透、击穿、雪崩的防护代码逐个落地,配合压测工具观察QPS变化,你会对Redis的能力边界有非常真实的感觉。
最后分享一个小技巧:把生产环境Redis的slowlog get 100定期翻一翻,那些执行时间超过10毫秒的命令,会清清楚楚告诉你哪些key的设计是不合理的。我靠这个命令发现过好几处大Value的隐患,改完主从延迟立刻降了一半。这大概是Redis运维里性价比最高的一项检查,比任何监控面板都直接。