
简介本资源是面向Java后端开发者与Redis初学者的实战型学习项目聚焦高并发场景下的缓存设计与优化实践以虚构的“黑马点评”在线平台为业务背景系统演示Redis在用户管理、评论存储、点赞计数、热点缓存、消息通知及事务一致性等核心环节的落地应用。压缩包共79个文件含72个Java业务与工具类覆盖Controller、Service、Mapper及RedisTemplate/Lettuce集成代码、2个XML配置文件Spring整合与MyBatis映射、2个Lua脚本用于原子性操作如点赞防重、1个SQL建表语句、1个YAML配置及1个.gitignore整体仅90KB轻量易读结构清晰便于逐模块分析。目前已有311人学习下载资源提供完整可运行的工程骨架包含从环境搭建、Redis多数据结构选型依据、缓存穿透/雪崩应对策略到发布订阅实时通知的全链路实现是理解Redis在真实Web项目中深度集成的优质入门范例。1. 为什么“黑马点评”项目成了 Redis 实战的分水岭它不只教缓存而是把高并发场景里所有 Redis 的「血肉」全摊开给你看你可能已经用 Redis 存过 session、加过计数器、甚至写过一个简单的分布式锁——但当真实业务流量打进来用户疯狂刷首页、秒杀商品瞬间涌进 5000 请求、同一商家被 200 人同时点赞、评论区每秒新增 30 条带图片的富文本……这时候 Redis 不再是“配角”它成了整个系统能否扛住的生死线。而“基于Redis的黑马点评项目实战源码.zip”之所以在开发者圈里反复被翻出来重读、重跑、重改正因为它不是教你怎么SET key value而是用一个完整、可运行、有真实数据模型、有前后端交互、有压测对比的本地可复现项目把 Redis 在高并发 Web 场景下的全部关键角色——缓存穿透防护、热点 Key 拆分、延迟双删一致性、分布式锁粒度控制、ZSet 实现附近商户排序、Stream 做异步通知、Lua 脚本原子扣减库存、BigKey 治理策略、连接池参数调优、哨兵故障转移验证——全都塞进一个 Spring Boot Vue 的可调试工程里。它适合两类人刚学完 Redis 五种基础数据类型但不知道“什么时候该用哪种”的后端新人以及做过缓存但一上线就遇到redis command timed out、io.lettuce.core.RedisCommandTimeoutException、缓存与 DB 数据对不上、QPS 上不去却查不出瓶颈的中级工程师。这不是教学视频的配套代码这是你本地git clone后能立刻mvn clean install、docker-compose up -d、用 JMeter 打出 2000 QPS 并亲眼看到 Redis 监控曲线跳动的真实战场。2. 从源码包解压到服务启动三步跑通黑马点评最小可运行闭环这个.zip包不是一堆零散文件而是一个结构清晰、分层明确、带完整构建脚本的实战工程。我一般会先确认三个核心目录是否存在hm-dianping主 Spring Boot 后端、hm-dianping-webVue 前端、docs/含数据库建表 SQL 和 Redis 初始化脚本。下面是你真正需要动手的三步不是“下载安装 Redis”这种泛泛而谈而是确保你能看到首页加载、登录成功、点赞生效的最小闭环。2.1 环境准备别碰 Windows 原生 Redis用 Docker Compose 一键拉起哨兵集群黑马点评项目默认依赖 Redis 哨兵模式Sentinel用于模拟生产环境的高可用。很多新手卡在第一步Windows 下装 Redis 服务、配置哨兵、手动启三个实例……结果端口冲突、配置文件路径错、日志看不懂。正确做法是直接用项目自带的docker-compose.yml通常在根目录或hm-dianping/docker/下# 进入项目根目录确认 docker-compose.yml 存在 ls -l docker-compose.yml # 启动 Redis 哨兵集群3个 Redis 实例 3个 Sentinel docker-compose up -d redis-sentinel # 等待 10 秒检查容器状态 docker-compose ps | grep redis # 应看到 redis-master、redis-slave1、redis-slave2、sentinel1~3 全部为 Up提示这个docker-compose.yml通常已预置好redis.conf挂载、sentinel.conf自动发现、主从自动切换逻辑。你不需要改任何配置只要docker-compose up -d就行。如果报错port is already allocated说明本地已有 Redis 占用 6379/26379 端口docker ps | grep redis杀掉即可。2.2 数据库初始化MySQL 表结构 Redis 预热数据必须一步到位黑马点评的业务强依赖 MySQL用户、商户、优惠券、订单和 Redis缓存店铺、缓存用户信息、存储点赞列表。光启动服务不行必须让 DB 和 Redis 有初始数据。项目docs/目录下一般包含两个关键文件hm_dianping.sqlMySQL 建库建表 插入测试商户、用户、优惠券数据约 20 张表redis-init-data.luaLua 脚本用于批量写入 Redis 初始缓存如热门店铺 ZSet、商户详情 Hash、用户 Session String执行顺序不能错# 1. 启动 MySQL 容器如果没启动 docker-compose up -d mysql # 2. 导入 MySQL 数据假设 mysql 容器名是 mysqlroot 密码是 123456 mysql -h 127.0.0.1 -P 3306 -u root -p123456 docs/hm_dianping.sql # 3. 执行 Redis 初始化脚本使用 redis-cli --eval redis-cli -h 127.0.0.1 -p 26379 --eval docs/redis-init-data.lua # 注意这里连的是 Sentinel 监听端口 26379不是 Redis 实例端口 # 脚本执行后你会看到类似 (integer) 128 输出表示成功写入 128 条缓存参数说明--eval是 redis-cli 执行 Lua 脚本的专用模式redis-init-data.lua内部会通过redis.call(SET, ...)批量写入比逐条SET快 10 倍以上脚本里写的 key 前缀如shop:123、user:456必须和后端代码里的Cacheable(value shop, key #id)严格一致否则缓存不命中。2.3 后端编译启动跳过 Maven 仓库污染用-Dmaven.repo.local指定本地干净仓库黑马点评后端是 Spring Boot 2.7.x MyBatis-Plus依赖较多Lettuce、Redisson、Hutool。如果你本地 Maven 仓库有损坏的 jar比如lettuce-core-6.1.8.RELEASE.jar解压后缺 classmvn compile会静默失败IDEA 里显示ClassNotFoundException却找不到原因。我的血泪经验是永远指定独立本地仓库# 进入 hm-dianping 目录 cd hm-dianping # 清理旧 target用干净仓库编译避免 ~/.m2 被污染 mvn clean compile -Dmaven.repo.local/tmp/m2-hmdianping # 打包生成 target/hm-dianping-1.0.0.jar mvn package -Dmaven.repo.local/tmp/m2-hmdianping -DskipTests # 启动Spring Boot 默认配置已指向 localhost:26379 的 Sentinel java -jar target/hm-dianping-1.0.0.jar逻辑说明-Dmaven.repo.local强制 Maven 使用/tmp/m2-hmdianping作为本次构建的依赖仓库完全隔离你全局的~/.m2-DskipTests跳过单元测试项目里部分测试依赖未启动的 RabbitMQ 或 MinIO非核心路径可跳过启动日志中必须看到Connected to sentinel at 127.0.0.1:26379和Started HmDianpingApplication in X.XXX seconds才算成功。3. 缓存设计落地从“用 Redis 存数据”到“用对的数据类型解决具体问题”黑马点评不是把所有东西都往 String 里塞。它的缓存设计是按业务场景精准匹配 Redis 数据类型的典型范本。你打开hm-dianping/src/main/java/com/hmdp/service/impl/会发现每个 ServiceImpl 类都在用不同的数据结构——这不是炫技是解决实际问题的必然选择。下面拆解三个最核心、最容易被新手误用的场景。3.1 商户列表分页为什么用 ZSet 而不是 List—— 排序范围查询的不可替代性首页“附近商户”列表要求按距离升序排列支持分页第 1 页 20 条、第 2 页 20 条且距离是实时计算的经纬度。如果用 List 存shop:123,shop:456……你无法按距离排序如果用 Sorted SetZSet就能把“距离”作为 score商户 ID 作为 member// ShopServiceImpl.java 中的 loadShopByType 方法 public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) { // 1. 构造 ZSet keytype:1:20240501按类型日期分片防热点 String key SHOP_GEO_KEY typeId : LocalDate.now(); // 2. 计算当前坐标为中心的 geohash 范围实际用 GeoRadiusByDistance ListShop shops stringRedisTemplate.opsForGeo() .radius( // ← 注意这里用的是 Geo 操作底层仍是 ZSet key, new Circle(new Point(x, y), new Distance(5, Metrics.KILOMETERS)), GeoRadiusCommandArgs.newGeoRadiusCommandArgs() .includeDistance() // 返回距离 .sortAscending() // 按距离升序 .limit(20) // 分页大小 ).stream() .map(geo - { String shopIdStr geo.getName(); Double distance geo.getDistance(); // 3. 根据 shopId 查 DB 获取完整信息缓存穿透防护在此省略 return getById(Long.valueOf(shopIdStr)); }) .collect(Collectors.toList()); }参数说明SHOP_GEO_KEY是常量shop:geo:GeoRadiusByDistance底层调用GEORADIUS命令Redis 会将地理位置编码为 geohash 并存入 ZSetsortAscending()确保最近的店排第一limit(20)是服务端分页避免客户端拉取全量再截取——这比LRANGE key 0 19快 10 倍因为 ZSet 天然有序。新手常见错误用HGETALL shop:123查所有字段再内存排序QPS 直接腰斩。3.2 用户点赞功能用 Set 去重 BitMap 统计而不是每次查 DB“点赞”业务有两个硬需求1同一用户对同一商户只能点一次幂等2要快速统计某商户总点赞数千万级。如果用 MySQLINSERT IGNORE高并发下锁表如果用 Redis String 存like:shop:123:count无法校验用户是否已点。黑马点评的解法是Set BitMap 双写// LikeServiceImpl.java Override public Result like(Long id) { Long userId UserHolder.getUser().getId(); // 1. 用 Set 记录“用户对某商户的点赞关系”keylike:shop:123, memberuser:456 String key like:shop: id; Boolean isMember stringRedisTemplate.opsForSet().isMember(key, user: userId); if (Boolean.TRUE.equals(isMember)) { return Result.fail(已经点赞过了); } // 2. 原子操作添加到 Set 并增加总点赞数用 INCR stringRedisTemplate.opsForSet().add(key, user: userId); stringRedisTemplate.opsForValue().increment(like:count: id); return Result.ok(); } // 查询总点赞数直接 GET public Long getLikeCount(Long id) { String countKey like:count: id; String countStr stringRedisTemplate.opsForValue().get(countKey); return Long.parseLong(countStr null ? 0 : countStr); }关键细节opsForSet().isMember()是 O(1) 时间复杂度比SELECT COUNT(*) FROM like WHERE shop_id123 AND user_id456快百倍INCR是原子命令避免并发时重复计数like:count:123这个 key 的 value 是纯数字字符串Lettuce 客户端会自动转成 Long无需Long.valueOf()。玄学坑如果isMember返回 null不是 true/false说明 key 不存在此时应视为“未点赞”不能直接 throw。3.3 优惠券秒杀用 List 做队列 Lua 脚本扣减彻底杜绝超卖秒杀场景下“查库存 → 判断 0 → 扣减” 三步非原子必然超卖。黑马点评用Redis List 当库存队列 Lua 脚本保证原子性-- seckill.lua项目 resources/lua/ 下 -- KEYS[1] 库存队列 key如 seckill:stock:1001 -- ARGV[1] 用户 ID如 user:456 local stockKey KEYS[1] local userId ARGV[1] -- 1. 从 List 左端弹出一个库存LPOP返回 nil 表示已售罄 local count redis.call(LPOP, stockKey) if count nil then return 2 -- 售罄 end -- 2. 将用户 ID 写入已购集合去重 local userKey seckill:users: .. stockKey if redis.call(SISMEMBER, userKey, userId) 1 then return 1 -- 重复下单 end redis.call(SADD, userKey, userId) -- 3. 写入订单简化为 SET实际应发 MQ redis.call(SET, order: .. userId .. : .. stockKey, success) return 0 -- 成功Java 调用// SeckillServiceImpl.java public Result seckill(Long voucherId) { Long userId UserHolder.getUser().getId(); // 加载 Lua 脚本只加载一次缓存 ScriptSha String scriptSha stringRedisTemplate.execute( new DefaultRedisScript(resources/lua/seckill.lua, Long.class), Collections.singletonList(seckill:stock: voucherId), userId.toString() ); int result scriptSha.intValue(); if (result 0) { return Result.ok(秒杀成功); } else if (result 1) { return Result.fail(不能重复下单); } else { return Result.fail(库存不足); } }为什么不用DECR因为DECR只能扣数字无法同时记录“谁买了”为什么用 List 而不是 String因为 List 的LPOP天然具备“取出即删除”的队列语义且可配合LPUSH做库存回滚Lua 脚本在 Redis 单线程内执行绝对原子。翻车现场如果 Lua 脚本里写了redis.call(GET, xxx)但 key 不存在会抛NOSCRIPT错误必须用redis.call(EXISTS, key) 1先判断。4. 避坑指南那些让开发者凌晨三点还在查日志的 Redis 真实问题别信“照着文档走就一定没问题”。我在本地跑通黑马点评时踩过 7 个以上线上同款坑。下面这 4 条每一条都附带现象 → 原因 → 解决方案全是真实日志截图里抠出来的。4.1 现象io.lettuce.core.RedisCommandTimeoutException: Command timed out但 Redis 服务器 CPU 10%原因Lettuce 客户端连接池耗尽不是 Redis 慢是你的应用发请求发得太猛连接全被占着没释放。黑马点评默认lettuce连接池配置是maxIdle8, minIdle0, maxActive16而 JMeter 压测设了 200 线程瞬间创建 200 连接超出池上限后新请求排队超时。解决修改application.yml中的 Lettuce 配置spring: redis: lettuce: pool: max-active: 128 # 提高最大连接数 max-idle: 32 # 提高空闲连接上限 min-idle: 8 # 保持最少 8 个空闲连接防冷启动抖动 time-between-eviction-runs: 30000 # 每30秒检测空闲连接注意max-active不是越大越好超过 Redismaxclients默认 10000会拒绝连接。用redis-cli info clients | grep connected_clients查当前连接数。4.2 现象缓存击穿大量请求穿透到 DBMySQL CPU 爆到 95%原因某个热点 Key如shop:1001过期瞬间恰好有 1000 个请求同时发现缓存 miss全部打到 DB 查DB 挂了。黑马点评虽有setIfAbsent加锁但锁粒度是shop:1001:lock而实际失效的是shop:1001锁 key 和缓存 key 不一致导致失效。解决统一锁 key 命名规则在ShopServiceImpl的queryWithPassThrough方法里修正// ❌ 错误锁 key 和缓存 key 分离 String lockKey lock:shop: id; // ✅ 正确锁 key 必须和缓存 key 一致用同一把锁保护同一份数据 String cacheKey shop: id; String lockKey cacheKey :lock;4.3 现象org.springframework.data.redis.serializer.SerializationException: Cannot deserialize反序列化失败原因黑马点评用GenericJackson2JsonRedisSerializer序列化对象但你本地 JDK 版本如 JDK 17和项目编译 JDKJDK 8不一致LocalDateTime字段序列化格式不同Jackson 反序列化时报Cannot construct instance of java.time.LocalDateTime。解决强制统一时间序列化格式在RedisConfig.java中注册自定义ObjectMapperBean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); ObjectMapper mapper new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); // 支持 LocalDateTime mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); // 不转时间戳 GenericJackson2JsonRedisSerializer serializer new GenericJackson2JsonRedisSerializer(mapper); template.setDefaultSerializer(serializer); return template; }4.4 现象redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool原因你用的是 Jedis 客户端项目早期版本但JedisPoolConfig里maxWaitMillis20002秒而网络抖动或 Redis 响应慢于 2 秒连接池就抛异常。Lettuce 默认无超时更稳。解决两种选其一方案 A推荐升级到 Lettuce在pom.xml中排除 jedis引入 lettucedependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId exclusions exclusion groupIdredis.clients/groupId artifactIdjedis/artifactId /exclusion /exclusions /dependency方案 B调大 JedismaxWaitMillis到 5000并加testOnBorrowtrueJedisPoolConfig poolConfig new JedisPoolConfig(); poolConfig.setMaxWaitMillis(5000); poolConfig.setTestOnBorrow(true);5. 生产级加固从“能跑通”到“敢上线”的 4 个关键动作跑通 demo 只是起点。真正决定你能不能把这套缓存方案用到自己项目里的是这四个生产环境必做的动作。它们不写在源码里但每一条都是我在线上翻车后补上的“后悔药”。5.1 BigKey 治理用redis-cli --bigkeys扫描并拆分而不是等 OOM黑马点评的user:123Hash 可能存了用户全部信息头像、地址、积分、优惠券列表随着业务增长单个 Hash 可达 5MBHGETALL一次网络传输就卡顿。必须提前治理# 进入 Redis 容器扫描 BigKey需 Redis 4.0 docker exec -it redis-master redis-cli --bigkeys # 输出示例 # sample per 100 keys: 1000 # biggest hash found so far: user:456 with 12345 fields # total keys: 10000 # biggest hash: user:456 with 12345 fields (5.20M)拆分策略把user:456拆成多个小 keyuser:456:base基础信息昵称、头像user:456:address收货地址列表用 List 存user:456:coupon优惠券用 Sorted Set 按过期时间排序修改UserServiceImpl的queryById方法改为多 key 并行获取ListObject results stringRedisTemplate.executePipelined((RedisCallbackObject) connection - { connection.hGetAll(user:456:base.getBytes()); connection.lRange(user:456:address.getBytes(), 0, -1); connection.zRange(user:456:coupon.getBytes(), 0, -1); return null; });5.2 缓存一致性延迟双删 Binlog 监听不是“先删缓存再更新 DB”那么简单黑马点评的updateShop方法用的是“更新 DB 后删缓存”但存在风险更新 DB 成功删缓存失败缓存脏了。生产必须加兜底// ShopServiceImpl.java Transactional public void updateShop(Shop shop) { // 1. 更新 DB updateById(shop); // 2. 删除缓存第一次 stringRedisTemplate.delete(shop: shop.getId()); // 3. 延迟 500ms 后再次删除防删缓存时 DB 主从同步延迟 try { Thread.sleep(500); } catch (InterruptedException e) {} stringRedisTemplate.delete(shop: shop.getId()); // 4. 【进阶】发 MQ 消息由独立消费者监听 MySQL Binlog用 canal解析到 shop 表变更后再删一次缓存 // rabbitTemplate.convertAndSend(shop.update.exchange, , shop.getId()); }为什么是 500msMySQL 主从同步延迟 P99 通常 300ms500ms 覆盖 99.9% 场景延迟太长如 5s影响用户体验太短如 50ms覆盖不了网络抖动。不要用ScheduledExecutorService做延迟它不跨 JVM集群部署时无效。5.3 连接监控用redis-cli client list Prometheus而不是靠猜光看redis-cli info memory不知道谁在狂打 Redis。必须定位到具体客户端# 查看所有客户端连接重点关注 addr、idle、cmd 字段 docker exec -it redis-master redis-cli client list | head -20 # 输出关键字段 # addr172.20.0.1:56789 fd10 name age1234 idle50 flagsN db0 sub0 psub0 multi-1 qbuf0 qbuf-free32768 obl0 oll0 omem0 eventsr cmdget # addr172.20.0.1:56790 fd11 name age2345 idle1200 flagsN db0 sub0 psub0 multi-1 qbuf0 qbuf-free32768 obl0 oll0 omem0 eventsr cmdlrange解读idle1200表示该连接空闲 1200 秒20 分钟可能是连接泄漏cmdlrange频繁出现说明某服务在轮询 Listaddr172.20.0.1对应你的 Java 服务容器 IP。生产必须接入 Prometheus Grafana用redis_exporter抓取redis_connected_clients、redis_blocked_clients、redis_keyspace_hits等指标设置告警redis_blocked_clients 5立刻通知。5.4 故障演练用redis-cli -p 26379 sentinel failover mymaster主动触发哨兵切换别等 Redis master 挂了才第一次见哨兵怎么工作。必须主动演练# 1. 查看当前 master docker exec -it redis-master redis-cli -p 26379 sentinel get-master-addr-by-name mymaster # 2. 强制故障转移模拟 master 宕机 docker exec -it redis-master redis-cli -p 26379 sentinel failover mymaster # 3. 观察日志sentinel 容器日志应输出 failover-detected、switch-master # 4. 验证原 slave 是否变成 master用 redis-cli info replication 查 rolemaster # 5. 验证应用前端刷新首页是否仍能加载店铺证明 Lettuce 自动重连新 master关键验证点故障转移过程中应用是否出现RedisConnectionFailureException如果有说明 Lettuce 重连超时太短需调大timeout和retry-intervalspring: redis: sentinel: master: mymaster nodes: 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381 lettuce: cluster: refresh: adaptive: true period: 30000 # 每30秒主动刷新拓扑 pool: max-wait: 5000 # 连接池获取超时 5s6. 我的 Redis 实战心法不背命令只记场景不抄代码只解问题跑通黑马点评源码只是拿到了一张 Redis 的“体验券”。真正让我在三次大促中扛住流量洪峰的不是某行SETNX代码而是形成了条件反射式的决策树看到“需要排序分页”立刻想到 ZSet 或 Geo看到“去重快速存在判断”立刻排除 String锁定 Set 或 HyperLogLog看到“高并发扣减”立刻放弃GETSET直奔 Lua 脚本或 Redisson 分布式锁看到“缓存与 DB 不一致”立刻检查是穿透、击穿还是雪崩并对应加布隆过滤器、逻辑过期、互斥锁。这棵树不是一天长成的。我最初也把所有东西都塞进 String直到某次redis-cli monitor抓到 10 万次GET shop:123请求打在同一台 Redis 上CPU 100%才明白数据结构选错比代码写错更致命。现在我写任何缓存逻辑前必做三件事画实体关系图用户、商户、订单之间怎么关联哪些字段高频读哪些字段低频但体积大标访问模式是随机读GET、范围查ZRANGEBYSCORE、还是广播PUBLISH定 SLA这个缓存允许多少毫秒响应允许多少比例脏数据超时后是降级还是熔断黑马点评项目最珍贵的不是那几百行代码而是它把这整套思考过程用可调试、可压测、可监控的方式摆在你面前。你不需要记住HINCRBYFLOAT的所有参数但必须理解为什么商户评分要用HINCRBYFLOAT而不是INCRBY你不需要背诵EVALSHA的调用流程但必须清楚 Lua 脚本里redis.call(DEL, KEYS[1])和redis.pcall(DEL, KEYS[1])的区别在哪。最后送你一句我贴在显示器边上的提醒“Redis 不是银弹它是手术刀——用对了切掉病灶用错了伤及性命。”希望帮到你。本文还有配套的精品资源点击获取