先交代一下:这份《Redis速记》不是什么系统性的官方文档翻译,而是我这几年在项目里用Redis时沉淀下来的一套“查漏补缺”笔记。里面覆盖了从安装部署、数据类型、Java集成、生产配置到常见报错的完整链路,适合正在准备Redis面试题的人、刚接手项目要快速排查缓存问题的人,以及第一次在生产环境搭建Redis集群的开发者。你不需要按顺序读,遇到对应场景直接跳到那一节就行。
1. 为什么你需要一份Redis速记
Redis恐怕是后端开发里“看起来简单、用起来全是坑”的典型代表。平时用CRUD接口写业务代码,好像Redis就是set一个key、get一个key的事;可真到了线上环境,缓存穿透、序列化报错、分布式锁失效、主从切换丢数据,每个问题都能让一个经验丰富的程序员忙活一整天。
我最早开始整理“Redis速记”这个习惯,是因为面试前临时抱佛脚。Redis的面试题范围实在太碎了,从底层数据结构到缓存一致性,从持久化机制到集群方案,各个知识点之间没有强关联,背了忘、忘了背。后来我索性换了个思路——不去背网上的面试题标准答案,而是把自己在项目里真正踩过的坑、真正用过的命令、真正配置过的参数一条条记下来。这份笔记越攒越多,后来竟然成了团队里新人的上手手册。
这份速记的核心价值在于“高频覆盖”。Redis的官方文档有几千页,但一个普通后端开发者在实际工作中反复用到的就那么几十个命令、十来个配置项、四五种典型场景。把高频内容吃透,比泛泛地翻完整个文档有用得多。
所以我的建议是:你不需要记住Redis支持的所有数据类型和所有命令,但必须对下面这几块内容形成条件反射——安装与环境配置、五大基础数据类型的使用场景、RedisTemplate的序列化机制、分布式锁的标准写法、生产环境的持久化和淘汰策略。这几块就是Redis面试题的高频区域,也是线上事故的高发区域。
2. 从零装好一个能用的Redis:安装与配置速记
2.1 Windows环境下装Redis的两种方式
官方其实没有发布Windows版本的Redis,Windows版是微软开源技术团队维护的分支。但这几年情况好多了,你直接搜“windows版本redis下载”,能找到GitHub上维护比较活跃的发行版,解压就能用。
安装步骤很简单,从GitHub下载zip包解压后,目录里会有redis-server.exe和redis-cli.exe。先双击redis-server.exe启动服务端,再双击redis-cli.exe,输入ping,返回PONG就算装好了。
如果想让Redis在Windows后台运行,可以用命令注册成系统服务:
redis-server.exe --service-install redis.windows.conf redis-server.exe --service-start注意:Windows版Redis的问题在于IOCP模型和Linux的epoll模型存在差异,高并发下性能表现不如Linux原生版本。所以我的建议是——Windows版只用来本地开发联调,生产环境一律用Linux。
如果你在Windows上装了Docker Desktop,那还有更省事的方式,直接跑容器:
docker run -d --name redis-dev -p 6379:6379 redis:7.2-alpine一句话总结Windows安装:本地开发用Docker容器最省心,不用处理注册服务、环境变量那一堆破事。
2.2 Linux环境下的Redis安装与守护进程配置
Linux下装Redis,我推荐直接编译安装,不要用系统包管理器自带的版本,因为很多发行版的Redis版本陈旧,缺少新特性。编译安装三步走:
wget https://download.redis.io/releases/redis-7.2.3.tar.gz tar xzf redis-7.2.3.tar.gz cd redis-7.2.3 make编译完成后,src目录下会出现redis-server和redis-cli。手动执行make install能把可执行文件复制到/usr/local/bin。
然后建议把配置文件复制到统一目录管理:
mkdir -p /etc/redis cp redis.conf /etc/redis/启动方式有两种:前台模式直接执行redis-server /etc/redis/redis.conf,生产环境更推荐用systemd管理。创建/etc/systemd/system/redis.service文件,内容大致是:
[Unit] Description=Redis Server After=network.target [Service] ExecStart=/usr/local/bin/redis-server /etc/redis/redis.conf ExecReload=/bin/kill -s HUP $MAINPID Restart=always User=redis Group=redis [Install] WantedBy=multi-user.target配置文件里有几个必改项,不改就是给自己埋雷:
daemonize:改为yes,让Redis后台运行(用systemd管理时保持no)requirepass:设置强密码,Redis默认无密码,暴露到公网等于裸奔bind:生产环境只绑定内网IP,不要绑0.0.0.0appendonly:改成yes,开启AOF持久化protected-mode:如果设置了密码,可以保持默认yes不变
2.3 可视化客户端:从Redis Desktop Manager到免费替代方案
“redis desktop manager”这个关键词的搜索量一直很高,说明大家还是喜欢用GUI工具看数据。RDM以前是免费的,后来改成付费下载了,新版本只能免费试用一段时间,很多项目组都换了替代品。
我目前主力使用的是Another Redis Desktop Manager(简称ARDM),完全免费开源,支持Windows/Linux/macOS。GitHub搜qishibo/AnotherRedisDesktopManager就行。它支持按key前缀过滤、命令行模式、内存分析,还能直接看key的TTL和序列化类型,基本能满足日常开发。
如果追求官方工具,Redis官方自己的RedisInsight也不错,免费的,界面比第三方的都漂亮。RedisInsight有个独门功能——内存分析(Memory Analysis),可以告诉你哪个key占用了大量内存,这在排查线上内存暴涨问题时特别有用。
连接配置很简单,填host、port、password三个字段就能连上。支持SSH隧道连接内网Redis,生产环境需要跳板机时可以用这个功能。
3. 数据类型与核心命令速记:面试和实操都够用
3.1 五种基础数据类型的命令表
Redis的数据类型是面试题的重灾区,但实际写代码时常用的命令就那么几条。我把它们整理成一张速查表,可以保存下来随时翻:
| 类型 | 常用命令 | 适用场景 | 注意事项 |
|---|---|---|---|
| String | SET、GET、SETNX、INCR、DECR、SETEX、MSET | 缓存、计数器、分布式锁、Session | INCR原子自增,适合秒杀库存 |
| Hash | HSET、HGET、HGETALL、HDEL、HINCRBY | 对象缓存(用户信息)、购物车 | 比String省内存,适合结构化字段 |
| List | LPUSH、RPUSH、LPOP、RPOP、LRANGE、LLEN | 最新消息列表、简单消息队列 | BLPOP阻塞读取可替代轮询 |
| Set | SADD、SREM、SMEMBERS、SISMEMBER、SINTER | 去重、共同好友、标签系统 | 无序但可以取交集并集差集 |
| ZSet | ZADD、ZRANGE、ZREVRANGE、ZRANGEBYSCORE、ZINCRBY | 排行榜、延迟队列、限流窗口 | 每个成员有score,按分数排序 |
String是Redis最基本的数据类型,value最大能存512MB,但它不是简单的字符串,底层可以是int、raw或embstr编码。使用INCR/DECR做计数器时,value必须是整数,否则会报ERR value is not an integer or out of range,这个错误后面在Java集成部分会详细讲。
Hash类型特别适合存对象。比如用户信息有username、age、email三个字段,用Hash存储只需要一个key,三个field,修改任何一个字段都不需要整存整取,比序列化成JSON字符串存String更加灵活。
List类型实现“最新列表”很方便。比如新闻系统的首页头条,用LPUSH news:latest articleId把新文章推入列表头,然后用LRANGE news:latest 0 9取前10条,就是天然的时间倒序列表。
Set类型做“共同好友”是经典用法。用户A的好友存在set:user:A,用户B的好友存在set:user:B,执行SINTER set:user:A set:user:B就能直接得到共同好友。
ZSet是Redis里最强大的数据类型。排行榜业务直接用ZADD leaderboard 100 userId添加分数,用ZREVRANGE leaderboard 0 9 WITHSCORES取前十名,分数自带排序,连业务层排序逻辑都省了。另外,利用score存时间戳,ZSet还能实现延迟队列——用ZRANGEBYSCORE delay_queue -inf now取出所有到期的任务。
3.2 高级数据类型:面试加分项与实战场景
基础五种类型之外,Redis还提供了几种高级数据类型,面试时能聊上一两句,说明你不是只会应付CRUD的“API调用员”。
- Bitmap(位图):本质上还是String,但是按位操作。签到场景最常用,用户签到用
SETBIT sign:20240101 100 1表示用户ID为100的用户在1月1日签到了,统计某天签到人数用BITCOUNT。一亿用户一天的签到记录只占约12MB,内存占用小到可以忽略。 - HyperLogLog:基数统计专用。统计UV(独立访客数)时,传统做法是用Set去重,但用户量大了Set内存吃不消。用
PFADD uv:20240101 userId添加访客,用PFCOUNT uv:20240101统计数量,误差只有0.81%,但内存占用恒定约12KB。 - Geo(地理位置):存储经纬度坐标,底层是ZSet实现。用它做“附近的人”功能非常方便,
GEOADD geo:city 116.397 39.909 "beijing"添加城市坐标,GEOSEARCH按半径搜索附近的点。 - Stream(流):Redis 5.0引入的消息队列数据结构,支持持久化和消费者组,比List做消息队列更可靠。
XADD添加消息,XREAD GROUP消费消息,XAUTOCLAIM处理未确认消息。
其中Stream是我现在比较推荐的队列方案,因为它在Redis里面就是原生的持久化队列,不需要依赖外部MQ。当然,如果公司已经在用RocketMQ或Kafka,就不要为了用Redis而用Redis,术业有专攻。
3.3 面试必问的底层原理:为什么Redis这么快
Redis面试题基本绕不开“为什么快”这个问题。我的速记版本是这样回答的:
第一,Redis使用内存存储,所有数据都在内存中操作,内存的随机读写速度是纳秒级的,比磁盘的毫秒级快了几个数量级。
第二,Redis设计了高效的数据结构。比如Hash类型的底层可能是ziplist或hashtable,ZSet的底层是跳表(skip list),跳表让有序集合的查找和插入都能达到O(logN)的时间复杂度。
第三,Redis是单线程模型。单线程避免了多线程编程中的上下文切换和锁竞争开销。这里的单线程指的是执行命令的网络IO和数据读写操作在单线程中串行执行,保证了原子性——所以单个命令的INCR操作天生就是线程安全的,不需要额外加锁。
第四,Redis使用IO多路复用。Redis 6.0之前,网络IO和命令执行都是单线程的,但依靠epoll这样的多路复用机制,单线程也能同时处理成千上万个客户端连接。
关于过期删除策略,Redis采用惰性删除 + 定期删除的组合。惰性删除是访问key时才检查是否过期,定期删除是每隔一段时间主动扫描部分过期key并删除。这种组合避免了只靠定期删除带来的CPU消耗,也解决了只靠惰性删除带来的“过期key一直占内存”的问题。
当内存满了的时候,会触发内存淘汰策略。maxmemory-policy可以配置为allkeys-lru(所有key中淘汰最久未使用的)、volatile-lru(只淘汰设置了过期时间的key)、allkeys-lfu(按访问频率淘汰)等。生产环境最常用的是allkeys-lru,因为业务缓存大部分都能接受被淘汰。
4. Redis在Java项目中的正确打开方式:序列化、分布式锁与常见报错
4.1 RedisTemplate的序列化配置:为什么key全是二进制乱码
Java接入Redis,最常用的就是Spring Data Redis提供的RedisTemplate。项目里引入spring-boot-starter-data-redis依赖后,最坑的地方就来了——默认的RedisTemplate使用JdkSerializationRedisSerializer进行序列化,存进Redis的key会带上一堆二进制前缀,用可视化工具根本看不清,value是Java对象的序列化字节,跨语言也没法读。
解决办法很简单:自定义一个RedisTemplate,把key的序列化器改成StringRedisSerializer,value的序列化器改成Jackson2JsonRedisSerializer或GenericJackson2JsonRedisSerializer。
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // key使用String序列化 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // value使用JSON序列化 GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringRedisSerializer); template.setHashKeySerializer(stringRedisSerializer); template.setValueSerializer(jsonRedisSerializer); template.setHashValueSerializer(jsonRedisSerializer); template.afterPropertiesSet(); return template; } }这里强烈建议直接用StringRedisTemplate配合手动JSON转换来操作数据。StringRedisTemplate的key和value都是String类型,序列化绝对不出幺蛾子:
@Autowired private StringRedisTemplate stringRedisTemplate; // 存 stringRedisTemplate.opsForValue().set("user:1", JSON.toJSONString(user), 30, TimeUnit.MINUTES); // 取 String json = stringRedisTemplate.opsForValue().get("user:1"); User user = JSON.parseObject(json, User.class);这种方案看起来多了一步序列化,但胜在可控。万一线上环境Redis里的value被其他服务改了格式,你也能直接用JSON工具手动解析,排查成本低得多。
4.2 Redis分布式锁的标准化写法与三个经典坑
Redis做分布式锁是高频面试题,也是线上高频踩坑点。先看标准写法,加锁命令:
SET lock:order:1001 unique_value NX PX 30000这条命令的意思是:只有key不存在时才设置成功(NX),同时设置30秒过期时间(PX)。为什么不用SETNX加EXPIRE两条命令?因为这两个操作不原子,如果SETNX成功但EXPIRE失败,锁就没有过期时间,一旦持有锁的进程崩溃,锁永远无法释放。
为什么value要用唯一值?这是为了防止误删别人的锁。场景是这样的:线程A加锁成功,处理业务超过了锁的过期时间(比如30秒),锁自动释放了;线程B拿到锁开始处理;此时线程A处理完,执行DEL lock:order:1001——它删掉的是线程B的锁。如果用唯一值作为value,释放锁时先比较value再删除,就能避免这个问题。
比较后再删除这两个操作也必须原子执行,所以要用Lua脚本:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 endJava里用DefaultRedisScript执行这段Lua脚本。但说实话,日常开发我更推荐直接用Redisson框架,它内置了分布式锁的完整实现:
RLock lock = redissonClient.getLock("order:" + orderId); if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { try { // 业务逻辑 } finally { lock.unlock(); } }Redisson做了“看门狗”续期机制,默认每10秒续期一次锁的过期时间(默认30秒),避免业务没处理完锁就过期的问题。
分布式锁的三个经典坑,我见过都发生了真实事故:
- 忘记设置过期时间:进程崩溃后锁永远不释放,形成死锁。
- 锁误删:A线程释放了B线程的锁,没有用唯一值校验。
- 主从切换丢锁:业务请求在Redis主节点加锁成功,主节点还没同步到从节点就宕机了,从节点晋升为主节点后锁丢了。这个问题要用RedLock算法或者干脆引入ZooKeeper分布式锁来规避。
4.3 RedisTemplate的increment()报错排查实录
“java中redis使用redistemplate的increment()报错不是integer or out of range”——这个问题在热搜词里出现,说明遇到的人不在少数。
完整报错一般是:
io.lettuce.core.RedisCommandExecutionException: ERR value is not an integer or out of range意思是执行INCR命令时,目标key的value不是整数,或者超出64位有符号整数范围。我第一次遇到的时候,第一反应是代码逻辑没写对,排查了半天,最后发现是序列化问题。
最典型的原因有两个:
原因一:value被序列化成了带引号的JSON字符串。默认的JdkSerializationRedisSerializer或者配置的Jackson2JsonRedisSerializer,会把一个Integer对象序列化成"1"而不是1——字符串带上了引号,Redis执行INCR时自然无法识别。解决办法:计数场景必须用StringRedisTemplate,它存的value就是纯字符串,opsForValue().increment(key)执行时Redis能正确识别数字。
原因二:同一个key被其他业务写入了非数字数据。比如先用set key "hello"存了字符串,后面又对同一个key执行increment,必报这个错。排查方式是用可视化客户端或者redis-cli查看这个key的value到底是什么。
我建议把排查思路总结为三步:
- 用
Redis Desktop Manager或redis-cli打开目标key,看value是否带引号或非数字字符。 - 检查
RedisTemplate的序列化配置。计数相关操作用StringRedisTemplate最稳妥。 - 检查这个key是否在项目其他地方被复用,如果是,换一个不冲突的key前缀。
4.4 缓存治理的三大经典问题:穿透、击穿、雪崩
缓存治理也是热搜词“redis缓存治理”背后的核心内容。这三个“击”几乎每个高并发项目都会遇到,面试必问,线上必踩:
缓存穿透:查询一个不存在的key,缓存和数据库都没有,请求直接打到数据库。黑客如果恶意构造大量不存在的key,数据库瞬间就会被打垮。解决方案:一是布隆过滤器提前拦截不存在的key;二是缓存空值,即使查到数据库为空也缓存一个null,过期时间设短一点(比如60秒)。
缓存击穿:某个热点key在缓存过期的瞬间,大量请求同时穿透到数据库。解决方案:一是互斥锁,只让一个请求去查数据库并重建缓存,其他请求等待;二是逻辑过期,缓存中不设置物理过期时间,而是存一个逻辑过期时间字段,独立线程定期刷新热点数据。
缓存雪崩:大量key在同一时间集体过期,或者Redis集群宕机,导致请求全部打到数据库。解决方案:一是过期时间加上随机数,避免集体过期;二是多级缓存,本地加一层Caffeine缓存,即使Redis挂了还有本地缓存扛住;三是服务熔断降级,数据库扛不住时直接返回默认值或提示信息。
5. 生产环境部署避坑:Docker Compose、主从复制与缓存治理
5.1 Docker Compose部署Redis的配置要点
搜索热词里有“redis docker compose 生产环境部署”,说明越来越多团队直接用容器化方式管理Redis。我最推荐的镜像就是官方redis镜像,不要用第三方精简版镜像,版本不透明,出了问题不好排查。
一个可用于生产环境的docker-compose.yml示例:
version: '3.8' services: redis: image: redis:7.2-alpine container_name: redis-master restart: always ports: - "6379:6379" volumes: - ./redis.conf:/usr/local/etc/redis/redis.conf - redis-data:/data command: ["redis-server", "/usr/local/etc/redis/redis.conf"] environment: - TZ=Asia/Shanghai volumes: redis-data:关键是配置文件redis.conf,生产环境至少要配置这几项:
bind 0.0.0.0 protected-mode yes requirepass your-strong-password appendonly yes appendfsync everysec maxmemory 2gb maxmemory-policy allkeys-lru slowlog-log-slower-than 10000 slowlog-max-len 128maxmemory的容量要根据服务器物理内存规划。我的建议是Redis实例占物理内存的50%到70%,留出足够的内存给操作系统和本机其他进程使用。如果部署的是主从复制架构,从节点也会占用同样多的内存,扩容前必须算清楚。
appendfsync everysec是持久化策略的推荐配置,每秒把AOF缓冲同步到磁盘,性能和数据安全的平衡点。如果数据不允许丢失,改成always,但性能会下降明显。
部署完成后用docker ps看容器是否启动,然后用docker exec -it redis-master redis-cli -a your-password ping验证能否返回PONG。
5.2 主从复制与哨兵配置速记
搜“docker安装redis主从”的人,我猜是想搭一套高可用的读写分离结构。先说主从复制集群怎么做。
首先准备主节点和从节点的配置文件。假设主节点配置redis-master.conf,从节点配置redis-slave.conf,从节点需要增加一行主节点信息:
replicaof 10.0.0.1 6379 replica-read-only yes在Docker环境下,用docker-compose编排三个服务(一主两从)比较方便。主节点和从节点都用同一个镜像,只是挂载的配置文件不同、端口映射不同。组成集群后,主节点负责写操作,从节点负责读操作,从节点通过异步复制同步主节点的数据。
数据同步过程分为两步:第一次建立主从关系时,主节点执行BGSAVE生成RDB快照,把快照发给从节点,这是全量同步;之后主节点将写命令记录在repl_backlog缓冲中,持续增量同步给从节点。这个机制Session说了太复杂,你只需要记住主从之间的数据同步不是实时的,存在秒级延迟,读从库大概率读到的是稍旧的数据。
主从复制主要解决的是读扩展和数据冗余。如果主节点宕机了,需要手动把从节点提升为主节点,这个过程生产环节应该用哨兵(Sentinel)自动完成。哨兵模式的部署更复杂,通常需要至少三个哨兵实例防止脑裂。我建议直接用Redis官方推荐的哨兵机制或者直接用Redis Cluster。如果项目处于中小规模,用一个主节点加一个从节点加三个哨兵就够用了。
5.3 慢查询日志与性能排查命令
Redis的慢查询日志不像MySQL那样家喻户晓,但排查线上性能问题非常有用。慢查询指的是执行时间超过slowlog-log-slower-than阈值的命令,默认10000微秒(10毫秒)。
集中排查慢查询的命令:
# 查看当前慢查询配置 CONFIG GET slowlog-log-slower-than # 设置阈值5ms CONFIG SET slowlog-log-slower-than 5000 # 查看最近的10条慢查询 SLOWLOG GET 10 # 查看慢查询条数 SLOWLOG LEN # 清空慢查询日志 SLOWLOG RESET线上遇到Redis响应变慢,第一件事就是查慢查询日志,看看是不是有KEYS *这种阻塞命令,或者有大批量数据的HGETALL、ZRANGE这类操作。这两个命令阻塞Redis是出了名的,严禁在生产环境使用。遍历key应该用SCAN命令配合游标分批获取。
日志方面,如果Redis容器运行在Docker环境,用docker logs redis-master查看最近输出。如果想记录所有客户端命令,可以在客户端执行MONITOR,但不建议在生产环境长期开着,它会大幅拉低Redis性能,只适合短时间的辅助排查。
5.4 缓存一致性的最终解决方案
缓存治理绕不开一个终极问题:数据库和缓存的数据如何保持一致。
目前业界最主流的是Cache Aside Pattern(旁路缓存模式)。读请求先查缓存,命中直接返回;未命中则查数据库,写入缓存后返回。写请求更新数据库,然后删除缓存。为什么更新数据库后是“删除缓存”而不是“更新缓存”?因为更新缓存需要额外考虑并发写竞争问题,删除缓存则让下次读请求乖乖去数据库取最新值再回填,简单可靠。
但这里有个经典坑:先更新数据库再删除缓存,如果删除失败,缓存里就是旧数据。我的解决方案是延迟双删:
- 更新数据库
- 删除缓存
- 休眠500毫秒(根据业务调整)
- 再次删除缓存
两次删除中间休眠的时间,是为了让并发读请求期间产生的旧缓存也有时间被覆盖,第二次删除把残留的旧缓存清掉。这个方案不是完美的,但实操中简单有效,比引入消息队列异步重试更轻量。
如果对一致性要求极高,可以引入Canal订阅MySQL binlog,异步刷新Redis缓存。这个方案的原理是:Canal伪装成MySQL从节点,接收binlog变更事件,业务服务监听事件后主动更新或删除缓存。即使缓存更新失败,也有消息队列做重试兜底。
6. 速记之外的几点个人体会
最后分享一点我个人的实战感受。
Redis的学习曲线不是线性的,它是跳跃式的。你可能花了一周把数据类型、命令都背熟了,但真正接手一个高并发项目时,遇到的问题跟文档里写的完全不一样——不是你不会用SET命令,而是不知道什么场景该用哪种缓存策略,什么情况下Redis的锁会失效,数据序列化方式不同会带来什么样的隐藏Bug。
所以我特别推荐两类人建立属于自己的“Redis速记”笔记。一类是准备面试的开发者,把高频面试题整理成自己的理解,不要背标准答案,因为面试官追问几个“为什么”就能看出你是背的还是懂的;另一类是刚接手新项目的人,遇到一个线上问题,就把排查过程、根因分析、解决方案记下来,下次再遇到同类问题,可以直接照着上次的路径下手。
一个小技巧:给这套速记建一个“踩坑记录”区域,专门记那些让你加班到凌晨的问题。工作越久你会发现,真正值钱的不是学了多深的理论,而是你踩过的坑比同事多、排查问题的路径比别人熟。拿着这份速记,能让后来者少走弯路,这就是它最大的价值。