ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Redis实战指南:从数据类型到分布式锁与缓存高可用

Redis实战指南:从数据类型到分布式锁与缓存高可用 我接触Redis的时间不算短了从最初的缓存工具到后来在分布式架构里扛流量、做锁、存会话Redis几乎贯穿了我做后端开发的大部分场景。如果你去翻任何一个技术社区的讨论区Redis相关的热搜词基本绕不开那几类redis数据类型、redis分布式锁、redis缓存穿透、redis持久化、redis集群。这些词背后其实就是大家在真实生产环境中反复遇到的核心问题。这篇文章我不打算照着官方文档给你背一遍命令而是站在实际开发的角度把这些高频场景一个个拆开讲清楚每个场景解决什么问题、怎么落地、有哪些坑是文档里不会告诉你的。适合谁来读刚入行的后端开发想系统理解Redis到底能干什么做了一段时间CRUD、准备在项目里引入Redis的Java或Go工程师以及马上要面试、需要把Redis相关知识从碎片整理成体系的同学。这篇文章把缓存设计、分布式锁、数据类型选型、持久化策略、集群部署这些核心话题串起来讲尽量说人话每个环节都给出可以拿去直接用的方案。1. 先搞清楚Redis为什么这么重要1.1 它到底是个什么东西Redis的全称是Remote Dictionary Server直译过来就是“远程字典服务器”。这里有两个关键词字典和远程。字典意味着它本质上存储的是键值对这一点和Java里的HashMap、Python里的dict在数据结构层面很像远程意味着它独立部署通过网络接口对外提供服务和本地内存变量有本质区别。很多新手第一次接触Redis会把它简单理解成“一个很快的数据库”这个说法方向没错但没说到根子上。Redis之所以快不是因为它是C语言写的当然这也是原因之一而是因为它所有的数据都存在内存里没有磁盘IO的参与。你想想内存的随机访问延迟是纳秒级别的而磁盘哪怕是用SSD延迟也要到微秒甚至毫秒级别。所以Redis单实例的QPS可以轻松到十万以上这决定了它的定位不是用来存海量数据的而是用来顶高并发读写的。从架构位置上看Redis通常被放在数据库和应用之间。数据库负责最终数据的一致性和持久化Redis负责挡住大部分读请求和一部分写请求让数据库的压力降下来。用一个生活化的类比数据库是仓库Redis是仓库门口的柜台。来买东西的人先问柜台有没有货柜台有就直接拿走柜台没有才去仓库翻。仓库可以不那么忙但柜台必须反应快、服务好。1.2 哪些场景天然适合用Redis从我的实际经验来看Redis的核心应用场景可以归纳成六类后面我会逐个展开讲。第一类是缓存这是Redis最广为人知、使用率最高的场景。热点数据、页面片段、接口响应结果都可以往Redis里塞。第二类是分布式锁在微服务架构里多个服务实例操作同一个资源时需要靠它来协调。第三类是计数器比如文章浏览量、库存扣减、点赞数这些需要高并发原子增减的场景。第四类是会话存储登录状态、用户信息用Redis集中管理服务实例可以无状态地横向扩容。第五类是消息队列用List或Stream实现简单的发布订阅和异步解耦。第六类是排行榜利用有序集合的特性可以轻松实现带权重的排序。这几类场景有个共同点要么对读写性能要求极高要么需要跨实例共享状态要么需要数据结构上的特殊支持。关系型数据库在这几件事上要么性能跟不上要么实现起来很别扭。Redis的定位就是“缓存加速 状态共享 原子操作”搞清楚了这一点你在选型的时候基本不会跑偏。2. 缓存设计Redis最核心的战场2.1 Cache Aside模式99%的项目在用缓存的主流设计模式叫Cache Aside中文叫“旁路缓存”。它的核心思路很简单读请求先查缓存缓存命中了就直接返回缓存没命中就去查数据库拿到数据后回填到缓存里同时把这次请求的响应返回给调用方。写请求则先更新数据库再删除缓存。为什么要用“更新数据库后删缓存”而不是“更新数据库后更新缓存”这里有个经典的坑。如果你选择更新缓存在高并发场景下两个并发请求A和B先后更新了数据库但B的写操作先执行完了缓存更新A的后执行结果缓存里留下的是旧数据数据库里是新数据两边不一致了。而删除缓存的话下次读请求发现没命中会重新从数据库拉数据回填天然保证了一致性。这个方案虽然多了一次缓存未命中的开销但稳定性高得多这也是业界普遍推荐的原因。还有个细节缓存设置过期时间时不要所有key用同一个值。比如某类数据会有定时任务批量更新那它的缓存过期时间就应该设置成“距离下个调度周期还剩下的时间”否则容易出现缓存刚过期、数据还没刷新请求又打到数据库上的情况。我在实际项目里见过不止一次因为过期时间设置不合理导致数据库在整点时刻被打爆的案例。2.2 缓存穿透、击穿、雪崩三兄弟必须分开治这三个问题是面试高频题也是生产事故的高发区。很多同学能背出定义但真到排查问题时就分不清了。我按自己的理解给它们做个区分。缓存穿透指查询一个根本不存在的数据缓存和数据库里都没有请求直接打到数据库上。如果攻击者恶意构造大量不存在的ID来请求数据库压力会瞬间爆掉。解决方案有三种。第一种是缓存空值查询结果为空时也往Redis里写一个key过期时间设短一些比如5分钟这样同一个不存在的key短时间内不会再次穿透到数据库。第二种是布隆过滤器把所有合法ID提前加载到布隆过滤器中请求进来先过过滤器过滤器中不存在就直接返回不查数据库。第三种是参数校验明显非法的请求在入口处就拦截掉。可以根据业务特点选择中小项目缓存空值性价比最高。缓存击穿指某个热点key刚好在某一瞬间过期而这个时候有大量并发请求都在查这个key结果全部穿透到数据库。这和穿透的区别在于穿透的是不存在的key击穿的是热点的、刚好过期了的key。解决方案就是加互斥锁——当缓存未命中时只有一个线程去数据库加载数据并回填缓存其他线程等待重试。用Redis的SETNX可以实现这个锁也可以直接用Redisson的getLock。另外一个思路是“逻辑过期”不给key设物理过期时间而是在value里存一个过期时间戳读取时判断是否过期过期则异步刷新这个方案更灵活但实现稍复杂。缓存雪崩指大量key在同一时刻全部过期或者Redis实例直接宕机导致所有请求都打到数据库。解决思路有两个方向。一是错开过期时间在设置缓存时给过期时间加一个随机值比如5分钟加1到60秒的随机偏移让key的过期时间分散开。二是Redis实现高可用部署主从加哨兵避免单点故障这个我在后面的集群章节会展开。有些项目还会做本地缓存兜底请求先查本地内存缓存再到Redis再到数据库多了一层缓冲但实现成本和数据一致性成本都要考虑。3. 分布式锁并发竞争下的协调者3.1 为什么单机锁不够用了传统单机应用下多个线程竞争同一资源用Java的synchronized或者ReentrantLock就能解决。但一旦应用拆成多个实例部署或者本身就是微服务架构每个实例是一个独立的JVM进程JVM级别的锁只对当前进程内的线程生效。三个实例同时执行一段写操作单机锁完全拦不住。我遇到过的一个实际案例某活动系统的优惠券发放接口部署了三个实例发券前要判断库存。单机锁情况下三个实例同时判断库存还有10张于是三个用户同时领取成功库存变成负数。改成Redis分布式锁之后三个实例中只有一个能拿到锁执行扣减另外两个被锁挡住问题立刻解决。这个场景就是分布式锁存在的意义跨进程、跨实例的互斥控制。3.2 SETNX方案的坑和Redisson的正确姿势最早的分布式锁实现是SETNX加EXPIRE两条命令先尝试setnx成功就说明拿到锁然后设置过期时间防止死锁。这个方案有几个隐患。第一个隐患是SETNX和EXPIRE不是原子的如果SETNX成功之后进程崩溃EXPIRE没执行锁永远不释放其他线程全部卡死。后来Redis官方推出了SET命令的扩展参数可以做到SET key value NX EX seconds一步完成解决了原子性问题。第二个隐患是锁过期时间怎么定。设短了业务没执行完锁就自动释放了别的线程趁虚而入设长了如果拿到锁的线程崩溃其他线程要等很久。一个可行的办法是开个后台线程定期续期也就是看门狗机制。Redisson这个Java客户端内置了这个能力拿到锁之后只要业务没结束看门狗会每隔一段时间自动给锁续期默认是锁超时时间的三分之一作为续期间隔。Redisson官方还提供了RLock接口使用方式非常接近Java的ReentrantLock这也是我推荐直接用Redisson而不是自己造轮子的原因。第三个隐患是锁的可重入性。同一个线程在已经持有锁的情况下再次获取同一把锁分布式锁要支持这种重入语义。Redisson的锁基于Redis Hash结构实现内部会记录持有锁的线程标识和重入次数完美支持重入。如果自己用SETNX实现还需要额外维护重入计数极其容易出错。// Redisson 分布式锁的标准用法 RLock lock redissonClient.getLock(order:create:lock); boolean isLocked false; try { // 尝试加锁最多等待3秒锁自动过期时间30秒 isLocked lock.tryLock(3, 30, TimeUnit.SECONDS); if (isLocked) { // 业务逻辑 doCreateOrder(); } else { throw new RuntimeException(系统繁忙请稍后重试); } } finally { // 释放锁时务必判断是否持有 if (isLocked lock.isHeldByCurrentThread()) { lock.unlock(); } }不要自己封装分布式锁工具类除非你确实需要对Redisson的底层做深度定制。网上很多项目里的自定义锁代码都有隐藏缺陷比如忘了设置过期时间、没处理锁续期、重入逻辑写错。用成熟客户端是最稳妥的方案。4. 五种数据类型每个都是为特定场景准备的4.1 String和Hash缓存的主力军String是Redis里最基础的数据类型value最大能存512MB。使用场景包括简单的KV缓存、自增计数器、分布式ID生成配合INCR命令、Session存储。INCR命令是原子自增多个客户端并发操作时不会出现超卖问题这也是秒杀系统里扣减库存的经典实现方式。但String有一个容易被忽略的问题存储一个包含多个字段的对象时如果用String要么序列化成JSON再存要么一个字段一个key。序列化成JSON的问题是只修改对象的某个字段时必须把整个对象拿出来反序列化、改完再序列化放回去数据量大的时候明显浪费IO和CPU。所以就有了Hash类型。Hash的每一个key下面可以挂多个fieldfield对应对象的属性。比如存用户信息用user:10001作为keyname、age、email作为field查询单个字段不需要把整个对象拎出来修改单个字段也只需要一条HSET命令。Redis在内部对小规模的Hash做了内存优化内存占用比同等数据的String更省。所以当你要缓存的对象有明确的多个属性且需要单独读写时优先考虑Hash而不是String。4.2 List、Set、ZSet从消息队列到排行榜List是一个双向链表可以从左或右推入、弹出元素。基于这个结构可以做简单的消息队列生产者用LPUSH往左边推消费者用BRPOP从右边阻塞弹出。BRPOP是阻塞式弹出没有消息时会一直等待直到超时或拿到数据这比写死轮询的效率高得多。当然Redis的List消息队列可靠性有限如果消费者消费完还没确认消息就崩溃了消息可能丢失所以严格意义上的消息中间件还是得用专业MQ但轻量场景下完全够用。Set是无序字符串集合自动去重支持交、并、差运算。典型场景包括点赞/收藏功能SADD、SREM、关注关系用两个Set分别存粉丝和关注的人SINTER求共同关注、以及去重统计每日活跃用户数用Set去重后SCARD。Set在做标签系统时也很好用比如给文章打标签每篇文章维护一个Set的标签按标签检索文章时用SMEMBERS取出来就行。ZSet是Set的升级版每个成员关联一个score作为排序权重。排行榜功能就是ZSet的拿手好戏ZADD往集合里添加成员和分数当分数变化时比如用户的积分增加了直接用ZADD更新ZSet内部自动维护排序。查询前十名用ZREVRANGE key 0 9查询自己的排名用ZRANK。除了排行榜ZSet还可以做延时队列把任务执行时间戳作为score用一个线程循环执行ZRANGEBYSCORE来拿当前时间点之前的任务处理这个技巧在很多定时任务框架里都能看到。我用一张表总结一下这五种类型和对应场景的关系方便你翻查数据类型底层结构核心能力典型应用String动态字符串原子自增/自减、KV存储缓存、计数器、SessionHash哈希表对象字段级读写用户信息、商品详情List双向链表阻塞弹出、消息有序轻量MQ、操作日志Set无序集合去重、交集并集差集点赞、关注关系、标签ZSet跳跃表字典按权重排序排行榜、延时队列、优先级任务5. 持久化和集群确保数据不丢、服务不停5.1 RDB与AOF两种持久化方式的取舍Redis的数据存在内存里如果进程退出或者机器宕机内存中的数据直接蒸发。所以Redis提供了两种持久化机制RDB快照和AOF追加日志。RDB是在指定时间间隔内把内存中的全部数据生成快照写入磁盘默认配置是save 900 1、save 300 10、save 60 10000意思是900秒内有1次写操作、300秒内有10次写操作、60秒内有10000次写操作时触发快照。也可以手动执行BGSAVE或SAVE触发。RDB的优点是文件体积小恢复速度快适合用做周期性备份缺点是快照时间点之间的数据可能丢失比如每60秒存一次快照那这60秒内的数据在宕机时全丢。AOF则是把每条写操作追加到日志文件末尾恢复时从头到尾重放一遍日志。AOF的fsync策略有三种always每条写命令都同步到磁盘最安全但性能损失大、everysec每秒同步一次性能和安全性的折中也是默认配置、no交给操作系统决定何时同步性能最好但可能丢几秒数据。AOF的优点是数据安全性高最多丢1秒的数据缺点是文件体积大恢复速度比RDB慢。新一代的AOF还支持混合持久化用RDB作为基础快照加上增量AOF日志兼顾恢复速度和数据安全。在线业务通常两种都开RDB负责定期快照备份AOF负责细粒度恢复。如果对数据丢失容忍度极低比如交易流水AOF的everysec几乎是底线配置。另外提醒一句持久化不是越频繁越好频繁写盘会拖慢Redis的性能bad fsync会导致Redis主进程阻塞这在官方文档里有明确警告。5.2 从单机到集群主从、哨兵、Redis Cluster单机Redis的性能再高也有上限而且有单点故障风险所以生产环境基本不会让Redis以单机形态运行。部署模式从简单到复杂有三个层级。**主从复制Master-Slave**是最基础的容灾方案。一个主节点负责写多个从节点同步主节点的数据并负责读。主节点挂了可以从从节点恢复数据但主节点故障时的自动切换还需要人工介入所以主从复制更像是数据备份方案。**哨兵模式Sentinel**在主从复制的基础上增加了哨兵进程哨兵负责监控所有节点的健康状态。当主节点故障时哨兵自动在从节点中选举一个新的主节点应用端通过哨兵获取当前主节点的地址整个过程对应用层透明实现了高可用。哨兵本身也建议部署奇数个3个或5个防止脑裂和误判。Redis Cluster则是真正的分布式方案。它将全部数据划分为16384个哈希槽每个节点负责一部分槽位。客户端根据key的CRC16算法计算哈希值并取模16384决定该key落在哪个槽、被路由到哪个节点。Cluster方案可以水平扩展数据自动分片节点故障时自动将槽位转移给其他节点适合数据量巨大、单机内存装不下的场景。对于中小型项目Redis主从加哨兵已经能覆盖绝大多数高可用需求数据量超过单机内存上限或者写入吞吐极高时才需要考虑Cluster。不要一开始就上Cluster管理复杂度完全不同。6. 常见问题与排查技巧实录6.1 安装和连接那些事Windows平台安装Redis可能是最多人遇到问题的地方。Redis官方并不支持Windows微软之前维护过一个移植版本后来停止更新了。目前Windows上最常用的有两个来源一是从Memurai或tporadowski维护的Windows版本下载二是通过WSL2安装Linux版Redis。我个人的建议是如果开发环境是Windows直接用WSL2最省心配置和Linux完全一致避免很多兼容性问题。Mac平台相对简单brew install redis一步到位装完用redis-server启动、redis-cli进入命令行。Docker安装则是最通用的方式docker run -d --name redis -p 6379:6379 redis注意要把数据目录挂载到宿主机持久化否则容器删掉数据就全没了。连接不上Redis时先检查三个地方端口是否被防火墙拦截Redis的bind配置是否限制了本机回环地址以及protected-mode是否开启。Redis默认的protected-mode在无密码配置时只允许本机回环访问很多新手改了bind却忘了关保护模式导致外部机器连不上报connection refused错误。生产环境务必设置密码修改redis.conf里的requirepass配置然后用redis-cli -a 密码登录。客户端连接工具方面Redis官方的RedisInsight是免费且功能够用的选择Another Redis Desktop ManagerAARDM界面更轻量也是不错的备选。6.2 运行时报错的排查思路实际运行中最常见的异常之一是类似“Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException”的报错。Lettuce是Spring Boot默认的Redis客户端这个异常通常意味着命令发送了但Redis没在超时时间内返回。原因可能是Redis实例负载极高大key操作阻塞了事件循环、客户端和Redis之间的网络抖动、或者连接池被占满导致请求排队。排查步骤是先看Redis实例的CPU和内存使用率再看慢查询日志slowlog get最后检查连接池配置。Spring Boot中可以通过spring.redis.timeout参数调大超时时间但治标不治本根因还是得定位。另一个高频问题是键的序列化方式和类型不匹配。Spring Boot中如果使用了RedisTemplate默认的序列化器是JdkSerializationRedisSerializerkey会变成类似“\xac\xed\x00\x05t\x00...”这样的乱码。不匹配的原因是写入时用了一种序列化器读取时用了另一种或者Redis里存的是String类型但代码里用Hash类型去读。解决方案是统一配置StringRedisSerializer给keyvalue则根据业务选择Jackson或者GenericJackson2JsonRedisSerializer。序列化问题不解决Redis Desktop Manager里看到的全是乱码排查问题非常痛苦。大key问题也是Redis性能杀手。一个key里塞了上百万的Hash字段或者一个List有几万条元素执行相关命令时Redis会被阻塞。排查大key用redis-cli --bigkeys它会扫描整个实例并输出最大的几个key。优化方向是拆分大key比如一个Hash拆成多个小Hash或者改用其他数据结构。Redis官方还有个叫unlink的命令可以异步删除大key避免阻塞主线程这个细节知道的人不多但很实用。6.3 缓存一致性Redis和数据源之间的数据打架缓存和数据库之间的数据一致性问题几乎每个用Redis的项目都会遇到。前面我讲写操作“先更新数据库再删除缓存”这个方案有个极端场景先更新数据库成功然后删除缓存失败那么缓存里的还是旧数据。解决思路是引入重试机制删除失败时把key丢进消息队列异步重试删除。还有一种叫“订阅数据库变更日志”的方案监听MySQL的binlog变更把变更同步到Redis这个方案适合缓存强一致的场景但架构复杂度偏高不强一致时用Cache Aside加短暂过期时间就够了。我个人在项目里通常的做法是核心数据走先更库再删缓存缓存过期时间设为5到15分钟即使删除缓存失败最多也就5到15分钟内存在不一致业务可接受。如果业务严格要求强一致那就不要用缓存直接走数据库强一致是需要付出性能代价的。7. 最后一个建议Redis的使用门槛不高但把它用好需要踩过很多坑才能有体感。我自己从一开始的无脑缓存到现在会主动去考虑过期时间、锁的粒度、持久化策略、部署模式中间经历了好几次线上事故的教训。如果你现在刚开始系统性学习Redis我的建议是先把五种数据类型的适用场景搞清楚再把缓存穿透、击穿、雪崩这三个问题各自对应的解决方案做一遍实验然后去了解一下你项目里Redis是怎么部署的单机、主从还是集群每种部署的优劣是什么。一步一步来这些东西看起来多但每一个都是前后端工程师面试必问、工作必用的核心技能。如果这篇文章能帮你少走一点弯路那就算没白写了。
返回列表