INCR/DECR等原子命令可替代“读-改-写”三步操作,避免并发下计数器更新丢失;其核心价值在于Redis单线程模型保障服务端一次完成整个操作,彻底消除竞态条件。

用 INCR/DECR 等原子命令替代“读-改-写”三步操作
最常见也最容易踩坑的场景,就是多个线程同时执行 get → 计算 → set。比如计数器递增,两个线程都读到 100,各自加 1 后都写回 101,结果丢了一次更新。
Redis 本身是单线程的,但它的原子命令(如 INCR、DECR、INCRBY、SETNX)能在服务端一次完成整个操作,客户端无需参与中间状态。
-
redisTemplate.opsForValue().increment("counter", 1)比先get再set安全得多,且性能更好 - 对字符串做拼接?用
APPEND;想设值但只在 key 不存在时生效?用setIfAbsent()(注意它不带过期时间) - 避免把业务逻辑挪到 Java 层做判断——只要能用原子命令表达的,就别拆成多条 Redis 命令
共享资源必须加锁时,别手写 SETNX + EXPIRE
如果操作不能靠原子命令覆盖(比如扣库存 + 写订单 + 发消息这一整套),就必须加分布式锁。但直接用 setIfAbsent("lock:order:123", "1") 然后调 expire() 是危险的:两步非原子,网络抖动或 Redis 响应慢会导致锁没过期时间,变成死锁。
真正安全的做法是使用原子命令 SET lock:key unique-value NX PX 30000 —— 这个“判断不存在 + 设值 + 设过期”必须一步到位。
- Spring Boot 项目里,直接用
Redisson的RLock.lock(),它底层就是封装了这个原子命令 - 自己手写 Lua 脚本解锁也行,但必须保证解锁时先校验 value 是否匹配,再
DEL,这两步也要原子执行 - value 必须全局唯一(推荐用 UUID + 线程ID),否则可能误删别人持有的锁
连接池配置不当会放大并发问题
Redis 服务端没问题,不代表客户端稳。当大量线程争抢 Redis 连接时,如果连接池太小,会卡在获取连接阶段,表现为超时、拒绝连接、响应毛刺,甚至触发熔断。
默认的 Lettuce 或 Jedis 连接池参数往往不适合高并发场景,尤其在 Spring Boot 自动配置下容易被忽略。
- Lettuce 推荐显式配置:
spring.redis.lettuce.pool.max-active=32、max-wait=3000、min-idle=8 - 连接数不是越多越好:过高会压垮 Redis,过低则排队严重;建议从 16–32 开始压测,观察
redis.clients.lettuce.core.RedisClient的连接等待日志 - 务必启用连接健康检测(
test-on-borrow=true或 Lettuce 的validateConnection),避免拿到已断连的脏连接
集群模式下跨槽操作天然不支持原子性
如果你用的是 Redis 集群(不是单机或哨兵),那要特别小心:同一个业务逻辑涉及的多个 key,如果落在不同 slot,就无法用事务或 Lua 脚本保证原子性。
比如订单号 order:123 和对应库存 stock:123,默认哈希后很可能分到不同节点,WATCH 或 MULTI/EXEC 会直接报错 CROSSSLOT Keys in request don't hash to the same slot。
- 强制共槽的最简单办法:用花括号包裹公共标识,例如
order:{123}和stock:{123},Lettuce 会只对花括号内内容哈希 - 集群下不要依赖
keys *、scan全局扫描,它们在各节点分别执行,结果不可靠 - 分布式锁的 key 也得带哈希标签,否则
lock:order:{123}才能确保锁和业务数据在同一个节点
真正难的不是写对一行 increment(),而是搞清哪部分该用原子命令、哪部分必须加锁、锁该加在哪一层(key 粒度?业务维度?)、以及集群拓扑如何影响你的键设计。这些决策一旦定错,压测时可能不暴露,上线后流量一上来就出数据错乱。











