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)

yaml 复制代码
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自动配置好了,如需自定义序列化):

java 复制代码
@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

xml 复制代码
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

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

java 复制代码
@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!
    正确用法:

    java 复制代码
    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存到过滤器,查询前先查,不存在直接返回。

布隆过滤器代码示例

java 复制代码
// 初始化:预计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)

java 复制代码
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压力大时直接返回降级数据。

随机过期时间代码

java 复制代码
// 设置库存缓存: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的致命缺陷:死锁+误删

错误示例

java 复制代码
// 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再删除)。

代码示例

java 复制代码
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。

代码示例

java 复制代码
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脚本(带集群兼容)

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调用脚本示例

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实现)

lua 复制代码
-- 参数:
-- 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调用

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)的命令。

查看慢查询

bash 复制代码
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

bash 复制代码
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变成你的"武器"!

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

相关推荐
一抓掉一大把3 小时前
RuoYi .net-实现商城秒杀下单(redis,rabbitmq)
redis·mysql·c#·rabbitmq·.net
tuokuac17 小时前
ps -ef | grep redis
数据库·redis·缓存
⑩-17 小时前
如何保证Redis和Mysql数据缓存一致性?
java·数据库·redis·mysql·spring·缓存·java-ee
刘一说19 小时前
深入理解 Spring Boot 中的 Redis 缓存集成:从基础配置到高可用实践
spring boot·redis·缓存
不见长安在21 小时前
redis集群下如何使用lua脚本
数据库·redis·lua
努力努力再努力wz21 小时前
【Linux进阶系列】:线程(上)
java·linux·运维·服务器·数据结构·c++·redis
苦学编程的谢1 天前
Redis_6_String
数据库·redis·缓存
thginWalker1 天前
图解Redis面试篇
redis
埃泽漫笔1 天前
Redis单线程还是多线程?
数据库·redis·缓存