模块七:分布式场景与Java实战

模块七:分布式场景与Java实战

都2026年了,你还在用SETNX+EX给Redis上锁?来,听我讲个翻车故事

一篇让你笑着学会分布式锁、限流、消息队列的Redis实战指南


一、那天凌晨3点,我被一条报警短信炸醒

兄弟们,先别急着看技术点,听我讲个真事儿。

那是某个周六的凌晨3点17分,我正做着“架构师带薪摸鱼”的美梦,突然被一条报警短信炸醒——“订单服务异常,大量请求超时”

我睡眼惺忪地打开电脑,看了一眼日志,好家伙,库存超卖了

你猜怎么着?我们的分布式锁是用SETNX+EXPIRE两条命令分开写的。业务高峰期,线程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);

这下原子性没问题了,但还有三个隐藏的坑:

  1. 锁过期了业务还没执行完→ 线程A还没处理完,锁被释放了,线程B拿到锁,两个线程同时操作共享资源

  2. 误删别人的锁→ 线程A慢,锁过期后线程B拿到了锁,线程A执行完直接del,把线程B的锁给删了

  3. 单点故障→ Redis主节点挂了,从节点还没来得及同步锁数据

2.3 方案三:Redisson(真·生产级方案)

Redisson 帮我们解决了上面所有问题,核心代码就几行:

java

RLock 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算法:

核心原理

  1. 客户端获取当前时间戳 T1

  2. 按顺序向N个(推荐5个)独立的Redis节点请求锁

  3. 当成功获取 >= N/2+1 个锁,且总耗时 < 锁过期时间,认为锁获取成功

  4. 锁实际有效时间 = 过期时间 - 总耗时

  5. 失败则向所有节点释放锁

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来滑动统计时间窗口内的请求数:

java

String 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的优雅实现)

令牌桶是限流算法中的“爱马仕”:

java

RRateLimiter 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还是Stream?

4.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 Stream(Redis 5.0+,推荐)

Stream是Redis亲儿子,支持持久化、消费者组、消息确认,已经可以当半个专业MQ用了:

java

// 生产者 Map<String, String> msg = new HashMap<>(); msg.put("orderId", "12345"); redisTemplate.opsForStream().add("stream:order", msg); // 消费者(需要引入Redis Stream的依赖) // 消费组 + 消息确认机制

友情提示:如果你真的需要可靠的消息队列,请用RocketMQ、Kafka或RabbitMQ。Redis做MQ只能算是“兼职”,别当主力用。


五、延时队列:用ZSet实现“30分钟未支付自动取消”

java

public void addDelayedTask(String taskId, long delayMillis) { long executeTime = System.currentTimeMillis() + delayMillis; redisTemplate.opsForZSet().add("delay:queue", taskId, executeTime); } public void processDelayedTasks() { while (true) { Set<String> 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:多台服务器共享登录态

有了Redis,Session共享变得很简单:

xml

<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>

yaml

spring: session: store-type: redis redis: namespace: "spring:session"

然后你的Controller里正常用HttpSession就行,Spring Session会自动把数据存到Redis里。

原理:Spring Session 通过过滤器SessionRepositoryFilter拦截请求,用RedisSessionRepository替换掉默认的HttpSession实现。


七、Spring Cache注解:一行代码搞定缓存

java

@Cacheable(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); }

配置RedisCacheManager:

java

@Bean 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,配置如下:

yaml

spring: redis: lettuce: pool: max-active: 20 # 最大连接数 max-idle: 10 # 最大空闲连接 min-idle: 5 # 最小空闲连接 max-wait: 3000ms # 获取连接超时时间

如果你用Druid监控连接池:

java

@Bean 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体积小,性能好需定义Schema
Kryo高性能线程不安全,需额外处理

生产级配置

java

@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // Key序列化用String RedisSerializer<String> stringSerializer = new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); // Value序列化用Jackson2Json Jackson2JsonRedisSerializer<Object> 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共享、缓存注解、连接池、序列化……每个点都能给你挖出坑来。

我的建议是

  1. 分布式锁用Redisson + 看门狗,别自己造轮子

  2. 限流用令牌桶,别用固定窗口

  3. 消息队列用Stream或专业MQ,别用List和Pub/Sub

  4. 序列化用Jackson2Json,别用JDK默认

  5. 连接池一定要配置,别用默认值

最后送大家一句话:技术选型没有银弹,只有最适合业务场景的。别盲目追求Redlock的高大上,也别嫌弃Redisson看门狗的“花里胡哨”。

好了,我要去复盘那天凌晨的翻车事故了。你们学废了吗?