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

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

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

去年双11,朋友的公司做秒杀活动:库存1000件商品,结果卖出了5000单——缓存雪崩导致数据库被压垮,库存校验形同虚设;

还有一次,用户查“不存在的商品详情”,每秒1万次请求直接打穿数据库,DB CPU飙到100%,系统宕机半小时……

这些问题,不是不懂Redis理论https://www.naquan.com/,而是不会“落地”:

连接池配置错了,导致连接泄漏;

缓存没做防穿透,被恶意请求搞垮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:同步阻塞,适合简单场景

依赖:需加jedis和spring-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 连接池最佳实践(避坑!)

参数别乱设:max-active太小会报“无法获取连接”;太大浪费资源(建议设为QPS的10%~20%);

Lettuce线程安全:RedisConnection是线程安全的,但自定义Connection要注意隔离;

哨兵/集群配置:确保nodes地址正确,Sentinel要连对主节点名称。

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

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

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

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

先看缓存三大问题全景图:

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

成因:请求查“数据库和缓存都没有的key”(比如恶意攻击查user:-1),每次都打DB。

解决方案:

空值缓存:把“不存在”的结果也缓存(比如set user:-1 "null",过期5分钟);

布隆过滤器(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。

解决方案:

逻辑过期:不设物理过期时间,把过期时间存到value里(比如{"value":"库存100","expire":1620000000}),查询时检查过期,过期则异步更新;

互斥锁:获取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小时+0~30分钟);

分级缓存:本地Caffeine+Redis,第一层过期时间长,第二层短;

熔断降级:用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 常用运维命令:查状态、找问题

记三个核心命令:INFO、MONITOR、SLOWLOG。

(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-policy为volatile-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 STATS的instantaneous_ops_per_sec;3. 检查网络延迟

内存不足 1. 看INFO MEMORY的used_memory_rss;2. 检查碎片率;3. 清理无用key

CPU过高 1. 用TOP看Redis进程CPU;2. 检查是否有大量计算(比如Lua脚本);3. 看MONITOR的异常请求

连接失败 1. 看INFO STATS的rejected_connections;2. 检查连接池参数;3. 看Sentinel/集群状态

结尾:Redis实战的核心逻辑

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

客户端集成:懂连接池,避免泄漏;

典型场景:缓存防穿透/击穿/雪崩,分布式锁用SET NX PX,秒杀用Lua脚本;

运维优化:会监控(INFO/MONITOR/SLOWLOG),会调优(内存/碎片/QPS)。

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

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

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

❤️ 如果你喜欢这篇文章,请点赞支持! 👍 同时欢迎关注我的博客,获取更多精彩内容!

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容