1. 从零开始认识Redis:它到底是什么,能解决什么问题
先聊一个很多新手都会问的问题:Redis到底是什么?网上资料一大堆,但大部分要么太学术,要么太零散。我用最简单的一句话概括:Redis是一个基于内存的键值对存储系统,你可以把它理解成一个跑在服务器上的超级字典——你给它一个key,它立刻还给你对应的value,速度极快,快到什么程度呢?官方数据是单机可以支撑十万甚至更高的QPS(每秒查询次数)。
我第一次接触Redis是在做一个高并发的商品秒杀项目,数据库扛不住瞬间流量,服务器CPU直接打满,页面响应时间从几十毫秒飙升到好几秒。后来把热点数据接入Redis做缓存,瞬间压力就降下来了。从那以后我就意识到,Redis不是“用不用”的问题,而是“怎么用好”的问题。
它到底能做什么?核心场景我总结成四类:
- 缓存:把热点数据、高频查询结果放在内存里,减少数据库压力,这是最普遍的使用方式。
- 分布式锁:多个服务实例同时操作同一份资源时,用Redis保证只有一个实例能拿到执行权,避免并发冲突。
- 消息队列:利用它的List结构或者Stream类型做简单的消息发布订阅,很多轻量级场景不必引入重型MQ。
- 计数器/排行榜:INCR、ZADD这些命令天然适合做点赞数、播放量、实时排行榜一类的统计功能。
这篇文章面向三类读者:刚接触Redis、急着做技术选型和架构设计的人;已经在项目里用了Redis但停留在“存进去、取出来”层面的开发人员;以及准备面试、需要系统梳理Redis知识体系的人。我会结合自己踩过的坑和实战代码,把安装、数据类型、持久化、主从哨兵、集群、分布式锁、可视化工具这些内容从头到尾捋一遍。
2. 安装Redis的完整实践:Windows、Linux和Docker三种路线
2.1 Windows下安装Redis的两种正确姿势
Redis官方并不原生支持Windows,官网下载页面通常只提供Linux版本。但国内很多开发者本地开发机就是Windows,所以这里有两种常见的解决路线。
第一种:使用微软维护的Windows移植版。这个方案的问题是版本较老,官方已停止维护,功能上缺少Redis 6.0以后的一些新特性(比如多线程IO、ACL权限控制)。如果你只是本地写写简单Demo,拿来做测试,上面能找到zip压缩包,解压后直接运行redis-server.exe即可。但要注意,这种老版本在Windows上兼容性一般,我第一次用时遇到过程序崩溃,后来就转向了第二种方案。
第二种:用WSL2装Linux子系统,然后在里面安装官方正版Redis。这种方式更接近生产环境,我个人比较推荐。具体流程是先打开Windows的“适用于Linux的Windows子系统”功能,然后从微软商店安装Ubuntu,进入Ubuntu终端后执行:
sudo apt update sudo apt install redis-server安装完成后,先修改配置再启动:
sudo vim /etc/redis/redis.conf # 将 supervised no 改为 supervised systemd # 将 bind 127.0.0.1 ::1 保留,本地访问足够 sudo systemctl start redis-server redis-cli ping # 如果返回 PONG,说明安装成功第三种是Windows上使用Docker Desktop跑Redis镜像,这个放到下一节一起讲,因为它和Linux版的Docker命令完全一致。
2.2 Docker镜像安装Redis:最省心的方式
我在多台服务器上部署Redis的经验告诉我,用Docker安装Redis是所有方式中最省心、最不易出错的。不管你是Ubuntu、CentOS还是Windows,只要装了Docker,命令基本统一。
拉取官方镜像:
docker pull redis:7.0运行一个带持久化和密码的实例:
docker run -d \ --name redis-server \ -p 6379:6379 \ -v /data/redis/conf/redis.conf:/etc/redis/redis.conf \ -v /data/redis/data:/data \ redis:7.0 redis-server /etc/redis/redis.conf解释一下这几个参数的含义:-d表示后台运行,--name给容器命名,-p把宿主机的6379端口映射到容器的6379端口,-v做目录挂载。这里有个关键细节:容器内的Redis默认是没有任何持久化配置的,如果直接docker run redis,容器删掉数据就全丢了。所以生产环境一定要挂载配置文件和data目录。
如果你是快速测试,不想写配置文件,可以这样简化:
docker run -d --name redis-test -p 6379:6379 redis:7.0然后进入容器查看日志:
docker logs -f redis-test注意:
docker run时如果没加--restart=always,服务器重启后容器不会自动拉起,生产环境建议加上。
2.3 Linux环境(CentOS/Ubuntu)源码编译安装
生产服务器上如果不想用Docker,想直接装原生Redis,常见有两种方式:包管理器安装和源码编译安装。
Ubuntu/Debian直接apt install redis-server就行,但CentOS默认仓库里的Redis版本可能比较旧。这里我推荐源码编译安装,因为可以精确控制版本。Redis 7.0版本的源码编译步骤如下:
wget https://download.redis.io/releases/redis-7.0.12.tar.gz tar -zxvf redis-7.0.12.tar.gz cd redis-7.0.12 make -j4 make installmake install默认会把可执行文件放到/usr/local/bin目录下,包括redis-server、redis-cli、redis-sentinel等。编译完成后,创建配置目录并拷贝配置:
mkdir -p /etc/redis cp redis.conf /etc/redis/然后修改几个关键配置项:
daemonize yes requirepass yourpassword appendonly yes启动并验证:
redis-server /etc/redis/redis.conf redis-cli -a yourpassword ping提示:源码编译时建议先安装gcc和make,CentOS执行
yum install -y gcc make,Ubuntu执行apt install -y build-essential,否则make会报错。
3. Redis核心数据类型详解:不只是存String
3.1 五种基础数据类型的使用场景与实操命令
Redis的数据类型是它的灵魂,面试必问,实际开发中也最常踩坑。我按使用频率逐个讲清楚。
String字符串类型:这是最基础的类型,可以存任意字符串,包括数字和二进制数据。典型用途是缓存用户信息、商品详情、Session共享。命令包括SET、GET、INCR、DECR、EXPIRE。举个例子:
SET user:1001 '{"name":"张三","age":18}' EXPIRE user:1001 3600 GET user:1001 INCR page:viewHash哈希类型:适合存对象,一个key对应一个field-value映射表。比如用户信息,如果用String存JSON,改一个字段得整体重新写入;用Hash的话,可以单独更新某个字段。命令有HSET、HGET、HGETALL、HDEL。
HSET user:1001 name "张三" age 18 city "北京" HGET user:1001 name HINCRBY user:1001 age 1List列表类型:底层是双向链表,支持从两头操作,适合做消息队列、最新消息列表。常用命令有LPUSH、RPUSH、LPOP、RPOP、LRANGE。我用它做过一个简单的站内信功能,新消息LPUSH进去,用户读取时LRANGE拉取。
LPUSH message:1001 "msg1" "msg2" LRANGE message:1001 0 -1Set集合类型:元素无序且不可重复,非常适合做去重、共同好友、标签系统。命令有SADD、SMEMBERS、SINTER(交集)、SUNION(并集)。
SADD user:1001:tags "java" "redis" "python" SADD user:1002:tags "redis" "mysql" SINTER user:1001:tags user:1002:tags # 返回交集,即两个用户共同关注的标签ZSet有序集合:在Set的基础上增加了score打分字段,Redis会按score自动排序。典型应用是排行榜、延时队列。命令有ZADD、ZRANGE、ZREVRANGE,其中ZREVRANGE按分数从高到低取排名。
ZADD leaderboard 89 "player1" 95 "player2" 78 "player3" ZREVRANGE leaderboard 0 -1 WITHSCORES3.2 Java中RedisTemplate操作数据类型的坑与正确姿势
实际Java项目里,我们不会直接敲Redis命令,而是用Spring Data Redis封装的RedisTemplate。这里有一个经典大坑:默认序列化方式导致存入Redis的key和value带着奇怪的前缀。
我第一次用RedisTemplate存数据,打开可视化工具一看,key变成了一串\xac\xed\x00\x05t\x00的乱码。这是因为默认用的JDK序列化将对象转成了二进制格式。解决办法是自定义一个RedisTemplate,把key的序列化器改成StringRedisSerializer,value则根据需要选择GenericJackson2JsonRedisSerializer或自定义的Jackson序列化器。
配置代码如下:
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); StringRedisSerializer keySerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(keySerializer); template.setHashKeySerializer(keySerializer); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.afterPropertiesSet(); return template; } }搞清楚序列化问题之后,还有一个使用上的坑:很多人不知道RedisTemplate和StringRedisTemplate是两回事。StringRedisTemplate默认所有key和value都按字符串处理,适合纯字符串场景。而自定义的RedisTemplate<String, Object>可以存对象。两者序列化方案不一致,混用会导致数据读出乱码,实际项目中要统一。
3.3 踩坑实录:increment()报错"not integer or out of range"的排查过程
这是我接手一个项目时真实遇到的问题。线上日志里一直在报Redis执行redisTemplate.opsForValue().increment(key)时出现异常,提示value不是整数或超出范围。刚开始我没当回事,以为是某个脏数据把这个key写入了非数字字符串。但后来发现告警频率越来越高,才意识到问题的严重性。
排查思路是这样一步步展开的:
第一步,确认异常来自哪一行代码。用分布式ID生成器的场景,有一个方法是用increment()实现每天自增ID,key的格式是biz:serial:20240601。这一步基本可以定位到业务含义。
第二步,连接Redis查看这个key当前的值。用redis-cli执行GET biz:serial:20240601,发现返回结果是一个很长的JSON字符串,里面还有一个用户昵称字段。看到这个我基本就明白了:这个key在别的地方被复用了,其他地方向同一个key写入了对象类型的值,序列化后是一串字符串,导致increment()执行时报错。
第三步,检查代码中所有使用到这个key的位置。最后发现是一个缓存用户资料的逻辑用了同样的key前缀,但业务含义完全不同。解决方法是把各业务线的key前缀彻底分离,并加上了项目名和业务模块名。我习惯的key命名格式是项目名:业务模块:业务标识:唯一ID,比如user-center:profile:1001和user-center:serial:20240601。
这个问题的根因其实是key命名不规范。Redis中所有类型的key共用一个全局命名空间,不同业务之间如果前缀没有隔离,出现这种冲突是必然的。排查这个问题的过程中,我也养成了一个习惯:在代码注释里写明每个key的数据类型和用途,同时每次打印日志时尽量带上key。
经验:Redis的
increment()只能操作整数值。如果value是浮点数,可以改用incrbyfloat;如果value里带引号或空格,执行也会失败。防患于未然的最佳做法是SET key 0初始化,确保value是整数类型。
4. Redis持久化机制:AOF和RDB怎么选
4.1 RDB快照的原理与配置
Redis是内存数据库,数据都在内存里,如果机器断电或进程崩溃,内存中的数据全部丢失。持久化就是为了解决这个问题。Redis提供两种持久化方案:RDB(Redis DataBase)和AOF(Append Only File)。
RDB的原理我打一个比方:它就是把当前内存中所有数据拍一张“快照”,写到磁盘上。恢复的时候直接把这张快照加载回内存,启动速度快,文件小,适合做备份和灾备。RDB的触发方式有三种:
- 手动执行
SAVE命令(阻塞)或BGSAVE命令(后台fork子进程执行,不阻塞) - 配置文件中设置自动快照条件,比如
save 900 1表示900秒内至少1次修改就自动触发 - 关闭Redis时自动保存
常见配置项如下:
save 900 1 save 300 10 save 60 10000 stop-writes-on-bgsave-error yes rdbcompression yes dbfilename dump.rdb dir /var/lib/redis需要注意,RDB的缺点是数据丢失窗口较大。假如你配置了60秒保存一次,那么在最后一次保存到故障之间这60秒内修改的数据,重启后就没了。对数据一致性要求高的场景,单靠RDB不够。
4.2 AOF日志的原理与三种写回策略
AOF的思路和RDB完全不同,它记录的是Redis收到的每一条写命令,追加到日志文件末尾。恢复数据时把文件里的命令重放一遍即可。这就好比记账明细:RDB只记录最终余额,AOF记下每一笔流水。
AOF的核心配置项是appendfsync,它决定日志刷盘的频率,有三种策略:
| 策略 | 行为 | 优点 | 缺点 |
|---|---|---|---|
| always | 每次写命令都同步刷盘 | 数据最安全,最多丢1条命令 | 性能最差,频繁IO |
| everysec | 每秒刷一次盘 | 性能和安全性平衡 | 极端情况下丢1秒数据 |
| no | 由操作系统决定刷盘时机 | 性能最好 | 数据丢失不可控 |
生产环境我一般推荐everysec,这也是Redis默认配置。它在性能和安全性之间取得平衡,绝大多数业务场景都能接受丢1秒以内的数据。
AOF还有压缩重写机制BGREWRITEAOF,它会根据当前数据集生成最精简的写命令集合,避免AOF文件无限膨胀。比如你对同一个key执行了100次INCR,重写后只会记录一条SET key 100。
4.3 混合持久化:生产环境的最优解
Redis 4.0以后提供了混合持久化方案:aof-use-rdb-preamble yes。它的思路是使用AOF日志作为主持久化方式,在AOF重写时,先写入一个RDB格式的快照作为文件头部,再继续追加后续的写命令。
这套方案的好处是:RDB格式加载速度快,AOF命令保证数据完整。宕机恢复时,先加载RDB快照,再重放增量命令,兼顾了启动速度和数据安全性。我在生产环境基本都采用这个配置。
我的配置文件核心片段供参考:
appendonly yes appendfilename "appendonly.aof" appendfsync everysec aof-use-rdb-preamble yes经验:开启AOF之后,如果你改过密码或者做数据迁移,一定要先确认AOF文件完整。我遇到过AOF文件因为磁盘写满而损坏导致Redis启动失败的情况,排查命令是
redis-check-aof --fix appendonly.aof。
5. Redis主从复制、哨兵与集群架构
5.1 主从复制:读写分离怎么配置
单机Redis有一个致命问题:一旦宕机,所有依赖它的服务全部瘫痪。解决高可用的第一步,是配置主从复制。
主从复制的原理一句话:主节点负责写,从节点负责读,主节点把写操作同步到从节点。这样就实现了读写分离和数据冗余。配置从节点非常灵活,可以在配置文件里写:
replicaof 192.168.1.100 6379 replica-read-only yes也可以在命令行动态执行:
redis-cli REPLICAOF 192.168.1.100 6379配置完成后,用INFO replication确认主从状态。如果看到role:slave,且master_link_status:up,说明同步正常。
在实际项目中,我遇到过主从数据延迟的问题。原因是Redis的复制默认是异步的,主节点执行写命令后立即返回,然后再异步同步给从节点。如果主从之间网络延迟大,或者从节点执行同步命令较慢,从节点读到的数据可能是旧的。对一致性要求高的读操作,建议直接读主节点,从节点只承担不敏感数据的查询。
5.2 哨兵模式:自动故障转移的关键
主从复制解决了数据冗余,但解决不了“主节点挂了怎么办”的问题。如果主节点宕机,整个系统还是无法写入。哨兵(Sentinel)就是为此设计的:它会持续监控主从节点的健康状态,当主节点不可用时,自动从从节点中选举出一个新的主节点。
用Docker快速搭建一套哨兵模式的完整流程:
# 创建目录 mkdir -p /data/redis/{master,slave1,slave2,sentinel1,sentinel2,sentinel3}主节点/data/redis/master/redis.conf配置:
port 6379 bind 0.0.0.0 protected-mode no daemonize yes appendonly yes从节点/data/redis/slave1/redis.conf配置:
port 6380 bind 0.0.0.0 protected-mode no daemonize yes replicaof 主节点IP 6379 appendonly yes另一个从节点配置类似,端口改成6381。这里有个关键点:从节点配置replicaof时要指向主节点的实际IP,不能写127.0.0.1,否则容器内访问会出问题。
哨兵节点配置:
port 26379 daemonize yes protected-mode no sentinel monitor mymaster 主节点IP 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 15000三个哨兵的sentinel monitor配置尽量一致。其中最后的数字2表示“至少2个哨兵同意主节点不可用,才触发故障转移”。如果哨兵数量少于3个,这个数字要相应调整,否则永远不会触发切换。
启动哨兵:
redis-sentinel /data/redis/sentinel/sentinel.conf验证哨兵是否正常工作,可以手动kill掉主节点进程,观察哨兵日志,大约几秒后它应该会重新选举出新的主节点。
经验:配置文件里
daemonize yes和Docker的-d参数可能会冲突,在容器里运行建议改为daemonize no,让Redis进程在前台运行由Docker管理。
5.3 Redis Cluster集群:数据分片与高可用
当数据量超过单机内存,就需要集群了。Redis Cluster通过数据分片(Sharding)把数据分散到多个节点上,总共16384个哈希槽(hash slot),每个节点负责其中一部分槽。写入一个key时,Redis计算CRC16(key) % 16384得到槽位,再把数据路由到对应节点。
搭建集群的要点:
# 假设有6个节点,端口6379-6384 redis-cli --cluster create 192.168.1.101:6379 192.168.1.102:6379 \ 192.168.1.103:6379 192.168.1.104:6380 \ 192.168.1.105:6380 192.168.1.106:6380 \ --cluster-replicas 1--cluster-replicas 1表示每个主节点带一个从节点。这样一个3主3从的集群就创建好了。验证方式:
redis-cli -c -p 6379 CLUSTER INFO redis-cli -c -p 6379 CLUSTER NODES使用集群时有一个必须注意的坑:多key操作需要所有key落在同一个槽里。比如MSET、MGET、事务、Lua脚本,如果涉及的key分散在不同节点上,Redis会直接报CROSSSLOT错误。解决办法是用{}指定hash tag:
# 这样的两个key会落在同一个槽 MSET user:{1001}:name "张三" user:{1001}:age 18总结下来,单机是入门,主从是备份,哨兵是高可用,集群是海量数据扩展。前面几步做扎实了,再上集群才不容易出问题。
6. Redis分布式锁:从简单SETNX到Redisson
6.1 分布式锁的经典实现与常见问题
分布式锁解决的痛点很典型:多个服务实例同时操作同一个共享资源(比如扣库存、发优惠券),本地语言层面的锁(如synchronized、Lock)只能锁住当前进程,无法跨服务互斥。这时候就需要一个公共的第三方来协调,Redis正是最常用的实现方案。
最原始的实现是SETNX key value,意思是“如果key不存在则设置”,返回1表示获取锁成功,返回0表示获取失败。释放锁时调用DEL key。
但直接用SETNX有几个严重问题。
第一个问题是死锁:如果获取锁的进程在释放锁之前崩溃,key永远不会被删除,其他进程永远拿不到锁。解决办法是给key设置过期时间,比如SET key value EX 30 NX,这样即使进程崩溃,锁也会在30秒后自动释放。
第二个问题是误删锁:假设A进程持有锁,由于执行时间超过了过期时间,锁自动释放了。此时B进程获取到同一个锁。等A执行完要释放锁时,如果直接DEL,就会把B持有的锁删掉。正确做法是在value里存一个唯一标识(比如UUID),释放锁之前先用GET判断value是否等于自己的唯一标识,相等才删除。这个判断和删除必须保证原子性,不能用两步操作,最好用Lua脚本:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end第三个问题是过期时间设置多久合理。设短了,业务没执行完锁就过期了;设长了,如果进程崩溃,其他进程等待太久。比较稳妥的方案是在获取锁之后,启动一个后台线程定期给锁续期,这个机制叫“看门狗”。但自己写这个逻辑非常容易出Bug,所以实际工程中我更推荐直接用Redisson框架。
6.2 Redisson实现分布式锁的实战案例
Redisson是Redis官方推荐的Java客户端之一,它把分布式锁封装成了类似ReentrantLock的使用方式,且内置了看门狗续期机制,默认过期时间30秒,每10秒自动续期一次。
先引入依赖:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.22.1</version> </dependency>然后配置连接:
spring: redis: redisson: config: | singleServerConfig: address: redis://127.0.0.1:6379 password: yourpassword使用锁的代码:
@Autowired private RedissonClient redissonClient; public void deductStock(String productId, int count) { RLock lock = redissonClient.getLock("lock:product:" + productId); try { // 尝试获取锁,最多等待5秒,锁自动释放时间30秒(看门狗会自动续期) boolean isLocked = lock.tryLock(5, TimeUnit.SECONDS); if (!isLocked) { throw new RuntimeException("系统繁忙,请稍后重试"); } // 业务逻辑:扣减库存 int stock = getStockFromRedis(productId); if (stock < count) { throw new RuntimeException("库存不足"); } setStockToRedis(productId, stock - count); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }这段代码有几个细节值得注意:tryLock(5, TimeUnit.SECONDS)表示最多等待5秒获取锁,超过就直接放弃,避免了线程无限阻塞。finally里释放锁之前要判断isHeldByCurrentThread(),防止在等待获取锁失败后误释放别人的锁。
Redisson还提供了公平锁getFairLock、读写锁getReadWriteLock,以及RSemaphore信号量,满足不同业务需求。没有特殊要求的话,默认的getLock就够用了。
7. Redis可视化工具推荐与对比
7.1 Redis Desktop Manager和Another Redis Desktop Manager
没有可视化工具之前,我调试Redis全靠命令行,key一多就很不直观。后来用上可视化工具,管理、排查数据确实方便不少。
先说经典工具Redis Desktop Manager(简称RDM),它是较早流行的Redis图形化客户端,支持Windows、macOS、Linux。连接Redis时需要填写host、port、auth信息,支持SSH隧道连接远程服务器。
但RDM在2022年之后变成了商业软件,免费版功能受限。社区随之推出了Another Redis Desktop Manager(简称ARDM),UI风格清爽,安装包体积小,支持key的模糊搜索、查看TTL、逐行编辑JSON数据,还支持多标签页连接多个Redis实例。我现在的主力工具就是ARDM,跨平台体验不错,GitHub上开源可以直接下载。
这两个工具的适用场景有区别。如果你只需要偶尔看看key、清空某个库、手动执行几条命令,ARDM免费版完全够用。如果团队里有人已经有了RDM的授权,用它的专业SSH通道管理线上服务器也很顺手。选型建议是团队统一,避免有人用命令行有人用不同工具,排查问题时协作效率更高。
7.2 通过Docker搭建Web版可视化工具
有些生产环境不允许开发人员直接连Redis端口,这时候可以部署一个Web版的可视化工具。我常用的方案是redisinsight,这是Redis官方出的可视化界面,功能最强,支持查看内存分析、慢日志、命令监控、CRDT。
用Docker一键启动:
docker run -d \ --name redis-insight \ -p 8001:8001 \ -v redisinsight:/db \ redis/redisinsight:latest启动后浏览器访问http://服务器IP:8001,界面里就可以添加Redis连接,填写IP、端口和密码,即可在线管理数据。RedisInsight对Redis 6.0+的支持非常完善,可以查看每个key的内存占用、执行命令的耗时分析,做性能调优时很实用。
7.3 快速查看Redis日志的几种方式
排查Redis问题,日志是第一步。日志的位置取决于你的启动方式和挂载路径。
直接运行redis-server时,日志默认输出到stdout(标准输出)。如果配置了logfile /var/log/redis/redis.log,则写入指定文件。用Docker部署时,可以用docker logs redis-server查看容器标准输出日志。如果挂载了配置文件并指定了logfile,日志会写到宿主机挂载目录中。
查看正在执行的命令的耗时,可以通过慢日志:
redis-cli SLOWLOG GET 10 redis-cli CONFIG SET slowlog-log-slower-than 10000第一条命令获取最近10条慢日志,第二条命令把慢日志阈值设置为10毫秒。生产环境我会把阈值设置成20毫秒,只关注明显拖慢系统的命令。通过慢日志配合MONITOR命令(调试用,生产慎用,会拖垮性能),基本能定位大多数性能问题。
8. 高频Redis面试题与避坑清单
8.1 和Redis相关的面试核心考点
最近几年面试基本绕不开Redis,我结合自己做面试官的经验,把考察频率高的几个问题整理成速查表:
| 考点 | 核心回答要点 | 易错点 |
|---|---|---|
| Redis为什么快 | 纯内存操作、单线程避免上下文切换和锁竞争、IO多路复用、高效的数据结构 | 不要只说“单线程”,要提到Redis 6.0的多线程IO |
| 缓存穿透 | 查询一个不存在的数据,请求穿透到数据库 | 解决方式:缓存空值、布隆过滤器 |
| 缓存击穿 | 某个热点key过期瞬间,大量请求同时打到数据库 | 解决方式:互斥锁、逻辑过期 |
| 缓存雪崩 | 大量key同时过期,或Redis宕机,导致数据库被打垮 | 解决方式:过期时间加随机值、集群高可用 |
| 持久化区别 | RDB快照 vs AOF日志 | 回答完区别后补充混合持久化 |
| 数据淘汰策略 | LRU、LFU、随机淘汰、不淘汰 | 八种策略要能说清楚,结合场景 |
| 分布式锁 | SETNX + 过期时间 + 唯一标识 + Lua脚本 | 必须讲清楚误删锁和死锁问题 |
| 主从复制原理 | 全量复制、增量复制、复制积压缓冲区 | 别漏了复制风暴的场景 |
回答这些问题时,单纯的背定义已经不够了。面试官更看重你能不能结合场景说明“为什么这么设计”。比如聊缓存穿透时,你可以补一句“如果业务数据本身就可能不存在,建议先用布隆过滤器过滤一定不存在的key,再让真实查询落到数据库”,这比背概念要加分。
8.2 日常开发必须养成的Redis好习惯
踩过的坑多了,自然就总结出一套使用规范。这里分享几个我一直在执行的Redis使用习惯,可以直接套用到自己项目里:
第一,key命名必须统一带前缀。格式建议是项目名:业务模块:业务标识:唯一ID,例如shop:cart:1001:SKU88321。好处是方便排查问题、按前缀批量清理、避免不同业务key冲突。
第二,给key设置过期时间。很多新手存数据后忘记设置TTL,导致Redis内存只增不减,最终触发内存淘汰策略,影响线上数据。除了少数确实需要常驻的配置数据,其他缓存数据都应该设置合理的TTL。
第三,大key和热key必须治理。大key指单个key的value过大(比如超过几MB),会导致Redis执行命令时阻塞其他请求;热key指单个key被高频访问,可能把某个分片打满。排查命令是redis-cli --bigkeys,它会扫描大key并给出统计。
第四,禁止在生产环境用KEYS命令。KEYS *会阻塞Redis主线程,数据量大时直接造成服务不可用,要用SCAN命令代替,游标式遍历不会阻塞。
第五,监控和告警要前置。建议给Redis加上内存使用率、连接数、命中率、主从同步延迟这几个监控指标并设置告警阈值。内存涨到80%就要告警,而不是等内存满了才被动处理。
8.3 Redis缓存治理的方向
缓存治理不是一次性的事,而是一个持续过程。我们项目里遇到过几次和缓存相关的线上事故,具体表现都是数据库被打崩,事后复盘基本都能归到缓存穿透、缓存击穿、缓存雪崩这三类。
针对这三类问题,我整理了一套完整的治理方案:
- 缓存穿透:引入布隆过滤器拦截不存在的key;查询结果为空时也缓存一个空值,过期时间设短一点(比如30秒),避免脏key堆积
- 缓存击穿:热点数据可以考虑逻辑过期,或者获取缓存时加互斥锁,保证只有一个线程去查数据库重建缓存
- 缓存雪崩:过期时间统一加随机值,比如
baseTime + Random.nextInt(300);Redis一定要部署成高可用集群,主从备份做到位
另外还有一个经常被忽视的细节:尽量减少缓存和数据库的数据不一致窗口。常见的做法是“先更新数据库,再删除缓存”,而不是“先删缓存再更新数据库”。因为后者在更新数据库失败时,缓存已经被删了,下一次请求就会直接打到数据库。而前者即使删除缓存失败,也只会短暂出现脏数据,影响小得多。如果对一致性要求很高,可以配合监听数据库的binlog,异步更新缓存。
9. 一些压箱底的经验和最后的建议
写这篇文章的时候,我特意把这些年用Redis踩过的大坑从头到尾回忆了一遍。每次线上出问题,最后定位到根因时都会发现:不是Redis本身不够稳定,而是我们对它的理解不够深,使用姿势不够规范。
我最想强调的一点是:不管你的项目是单体架构还是微服务架构,Redis的key设计和序列化方案一定要在项目初期就定好规范。因为这个东西一但铺开,后期改起来成本极高。我们项目组后来重新梳理缓存规范时,光迁移线上存量key就花了好几周的时间,中间还出过一次读不到数据的事故。前车之鉴,提醒大家注意。
如果你想尽快掌握Redis,我的建议是:先用Docker跑一个实例,把命令行的增删改查摸一遍,再装一个可视化客户端,熟悉数据结构的变化过程。然后试着用Java或你熟悉的语言写一个RedisTemplate操作Demo,把String、Hash、List、Set、ZSet都过一遍。接着搭建一套主从加哨兵的最小环境,手动kill主节点,观察哨兵怎么切换。等你亲手操作完这四步,你对Redis的掌握程度就已经超过很多只背面试题的人了。
如果在实际操作中遇到问题,优先看redis.log日志,日志里找不到线索就看SLOWLOG,这两个工具能帮你解决90%的疑问。
最后再分享一个小技巧:生产服务器的Redis配置里,我一般会在redis.conf末尾加上一段注释,记录当前实例负责哪些业务、主从拓扑关系、负责人联系方式。下次接手的人或者出问题时的值班人员看到这段注释,能少走很多弯路。