Redis实战终极指南:从客户端集成到性能优化,手把手教你避坑【第四部分】

书接上篇,已经降到了Redis主从、哨兵、集群。本篇继续深入Redis的核心重点功能的讲解.让你对Redis的理解不止于缓存数据...

开篇:那些年踩过的Redis线上坑

去年双11,朋友的公司做秒杀活动:库存1000件商品,结果卖出了5000单——缓存雪崩导致数据库被压垮,库存校验形同虚设;
还有一次,用户查“不存在的商品详情”,每秒1万次请求直接打穿数据库,DB CPU飙到100%,系统宕机半小时……

这些问题,不是不懂Redis理论,而是不会“落地”

  • 连接池配置错了,导致连接泄漏;
  • 缓存没做防穿透,被恶意请求搞垮DB;
  • 秒杀用了普通扣库存,没保证原子性,超卖严重。

本文用「代码+图+真实坑点」,把Redis从“玩具”变成“武器”——学完就能直接用到项目里

一、第10章:Java与Redis客户端集成

Redis是C写的,Java要连它得靠客户端。主流选手是Lettuce(Spring Boot 2.x默认,异步非阻塞)和Jedis(经典同步,适合简单场景)。

1.1 先搞懂:Redis的3种部署模式

连客户端前,必须明确Redis的架构——这决定了连接方式:

  • 单机:单节点,适合开发/测试;
  • 哨兵(Sentinel):主从+监控,自动故障转移(主挂了从顶上);
  • 集群(Cluster):分片存储,高可用+横向扩容(数据分散到多个节点)。

1.2 Spring Boot集成:Lettuce vs Jedis

(1)Lettuce:异步非阻塞,Spring Boot默认

依赖:不用额外加,spring-boot-starter-data-redis已包含。

配置文件(application.yml)

spring:   redis:     # 单机模式(注释掉sentinel/cluster)     host: localhost     port: 6379     password: "" # 无密码留空          # 哨兵模式(用这个要去掉host/port)     sentinel:       master: mymaster # 主节点名称       nodes: 192.168.1.100:26379,192.168.1.101:26379 # Sentinel地址          # 集群模式(用这个要去掉host/port/sentinel)     cluster:       nodes: 192.168.1.103:6379,192.168.1.104:6379 # 集群节点       max-redirects: 3 # 最大重定向次数(找不到节点时重试)          lettuce:       pool:         max-active: 8 # 最大连接数(根据QPS调,比如1000QPS设10~20)         max-idle: 8 # 最大空闲连接(避免频繁创建)         min-idle: 0 # 最小空闲连接         max-wait: -1ms # 连接不足时无限等(生产环境建议设1s) 

配置类(Spring Boot自动配置好了,如需自定义序列化):

@Configuration public class RedisConfig {     @Bean     public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory factory) {         RedisTemplate<String, Object> template = new RedisTemplate<>();         template.setConnectionFactory(factory);         // Key用String序列化,Value用JSON(避免乱码)         template.setKeySerializer(RedisSerializer.string());         template.setValueSerializer(RedisSerializer.json());         return template;     } } 

(2)Jedis:同步阻塞,适合简单场景

依赖:需加jedisspring-boot-starter-data-redis

<dependency>     <groupId>redis.clients</groupId>     <artifactId>jedis</artifactId> </dependency> 

配置类(手动管理连接池):

@Configuration public class JedisConfig {     @Bean     public JedisPool jedisPool() {         JedisPoolConfig poolConfig = new JedisPoolConfig();         poolConfig.setMaxTotal(8); // 最大连接数         poolConfig.setMaxIdle(8); // 最大空闲         poolConfig.setMinIdle(0); // 最小空闲         poolConfig.setMaxWait(Duration.ofMillis(3000)); // 连接超时3秒         return new JedisPool(poolConfig, "localhost", 6379);     } } 

坑点预警

  • Jedis必须close():用try-with-resources或手动close(),否则连接泄漏,最终OOM!
    正确用法:
    try (Jedis jedis = jedisPool.getResource()) {     jedis.set("key", "value"); } // 自动归还连接 

1.3 连接池最佳实践(避坑!)

