简介:本资源是面向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-web(Vue 前端)、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.sql:MySQL 建库建表 + 插入测试商户、用户、优惠券数据(约 20+ 张表)redis-init-data.lua:Lua 脚本,用于批量写入 Redis 初始缓存(如热门店铺 ZSet、商户详情 Hash、用户 Session String)
执行顺序不能错:
# 1. 启动 MySQL 容器(如果没启动) docker-compose up -d mysql # 2. 导入 MySQL 数据(假设 mysql 容器名是 mysql,root 密码是 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解压后缺 class),mvn 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 Set(ZSet),就能把“距离”作为 score,商户 ID 作为 member:
// ShopServiceImpl.java 中的 loadShopByType 方法 public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) { // 1. 构造 ZSet key:type:1:20240501(按类型+日期分片,防热点) String key = SHOP_GEO_KEY + typeId + ":" + LocalDate.now(); // 2. 计算当前坐标为中心的 geohash 范围(实际用 GeoRadiusByDistance) List<Shop> 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 并存入 ZSet;sortAscending()确保最近的店排第一;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 记录“用户对某商户的点赞关系”:key=like:shop:123, member=user: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_id=123 AND user_id=456快百倍;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连接池配置是maxIdle=8, minIdle=0, maxActive=16,而 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 现象:缓存击穿,大量请求穿透到 DB,MySQL 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)和项目编译 JDK(JDK 8)不一致,LocalDateTime字段序列化格式不同,Jackson 反序列化时报Cannot construct instance of java.time.LocalDateTime。 - 解决:强制统一时间序列化格式,在
RedisConfig.java中注册自定义ObjectMapper:@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, 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里maxWaitMillis=2000(2秒),而网络抖动或 Redis 响应慢于 2 秒,连接池就抛异常。Lettuce 默认无超时,更稳。 - 解决:两种选其一
- 方案 A(推荐):升级到 Lettuce,在
pom.xml中排除 jedis,引入 lettuce:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </exclusion> </exclusions> </dependency> - 方案 B:调大 Jedis
maxWaitMillis到 5000,并加testOnBorrow=true:JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxWaitMillis(5000); poolConfig.setTestOnBorrow(true);
- 方案 A(推荐):升级到 Lettuce,在
5. 生产级加固:从“能跑通”到“敢上线”的 4 个关键动作
跑通 demo 只是起点。真正决定你能不能把这套缓存方案用到自己项目里的,是这四个生产环境必做的动作。它们不写在源码里,但每一条都是我在线上翻车后补上的“后悔药”。
5.1 BigKey 治理:用redis-cli --bigkeys扫描并拆分,而不是等 OOM
黑马点评的user:123Hash 可能存了用户全部信息(头像、地址、积分、优惠券列表),随着业务增长,单个 Hash 可达 5MB+,HGETALL一次网络传输就卡顿。必须提前治理:
# 进入 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拆成多个小 key
user:456:base(基础信息:昵称、头像)user:456:address(收货地址列表,用 List 存)user:456:coupon(优惠券,用 Sorted Set 按过期时间排序)
修改UserServiceImpl的queryById方法,改为多 key 并行获取:List<Object> results = stringRedisTemplate.executePipelined((RedisCallback<Object>) 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()); }为什么是 500ms?MySQL 主从同步延迟 P99 通常 <300ms,500ms 覆盖 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 # 输出关键字段: # addr=172.20.0.1:56789 fd=10 name= age=1234 idle=50 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=get # addr=172.20.0.1:56790 fd=11 name= age=2345 idle=1200 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=lrange解读:
idle=1200表示该连接空闲 1200 秒(20 分钟),可能是连接泄漏;cmd=lrange频繁出现,说明某服务在轮询 List;addr=172.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 查 role=master) # 5. 验证应用:前端刷新首页,是否仍能加载店铺(证明 Lettuce 自动重连新 master)关键验证点:故障转移过程中,应用是否出现
RedisConnectionFailureException?如果有,说明 Lettuce 重连超时太短,需调大timeout和retry-interval:spring: 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 # 连接池获取超时 5s
6. 我的 Redis 实战心法:不背命令,只记场景;不抄代码,只解问题
跑通黑马点评源码,只是拿到了一张 Redis 的“体验券”。真正让我在三次大促中扛住流量洪峰的,不是某行SETNX代码,而是形成了条件反射式的决策树:
- 看到“需要排序+分页”,立刻想到 ZSet 或 Geo;
- 看到“去重+快速存在判断”,立刻排除 String,锁定 Set 或 HyperLogLog;
- 看到“高并发扣减”,立刻放弃
GET+SET,直奔 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 不是银弹,它是手术刀——用对了,切掉病灶;用错了,伤及性命。”希望帮到你。
本文还有配套的精品资源,点击获取