都2026年了你还在用SETNXEX给Redis上锁来听我讲个翻车故事一篇让你笑着学会分布式锁、限流、消息队列的Redis实战指南一、那天凌晨3点我被一条报警短信炸醒兄弟们先别急着看技术点听我讲个真事儿。那是某个周六的凌晨3点17分我正做着“架构师带薪摸鱼”的美梦突然被一条报警短信炸醒——“订单服务异常大量请求超时”。我睡眼惺忪地打开电脑看了一眼日志好家伙库存超卖了。你猜怎么着我们的分布式锁是用SETNXEXPIRE两条命令分开写的。业务高峰期线程A刚set完key还没来得及设置过期时间线程B就趁虚而入了——锁失效库存直接干穿。那一刻我感觉自己不是架构师是背锅侠。所以今天咱们就来聊聊Redis分布式锁到底该怎么玩以及顺带把限流、消息队列、延时队列、Session共享、缓存注解、连接池、序列化这些Redis实战全家桶一锅端了。二、分布式锁从翻车到真香2.1 方案一SETNX EXPIRE千万别这么写这段代码我愿称之为“P0级事故制造机”java// 千万别这么写 jedis.setnx(lock:order, thread-1); jedis.expire(lock:order, 30);问题在于这两条命令不是原子的。如果setnx成功expire还没来得及执行JVM FullGC了、进程挂了、或者网络抖了一下——锁永不过期所有请求直接堵死。2.2 方案二SET NX EX原子操作但依然有坑Redis 2.6.12 之后支持了原子操作java// 好一点但还不够 jedis.set(lock:order, thread-1, NX, EX, 30);这下原子性没问题了但还有三个隐藏的坑锁过期了业务还没执行完→ 线程A还没处理完锁被释放了线程B拿到锁两个线程同时操作共享资源误删别人的锁→ 线程A慢锁过期后线程B拿到了锁线程A执行完直接del把线程B的锁给删了单点故障→ Redis主节点挂了从节点还没来得及同步锁数据2.3 方案三Redisson真·生产级方案Redisson 帮我们解决了上面所有问题核心代码就几行javaRLock lock redissonClient.getLock(lock:order); try { // 尝试加锁等待10秒锁有效期30秒 if (lock.tryLock(10, 30, TimeUnit.SECONDS)) { // 业务逻辑 doSomething(); } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }你看代码不长但背后的门道可多了。2.4 看门狗Watchdog机制——Redisson的“续命神器”Redisson最骚的设计就是这个看门狗。默认锁过期时间是30秒每10秒检查一次如果业务还没结束自动帮你续期到30秒就像你打游戏快超时了系统自动帮你续钟原理Redisson在持有锁的客户端后台启动一个定时任务每隔internalLockLeaseTime / 3的时间默认10秒去刷新锁的过期时间。但是注意看门狗只在没有手动指定过期时间时才生效。如果你传了leaseTime对不起它就不管了。踩坑提醒如果你的业务执行时间超过锁超时时间又没有用Redisson的自动续期就会出现“锁提前释放多个线程同时执行”的问题。别问我怎么知道的。2.5 Redlock算法Redis官方的“终极方案”单Redis节点有单点故障风险Redis官方提出了Redlock算法核心原理客户端获取当前时间戳 T1按顺序向N个推荐5个独立的Redis节点请求锁当成功获取 N/21 个锁且总耗时 锁过期时间认为锁获取成功锁实际有效时间 过期时间 - 总耗时失败则向所有节点释放锁java// Redisson默认支持Redlock RLock lock1 redissonClient1.getLock(lock:order); RLock lock2 redissonClient2.getLock(lock:order); RLock lock3 redissonClient3.getLock(lock:order); RLock lock4 redissonClient4.getLock(lock:order); RLock lock5 redissonClient5.getLock(lock:order); RedissonRedLock redLock new RedissonRedLock(lock1, lock2, lock3, lock4, lock5); redLock.lock();但是没错又有但是这个算法被分布式系统专家 Martin Kleppmann就是写《数据密集型应用系统设计》那位大佬公开质疑过Redlock依赖系统时钟如果时钟发生跳变算法就废了。我的建议绝大多数业务场景单节点Redis Redisson看门狗足够了。如果你真的需要强一致性出门左转 ZooKeeper 或 etcd别用Redis做分布式锁。三、限流别让流量把服务器干趴了3.1 固定窗口最容易翻车java// 固定窗口1分钟内最多100次 String key rate:limit: userId; Long count redisTemplate.opsForValue().increment(key); if (count 1) { redisTemplate.expire(key, 60, TimeUnit.SECONDS); } if (count 100) { return 请求过于频繁; }问题窗口边界流量突刺。比如59秒请求了100次61秒又请求了100次2秒内200次请求系统直接GG。3.2 滑动窗口更精确用ZSet来滑动统计时间窗口内的请求数javaString key rate:sliding: userId; long now System.currentTimeMillis(); // 移除1分钟之前的数据 redisTemplate.opsForZSet().removeRangeByScore(key, 0, now - 60 * 1000); // 统计当前窗口请求数 Long count redisTemplate.opsForZSet().zCard(key); if (count 100) { return 请求过于频繁; } // 记录本次请求 redisTemplate.opsForZSet().add(key, String.valueOf(now), now);滑动窗口解决了固定窗口的边界问题但数据量大了之后ZSet的内存占用有点感人。3.3 令牌桶Redisson的优雅实现令牌桶是限流算法中的“爱马仕”javaRRateLimiter limiter redissonClient.getRateLimiter(rate:limiter); // 设置速率每1秒生成10个令牌 limiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.SECONDS); if (limiter.tryAcquire(1)) { // 拿到令牌执行业务 doSomething(); } else { return 请求过于频繁; }令牌桶允许一定的突发流量令牌可以累积比固定窗口和滑动窗口都更平滑。四、消息队列List、Pub/Sub还是Stream4.1 List简单MQ不推荐生产用java// 生产者 redisTemplate.opsForList().leftPush(queue:order, orderJson); // 消费者 String order redisTemplate.opsForList().rightPop(queue:order);List做消息队列最大的问题是没有确认机制消费失败消息就丢了。4.2 Pub/Sub发布订阅消息不持久java// 生产者 redisTemplate.convertAndSend(channel:order, orderJson); // 消费者实现MessageListener public void onMessage(Message message, byte[] pattern) { // 处理消息 }致命伤如果消费者离线消息直接丢失。没人消费就丢了不带商量的。4.3 StreamRedis 5.0推荐Stream是Redis亲儿子支持持久化、消费者组、消息确认已经可以当半个专业MQ用了java// 生产者 MapString, String msg new HashMap(); msg.put(orderId, 12345); redisTemplate.opsForStream().add(stream:order, msg); // 消费者需要引入Redis Stream的依赖 // 消费组 消息确认机制友情提示如果你真的需要可靠的消息队列请用RocketMQ、Kafka或RabbitMQ。Redis做MQ只能算是“兼职”别当主力用。五、延时队列用ZSet实现“30分钟未支付自动取消”javapublic void addDelayedTask(String taskId, long delayMillis) { long executeTime System.currentTimeMillis() delayMillis; redisTemplate.opsForZSet().add(delay:queue, taskId, executeTime); } public void processDelayedTasks() { while (true) { SetString tasks redisTemplate.opsForZSet() .rangeByScore(delay:queue, 0, System.currentTimeMillis(), 0, 10); for (String task : tasks) { // 处理任务 redisTemplate.opsForZSet().remove(delay:queue, task); } Thread.sleep(1000); } }典型场景订单30分钟未支付 → 自动取消定时提醒重试机制注意这个方案是“轮询”的方式不是“推送”延迟精度在秒级。如果需要毫秒级精度用RocketMQ的延迟消息或者Netty的时间轮。六、分布式Session多台服务器共享登录态有了RedisSession共享变得很简单xmldependency groupIdorg.springframework.session/groupId artifactIdspring-session-data-redis/artifactId /dependencyyamlspring: session: store-type: redis redis: namespace: spring:session然后你的Controller里正常用HttpSession就行Spring Session会自动把数据存到Redis里。原理Spring Session 通过过滤器SessionRepositoryFilter拦截请求用RedisSessionRepository替换掉默认的HttpSession实现。七、Spring Cache注解一行代码搞定缓存javaCacheable(value user, key #userId) public User getUser(Long userId) { return userMapper.findById(userId); } CacheEvict(value user, key #userId) public void updateUser(Long userId, User user) { userMapper.update(user); } CachePut(value user, key #userId) public User saveUser(Long userId, User user) { return userMapper.save(user); }配置RedisCacheManagerjavaBean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(10)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(factory).cacheDefaults(config).build(); }三兄弟的区别Cacheable有缓存取缓存没有就执行方法并缓存CacheEvict删缓存CachePut执行方法并把结果放缓存每次都会执行八、连接池配置别让连接成为瓶颈Spring Boot 2.x 默认用 Lettuce配置如下yamlspring: redis: lettuce: pool: max-active: 20 # 最大连接数 max-idle: 10 # 最大空闲连接 min-idle: 5 # 最小空闲连接 max-wait: 3000ms # 获取连接超时时间如果你用Druid监控连接池javaBean public DruidDataSource dataSource() { DruidDataSource ds new DruidDataSource(); ds.setUrl(jdbc:mysql://...); ds.setUsername(root); ds.setPassword(password); ds.setInitialSize(5); ds.setMaxActive(20); ds.setMinIdle(5); ds.setMaxWait(60000); ds.setTimeBetweenEvictionRunsMillis(60000); ds.setMinEvictableIdleTimeMillis(300000); // 开启监控 ds.setFilters(stat,wall); return ds; }九、序列化方案选错了要出大事序列化方式优点缺点JDK默认简单JDK内置体积大不可读性能差Jackson2Json可读性好Spring官方推荐体积稍大有安全漏洞风险FastJson性能好阿里出品安全漏洞频出慎重升级版本Protostuff体积小性能好需定义SchemaKryo高性能线程不安全需额外处理生产级配置javaBean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); // Key序列化用String RedisSerializerString stringSerializer new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); // Value序列化用Jackson2Json Jackson2JsonRedisSerializerObject jsonSerializer new Jackson2JsonRedisSerializer(Object.class); ObjectMapper mapper new ObjectMapper(); // 开启多态类型支持存子类对象时能正确反序列化 mapper.activateDefaultTyping( LazyCollectionSerializer.getDefaultPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL ); jsonSerializer.setObjectMapper(mapper); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; }血的教训千万别用JDK默认序列化存一个对象进去Redis里全是乱码连排查问题都费劲。十、写在最后Redis这东西说起来简单——不就是个缓存嘛。但真到生产环境分布式锁、限流、消息队列、延时队列、Session共享、缓存注解、连接池、序列化……每个点都能给你挖出坑来。我的建议是分布式锁用Redisson 看门狗别自己造轮子限流用令牌桶别用固定窗口消息队列用Stream或专业MQ别用List和Pub/Sub序列化用Jackson2Json别用JDK默认连接池一定要配置别用默认值最后送大家一句话技术选型没有银弹只有最适合业务场景的。别盲目追求Redlock的高大上也别嫌弃Redisson看门狗的“花里胡哨”。好了我要去复盘那天凌晨的翻车事故了。你们学废了吗