  1. 参数别乱设max-active太小会报“无法获取连接”;太大浪费资源(建议设为QPS的10%~20%);
  2. Lettuce线程安全RedisConnection是线程安全的,但自定义Connection要注意隔离;
  3. 哨兵/集群配置:确保nodes地址正确,Sentinel要连对主节点名称。

二、第11章:Redis典型应用场景与实战

这部分是Redis的“灵魂”——解决真实业务问题。

2.1 缓存问题:穿透、击穿、雪崩,一次性根治

缓存的核心矛盾:缓存与数据库的一致性,但这三个问题是“缓存失效导致DB压力爆炸”。

先看缓存三大问题全景图

(1)缓存穿透:查不存在的key,打穿DB

  • 成因:请求查“数据库和缓存都没有的key”(比如恶意攻击查user:-1),每次都打DB。
  • 解决方案
    1. 空值缓存:把“不存在”的结果也缓存(比如set user:-1 "null",过期5分钟);
    2. 布隆过滤器(Bloom Filter):提前把所有存在的key存到过滤器,查询前先查,不存在直接返回。

布隆过滤器代码示例

// 初始化:预计100万元素,误判率0.01% BloomFilter<Long> bloomFilter = BloomFilter.create(     Funnels.longFunnel(),      1000000,      0.0001 );  // 启动时加载所有存在的key(比如从数据库查所有用户ID) List<Long> userIds = userRepository.findAllIds(); userIds.forEach(bloomFilter::put);  // 查询时先过过滤器 public User getUserById(Long userId) {     // 1. 布隆过滤器判断:肯定不存在→直接返回     if (!bloomFilter.mightContain(userId)) return null;     // 2. 查缓存     String key = "user:" + userId;     User user = redisTemplate.opsForValue().get(key);     if (user != null) return user;     // 3. 查数据库     user = userRepository.findById(userId).orElse(null);     if (user == null) {         // 空值缓存,防止下次再查         redisTemplate.opsForValue().set(key, "null", 5, TimeUnit.MINUTES);         return null;     }     // 4. 写入缓存     redisTemplate.opsForValue().set(key, user, 10, TimeUnit.MINUTES);     return user; } 

(2)缓存击穿:热点key过期,瞬间打穿DB

