书接上篇,已经降到了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:同步阻塞,适合简单场景
依赖 :需加jedis和spring-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!
正确用法:javatry (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存到过滤器,查询前先查,不存在直接返回。
- 空值缓存 :把"不存在"的结果也缓存(比如
布隆过滤器代码示例:
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。
- 解决方案 :
- 逻辑过期 :不设物理过期时间,把过期时间存到value里(比如
{"value":"库存100","expire":1620000000}),查询时检查过期,过期则异步更新; - 互斥锁:获取key时加锁,只有一个线程查DB,其他线程等待。
- 逻辑过期 :不设物理过期时间,把过期时间存到value里(比如
互斥锁代码示例(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小时+0~30分钟); - 分级缓存:本地Caffeine+Redis,第一层过期时间长,第二层短;
- 熔断降级:用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 常用运维命令:查状态、找问题
记三个核心命令: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)的命令。
查看慢查询:
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-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:
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 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变成你的"武器"!
(全文完,觉得有用就点个赞吧~)