1. Redis乐观锁原理与实战应用
Redis乐观锁是一种高效的并发控制机制,特别适合读多写少的场景。与悲观锁不同,乐观锁假设并发操作时冲突很少发生,因此不在操作数据时加锁,而是在更新数据时检查数据是否被其他线程修改过。
1.1 乐观锁核心机制
Redis通过四个关键命令实现乐观锁:
- WATCH:监视一个或多个key,如果在事务执行前这些key被其他客户端修改,事务将被中断
- MULTI:标记事务块的开始,后续命令会被排队但不立即执行
- EXEC:执行事务块内的所有命令
- DISCARD:取消事务,放弃执行事务块内的所有命令
关键点:WATCH命令实际上是为Redis事务添加了CAS(Compare-And-Swap)特性。当EXEC被调用时,Redis会检查被WATCH的key是否被修改过,如果被修改则整个事务不会执行。
1.2 库存扣减实战案例
下面是一个完整的Java实现示例,使用Jedis客户端演示如何在电商库存扣减场景中应用Redis乐观锁:
public class InventoryService { private static final String STOCK_KEY = "product:1001:stock"; private JedisPool jedisPool; public boolean deductStock(int quantity) { try (Jedis jedis = jedisPool.getResource()) { int retry = 3; // 最大重试次数 while (retry-- > 0) { // 1. 监视库存key jedis.watch(STOCK_KEY); // 2. 获取当前库存 int currentStock = Integer.parseInt(jedis.get(STOCK_KEY)); if (currentStock < quantity) { jedis.unwatch(); return false; // 库存不足 } // 3. 开启事务 Transaction tx = jedis.multi(); // 4. 扣减库存 tx.decrBy(STOCK_KEY, quantity); // 5. 执行事务 List<Object> results = tx.exec(); if (results != null) { // 事务执行成功 return true; } else { // 事务执行失败(被其他客户端修改) try { Thread.sleep(100); // 等待短暂时间后重试 } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } } return false; // 重试次数用尽 } } }1.3 关键问题与解决方案
问题1:为什么需要重试机制?当多个客户端同时修改同一个key时,只有一个客户端的事务能执行成功,其他客户端需要重试。实际项目中建议:
- 设置合理的重试次数(通常3-5次)
- 每次重试间加入随机等待时间(避免活锁)
问题2:WATCH的性能影响监视大量key会影响Redis性能,建议:
- 只WATCH真正需要监视的key
- 及时使用UNWATCH取消不再需要的监视
- 对于热点key,考虑使用分布式锁或Lua脚本替代
2. Lettuce客户端实现详解
Lettuce作为现代Redis客户端,提供了更优雅的API来实现乐观锁。以下是使用Lettuce的Reactive API实现:
public class LettuceInventoryService { private static final String STOCK_KEY = "product:1001:stock"; private RedisClient redisClient; public Mono<Boolean> deductStock(int quantity) { return Mono.usingWhen( redisClient.connect().reactive(), connection -> { return connection.watch(STOCK_KEY) .then(connection.get(STOCK_KEY)) .flatMap(stockStr -> { int stock = Integer.parseInt(stockStr); if (stock < quantity) { return connection.unwatch().then(Mono.just(false)); } return connection.multi() .then(connection.decrBy(STOCK_KEY, quantity)) .then(connection.exec()) .map(results -> results != null); }) .retryWhen(Retry.backoff(3, Duration.ofMillis(100))); }, RedisConnection::close ); } }2.1 Lettuce与Jedis对比
| 特性 | Jedis | Lettuce |
|---|---|---|
| 连接模式 | 阻塞式 | 异步/响应式 |
| 线程安全 | 每个连接非线程安全 | 连接线程安全 |
| 事务支持 | 基本事务支持 | 更丰富的事务API |
| 性能 | 较高 | 更高(尤其在高并发场景) |
| 适用场景 | 简单应用 | 复杂、高并发应用 |
3. Spring Data Redis集成方案
Spring生态提供了更便捷的方式来使用Redis乐观锁:
@Repository public class SpringInventoryRepository { @Autowired private RedisTemplate<String, String> redisTemplate; public boolean deductStock(String productId, int quantity) { return redisTemplate.execute(new SessionCallback<Boolean>() { @Override public Boolean execute(RedisOperations operations) { String key = "product:" + productId + ":stock"; operations.watch(key); String stockStr = (String) operations.opsForValue().get(key); int stock = Integer.parseInt(stockStr); if (stock < quantity) { operations.unwatch(); return false; } operations.multi(); operations.opsForValue().decrement(key, quantity); List<Object> results = operations.exec(); return results != null && !results.isEmpty(); } }); } }3.1 Spring事务管理要点
- 连接绑定:SessionCallback确保所有操作在同一个连接中执行
- 序列化配置:确保RedisTemplate配置了正确的序列化器
- 异常处理:Spring不会自动重试,需要自行实现重试逻辑
- @Transactional:虽然可用但不推荐,编程式事务控制更灵活
4. 发布订阅模式深度解析
Redis的Pub/Sub模式适合构建实时消息系统,但有几个关键特性需要注意:
4.1 核心特性
- 无持久化:消息不会持久化,离线客户端会丢失消息
- 无确认机制:发送者不知道消息是否被接收
- 广播模式:所有订阅者都会收到消息
- 模式匹配:支持通配符订阅(如news.*)
4.2 Spring集成实现
@Configuration class RedisPubSubConfig { @Bean RedisMessageListenerContainer container(RedisConnectionFactory factory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(factory); container.addMessageListener(messageListener(), new ChannelTopic("orders")); return container; } @Bean MessageListener messageListener() { return (message, pattern) -> { System.out.println("收到订单消息: " + message); // 业务处理逻辑 }; } } @Service class OrderPublisher { @Autowired private RedisTemplate<String, Object> redisTemplate; public void publishOrder(Order order) { redisTemplate.convertAndSend("orders", order); } }4.3 生产环境注意事项
- 消息堆积:当消费者处理速度跟不上生产速度时,会导致Redis内存增长
- 故障转移:主从切换时可能丢失部分消息
- 监控:需要监控订阅者数量和生产消费速率
- 替代方案:对于重要消息,考虑使用Redis Stream
5. Redis Stream实战应用
Redis 5.0引入的Stream解决了Pub/Sub的多个痛点:
5.1 核心优势
- 消息持久化:消息会被保存直到被显式删除
- 消费者组:支持多个消费者分摊处理消息
- 消息确认:消费者需要显式确认消息处理完成
- 回溯消费:可以重新处理历史消息
5.2 典型电商场景实现
@Component public class OrderStreamConsumer { @Autowired private RedisConnectionFactory factory; @PostConstruct public void init() { StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, Order>> options = StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder() .pollTimeout(Duration.ofSeconds(1)) .build(); StreamMessageListenerContainer<String, ObjectRecord<String, Order>> container = StreamMessageListenerContainer.create(factory, options); container.receive(Consumer.from("order-group", "consumer1"), StreamOffset.create("order-stream", ReadOffset.lastConsumed()), message -> { Order order = message.getValue(); processOrder(order); // 注意:需要手动ACK }); container.start(); } private void processOrder(Order order) { // 订单处理逻辑 } } @Service public class OrderStreamProducer { @Autowired private RedisTemplate<String, Object> redisTemplate; public void createOrder(Order order) { ObjectRecord<String, Order> record = StreamRecords.newRecord() .in("order-stream") .ofObject(order) .withId(RecordId.autoGenerate()); redisTemplate.opsForStream().add(record); } }5.3 消费者组管理要点
- 消费者故障处理:使用XPENDING检查未确认消息
- 消息重试:通过XCLAIM转移未处理消息的所有权
- 消费者平衡:合理设置消费者数量避免某些消费者过载
- 监控指标:监控待处理消息数量和消费者延迟
6. Lua脚本高级应用
Redis Lua脚本提供了原子性操作能力,特别适合复杂业务逻辑:
6.1 秒杀系统实现
-- KEYS[1]: 商品库存 -- KEYS[2]: 秒杀成功用户集合 -- ARGV[1]: 用户ID -- ARGV[2]: 当前时间戳 local stock = tonumber(redis.call('GET', KEYS[1])) if stock <= 0 then return 0 end if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then return 0 end redis.call('DECR', KEYS[1]) redis.call('SADD', KEYS[2], ARGV[1]) redis.call('EXPIRE', KEYS[2], 3600) -- 1小时有效期 return 16.2 Lua脚本最佳实践
- 参数校验:在脚本开始处验证参数有效性
- 错误处理:使用pcall捕获Redis命令错误
- 脚本缓存:使用EVALSHA替代EVAL提高性能
- 复杂度控制:避免在脚本中执行耗时操作
- 可读性:添加适当的注释说明业务逻辑
7. 性能优化与监控
7.1 乐观锁性能调优
- 减少WATCH的key数量:只监视真正需要的关键key
- 合理设置重试策略:根据业务特点调整重试次数和间隔
- 热点key分离:将热点数据拆分到多个key
- 本地缓存:结合本地缓存减少Redis访问
7.2 Redis监控关键指标
- 事务执行统计:监控multi/exec的成功失败率
- 内存使用:关注stream和pub/sub的内存增长
- 网络流量:监控大value和频繁操作
- 慢查询:定期检查慢查询日志
8. 生产环境经验总结
- 超时设置:合理配置连接和操作超时
- 连接池配置:根据并发量调整连接池大小
- 序列化选择:使用高效的序列化方案(如Protobuf)
- 灾备方案:设计好故障转移和数据恢复流程
- 压测:上线前进行充分的压力测试
在实际项目中,我们曾遇到因WATCH过多key导致的性能问题,最终通过以下方案解决:
- 将关联key的修改合并到单个hash结构
- 使用Lua脚本替代部分乐观锁场景
- 对非关键路径采用最终一致性方案
Redis的高级特性虽然强大,但需要根据具体业务场景合理选择。乐观锁适合冲突较少的场景,而高并发写入可能需要考虑分布式锁或队列方案。Stream提供了可靠的消息机制,但相比专业消息队列功能仍然有限,在消息堆积和消费者管理方面需要额外注意。