  • 成因:某个超级热点key(比如爆款商品库存)过期瞬间,大量请求同时打DB。
  • 解决方案
    1. 逻辑过期:不设物理过期时间,把过期时间存到value里(比如{"value":"库存100","expire":1620000000}),查询时检查过期,过期则异步更新;
    2. 互斥锁:获取key时加锁,只有一个线程查DB,其他线程等待。

互斥锁代码示例(SET NX PX)

public Integer getStock(String productId) {     String key = "stock:" + productId;     // 1. 查缓存     Integer stock = redisTemplate.opsForValue().get(key);     if (stock != null) return stock;     // 2. 加互斥锁(锁key=lock:stock:productId,过期30秒)     String lockKey = "lock:stock:" + productId;     Boolean lockResult = redisTemplate.opsForValue().setIfAbsent(         lockKey, "1", 30, TimeUnit.SECONDS     );     if (lockResult) {         try {             // 3. 再次查缓存(防止等待时其他线程已更新)             stock = redisTemplate.opsForValue().get(key);             if (stock != null) return stock;             // 4. 查数据库             stock = productRepository.getStockById(productId);             // 5. 写入缓存(逻辑过期:1小时+随机0~30分钟)             int baseExpire = 3600;             int randomExpire = new Random().nextInt(1800);             redisTemplate.opsForValue().set(key, stock, baseExpire + randomExpire, TimeUnit.SECONDS);             return stock;         } finally {             // 6. 释放锁:Lua脚本保证原子性(避免删错别人的锁)             String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) end";             redisTemplate.execute(                 new DefaultRedisScript<>(script, Long.class),                  Collections.singletonList(lockKey), "1"             );         }     } else {         // 拿不到锁,重试或返回降级         return -1;     } } 

(3)缓存雪崩:大量key同时过期,DB崩溃

  • 成因:一批key的物理过期时间相同(比如都设为1小时),到期瞬间大量请求打DB。
  • 解决方案
    1. 随机过期时间:基础过期时间加随机值(比如1小时+0~30分钟);
    2. 分级缓存:本地Caffeine+Redis,第一层过期时间长,第二层短;
    3. 熔断降级:用Sentinel/Hystrix,DB压力大时直接返回降级数据。

随机过期时间代码

// 设置库存缓存:1小时+随机0~30分钟 int baseExpire = 3600; int randomExpire = new Random().nextInt(1800); redisTemplate.opsForValue().set("stock:123", 100, baseExpire + randomExpire, TimeUnit.SECONDS); 

2.2 分布式锁:别再用SETNX乱搞了!

分布式锁的核心:互斥、防死锁、容错。很多人用SETNX踩坑:

(1)SETNX的致命缺陷:死锁+误删

错误示例

// 1. 加锁(没设过期时间→线程挂了,锁永远在) Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock:order", "1"); if (lock) {     try {         // 执行业务     } finally {         // 2. 直接删锁→如果业务超时,锁过期了,删的是别人的锁!         redisTemplate.delete("lock:order");     } } 

问题

  • 没设过期时间→死锁;
  • 业务超时→误删其他线程的锁。

(2)正确姿势:SET ... NX PX + Lua脚本

Redis 2.6+支持SET key value NX PX milliseconds(互斥+自动过期),释放锁用Lua脚本保证原子性(检查锁的owner再删除)。

代码示例

public void createOrder(String orderId) {     String lockKey = "lock:order:" + orderId;     String owner = UUID.randomUUID().toString(); // 唯一owner,避免误删     long expireTime = 30000; // 30秒过期          // 1. 加锁     Boolean lockResult = redisTemplate.opsForValue().setIfAbsent(         lockKey, owner, expireTime, TimeUnit.MILLISECONDS     );     if (lockResult) {         try {             // 执行业务(比如创建订单)             orderService.create(orderId);         } finally {             // 2. 释放锁:Lua脚本保证原子性             String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) end";             redisTemplate.execute(                 new DefaultRedisScript<>(script, Long.class),                  Collections.singletonList(lockKey), owner             );         }     } else {         throw new RuntimeException("重复请求,请稍后重试");     } } 

关键点

  • owner用UUID:避免不同线程的锁互相误删;
  • Lua脚本:保证“检查owner”和“删锁”是原子操作。

(3)Redlock算法:争议与适用场景

Redlock是多Redis实例的分布式锁,需要获取多数实例的锁才算成功。

  • 争议:若Redis实例时钟漂移,可能导致锁失效;
  • 适用场景:对一致性要求极高的场景(比如金融交易),否则单实例锁足够。

2.3 秒杀系统:Redis原子操作是核心

秒杀的本质:高并发下的库存扣减,必须用原子操作避免超卖。

(1)原子扣减:DECR命令

Redis的DECR是原子操作,扣减后返回剩余库存,直接判断是否≥0。

代码示例

public boolean seckill(String productId, int quantity) {     String stockKey = "stock:seckill:" + productId;     // 1. 原子扣减库存     Long remaining = redisTemplate.opsForValue().decrement(stockKey, quantity);     if (remaining != null) {         if (remaining >= 0) {             // 扣减成功,异步发MQ处理订单             sendOrderMessage(productId, quantity);             return true;         } else {             // 超卖回滚             redisTemplate.opsForValue().increment(stockKey, quantity);             return false;         }     }     return false; } 

(2)优化:Lua脚本封装“扣库存+写日志”

分两次操作会不一致(扣了库存但日志没写),Lua脚本保证原子性

完整秒杀Lua脚本(带集群兼容)

-- 参数说明(严格区分 KEYS 和 ARGV!) -- KEYS[1]: 库存键(如 stock:{seckill}:123 → 集群下用哈希标签保证同一Slot) -- KEYS[2]: 秒杀日志键(如 seckill:log:{seckill}:123) -- ARGV[1]: 扣减数量(字符串,如"2") -- ARGV[2]: 商品ID(字符串,如"123")  -- 1. 原子扣减库存 local remaining = redis.call('DECRBY', KEYS[1], ARGV[1]) -- 2. 库存不足→回滚+返回失败 if remaining < 0 then     redis.call('INCRBY', KEYS[1], ARGV[1])     return 0 end  -- 3. 库存充足→记录日志(ZSET存订单,分数=时间戳) local orderId = string.format("order:%s:%d", ARGV[2], redis.call('TIME')[1]) local score = tonumber(redis.call('TIME')[1]) redis.call('ZADD', KEYS[2], score, orderId) -- 4. 日志设过期时间(保留1小时) redis.call('EXPIRE', KEYS[2], 3600)  -- 5. 返回成功 return 1 

关键说明:KEYS与ARGV的区别

  • KEYS数组:传递要操作的Redis键,集群下必须同一Slot(用哈希标签,比如{seckill}:123);
  • ARGV数组:传递非键的业务参数,无需集群检查,但需手动转换类型(比如tonumber(ARGV[1]))。

Java调用脚本示例

// 1. 定义Lua脚本 String seckillScript = "..."; // 上面的脚本内容 DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(seckillScript, Long.class);  // 2. 准备参数:KEYS(带哈希标签) + ARGV(扣减数量+商品ID) List<String> keys = Arrays.asList("stock:{seckill}:123", "seckill:log:{seckill}:123"); Object[] args = new Object[]{"2", "123"};  // 3. 执行脚本 Long result = redisTemplate.execute(redisScript, keys, args);  // 4. 处理结果 if (result == 1) {     sendOrderMessage("123", 2); // 异步发订单消息     return "秒杀成功!"; } else {     return "库存不足!"; } 

2.4 限流:滑动窗口比固定窗口更准

限流的核心:控制单位时间内的请求量,滑动窗口更准确(避免固定窗口的“边界突刺”)。

滑动窗口Lua脚本(ZSET实现)

-- 参数: -- KEYS[1]: 限流key(如 rate_limit:user:123) -- ARGV[1]: 当前时间戳(毫秒) -- ARGV[2]: 窗口大小(毫秒,如60000=1分钟) -- ARGV[3]: 阈值(如100=1分钟最多100次)  -- 1. 删除窗口外的旧请求 redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, tonumber(ARGV[1]) - tonumber(ARGV[2])) -- 2. 计算窗口内请求数 local count = redis.call('ZCARD', KEYS[1]) -- 3. 未超阈值→添加请求+设置过期时间 if count < tonumber(ARGV[3]) then     redis.call('ZADD', KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[1]))     redis.call('EXPIRE', KEYS[1], (tonumber(ARGV[2])/1000)+1)     return 1 -- 允许 else     return 0 -- 拒绝 end 

Java调用

public boolean rateLimit(String userId) {     String key = "rate_limit:user:" + userId;     long now = System.currentTimeMillis();     long window = 60000; // 1分钟     long limit = 100; // 1分钟最多100次     return redisTemplate.execute(         new DefaultRedisScript<>(script, Long.class),         Collections.singletonList(key),         now, window, limit     ) == 1; } 

三、第12章:Redis运维与性能优化

Redis的运维重点是“监控+调优”,避免线上故障。

3.1 常用运维命令:查状态、找问题

记三个核心命令:INFOMONITORSLOWLOG

(1)INFO:查Redis整体状态

INFO是最常用的监控命令,重点看这几个指标:

  • INFO MEMORY:内存使用情况(used_memory_rss物理内存、mem_fragmentation_ratio碎片率);
  • INFO STATS:请求量、连接数(instantaneous_ops_per_sec每秒请求数、rejected_connections拒绝连接数);
  • INFO PERSISTENCE:持久化状态(rdb_last_save_time上次RDB保存时间)。

示例输出

# Memory used_memory: 1024000 used_memory_rss: 1200000 mem_fragmentation_ratio: 1.17 # 碎片率>1.5需整理 maxmemory_policy: volatile-lru # 淘汰策略(建议设这个) 

(2)MONITOR:看实时命令(慎用!)

MONITOR会打印所有实时命令,影响性能,但能快速定位慢查询或异常请求。

示例输出

1620000000.123456 [0 127.0.0.1:12345] "GET" "user:123" 1620000000.234567 [0 127.0.0.1:12346] "KEYS" "*" # 危险命令! 

(3)SLOWLOG:找慢查询

SLOWLOG记录执行时间超过slowlog-log-slower-than(默认10ms)的命令。

查看慢查询

SLOWLOG GET # 查看所有慢查询 SLOWLOG GET 1 # 查看最近1条 

示例输出

1) 1) (integer) 14 # 慢查询ID    2) (integer) 1700000000 # 时间戳    3) (integer) 100 # 执行时间(微秒)    4) 1) "KEYS" # 危险命令       2) "*" 

优化慢查询

