Redis核心知识点详解:从数据结构到高并发缓存实践 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 worldSET表示写入一个键值对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 bobexample.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日志AOFAppend 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, nxTrue, ex10) if is_mutex_ok: value db.query() redis.set(hot_key, value, ex60) 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随机淘汰所有keyvolatile-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(hostlocalhost, port6379, db0) def get_product_detail(product_id): cache_key fproduct: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), exexpire) return product扣减库存def reduce_stock(product_id): key fproduct: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。掌握了这篇基础内容之后这些东西的理解也会顺畅很多。