1. 为什么我建议你认真学一下Redis
先说结论:Redis几乎是现代后端开发绕不开的一个组件,做Web开发、做接口服务、做数据分析、甚至做AI应用的工程落地,都可能碰到它。如果你面试后端岗位,十次里面有七八次会被问到Redis;如果你接手一个老项目,大概率也会看到Redis的身影。早点把它弄清楚,后面省很多事。
我第一次接触Redis其实是有点抵触的,因为当时觉得“缓存嘛,不就是存个键值对”,没必要专门学。后来真上了场景才发现,这句话就像说“汽车嘛,不就是四个轮子加个沙发”一样,大方向没错,但离真正会开还差得很远。你要往缓存里写什么、什么时候失效、内存不够怎么办、数据崩了会不会丢、多个实例怎么同步,这些才是真正决定系统稳不稳的关键。
Redis本质上是一个开源的、基于内存的键值存储系统。它把数据放在内存里,所以读写速度非常快,单实例QPS轻轻松松能到十万以上,这是传统关系型数据库很难做到的。同时它又不只是简单的get/set,它还提供了丰富的数据结构,比如字符串、哈希、列表、集合、有序集合,还有发布订阅、Lua脚本、事务、持久化、分布式锁等一堆实用功能。
记住一个核心概念:Redis被称为“数据结构服务器”,而不是“缓存的数据库”,因为它的核心卖点就是那五种基本类型和在此基础上衍生出的能力。理解了这个定位,你就知道它为什么能在缓存之外还承担那么多职责。
这篇内容适合谁?已经会写代码但对Redis只有模糊概念的开发者,准备面试需要系统梳理Redis知识点的同学,以及在生产环境里用Redis但经常遇到诡异问题的朋友。我会把安装部署、核心数据类型、典型场景、持久化机制、常见坑全部过一遍,全程用自己的实际经验来讲。
1.1 先弄明白Redis到底解决了什么问题
在讨论命令和配置之前,先把“为什么需要Redis”这件事想明白。日常开发中最典型的问题有三个。
第一个是性能瓶颈。数据库的磁盘IO和行级操作再快,也扛不住高并发下的重复查询。比如一个热门商品详情页,每秒几千人访问,每次都去MySQL里查库存和价格,数据库会被打成热点。Redis把所有高频数据塞到内存里,查询只在内存中完成,速度快了两个数量级。
第二个是数据结构的低效处理。很多业务场景需要临时存储一些结构化的数据,比如一个用户最近浏览过的商品列表、一个直播间的在线观众集合、一个排行榜。用关系型数据库当然也能做,但每条记录都要建表、加索引、写SQL,特别繁琐。Redis的List、Set、ZSet直接对应这些场景,一个命令就能搞定。
第三个是系统组件之间的协作。分布式系统里,多个服务实例需要同步状态,谁在运行、谁持锁、哪个节点离线,这些轻量级协调信息放数据库太重,放本地内存又不对,Redis就成了一个比较稳妥的中间地带。
Redis的定位很清晰:它不是一个万能的数据库,而是高并发场景下的一种加速器和粘合剂。想清楚这一点,你在做技术选型时就不会迷茫。
1.2 什么人应该看这篇内容
如果你正在学后端开发,还没有接触过Redis,这篇可以作为你的第一份入门资料。我会从零开始演示安装,命令一条一条讲,数据结构一个一个拆。
如果你已经用了Redis,但只是简单当缓存用,比如put一个key、get一个value,那我建议你看看后面关于数据结构和场景的章节。Redis至少有五成以上的能力你可能还没用到,而这些能力能让你的代码结构简洁很多。
如果你正在准备面试,那这篇内容其实很多考点都覆盖了。缓存穿透、缓存击穿、缓存雪崩、持久化机制、分布式锁、过期策略,这些高频问题我都会聊到,而且是用实际场景来聊,不是死背概念。
好了,废话不多说,直接进入正题。
2. 安装、启动和第一行命令
2.1 安装Redis的几种方式
本地学习的话,我建议直接用Docker跑一个实例,干净、快速、不污染系统环境。装完之后随时可以删了重建,换版本也很方便。
# 拉取镜像并启动一个名为redis-test的容器 docker run --name redis-test -p 6379:6379 -d redis:7这条命令会拉取最新的Redis 7镜像,把容器内的6379端口映射到本机的6379,这样我们直接用Redis客户端就能连上。
如果你不想用Docker,也可以直接用系统包管理器安装。
# Debian/Ubuntu sudo apt-get install redis-server # CentOS/RHEL sudo yum install redis安装完成后,启动服务和验证状态:
sudo systemctl start redis-server sudo systemctl status redis-server还有一种方式是从源码编译,这个一般用于定制化安装或者生产环境自己编译优化。步骤是下载源码包、解压、进入目录执行make和make install。我个人建议新手不要一上来就搞源码编译,除非你已经很清楚自己需要哪些编译选项,否则纯粹是浪费时间。
2.2 连接Redis并执行第一条命令
安装好之后,用Redis自带的命令行客户端连接:
redis-cli # 如果连接成功会看到 127.0.0.1:6379>如果Redis不是跑在本机,或者改了端口,可以指定地址和端口连接:
redis-cli -h 192.168.1.100 -p 6380连接成功后,先执行一条最简单的命令试试:
127.0.0.1:6379> SET hello world OK 127.0.0.1:6379> GET hello "world"SET表示写入一个键值对,GET表示读取。OK和"world"就是Redis给我们的响应。所有功能都逃不开这个套路:发命令、收响应。
如果要查看当前Redis里有多少个键,可以用DBSIZE。如果要清空当前库,可以用FLUSHDB,但慎用,生产环境一旦执行就是灾难。
127.0.0.1:6379> DBSIZE (integer) 1 127.0.0.1:6379> KEYS * 1) "hello"这里我要多说一句,KEYS *在生产环境千万不要乱用,它会阻塞Redis主线程,数据量大的时候直接卡死。后面我会专门讲这个问题。
2.3 几个实用配置项和密码设置
Redis默认配置可以跑通开发环境,但有几个配置项我建议你一开始就设置好。
首先是绑定地址,默认配置通常只允许本机访问。如果你需要远程连接,需要修改bind配置和protected-mode选项,但我要强调,直接把Redis暴露到公网是非常危险的做法,Redis此前多次因为未授权访问被入侵。千万别图省事不做鉴权。
设置密码很简单,在redis.conf里找到requirepass这一行,改成你要的密码:
requirepass your_strong_password改完保存并重启Redis,再连接就需要先认证:
redis-cli -a your_strong_password或者连接后执行AUTH命令:
127.0.0.1:6379> AUTH your_strong_password OK另外一个很关键的配置是maxmemory,也就是最大内存限制。Redis默认可以用完系统的所有内存,这在生产环境里是个隐患。建议设置一个合理上限,例如:
maxmemory 512mb当数据量达到上限之后,Redis会根据淘汰策略来清理旧数据,后面我会具体说。
提示:改配置文件之前先备份原文件。我第一次手动把配置改挂的时候花了半小时才找到问题,就是一个空格导致配置项没被识别。
2.4 图形化工具
如果觉得命令行不够直观,可以用一些图形化管理工具。常见的包括Another Redis Desktop Manager、RedisInsight等。安装之后填上主机地址、端口、密码就能连接,可以方便地浏览key、查看value、执行命令。
不过我要提醒一句,不管用什么图形化工具,命令行这个基本功不能丢。因为很多运维场景只有命令行,比如服务器上没有桌面环境,或者容器内连接Redis,你总不能每次都装一个图形工具。
我自己的习惯是:日常排查用命令行,批量查看数据和检查key分布用图形工具。两者互补,效率最高。
3. 五大核心数据结构,一个都不能少
3.1 String:最基础但最容易用错的类型
String是Redis里最简单的类型,也是我们最常用的类型。它不只是存字符串,数字、JSON序列化后的字符串、甚至是二进制数据都可以存。单次存储的最大值是512MB。
常用命令:
SET key value # 写入 GET key # 读取 MSET k1 v1 k2 v2 # 批量写入 MGET k1 k2 # 批量读取 INCR key # 自增1 DECR key # 自减1 EXPIRE key seconds # 设置过期时间 TTL key # 查看剩余存活时间这里的INCR和DECR用得非常多,比如计数器、库存扣减、访问次数统计,都要靠这种原子自增操作。
String最容易踩的坑是什么?是很多人在存复杂对象时手动做对象的拼串和解析。正确做法是先把对象序列化成JSON,然后用一个key去存,读取后再反序列化。但我见过不少人把JSON展开成多个key来存,比如用户对象的每个字段都单独存一个key,这样不仅代码冗余,还丧失了原子性。
官方文档里专门有提到String的使用场景,包括缓存对象、计数器、分布式会话等。凡是能序列化成字符串的,都可以用String来扛。
3.2 Hash:存对象的正确姿势
Hash类型在Redis里表示一个键对应一个“字段-值”的映射表,听上去很适合存对象,也确实最常用。
HSET user:1001 name "Alice" age 25 HGET user:1001 name HGETALL user:1001 HDEL user:1001 age实际场景中,用户信息、商品详情、配置项,这类“一条记录有很多字段”的数据都可以用Hash。它可以只读取和更新其中某一个字段,不需要把整个对象序列化出来。这一点比String存JSON更灵活,尤其在只更新一个字段时能省不少流量。
Hash的一个小技巧是配合批量命令:
HMSET user:1002 name "Bob" age 30 email "bob@example.com" HMGET user:1002 name email要注意的是,Redis 4.0之后HMSET已经被标记为不建议使用的旧命令,功能上和HSET带多组参数是一样的,所以直接用HSET就行。
我见过一个比较典型的误区是,有人用Hash存了上百万个key,每个key的field数量特别多,结果内存占用比预计高很多。这是因为Hash在field数量少的时候有一种内部编码叫ziplist,相对省内存;但一旦field数量超标,就会升级为hashtable编码,内存开销陡增。这也是一个值得留意的点。
3.3 List:消息队列和操作历史
List类型就是一个双向链表,两头都可以push和pop。
LPUSH queue task1 RPUSH queue task2 LPOP queue RPOP queue LRANGE queue 0 -1List的典型用途有三个。
第一是当作简单的消息队列。生产者LPUSH,消费者RPOP,一进一出就实现了最简单的队列语义。但这只能用来接轻量级的任务,如果想要消息可靠性、ACK机制、重新投递,还是得用专业的消息队列系统。
第二是保存用户的操作历史。比如一个用户最近的浏览记录,每次浏览往List里push一条,再通过LTRIM修剪列表长度,只保留最近20条。
LPUSH user:1001:history article:1001 LTRIM user:1001:history 0 19第三是取最近N条数据。配合LRANGE可以很方便地分页,比如取最近十条操作:
LRANGE user:1001:history 0 9我对List的建议是:别在高并发场景下把它当成唯一队列,因为BRPOP、LPUSH这些命令虽然好用,但出错重试、消息丢失这些机制完全要靠业务自己补,成本不低。
3.4 Set:去重是它的看家本领
Set是无序的字符串集合,它最核心的特点就是自动去重,并且支持集合运算。
SADD tags:article:1 "java" "redis" SADD tags:article:2 "java" "mysql" SMEMBERS tags:article:1 SINTER tags:article:1 tags:article:2 # 交集 SUNION tags:article:1 tags:article:2 # 并集 SDIFF tags:article:1 tags:article:2 # 差集Set的应用场景很多。
一是活动去重。用户每参加一个抽奖活动,把他的ID加到一个Set里,天然防止重复参与和重复中奖。
二是标签系统。一篇文章有多少个标签,往Set里加就行了,统计标签数量就是SCARD。
三是共同好友。两个Set做交集,瞬间算出共同关注了哪些人。
还有一个很有趣的玩法是随机抽取,SPOP可以从集合里随机弹出元素,适合做抽奖类功能:
SPOP lottery:1003.5 ZSet:排序由你来定
ZSet,也叫有序集合。它在Set的基础上给每个元素加了一个分数,Redis按照分数从小到大自动排序。这是我认为Redis中最值得深入理解的一个类型。
ZADD leaderboard 100 "player1" ZADD leaderboard 90 "player2" ZRANGE leaderboard 0 -1 ZREVRANGE leaderboard 0 -1 ZSCORE leaderboard player1 ZINCRBY leaderboard 10 player1ZSet的典型场景非常多。
排行榜是最经典的场景。实时更新积分,用ZINCRBY给某个用户加分,排行榜自然就排出来了:
ZINCRBY user:score:2024 10 "user_id_123" ZREVRANGE user:score:2024 0 9 WITHSCORES延迟队列也可以用它实现。把任务的到期时间戳作为score,轮询ZRANGEBYSCORE把score小于当前时间戳的任务取出来执行。
限流也能借助它。滑动窗口限流的一种实现就是,把每次请求的时间戳加到ZSet里,统计一段时间内的请求数,超过阈值就拒绝。
ZREMRANGEBYSCORE rate:api:1 0 (now - window) ZCARD rate:api:1ZSet的内存占用比前几种类型要大,因为它除了存数据本身,还要维护一套跳跃表结构用于排序。如果数据量极大,要考虑内存成本。
注意:ZSet的score是双精度浮点数,如果数值精度要求特别高,比如需要精确到分甚至更小,建议用整数保存最小单位,不要存小数。
4. 持久化机制:崩溃了数据还在吗
4.1 RDB快照
Redis的所有数据都在内存里,如果进程退出或机器断电,内存数据就没了。为了应对这种情况,Redis提供了两种持久化机制,第一种是RDB。
RDB会根据配置生成一个二进制的快照文件,把某个时间点的数据完整保存到磁盘。默认的规则一般是:
save 900 1 save 300 10 save 60 10000含义是:900秒内有1次写操作就生成快照,300秒内有10次写操作就生成,60秒内有10000次写操作就生成。
RDB的优点是可以快速加载恢复数据,文件紧凑、适合备份。缺点是快照之间的数据如果丢失,就只能恢复到最近一次快照的时间点,中间的数据会丢。
我自己在实操中会周期性手动执行BGSAVE,生成备份文件,再把文件同步到异地存储。这样即使整个服务器挂掉,也能从备份恢复。
4.2 AOF日志
AOF(Append Only File)把每次写操作以日志形式追加到文件中。开启方式是:
appendonly yes appendfsync everysecappendfsync有三个选项:always表示每个命令都同步刷盘,最安全但性能最差;everysec表示每秒刷一次,性能和安全性平衡;no表示由操作系统决定何时刷盘,性能最好但可能丢更多数据。
实际生产环境我建议选择everysec,这也是Redis官方推荐的折中选择。崩溃时最多丢1秒的数据,大部分业务都能接受。
AOF文件会不断增加,Redis会用后台进程来重写AOF文件,去掉其中的冗余命令,保持文件精简。相关的配置项是:
auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb4.3 官方推荐:混合持久化
Redis 4.0之后支持混合持久化。开启方式:
aof-use-rdb-preamble yes混合持久化的思路是,AOF文件的前半部分是RDB格式的快照,后半部分是增量命令日志。这样重启时加载速度比纯AOF快,又比纯RDB丢失的数据更少。
如果业务对数据完整性要求比较高的场景,建议把持久化策略设置为混合模式。对于允许丢失少量数据的纯缓存场景,甚至可以全关持久化,换来最高的性能。
5. 缓存三大难题,你必须能讲清楚
5.1 缓存穿透
缓存穿透指的是查询一个数据库中不存在的数据,缓存里自然也不存在,于是每次请求都直接打到数据库上。如果这是恶意攻击,大量不存在的ID进来,数据库压力会瞬间飙升。
解决办法有几种。
第一种是缓存空值。当数据库查不到结果时,也把这个key写入缓存,值设为一个特殊标识,过期时间设短一点,比如1分钟。这样短时间内同样的请求就不会直接打穿到数据库。
第二种是使用布隆过滤器。把所有可能存在的ID提前写入布隆过滤器,查询之前先判断ID是否可能存在,如果过滤器判断不存在,直接返回,不再查数据库。
我在项目里经常组合使用这两种方式,小规模场景用空值缓存就够,大规模场景再考虑布隆过滤器。
5.2 缓存击穿
缓存击穿指某个热点key在失效的瞬间,大量并发请求同时打到数据库,导致数据库瞬间压力过大。
互斥锁是比较常见的方式。在缓存失效后,先让一个线程去数据库加载数据并重建缓存,其他线程等待重建完成后再从缓存读取。
# 伪代码,用setnx实现互斥锁 is_mutex_ok = redis.set("lock:hot_key", "1", nx=True, ex=10) if is_mutex_ok: value = db.query() redis.set("hot_key", value, ex=60) redis.delete("lock:hot_key") else: time.sleep(0.05) value = redis.get("hot_key")逻辑过期也是一种思路,不直接把缓存key设过期,而是存一个逻辑过期时间,在读取时判断逻辑时间是否过期。如果过期了,先返回旧数据,再异步去刷新缓存。
5.3 缓存雪崩
缓存雪崩指大量key同时失效,或者缓存服务整体宕机,导致所有请求全部打到数据库。前者可以通过过期时间加随机值来规避,例如:
expire_time = base_expire_time + random.randint(0, 60)这样每个key的过期时间都不同,不会在同一时刻集体失效。
后者则需要通过高可用方案来解决,包括Redis主从架构、哨兵模式、甚至集群模式。这些内容比较深,新手可以先了解概念,知道有这么回事,等真正需要时再深入研究。
6. 常见问题和排查技巧
6.1 KEYS命令把Redis卡死了
这个坑我见过太多次了。生产环境里有人执行KEYS *去查匹配的key,数据量几百万时,主线程直接被阻塞,所有请求都变得很慢甚至超时。
正确做法是使用SCAN命令进行渐进式遍历:
SCAN 0 MATCH user:* COUNT 100SCAN是一次一次迭代返回,不会阻塞主线程,虽然速度比KEYS慢,但不会拖垮服务。
如果只是想知道key的个数,DBSIZE就够用,不要用KEYS去数。
6.2 Redis内存满了怎么办
当达到maxmemory设置的限额之后,Redis会根据maxmemory-policy来淘汰key。常见策略有:
- noeviction:不淘汰,写入直接报错
- allkeys-lru:从所有key里淘汰最近最少使用的
- volatile-lru:从设置了过期时间的key里淘汰最近最少使用的
- allkeys-random:随机淘汰所有key
- volatile-ttl:从设置了过期时间的key里淘汰即将过期的
缓存场景一般用allkeys-lru,会话场景可以用volatile-lru,带有持久性的数据不能轻易淘汰,需要从架构上避免放入同一实例。
我在实际项目里看到过一个有意思的教训:有的人把maxmemory设得很小,比如64MB,然后正常业务一跑就报OOM错误。排查了很久才发现是测试环境的配置没更新,所以每次调配置都要确认真正生效。
6.3 Big Key问题
一个key的value特别大,比如几兆甚至几十兆,这就是Big Key。它会带来几个问题:读取该key时耗时高、网络流量大、删除或序列化时阻塞主线程。
排查Big Key可以用:
redis-cli --bigkeys它会扫描整个实例并给出最大的几个key。但是要注意,线上实例要谨慎使用,因为扫描也可能带来压力,建议在低峰期执行。
解决办法是拆分大key,或者根据业务场景合理设计数据结构。例如一个List存了十几万条消息,可以考虑按时间或类型拆成多个key。
6.4 过期删除策略
Redis删除过期key的方式有两种:惰性删除和定时删除结合使用。惰性删除指访问key时发现已过期就当场删除;定期删除指每隔一段时间抽取一批设置了过期时间的key,把其中过期的删掉。
这也是面试常考的点。理解它的意义在于:过期key不一定被立即删除,如果一个key过期了但没被访问,它可能还会占据内存。所以如果对内存特别敏感,不能只依赖过期机制,还要配合主动淘汰策略。
6.5 连接数打满
出现连接数打满时,一般先检查是不是有连接泄漏。很多语言客户端如果使用不当,连接池没有被正确归还,就会出现连接持续增长直到占满。
排查步骤一般是:
# 查看当前连接数 INFO clients # 查看最大连接数配置 CONFIG GET maxclients代码层面要检查连接池的配置和使用方式,比如Java的Lettuce、Python的redis-py、Go的go-redis,都必须确保用完即归还连接。
我见到过一些团队为了省事用单例客户端但关闭了连接池复用,结果每个请求创建一个新连接,高并发下一分钟内就会打满。
7. 场景实战:一个完整的小案例
7.1 需求描述
我拿一个常见的电商场景来串一遍:用户查看商品详情,详情数据中有商品基本信息、库存、评论数、评分。这个接口会被频繁访问,数据库压力很大。我们要用Redis做缓存来扛住高并发读取。
7.2 方案设计
商品基本信息用Hash存,字段包括名称、价格、库存、评论数、评分。库存用String存一个数字,方便直接用DECR来做扣减。评论数可以用String类型配合INCR来累加。商品详情页的完整对象用String存一份JSON,加速读取。
缓存更新采用Cache Aside模式:读的时候先读缓存,缓存没有就查数据库,然后回填缓存;写的时候先更新数据库,再删除缓存。
7.3 伪代码演示
import redis import json r = redis.Redis(host="localhost", port=6379, db=0) def get_product_detail(product_id): cache_key = f"product:detail:{product_id}" data = r.get(cache_key) if data is not None: return json.loads(data) # 缓存未命中,查询数据库 product = query_db(product_id) if product is None: return None # 回填缓存,设置过期时间加随机值 expire = 300 + (product_id % 60) r.set(cache_key, json.dumps(product), ex=expire) return product扣减库存:
def reduce_stock(product_id): key = f"product:stock:{product_id}" remaining = r.decr(key) if remaining < 0: # 超卖风险,需要回滚或标记异常 r.incr(key) return False return True注意这里的超卖检查不是完整的解决方案,真实场景有很多并发控制细节。但我用这个例子想说明的是:Redis的一些原子操作确实能够直接简化业务逻辑,不用引入分布式锁也能做到一些基础的并发控制。
7.4 这个案例说明了什么
这个案例把所有核心内容串起来了:String存JSON做整体读取,Hash做结构化存储,INCR/DECR做计数和扣减,过期时间防脏数据留存。理解了这些,你至少能解决大多数日常开发问题了。
8. 实操中的个人小结
写了这么多,最后分享一点我自己的体会。学Redis不是背命令,而是理解数据结构与场景的匹配关系。很多年前我学的时候,上来就背了一大堆命令,但遇到实际问题还是懵。后来我换了个思路,先想清楚业务里有哪些数据结构,再考虑Redis里的哪种类型能落地,这样学习的效率高了很多。
另一个体会是,虽然Redis易上手,但生产环境里想用好它,必须提前把监控和告警做好。至少要知道内存使用率、连接数、命中率这几个指标,不要等到服务出了问题才去想对策。
还有一个小建议,所有的配置变更都要先在测试环境验证,并且确认重启后的加载结果。别问我为什么这么强调,问就是经历过多台实例配置不一致的惨痛教训。
对于扩展学习的方向,建议下一步去研究发布订阅、Lua脚本、分布式锁、Redis Cluster。掌握了这篇基础内容之后,这些东西的理解也会顺畅很多。