  • 禁止KEYS *→改用SCAN
  • 避免HGETALL→改用HMGET取指定字段;
  • 用索引代替LIKE→比如ZSET存用户名,用ZRANGEBYLEX搜索。

3.2 内存优化:省内存=省钱

Redis内存优化的核心:用对数据结构+减少碎片

(1)选对数据结构:少用String存复杂数据

比如存用户属性:

  • 错误set user:123 "{"name":"张三","age":18}"(String存JSON,占100字节);
  • 正确hset user:123 name 张三 age 18(Hash,占50字节,压缩存储)。

(2)碎片整理:解决碎片率高的问题

碎片率高(mem_fragmentation_ratio > 1.5)的原因是频繁修改key导致内存分配/释放。

解决方法

  • Redis 4.0+:MEMORY PURGE(需开activedefrag yes);
  • 低于4.0:重启Redis(先备份);
  • 调整maxmemory-policyvolatile-lru,自动淘汰过期key。

(3)避免内存泄漏:及时删无用key

EXPIRE设过期时间,或定期清理(比如SCAN遍历user:*,删除30天未登录的用户)。

3.3 性能基准测试:用redis-benchmark测QPS

redis-benchmark是Redis自带的性能测试工具,测QPS、延迟等。

常用参数

  • -h:Redis地址;
  • -p:端口;
  • -c:并发数;
  • -n:总请求数;
  • -t:测试命令(如-t set,get)。

示例:测单节点SET QPS

redis-benchmark -h localhost -p 6379 -c 100 -n 100000 -t set 

示例输出

====== SET ======   100000 requests completed in 0.1 seconds   throughput: 1000000 requests per second # QPS 100万 

3.4 常见问题排查:按图索骥

遇到问题不要慌,按下面的步骤查:

问题 排查步骤
延迟高 1. 用SLOWLOG找慢查询;2. 看INFO STATSinstantaneous_ops_per_sec;3. 检查网络延迟
内存不足 1. 看INFO MEMORYused_memory_rss;2. 检查碎片率;3. 清理无用key
CPU过高 1. 用TOP看Redis进程CPU;2. 检查是否有大量计算(比如Lua脚本);3. 看MONITOR的异常请求
连接失败 1. 看INFO STATSrejected_connections;2. 检查连接池参数;3. 看Sentinel/集群状态

结尾:Redis实战的核心逻辑

Redis不是“缓存数据库”那么简单,它是解决高并发、数据一致性的利器

  • 客户端集成:懂连接池,避免泄漏;
  • 典型场景:缓存防穿透/击穿/雪崩,分布式锁用SET NX PX,秒杀用Lua脚本;
  • 运维优化:会监控(INFO/MONITOR/SLOWLOG),会调优(内存/碎片/QPS)。

最后送你一句话:Redis的坑,都是“想当然”埋的——比如忘了close Jedis,比如没给KEYS加哈希标签,比如用KEYS *查数据。

多动手,多踩坑,才能把Redis变成你的“武器”!

(全文完,觉得有用就点个赞吧~)

发表评论

评论已关闭。

相关文章