Redis实战速记:从数据类型到分布式锁与缓存治理

发布时间:2026/9/15 10:28:59
Redis实战速记:从数据类型到分布式锁与缓存治理 Redis这个东西没实际用过的人总觉得它就是个缓存用过的人才知道它远远不止缓存这么简单。我这些年做后端几乎每个项目都离不开它从最早只会set、get到后来在产线上排查大Key、慢查询、分布式锁失效踩了不少坑。这份“Redis速记”是我平时随手记下来的核心要点今天整理成一篇完整的内容覆盖从安装部署、数据类型、Java客户端实操到缓存治理、分布式锁、生产排障的完整链路。无论是刚入门的新手还是已经用了一段时间但想系统梳理一遍的开发者都能直接对应自己的阶段来查漏补缺。我不是来给你背八股文的这篇文章里的每一条命令、每一段配置、每一个问题排查都是我在真实环境里验证过的。所以读的时候别急着往下翻遇到和自己场景相关的部分可以停下来想一想如果现在出问题我能不能按这个思路定位到根因1. Redis到底解决了什么问题——先把本质搞清楚1.1 一个“快递柜”的类比Redis是什么、能做什么我第一次给别人讲Redis的时候喜欢用快递柜来打比方。数据库就像一个大仓库什么东西都往里放东西多了每次存取都得跑一趟耗时耗力。Redis就像一个放在仓库门口的快递柜高频取用的东西直接放柜子里随取随用秒开秒关省去了每次往仓库深处跑的时间。这个类比虽然不是百分之百严谨但胜在直观。Redis本质上是基于内存的键值存储系统数据主要存放在内存读写速度极快。它支持多种数据结构不只是String还有Hash、List、Set、ZSet等等所以它不仅仅能做缓存还能做分布式锁、计数器、排行榜、消息队列、布隆过滤器等一堆事情。Redis解决的痛点很直接高并发下数据库扛不住频繁查询用Redis挡在前面把热点数据“前置”到内存里。同时它还具备持久化能力即使进程重启数据也能通过RDB快照或AOF日志恢复不是那种“一重启就啥都没了”的纯内存玩意儿。1.2 性能底座的秘密全内存单线程IO多路复用很多人问过我同一个问题Redis为什么快官方数据是读写性能可以达到10万 QPS这个量级对于绝大多数业务场景来说完全够用了。背后有三个关键原因。第一数据在内存中内存的随机访问延迟是纳秒级而磁盘的顺序访问都要毫秒级中间差了好几个数量级。第二Redis的网络模型基于IO多路复用单线程可以同时处理成千上万个客户端连接避免了多线程上下文切换和锁竞争的开销。第三Redis的核心操作本身就是内存操作时间复杂度大多是O(1)级别比如哈希表查找、列表头尾操作这决定了它在单线程模型下也能跑得飞快。单线程这一点不少人会理解偏以为单线程就是性能瓶颈。实际上Redis的单线程仅仅指“事件处理器”这部分真正耗时的磁盘持久化、AOF重写、主从同步等都有子进程或后台线程来处理主线程只管读写请求。这也是为什么Redis能保持极高吞吐量的关键设计。1.3 哪些场景该用、哪些场景不该用技术选型最忌讳“手里拿着锤子看什么都像钉子”。Redis虽然强但也不是万能药。我用Redis用得最多的是这几类场景热点数据缓存商品详情、用户信息、配置数据等读多写少的数据缓存到Redis扛住高并发读。分布式锁多个服务实例同时对同一资源做操作时用Redis锁保证互斥。计数器秒杀库存扣减、点赞数、访问量统计用INCR命令原子性自增。排行榜使用ZSet的分数排序能力非常方便。会话共享多实例部署时把Session存到Redis实现登录状态共享。但有些场景我建议慎用甚至不用Redis。比如数据量极大且访问频率很低的数据放内存里就是烧钱强事务性、多表联动的复杂查询这更适合关系型数据库来处理需要复杂条件查询的数据Redis只是键值结构没有SQL那种灵活筛选能力硬要做会非常痛苦。有一个我一直坚持的选型原则用Redis来加速数据访问而不是替代数据存储。核心数据必须以MySQL或其他持久化存储为准Redis只做加速层允许一定程度的丢失或过期。2. 从安装到运行本地、Windows、Docker一条龙2.1 本地快速起一个RedisLinux/Mac在Linux或Mac上安装Redis非常简单两种方式我常用一是直接通过包管理器安装比如apt install redis-server或brew install redis二是源码编译安装适合需要指定版本或做定制的情况。我推荐练习时用源码编译能让你对Redis的安装结构更熟悉。具体步骤如下wget https://download.redis.io/releases/redis-7.2.4.tar.gz tar xzf redis-7.2.4.tar.gz cd redis-7.2.4 make make install编译完成后先启动服务redis-server默认情况下它监听6379端口不带任何配置也可以正常跑起来。再开一个终端用自带的客户端连上去试试redis-cli 127.0.0.1:6379 ping PONG能返回PONG就说明整个链路是通的。调试的时候我建议直接前台启动日志会直接输出到终端退出用CtrlC就行。真正部署到服务器再使用redis-conf配合daemonize yes或systemd托管。2.2 Windows下怎么用RedisWindows上使用Redis是个老话题了。官方其实并不正式支持Windows但微软早年维护过一个版本社区也有迁移版本。目前Windows开发环境中我比较推荐用以下两种方式。第一种是使用Memurai或tporadowski的Redis Windows移植版。这些都能在Windows上作为服务运行支持大部分Redis功能。虽然版本跟进有时滞后但日常开发调试够了。第二种就是直接在Windows上用WSL或Docker Desktop。我的建议是能用Docker就跑Docker这是最省事也最接近Linux生产环境的方式因为Redis的很多配置、权限和行为都依赖Linux内核特性比如forkWindows原版在某些特性上确实有差异。如果你只是想在Windows上快速体验可以直接去下载Redis-x64-XXX.zip解压后运行redis-server.exe再运行redis-cli.exe跟Linux下几乎一样。注意这类包多数没有官方安全维护生产环境不要用。2.3 Docker与docker compose生产部署要点Docker部署Redis是当前生产环境的主流方式。镜像直接拉取官方版本即可docker pull redis:7.2 docker run -d --name redis \ -p 6379:6379 \ -v /data/redis/conf:/usr/local/etc/redis \ -v /data/redis/data:/data \ redis:7.2 redis-server /usr/local/etc/redis/redis.conf注意我挂载了一个配置目录和一个数据目录。这两个挂载点非常关键数据目录用于持久化RDB和AOF文件否则容器一删数据全没配置目录用于挂你自定义的redis.conf。对于稍微复杂一点的生产环境我更推荐docker compose来定义服务。下面是一份可以直接上手的配置凡是生产建议至少要补上密码、持久化、日志这几个部分services: redis-master: image: redis:7.2 container_name: redis-master restart: always ports: - 6379:6379 command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes --appendfsync everysec --save 900 1 --save 300 10 volumes: - /data/redis/data:/data - /data/redis/logs:/logs logging: driver: json-file options: max-size: 50m max-file: 5这里我直接用command传配置项好处是显式、不容易遗漏缺点是不适合特别复杂的配置你不可能把全部100多个配置全写在这。所以配置复杂时建议把redis.conf挂载进去。关于“docker compose生产环境部署”这个热词我还想说一下容器编排的几个硬性要求。一是配置密码默认不带密码的Redis在公网上就是裸奔被扫描到很容易被入侵二是设置持久化生产环境至少开启AOF配合RDB做兜底三是端口隔离不是所有环境都需要把6379暴露到公网更安全的是让业务容器和Redis容器在同一网络内通过容器名互通。3. 九大数据类型速记面试和实战全靠它3.1 五个基础类型怎么选Redis一共有五种基本数据类型它们构成了绝大多数应用的基石。完整速查如下类型底层实现常用命令典型场景StringSDS动态字符串SET、GET、INCR、DECR、SETNX缓存、计数器、分布式锁Hash哈希表压缩列表HSET、HGET、HGETALL对象存储、购物车List快速链表压缩列表LPUSH、RPUSH、LPOP、LRANGE消息队列、时间线Set哈希表整数集合SADD、SMEMBERS、SINTER、SUNION去重、共同好友、标签ZSet跳跃表哈希表ZADD、ZRANGE、ZRANGEBYSCORE排行榜、延时队列刚入门的朋友最纠结的是Hash和String怎么选。简单说如果你要存的是一个“对象”并且会频繁修改对象的某个字段用Hash最合适。比如用户信息用HSET user:1001 name 张三后续单独改手机号就是HSET user:1001 phone 138xxxx不会像String那样需要先GET整个JSON字符串再反序列化再覆盖。但如果只是单纯地存一段JSON且读取频次极高、修改频次极低用String更简单。ZSet是我非常喜欢的一个类型排行榜这种需求最典型的实现就是它。每个成员附带一个分数Redis内部通过跳跃表按分数排序插入、查找、删除都是对数级复杂度比你在业务层排序然后存List要靠谱得多。3.2 高级结构Bitmap、HyperLogLog、GEO除了上面五个基础类型Redis还提供了几个高级数据结构虽然很多开发者平时不太用但在特定场景下极其高效。Bitmap本质上是String类型的一种位操作视角。可以把它理解成一个由0和1组成的数组用SETBIT、GETBIT、BITCOUNT操作。经典场景是用户签到365天对应365个位一个人一年的签到记录只用几十个字节。如果用普通KV存储这个数据量会膨胀得很夸张。HyperLogLog是用来做基数统计的比如统计一个页面的独立访客数UV。它基于概率算法标准误差约0.81%但用极小的内存就能统计海量的独立元素。我实测过存几百万个独立用户的ID内存占用仍然只有十几KB左右这比存储所有ID的集合要省几个数量级。GEO用于地理位置计算底层是ZSet结构。存储经纬度坐标可以计算两点的距离也可以实现“附近的商家”这类查询。GEOADD存入坐标GEOSEARCH做半径搜索。移动端业务做位置相关功能时很实用。3.3 一张表搞定类型选择经常有小伙伴问我“我到底该用哪个类型还是直接全用String存JSON”我的判断逻辑很简单需要计数、自增String需要一个对象且频繁改部分字段Hash需要顺序读写像队列一样操作List需要去重、交集、并集Set需要按分数排名ZSet需要统计独立用户量HyperLogLog需要位运算级别的状态标记Bitmap需要地理位置计算GEO记住选类型的核心依据是下一步要对这个数据做什么操作而不是现在有没有现成的JSON结构。还有一个新手误区把所有数据都序列化成JSON字符串塞进String。这个做法在数据量小、访问模式简单时没问题但一旦你需要对数据的一部分做原子更新或者需要做集合运算就会很别扭。所以从一开始就养成“按操作选类型”的习惯会更省事。4. 客户端、可视化工具与Java实操4.1 redis-cli与可视化客户端RDM、ARDM命令行工具redis-cli是排查问题的第一选择。很多人一上来就装可视化工具但遇到线上问题能最快的反而是这个黑色的命令行窗口。常用的写法redis-cli -h 127.0.0.1 -p 6379 -a yourpassword进去以后除了增删改查有几个命令是排查问题必需的# 查看所有key生产环境慎用会阻塞 KEYS * # 查看key的剩余过期时间 TTL key # 查看key内部的编码类型 OBJECT ENCODING key # 查看当前连接数和服务端信息 INFO clients INFO memory可视化客户端方面两个工具我用得最多一个是曾经的经典Redis Desktop ManagerRDM另一个是它的社区分支Another Redis Desktop ManagerARDM。RDM老版本有内存泄露的问题需要下载较新的版本ARDM是跨平台、开源的界面和功能几乎完全覆盖日常使用需求我目前主力用ARDM。用可视化工具最大的好处是可以直观地浏览key、查看某个key的TTL、内存占用以及value的结构。当你需要确认某个Hash里具体有哪些字段、ZSet里成员的分数排序时比命令行效率高很多。但注意连接生产环境时别拿可视化工具去执行KEYS *这种重命令点错了可能直接把线上Redis拖垮。4.2 Java接入Jedis还是RedisTemplateJava生态里操作Redis主流方式就是Jedis和Spring Data RedisRedisTemplate。我个人实际用的最多的是RedisTemplate因为大部分项目都在Spring Boot里集成非常自然。先看RedisTemplate的典型用法// 注入 Autowired private StringRedisTemplate stringRedisTemplate; // 或注入RedisTemplate Autowired private RedisTemplateString, Object redisTemplate;这里有个非常重要的点容易被忽略RedisTemplate默认使用的序列化器是JDK序列化存进去的对象是一长串二进制数据在redis-cli里看就是\xAC\xED\x00\x05...这样的乱码。这既不直观还会让key的排查变得很难受。所以生产环境我建议要么直接用StringRedisTemplate要么把RedisTemplate的序列化器改成JSON序列化RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); // key使用String序列化 template.setKeySerializer(new StringRedisSerializer()); // value使用JSON序列化 template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet();顺带说一下StringRedisTemplate和RedisTemplate的区别。StringRedisTemplate的key和value都是String类型序列化器固定是String方式适合存纯字符串RedisTemplate则更通用可以存对象。平时如果用RedisTemplate存字符串容易遇到一个问题——你在redis-cli里能读到但Java的get返回null原因就是key的序列化方式不一致。排查这类问题时先检查序列化器。4.3 RedisTemplate.increment()报错实录热词里有一个非常细的报错“java中redis使用redistemplate的increment()报错不是integer or out of range”。这个我实际遇到过一次而且定位过程很有代表性。先看报错本身org.springframework.dao.InvalidDataAccessApiUsageException: ERR value is not an integer or out of range这个意思很直白Redis执行INCR时发现value不是整数或者超出了64位有符号整数的范围。我用StringRedisTemplate执行increment()时触发第一反应是有人往这个key里塞了非数字字符串。复现路径是这样的127.0.0.1:6379 SET app:count abc OK 127.0.0.1:6379 INCR app:count (error) ERR value is not an integer or out of range这里必须把value设为abcINCR就直接炸了。但在Java项目中我第一次排查时在redis-cli里看到的value却是乱码。这就更迷惑了明明是StringRedisTemplate为什么value是乱码后来发现真正写入那个key的组件用的是另一个使用JDK序列化的RedisTemplate。它写入的字节序列在INCR看来就是“不是整数”。所以这个问题的根因有两个层面原因一key对应的value本来就不是数字。比如初始值就没设置或者有人存了abc、JSON串等。解决方式是先确认业务逻辑确保这个key只会被整数操作。可以在INCR前用SETNX初始化为0并约定所有写入都走同一个序列化器。原因二序列化方式不一致导致value被存成二进制。比如同一个key有时走RedisTemplateString, Object有时走StringRedisTemplate前者用JDK序列化存进去的是\xAC\xED...到StringRedisTemplate时反序列化不出来传给Redis的是什么是原始字符串\xAC\xED...当然不是整数。排查这个问题的标准步骤先用redis-cli连上去GET这个key看原始value是什么样的。如果看到乱码检查代码里对这个key的写入方和读取方序列化器是否一致。如果看到的是正常的abc说明确实有人写入了非数字检查业务逻辑。看TTL确认key是否有过期时间有没有可能被多个业务复用导致交叉覆盖。后来我把这个项目统一改成了StringRedisTemplate 显式类型转换所有计数的key在写入前都先SETNX 0这个报错就再没出现过。5. 分布式锁与缓存治理高并发场景的硬仗5.1 分布式锁的演进从SETNX到Redisson分布式锁是Redis最经典的用法之一。很多系统从单机演进到多实例后Java的synchronized锁只能锁住当前JVM内部没法跨进程协同所以需要一把“大家都能看到”的锁Redis就是最自然的实现介质。第一代做法用SETNXSETNX lock_key unique_value # 返回1表示获取锁成功返回0表示锁已被占用问题在于如果拿到锁的线程崩了没有DEL释放锁锁就永远不释放了。所以加过期时间SET lock_key unique_value EX 10 NX。这条命令同时保证原子性这是老版本SETNXEXPIRE两步操作容易踩的坑两步之间进程崩溃会导致锁没设过期时间。但即使这样还有几个问题锁过期了但业务还没执行完怎么办A线程锁过期释放B线程拿到锁A执行完删除锁时把B的锁删了。Redis主从切换时锁丢失怎么办有RedLock方案但过于复杂普通业务不推荐轻易实现。目前生产环境最成熟的方案是Redisson。它是一个Java客户端库提供封装好的RLock并且内部实现了看门狗自动续期机制默认锁超时30秒如果业务还没执行完会自动续期避免“锁过期但业务还在跑”的问题。使用示例RLock lock redissonClient.getLock(order:pay:10001); if (lock.tryLock(5, 10, TimeUnit.SECONDS)) { try { // 业务逻辑 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }这里我再强调一个实战教训分布式锁的粒度一定要小。锁的key越细化并发能力越强。比如支付场景锁key应该是order:pay:10001这样按订单维度而不是一个全局的order:pay否则所有订单的支付都被同一个锁串行化性能直接崩。还有很多人喜欢在一把锁内部做一堆耗时的远程调用这是最差的设计。锁内只该做必须互斥的操作其他事情尽量放到锁外不然并发能力会急剧下降。5.2 缓存穿透、击穿、雪崩怎么治缓存治理是面试高频也是生产必踩。这三个场景我分开来说。缓存穿透查询一个不存在的key请求每次都穿透缓存打到数据库数据库压力飙升。最常见是有人拿一个不存在的ID来刷接口。解决方案我用得比较多的是布隆过滤器Bloom Filter在缓存前面再加一层过滤判断这个key是否存在。如果不存在就直接返回连缓存都不查。另一种简单的办法是对于不存在的key也缓存一个空值比如空字符串或特殊标记设置较短的过期时间如5分钟。这能在一定程度上缓解穿透。缓存击穿某个热点key在缓存过期的那一瞬间有大量请求同时打过来全部穿透到数据库。这和穿透的区别是key是真实存在的只是刚好在过期时间点上被大量请求并发打到。常用解决方案是互斥锁或者逻辑过期。互斥锁的思路是当一个线程发现缓存过期时先拿锁只有拿到锁的线程能去查数据库其余线程等待一段时间后重新查缓存。逻辑过期则是给value包装一个超时时间异步刷新缓存。缓存雪崩大量key同时过期或者Redis节点宕机导致流量最终全部打到数据库。这个相比击穿更为严重涉及面更大。解决雪崩的办法过期时间加随机偏差比如在基础过期时间上增加一个随机值避免大量key在同一时刻过期。多级缓存本地缓存比如Caffeine RedisRedis挂了流量先顶在本地缓存上。Redis高可用主从哨兵或Cluster模式避免单点故障。还有一个概念很多人搞混我在这里理一遍穿透是“查不存在的数据”击穿是“热点key过期瞬间被高并发打穿”雪崩是“大面积key同时过期或Redis宕机”。面试时能把这个区分讲清楚对方就知道你是真做过的。6. 面试答题与生产排障速查6.1 高频面试题速记Redis相关的面试题归结起来最常问的就这几类。我按我的理解写一下回答的核心点。Redis为什么快内存存储、单线程模型避免锁竞争、IO多路复用网络模型再加上高效的数据结构设计。RDB和AOF的优缺点RDB是定期全量快照恢复速度快但可能丢失最后一次快照后的数据AOF是追加写日志数据更完整但文件大、恢复慢。生产环境推荐两者结合AOF的appendfsync设为everysec可以兼顾性能和数据安全。Redis单线程为什么还能处理高并发因为瓶颈在网络IO和内存不是CPU。单线程反而少了锁竞争和上下文切换的开销。对于CPU密集型的操作Redis确实不适合但作为缓存系统它的场景天然就是IO密集。Redis的过期删除策略惰性删除 定期删除结合。惰性删除是访问时才检查是否过期定期删除是每隔一段时间主动删除一批过期key。同时内存淘汰策略会在内存满时触发如allkeys-lru、volatile-ttl等。Redis持久化对性能影响大吗在appendfsync everysec配置下持久化性能影响相对可控。需要注意的是AOF重写和RDB的快照生成会fork子进程内存占用翻倍风险需要关注尤其大实例。分布式锁怎么实现核心是SET key value EX ttl NX保证原子性value要唯一标识获取者释放时用Lua脚本对比value再删除避免误删别人的锁。复杂场景直接上Redisson让看门狗自动续期。6.2 日志、慢查询、大Key排查实录生产环境Redis出问题最常见的表现就是响应变慢。排障顺序我一般是这样。先看INFO commandstats和INFO stats确认有没有大量慢命令。再看SLOWLOG GET这个命令能列出最近的慢查询# 查看最近10条慢查询 SLOWLOG GET 10 # 查看慢查询阈值单位微秒 CONFIG GET slowlog-log-slower-than如果发现KEYS *、HGETALL一个大Hash、ZRANGE一个超大的ZSet耗时长那基本就是大Key在拖后腿。大Key会对Redis产生几个影响单次命令阻塞线程、网络传输巨大、fork持久化时内存和IO暴涨、数据迁移卡顿。找大Key的命令redis-cli --bigkeys它会遍历分析并列出最大的key但注意这个命令本身也可能因为遍历造成阻塞建议在业务低峰期执行。如果你的Redis已经卡到没法用命令行那就得果断做拆分或淘汰。日志这块Redis默认不打印普通操作日志只有错误和启动信息。所以排查问题时我习惯先把日志级别调到verbose或debug仅限测试环境生产环境保持notice然后观察Redis log里有没有Slow query相关的警告以及是否频繁出现Background saving terminated by signal这种信号错误。还有一个经常被忽视的配置项是maxmemory-policy。很多生产事故的根因是没配内存淘汰策略内存打到100%后Redis变成只读不可写所有写入操作直接报错OOM command not allowed when used memory maxmemory。核心业务如果有缓存写入需求出一丁点问题都是灾难。所以生产环境我至少建议maxmemory 4gb maxmemory-policy allkeys-lruallkeys-lru表示对所有key都进行LRU最近最少使用淘汰。有些场景也可以选用volatile-lru只淘汰设置了过期时间的key要求你代码里所有缓存都有合理的TTL。6.3 一个典型的排障时间线最后我分享一个真实处理过的慢Redis问题整个排查链路很有参考价值。某天线上收到告警某个核心服务的Redis读写P99从5毫秒飙升到2秒数据库压力同步上升。我先看了慢查询日志发现大量SMEMBERS命令来自一个Set类型的key。这个key是“用户关注列表”早期设计的缓存策略是直接把整个用户Set原样缓存随着业务增长一个头部用户的关注列表膨胀到了几十万成员导致每次SMEMBERS都在传输几十万元素。处理办法是拆缓存粒度不再缓存完整Set为单key而是改成按页查询或者用ZSet只缓存目标对象的ID和最近交互时间再通过批量接口回源数据库。改完后P99降回10毫秒以内。这个案例想说明一个道理Redis的坑往往不在Redis本身而在你的数据模型设计。动不动把所有数据塞进一个大key、大Hash里初期没事数据一涨就是事故。所以设计阶段就需要预估数据量和访问模式提前规划key的拆分和过期策略。写在最后的几个习惯说几个我坚持了多年的小习惯可能比上面这些具体命令更有价值。第一次新环境联调Redis永远先执行INFO server确认版本、运行时长、连接数。不要上来就乱写乱读尤其生产环境方向搞反了很容易把自己坑了。给所有key设计统一的前缀规范比如业务名:模块名:业务ID。这个习惯能让你在排查问题时高效区分数据来源也能配合SCAN做数据清理。生产环境千万不要用KEYS *一个大数据集上执行就可能让Redis阻塞好几秒正确的替代是SCAN 0 MATCH prefix:* COUNT 1000。凡是需要计数自增的key代码里保证先初始化或者用INCR的返回值做判断避免INCR一个不存在的key或者一个被存成字符串的值。Redis的报错信息有时候很简略value is not an integer or out of range背后可能是序列化器不一致、业务覆盖、初始化缺失等一堆问题定位时先把“这个key最近被谁写过、用什么序列化器写过”搞清楚比直接怀疑Redis要靠谱得多。Redis的“速记”到头来其实就是两个词看需求选类型按场景配策略。把这两个原则吃透无论你面对的是缓存、锁、还是队列思路都不会乱。