ARTICLE DETAIL

资讯详情

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

基于Redis的高并发缓存实战:分布式锁与穿透击穿雪崩治理

基于Redis的高并发缓存实战:分布式锁与穿透击穿雪崩治理 简介面向毕业设计与高并发缓存实战的完整Redis项目资源适合希望深入掌握Redis核心原理、缓存策略与高并发场景落地的开发者。压缩包共79个文件以72个Java源码类文件为主同时包含XML配置文件、Lua脚本、YAML配置与SQL初始化脚本整体体积仅93KB结构紧凑、模块边界清晰。项目内容覆盖常用数据结构、RDB与AOF持久化机制、消息发布订阅、事务与管道操作以及集群的搭建和数据分片策略同时按实际业务完成了缓存系统从架构设计、编码实现到压测验证的完整闭环。通过该项目可掌握高并发环境下的缓存读写优化、数据库压力缓解和系统横向扩容方法为毕业设计或工程实践提供可直接参考的完整样例。目前已有48人学习适合正在准备课设、毕设或面试的开发者系统学习。1. 基于Redis实战的高并发缓存项目在解决什么问题不是把数据放进内存就完事秒杀刚开始的一瞬间几万个请求同时打向商品详情页如果每次都回源数据库连接池几秒就会被榨干接口延迟从几十毫秒一路退化成超时。基于Redis实战的高并发缓存项目练的就是这件事把热点读的压力挡在数据库前面同时保证缓存与数据库最终一致。它把一条完整落地链路拆成可执行步骤——Redis安装与配置、数据类型选型、过期淘汰策略、分布式锁、缓存穿透/击穿/雪崩治理再到JMeter压测和集群选型。适合正在准备后端面试的Java工程师也适合在业务里被缓存一致性和缓存失效问题追着跑的人。照着复现一遍等于提前踩完了高并发缓存最常见的坑。2. 先搭起高并发缓存的最小骨架环境、数据类型与过期机制高并发缓存项目不能一上来就上集群先把单机跑顺。我一般先把本机或一台测试机的Redis拉起来把读写链路跑通再去谈分布式锁和缓存治理。不在单机阶段把数据类型、过期策略和序列化方式定好后面压测一打全是脏数据排错成本极高。2.1 本地快速拉起一个Redis安装命令、核心配置与可视化工具常见做法是在Linux服务器上装Redis。测试环境图省事可以用包管理器但建议至少用源码方式装一次方便锁定版本和看编译输出# Ubuntu / Debian 系快速安装 sudo apt-get update sudo apt-get install -y redis-server # 源码安装便于指定版本 wget https://download.redis.io/releases/redis-7.0.14.tar.gz tar xzf redis-7.0.14.tar.gz cd redis-7.0.14 make sudo make install编译通过后先前台启动一次确认没有报错再转后台redis-server。生产习惯是用配置方式启动redis-server /etc/redis/redis.conf。源码编译的Redis默认不带systemd脚本我用nohup redis-server 起测试实例线上则用systemd或容器托管。拿到一个能跑的Redis后先改四个参数避免后患。打开redis.conf把监听地址、密码、内存上限和持久化策略一次调好# 绑定内网网卡不要裸暴露到公网 bind 127.0.0.1 192.168.1.100 # 生产环境必须设密码 requirepass yourstrongpassword # 内存上限防止OOM把整个机器拖死 maxmemory 2gb maxmemory-policy allkeys-lru # RDB AOF 双开注意性能和安全的取舍 appendonly yes appendfsync everysecbind只允许本机和内网IP访问是防止缓存被外部扫描的第一道门。requirepass不设的话Redis基本等于裸奔。maxmemory设成物理内存的一半左右避免Redis吃满内存后触发系统OOM Killer。appendfsync everysec是性能和安全的中间值always太慢no容易丢数据。排查数据写没写进去时不一定非开可视化工具。我习惯先用redis-cli直接敲命令验证redis-cli -a $REDIS_PWD GET product:1001。要看某个key多久过期用TTL product:1001要看哪些key占内存大用redis-cli --bigkeys。可视化工具用来观察整体结构更直观Redis Desktop Manager和Another Redis Desktop Manager都可以我一般拿它看key前缀分布和过期情况不会拿它做线上写操作。2.2 用Spring Boot写最小缓存读写String、Hash还是对象序列化后端整合Redis常见做法是用Spring Boot的spring-boot-starter-data-redis。先配置连接池和序列化方式spring: data: redis: host: 127.0.0.1 port: 6379 password: ${REDIS_PWD} timeout: 3s lettuce: pool: max-active: 50 max-idle: 20 min-idle: 5 max-wait: 300ms连接池参数里max-active是核心。50个连接应对单机压测够用但注意这只是上限不是初始值。min-idle设5避免流量突增时临时创建连接产生延迟。timeout设3秒防止Redis阻塞时客户端无限等待。读写代码我倾向于用StringRedisTemplate而不是RedisTemplate。RedisTemplate默认的JDK序列化会把key和value都变成一串\xAC\xED开头的乱码调试和排查都很难受。StringRedisTemplate把一切当字符串处理配合JSON做序列化最直接Service public class ProductCacheService { private final StringRedisTemplate stringRedisTemplate; private final ObjectMapper objectMapper new ObjectMapper(); public Product getProduct(Long id) throws JsonProcessingException { String key product:detail: id; String json stringRedisTemplate.opsForValue().get(key); if (json ! null) { return objectMapper.readValue(json, Product.class); } // 未命中缓存回源数据库 Product product loadFromDb(id); if (product ! null) { stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(product), Duration.ofMinutes(30)); } return product; } }这里set操作带了Duration等于同时把TTL设好比先set再expire少一次网络往返。为什么用String因为JSON可读、跨语言、方便排查线上出问题能用肉眼看出value格式对不对。Hash则更适合热更新场景比如商品详情只改价格字段时用Hash的HSET product:hash:1001 price 99.9只写一个字段而不是把整个JSON拿出来反序列化再写回去。数据类型选择原则很简单读多写少用String字段级更新频繁用Hash计数场景用String加INCR命令。2.3 缓存过期与淘汰策略参数怎么定才不被流量打穿每个key设多长TTL是高并发缓存项目里最考经验的地方。太短缓存形同虚设太长数据不新鲜。我一般按业务容忍度分四类业务场景TTL建议说明热点商品详情30分钟到2小时容忍短暂不新鲜验证码5分钟必须严格过期库存计数不设TTL或30秒配合分布式锁更新用户会话信息2小时有活动时滑动续期注意TTL不能全员写死。一个商品详情页在零点整同时过期会引发一次微型雪崩。解决办法是加随机偏移我习惯在固定TTL基础上加0到60秒的随机值Duration ttl Duration.ofMinutes(30) .plus(Duration.ofMillis( ThreadLocalRandom.current().nextLong(0, 60_000)));maxmemory-policy决定内存满了之后谁被淘汰。默认noeviction在写新key时会直接报错绝对不能用在生产。我线上常用allkeys-lru让Redis按最近最少使用淘汰对热点读场景最省心。volatile-lru只淘汰设置了TTL的key适合缓存和永久数据混用的实例。如果业务数据有明显冷热区分又不想手动管理allkeys-lru是最省事的选择。3. 真正扛高并发的关键分布式锁与缓存穿透/击穿/雪崩治理单机缓存跑通只是起点。真正让高并发缓存项目值钱的是并发场景下的锁和三类经典问题缓存穿透、缓存击穿、缓存雪崩。这三个词面试必问线上也必踩我一个个拆开讲。3.1 用SETNX到Redisson改造分布式锁三个参数定生死缓存重建场景里如果热点key失效几十个线程同时回源数据库MySQL会瞬间被打满。常见做法是给缓存重建加分布式锁只允许一个线程回源其余线程等待后复用第一个线程写的缓存。先看Redis原生命令版本。Redis从2.6.12开始SET命令可以把加锁和过期一次完成# 返回OK代表抢锁成功返回nil代表锁已被占用 SET lock:product:1001 8f3b2c9e NX EX 30NX表示只有key不存在时才写入EX 30表示锁自动过期30秒。value一定不能写死要写一个随机UUID这样释放锁时能确认是自己的锁。释放锁不能直接DEL否则可能把别人刚抢到的锁删掉。要保证原子性用一段Lua脚本if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这段Lua先校验value是否对得上对得上才删这是分布式锁最基本的安全底线。但原生SETNX有个隐患如果业务执行超过30秒锁自己过期了第二个线程进来第一个线程还没执行完锁就形同虚设。我线上更推荐用Redisson它提供的watchdog机制会自动续期默认每10秒检测一次锁是否仍持有自动把锁有效期延长到业务结束RLock lock redissonClient.getLock(lock:product:1001); boolean locked lock.tryLock(0, 30, TimeUnit.SECONDS); if (!locked) { throw new BizException(排队中请稍后重试); } try { // 只允许一个线程回源重建缓存 Product product loadFromDb(id); stringRedisTemplate.opsForValue().set(key, productJson, Duration.ofMinutes(30)); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }tryLock三个参数分别是最长等待时间、锁租约时间、时间单位。最长时间设0表示抢不到就立刻返回适合秒杀场景让用户排队重试。租约时间设30秒业务超过这个时间锁会释放防止持有锁的节点宕机后锁和业务一起挂掉。用Redisson时注意加锁操作本身也可能抛异常要放在try外面否则锁初始化失败会被当成业务异常吞掉。3.2 缓存穿透、击穿、雪崩的治理空缓存、互斥重建与随机TTL缓存穿透是查询一个根本不存在的数据比如恶意请求商品ID为-1的接口。缓存里没有数据库里也没有每次请求都回源数据库压力直接爆表。解决思路是给不存在的key也写一个空缓存TTL设短一点比如30秒Product product loadFromDb(id); if (product null) { // 空缓存兜底防止穿透 stringRedisTemplate.opsForValue().set( product:detail: id, EMPTY, Duration.ofSeconds(30)); return null; }更彻底的做法是前置布隆过滤器把存在的ID放在过滤器里请求先过过滤器不在直接返回。布隆过滤器有误判率参数一般设1%以内就够用。它的缺点是维护成本高新增商品要同步更新位图适合ID范围稳定的场景。缓存击穿是热点key刚好过期大量请求同时打进来。穿透是查不存在的数据击穿是查存在但缓存刚好失效的数据。击穿的标准解法是互斥锁只有抢到锁的线程回源其余线程短暂等待后重查缓存。我常用SET key value NX EX做轻量锁比Redisson更轻适合单机压测练手String lockKey lock:product:detail: id; Boolean ok stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(5)); if (Boolean.TRUE.equals(ok)) { try { Product product loadFromDb(id); stringRedisTemplate.opsForValue().set(cacheKey, json, Duration.ofMinutes(30)); } finally { stringRedisTemplate.delete(lockKey); } } else { // 没抢到锁的线程等50ms后重查缓存 Thread.sleep(50); return getProduct(id); }这个是递归重查缓存刚被重建第二次查询就能命中。注意Thread.sleep时间不要超过锁的过期时间否则缓存还没建好锁就没了后续请求还是会回源。缓存雪崩比击穿更广是大批量key同时过期或Redis节点宕机。解决思路分散化TTL加随机偏移应对批量过期主从哨兵应对单点故障服务端做多级缓存兜底本地进程内缓存扛第一波Redis扛第二波数据库只接收穿透下来的请求。3.3 缓存与数据库一致性先更新库还是先删缓存缓存项目绕不开一致性。常见做法是Cache Aside旁路缓存读的时候先读缓存未命中回源数据库再写缓存写的时候先更新数据库再删除缓存。删除缓存而不是更新缓存原因有两个。第一更新缓存意味着每次写请求都要写Redis如果数据库更新频率高缓存会被频繁重写白白浪费性能。第二并发更新同一个key后写的值可能覆盖先写的缓存里存的东西和数据库不一致。删除缓存则让下次读请求自然回源重建收敛到正确值。那为什么不是先删缓存再更新数据库因为两个操作之间有一个时间窗口。线程A先删缓存线程B读到缓存为空回源刚好数据库还是旧值把旧值写回缓存之后线程A才把新值更新到数据库缓存就永久是脏的了。先更新数据库再删缓存虽然删缓存前有一小段时间缓存是旧值但最终会被一次删除纠正收敛速度快得多。这个方案并非完全没有坑。如果删除缓存失败比如Redis命令超时缓存里还是旧值。线上通用做法是延迟双删更新数据库后删除缓存隔几百毫秒再删一次即使第一次删除失败还有第二次兜底。实际项目里我更建议把删除缓存动作做成可靠消息或本地任务表异步重试几次。对一致性要求极高的账务类数据不要走缓存直接走数据库强一致缓存只服务读多写少的数据。4. 用压测把高并发缓存项目跑穿JMeter、Redis监控与集群选型写代码谁都会但高并发项目跑不跑得住必须压测说了算。压测不是简单开几个线程打一打要看缓存命中率、TPS拐点和慢日志变化。这一章讲我把单机缓存项目验证到能上线的三板斧。4.1 用JMeter打高并发压测线程组、聚合报告与三个必调参数JMeter是压测高并发缓存项目的标准工具。常见做法是先用GUI把测试计划配好再命令行跑回归。线程组是最核心的组件参数直接影响压测结果参数建议值作用线程数200到1000模拟并发用户数Ramp-up30到60秒让压力缓慢爬升不要瞬间压垮循环次数100到500保证每个线程有足够请求量压测需要把Redis缓存先预热还是先清空看目标而定。测缓存穿透我用redis-cli把相关key清空再打测综合性能先灌一批热点数据再打。JMeter加HTTP请求采样器配置里把连接超时设为1000ms、响应超时设为3000ms超过就当成失败这样能暴露Redis慢命令的问题。命令行跑压测是上线前的固定动作jmeter -n -t high-concurrency.jmx -l result.jtl -e -o report/跑完后重点看聚合报告里的三个数字吞吐量、90%响应时间和异常率。吞吐量就是每秒能处理多少请求90%响应时间反映用户体验异常率超过0.1%就必须查日志。第一次压测我一般只开200并发观察数据库连接池和Redis内存变化再逐步加到500、1000找到TPS拐点。4.2 Redis慢日志与INFO命令压测时的三个监控手段压测过程中不能只盯着JMeterRedis侧的监控才决定瓶颈分析方向。我会同时开三个终端分别跑慢日志、INFO统计和bigkeys扫描# 查看最近10条慢日志 redis-cli -h 127.0.0.1 -p 6379 -a $REDIS_PWD slowlog get 10 # 缓存命中率核心指标 redis-cli info stats | grep -E keyspace_hits|keyspace_misses # 各命令调用次数统计找出热点命令 redis-cli info commandstats | head -30慢日志默认阈值是10000微秒也就是10毫秒。压测时如果发现大量命令超过这个阈值说明Redis执行被阻塞或key太大。INFO命令里keyspace_hits和keyspace_misses是缓存治理最核心的两个数字命中率公式是hits除以hits加misses。线上运营正常时命中率低于80%就要警惕压测时如果命中率骤降多半是缓存预热不够或者TTL设置不合理。--bigkeys扫描可以找出大key。一个value超过1MB的key在高并发下会成为热点瓶颈读写都慢。压测前先跑一遍这个命令把大key拆掉或换数据结构不然压到一半Redis卡死会以为是代码问题。4.3 从单机到集群主从、哨兵和Cluster的边界单机压测通过后下一个问题是走到哪一步要开始上集群。常见做法分三档。主从复制解决的是读压力。一台master挂多台slave读流量分配到slavemaster只处理写。主从同步是异步的slave上可能有短暂数据延迟对一致性敏感的页面不要走slave。哨兵模式解决的是高可用。master挂了哨兵自动把slave提升为master应用端通过哨兵感知新的master地址。但一台master依然有内存上限数据量超过单机内存还是撑不住。Cluster模式解决的是容量和写扩展。数据按slot均匀分布到多台节点写入可以水平扩展。Cluster的代价是客户端复杂度高多key操作要确认它们在同一个slot事务能力也受限。我见过不少团队在数据量不到5GB时就上了Cluster运维把slot迁移和reshard折腾得苦不堪言。我的建议是先压测如果单机8GB内存和哨兵模式能扛住未来一年的峰值就不要上Cluster。锁和数据一致性复杂度不值得为不存在的高并发买单。5. 高并发缓存项目最容易翻车的5个坑现象、原因与解决高并发缓存项目做完压测和上线过程会暴露一堆只靠读文档发现不了的问题。下面这5条是我反复踩过、也帮别人排查过最多的坑每一条都按现象、原因、解决的顺序拆开讲你可以直接对照自己的项目。5.1 并发一高数据库连接池就报错缓存形同虚设现象压测线程从200加到500时数据库连接池开始抛Connection is not available, request timed out但Redis的CPU和内存都正常。原因查了下缓存的命中率发现大量请求在查一个不存在的ID比如商品ID为负数或者已被下架的数据。这些key缓存里没有回源逻辑每次都查数据库缓存完全没有兜住。这是典型的缓存穿透恶意请求拿不存在的ID反复打接口时尤其严重。解决给查询结果为空的情况写临时缓存TTL设30秒同时在前置接口做参数校验负数ID直接返回异常。更稳妥的方案是在缓存层加布隆过滤器ID不在集合里的请求直接丢弃不用碰数据库。5.2 分布式锁抢到了还是超卖库存扣成负数现象压测秒杀接口时库存总数1000件实际卖出去1200件日志里每个线程都声称自己抢到了锁。原因锁没抢到不等同于业务没执行。我把锁的tryLock返回值当成成功标志但没在失败时终止流程线程继续往下执行扣库存。另外一个更隐蔽的原因是锁value写死了固定字符串线程A释放锁时把线程B的锁也删了。两个线程同时持锁超卖就必然发生。解决tryLock返回false必须直接抛异常或返回“抢购失败”不能继续往下走。释放锁改用Lua脚本比对value再删除确保只能删自己的锁。用Redisson的话配置watchdog自动续期避免业务执行超过锁租约时间。5.3 同期设置的缓存key在同一秒失效缓存命中率一夜回到解放前现象零点过后缓存命中率从95%暴跌到40%数据库压力暴涨持续一两分钟后才恢复。看Redis慢日志大批KEYS命令和同前缀的GET命令同时出现。原因所有商品详情key都是写入时固定加30分钟TTL同一批商品在同一时间失效形成批量缓存雪崩。虽然Redis还活着但数据库被回源流量打蒙了接口响应时间翻了十倍。解决TTL不再写死写入时加随机偏移让过期时间分散在前后一分钟内。同时给热点商品加一个主动续期逻辑访问时如果TTL低于阈值就把TTL续到30分钟相当于给热点数据续命。5.4 Redis可视化工具里全是乱码value长这样\xAC\xED\x00\x05t现象用Redis Desktop Manager打开缓存实例key显示成\xAC\xED\x00\x05tProductvalue也是\xAC\xED\x00\x05开头的一串乱码命令行里却能看到业务字符串。原因RedisTemplate默认使用JdkSerializationRedisSerializerJava对象先被JDK序列化成二进制再存进Redis。JDK序列化后的数据带类型签名可读性极差还会在value里附带类路径占空间。这不是Redis的问题是客户端序列化方式选错了。解决统一改用StringRedisTemplatekey和value都存字符串。对象先转JSON字符串再写入读取时再反序列化。如果必须用RedisTemplate把valueSerializer替换成GenericJackson2JsonRedisSerializerkey保持StringRedisSerializer。改完后可视化工具和命令行看到的内容都应该是可读JSON。5.5 Redis command timed outLettuce连接池被打满现象高并发压测时应用日志频繁报Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException几分钟后接口大面积超时。原因Lettuce作为Spring Boot默认客户端是异步的但如果connection pool配置不对它会把请求阻塞在等待连接上。我遇到过把max-idle设成8、max-active设成16的小池子压到200并发时所有连接都被占满新请求全部超时。另外一个原因是Redis侧有慢命令比如处理很大的Hash单个命令耗时超过客户端的timeout阈值。解决先把max-active调到压测并发数以上的值比如500并发配80个连接max-wait设300到500毫秒。然后查Redis慢日志找到耗时超过客户端timeout的命令拆大key或换数据结构。如果业务对单点延迟敏感客户端timeout和Redis慢日志阈值要同步调让超时先于队列堆积出现便于提前发现瓶颈。6. 缓存Key命名与上线前的自查习惯高并发缓存项目收尾时我坚持做三件事key命名统一、TTL随机化、上线前压测。key命名我最常用的是“业务:场景:ID:版本”格式比如mall:product:detail:1001:v2。业务前缀方便按产品线隔离场景前缀方便定位用途ID是具体数据版本号是给缓存结构升级留的后路。用冒号而不是下划线因为Redis Desktop Manager和另一个可视化工具会把冒号自动折叠成目录树排查效率高很多。上线前我有个固定checklist抽查高流量缓存key是不是都带了随机TTL偏移用redis-cli --bigkeys确认没有超过1MB的大key跑一次200并发的压测看缓存命中率和慢日志有没有异常确认Redis可视化工具里能看到正常JSON而不是JDK序列化的乱码。这套动作看起来琐碎但每次都能拦住至少一个上线事故。我的个人习惯是把线上缓存问题当成黑匣子来记录什么时间点命中率下降、当时连着做了多少次回源、Redis慢日志里是哪类命令记多了会发现大多数缓存事故的根源就那几个。高并发缓存项目最大的价值不是把Redis用熟而是学会在数据库被打爆之前靠监控数据把痛点定位出来。希望这些参数和踩坑记录能帮你少走一段弯路希望帮到你。本文还有配套的精品资源点击获取
返回列表