Redis 学习笔记(四):揭秘 Redis 的三大核心特性
掌握基本命令后,是否以为 Redis 的强大之处仅此而已?
其实不然!Redis 能在众多数据库中脱颖而出,除了依赖其内存操作的极致性能外,更关键的是它背后一系列保障系统高可用、高性能与高可靠的核心机制。
本文将深入解析 Redis 三大隐藏功能:
- 持久化机制:断电也不丢数据;
- 过期与淘汰策略:智能控制内存使用,避免“爆仓”;
- 分布式锁:在高并发场景下确保资源安全。
我们将不仅说明“是什么”,还会剖析“为什么需要”,并通过 Spring Boot + StringRedisTemplate 实战代码演示,帮助你真正理解并掌握这些高级特性的应用方式。
一、持久化机制:为数据加装“保险箱”
为何必须做持久化?
Redis 是基于内存的数据存储系统,读写速度极快。但一旦服务器宕机或断电,内存中的所有数据都会消失。对于缓存类应用这或许可接受,但如果 Redis 承担了核心业务数据的存储任务(例如秒杀库存、用户会话等),数据丢失将带来严重后果。
为此,Redis 提供了持久化(Persistence)功能,能够将内存数据定期或实时地保存到磁盘,重启时自动恢复,相当于给数据上了“保险”。
目前主流的持久化方式有:RDB、AOF,以及从 Redis 4.0 开始支持的混合模式。
1. RDB(Redis Database)——定时生成数据快照
原理:在指定时间点对整个 Redis 内存状态进行一次完整备份,生成一个二进制格式的快照文件(dump.rdb)。
dump.rdb
优点
- 恢复速度快:直接加载二进制文件,效率极高;
- 文件紧凑:适合用于备份和灾难恢复;
- 对主进程影响小:通过 fork 子进程完成写盘操作,主线程几乎无阻塞。
缺点
- 可能丢失最近数据:如设置每5分钟保存一次,宕机时最多丢失5分钟内的变更;
- 大内存环境下 fork 成本高:当内存占用达到几十GB级别时,fork 可能导致短暂卡顿。
配置示例(redis.conf)
save 900 1 # 900秒内至少1次修改,触发快照
save 300 10 # 300秒内至少10次修改
save 60 10000 # 60秒内至少10000次修改
Spring Boot 中手动触发 RDB(了解用途即可)
@Autowired
private StringRedisTemplate redisTemplate;
public void triggerRDB() {
redisTemplate.execute((RedisCallback<String>) connection -> {
return connection.execute("BGSAVE", new byte[0][]).toString();
});
}
适用场景:适用于允许少量数据丢失但要求快速恢复的场景,比如缓存、临时排行榜等。
2. AOF(Append Only File)——记录每一次写操作
原理:将每一个写命令(如 SET、DEL、INCR 等)以文本形式追加到日志文件末尾,Redis 重启时通过重放这些命令重建数据。
SET
INCR
HSET
appendonly.aof
优点
- 数据安全性更高:可配置为每秒同步(fsync every second)甚至每次写入都同步,极大减少数据丢失风险;
- 日志可读性强:AOF 文件是纯文本格式,必要时可通过人工干预修复错误。
缺点
- 日志体积增长快:尤其在高频写入场景下,文件迅速膨胀;
- 恢复速度较慢:需逐条执行命令重建数据,数据量大时启动耗时长;
- 性能略低:特别是在每次写入即刷盘(always 模式)下,I/O 压力较大。
AOF 同步策略配置(redis.conf)
everysec
always
appendonly yes
appendfsync everysec # 推荐:每秒同步一次,平衡性能与安全
# appendfsync always # 每次写都同步(最安全,但性能差)
# appendfsync no # 由操作系统决定(最快,但风险高)
AOF 重写机制(Rewrite)
为防止 AOF 日志无限增长,Redis 支持 AOF 重写功能:根据当前数据状态生成一个新的最小化 AOF 文件,去除冗余命令。
public void triggerAOFRewrite() {
redisTemplate.execute((RedisCallback<String>) connection -> {
return connection.execute("BGREWRITEAOF", new byte[0][]).toString();
});
}
适用场景:适用于对数据完整性要求高的业务系统,如金融交易记录、订单状态管理等。
3. 混合持久化:兼顾速度与安全的最佳实践
自 Redis 4.0 起引入的混合持久化模式,结合了 RDB 与 AOF 的优势:
- AOF 文件前半部分为 RDB 格式的二进制快照;
- 后半部分为增量的 AOF 命令日志;
- 重启时先加载 RDB 快照(快速),再重放少量新增命令(精确)。
这种方式既提升了恢复效率,又最大限度减少了数据丢失风险。
开启混合持久化的配置(redis.conf)
aof-use-rdb-preamble yes
强烈建议在生产环境中启用混合持久化模式!
二、过期与淘汰策略:科学管理内存使用
为何需要过期与淘汰机制?
Redis 数据常驻内存,若不加以控制,极易因数据堆积导致内存溢出(OOM)。因此,必须通过两种手段实现内存“瘦身”:
- 过期策略:自动清理设置了 TTL 的失效键;
- 淘汰策略:当内存达到上限时,按规则驱逐部分数据。
内存是一种有限的资源。如果数据只写入而不清理,Redis 很容易因超出内存限制而发生 OOM(Out Of Memory)错误。
为此,Redis 提供了两种核心机制来管理内存:
- 过期机制(Expiration):为键设置生存期限,时间到达后自动删除;
- 淘汰机制(Eviction):当内存使用达到设定上限时,按照预设策略清除部分数据以释放空间。
1. 过期策略:让数据拥有“有效期”
在实际开发中,我们常通过 Spring Boot 的 StringRedisTemplate 来操作 Redis。以下是一些典型应用场景:
@Autowired
private StringRedisTemplate redisTemplate;
// 场景一:短信验证码缓存,5分钟有效
public void sendSmsCode(String phone, String code) {
redisTemplate.opsForValue().set(
"sms:code:" + phone,
code,
Duration.ofMinutes(5) // 自动设置过期时间
);
}
// 场景二:用户会话信息缓存,1小时后失效
public void cacheUserSession(String userId, String token) {
redisTemplate.opsForValue().set(
"session:user:" + userId,
token,
Duration.ofHours(1)
);
}
// 查询某个 key 剩余存活时间(单位:秒)
public Long getTtl(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
上述方法在设置值的同时支持直接指定过期时间,具备原子性,无需分步执行 set 和 expire 操作。
StringRedisTemplate
该特性由 Redis 的 SET 命令结合扩展参数实现,天然保证了原子性。
set(key, value, duration)
Redis 如何处理过期 key?
Redis 并不会在 key 到期的瞬间立即删除,而是采用“惰性删除 + 定期删除”的混合模式进行清理:
- 惰性删除:每次访问一个 key 时,系统会检查其是否已过期,若已过期则立即删除并返回 null;
- 定期删除:Redis 启动后台任务,周期性地随机选取一部分设置了过期时间的 key 进行扫描,主动清理已过期的数据,防止大量过期 key 积压。
注意:若大量 key 被设置在同一时刻过期,可能导致短时间内 CPU 使用率急剧上升,这也是引发“缓存雪崩”的潜在原因之一。
2. 内存淘汰策略:当内存不足时如何应对?
当 Redis 实例的内存使用量达到配置上限(如 maxmemory 所限定),系统将根据预先设定的淘汰策略(eviction policy)移除部分数据。
maxmemory
常见淘汰策略及其适用场景
| 策略 | 说明 | 适用场景 |
|---|---|---|
noeviction |
默认策略,不淘汰任何数据,写操作将返回错误 | 要求数据绝对不能丢失的场景 |
volatile-lru |
仅对设置了过期时间的 key 使用 LRU 算法淘汰最近最少使用的数据 | 最常用,适用于标准缓存场景 |
allkeys-lru |
对所有 key 应用 LRU 算法淘汰 | 缓存为主,且存在未设置过期时间的 key |
volatile-ttl |
优先淘汰剩余生存时间最短的 key | 希望尽快清理即将过期的数据 |
volatile-random |
从带过期时间的 key 中随机选择淘汰 | 简单快速,但无访问频率考量 |
allkeys-random |
从所有 key 中随机淘汰 | 极少使用,通常不推荐 |
maxmemory 2gb
maxmemory-policy volatile-lru
noeviction
volatile-lru
allkeys-lru
volatile-ttl
volatile-random
allkeys-random
LRU(Least Recently Used) 算法基于“热点数据应常驻内存”的原则,优先保留最近被频繁访问的数据,是缓存系统中最广泛采用的淘汰逻辑。
Spring Boot 缓存实践示例
// 商品详情缓存,设置2小时过期
public void cacheProduct(Long productId, String productJson) {
redisTemplate.opsForValue().set(
"product:" + productId,
productJson,
Duration.ofHours(2)
);
}
当内存紧张时,长时间未被访问的商品数据将依据淘汰策略被清除。下一次请求若未能命中缓存,则需回源查询数据库,并重新写入缓存。
最佳实践建议:所有缓存 key 都应设置合理的过期时间,并配合使用 volatile-lru 或其他合适的淘汰策略,兼顾性能与稳定性。
volatile-lru
三、分布式锁:并发控制的“协调者”
为何需要分布式锁?
在单机应用中,可通过 synchronized 或 ReentrantLock 实现线程级别的互斥访问。
synchronized
ReentrantLock
但在分布式环境下,多个服务实例并行运行,本地锁无法跨进程生效。此时必须依赖外部协调机制。
例如,在“秒杀”场景中,1000 名用户争抢一件商品库存,必须确保只有一个请求能成功扣减库存,避免超卖问题。
得益于其单线程模型和原子性操作能力,Redis 成为实现分布式锁的理想工具。
正确实现分布式锁的三大关键要素
- 互斥性:任意时刻,仅有一个客户端可以持有锁;
- 防死锁:锁必须设置超时时间,防止客户端异常崩溃导致锁无法释放;
- 防误删:每个客户端只能释放自己持有的锁,不可删除他人创建的锁。
推荐实现方案
自 Redis 2.6.12 版本起,SET 命令支持扩展参数:
SET key value NX EX seconds
SET
NX(Not eXists):仅当 key 不存在时才进行设置,确保加锁的原子性和唯一性;EX(seconds):设定 key 的过期时间(秒级),防止死锁。
NX
EX
虽然 Spring Data Redis 的 setIfAbsent() 方法可模拟 NX 行为,但若额外设置过期时间需分两步操作,无法保证原子性。
StringRedisTemplate
setIfAbsent
expire
因此,生产环境推荐使用 RedisScript 执行 Lua 脚本,将加锁与解锁操作封装为原子性流程,彻底避免竞态条件。
使用 Spring Boot 实现基于 StringRedisTemplate 的分布式锁机制
一、定义 Lua 脚本实现原子操作
为了保证加锁与释放锁的原子性,采用 Redis 中的 Lua 脚本来执行关键逻辑。
加锁操作通过 SET 命令结合 NX(不存在则设置)和 EX(设置过期时间)选项完成:
private static final String LOCK_SCRIPT =
"if redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then " +
" return 1 " +
"else " +
" return 0 " +
"end";
解锁时需先校验持有者身份(requestId),防止误删其他线程持有的锁:
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
dump.rdb
二、构建分布式锁工具类
封装一个可复用的 Redis 分布式锁组件,利用 Spring 提供的 StringRedisTemplate 执行脚本。
@Component
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
private final RedisScript<Long> lockScript =
RedisScript.of(LOCK_SCRIPT, Long.class);
private final RedisScript<Long> unlockScript =
RedisScript.of(UNLOCK_SCRIPT, Long.class);
/**
* 尝试获取分布式锁
* @param lockKey 锁的键名,例如 "lock:seckill:1001"
* @param requestId 请求唯一标识,推荐使用 UUID
* @param expireSeconds 锁自动过期时间(单位:秒)
* @return 获取成功返回 true,否则 false
*/
public boolean tryLock(String lockKey, String requestId, long expireSeconds) {
Long result = redisTemplate.execute(
lockScript,
Collections.singletonList(lockKey),
requestId,
String.valueOf(expireSeconds)
);
return result != null && result == 1L;
}
/**
* 释放已持有的锁
* @param lockKey 锁的键名
* @param requestId 请求唯一标识,必须与加锁时一致
* @return 释放成功返回 true,否则 false
*/
public boolean unlock(String lockKey, String requestId) {
Long result = redisTemplate.execute(
unlockScript,
Collections.singletonList(lockKey),
requestId
);
return result != null && result == 1L;
}
}
三、在实际业务场景中应用(以商品秒杀为例)
将上述锁机制应用于高并发场景,如商品抢购流程中防止超卖问题。
@Service
public class SeckillService {
@Autowired
private RedisDistributedLock redisLock;
@Autowired
private StringRedisTemplate redisTemplate;
public void seckill(String userId, Long productId) {
String lockKey = "lock:seckill:" + productId;
String requestId = UUID.randomUUID().toString();
// 尝试获取锁,设置30秒自动失效
if (redisLock.tryLock(lockKey, requestId, 30)) {
try {
// 查询当前商品库存
String stockStr = redisTemplate.opsForValue().get("stock:" + productId);
if (stockStr != null && Integer.parseInt(stockStr) > 0) {
// 扣减库存(生产环境建议使用 Lua 脚本保障原子性)
redisTemplate.opsForValue().decrement("stock:" + productId);
// 后续处理:创建订单等业务逻辑...
}
} finally {
// 确保锁被正确释放
redisLock.unlock(lockKey, requestId);
}
} else {
throw new RuntimeException("获取锁失败,可能正在处理中");
}
}
}
该方案通过 Lua 脚本确保了加锁与解锁过程的原子性,配合唯一标识(requestId)避免了锁误释放的问题,适用于大多数需要互斥访问的分布式场景。
} else {
System.out.println("手慢了,没抢到锁");
}
} else {
if (redisLock.tryLock(lockKey, requestId, expireTime)) {
try {
// 执行秒杀逻辑
int stock = getStock();
if (stock > 0) {
deductStock();
System.out.println(userId + " 秒杀成功!");
} else {
System.out.println("库存不足");
}
} finally {
// 确保释放锁
redisLock.unlock(lockKey, requestId);
}
} else {
System.out.println("获取锁失败");
}
}
核心要点解析
在实现基于 Redis 的分布式锁时,以下关键点必须严格遵循:
- requestId 必须全局唯一:用于在解锁阶段校验操作身份,防止误删其他客户端持有的锁;
- 加锁与解锁均需使用 Lua 脚本:保障操作的原子性,避免因网络或执行时序问题导致锁机制失效;
- 锁的过期时间应略长于业务执行周期:既要防止死锁,也不能设置过长而影响系统响应效率。
requestId
进阶优化建议
在生产环境中,推荐考虑引入 Redisson 框架。该组件基于 Netty 实现高性能通信,并通过 Watchdog 机制实现锁的自动续期,支持可重入、读写锁等高级特性。尽管如此,掌握底层原理仍是合理使用和排查问题的前提。
Redis 三大核心机制速览
| 特性 | 核心价值 | 推荐配置/用法 | Spring Boot 使用注意 |
|---|---|---|---|
| 持久化 | 避免数据丢失,提升可用性 | RDB 与 AOF 混合模式 | 可通过 触发原生命令(如 BGSAVE) |
| 过期 & 淘汰策略 | 有效控制内存占用 | 配合合理的 TTL 设置 |
利用 原子化设置键值与过期时间 |
| 分布式锁 | 确保高并发下的数据一致性 | 采用 Lua 脚本实现 SETNX + EXPIRE(SET NX EX),并安全释放锁 | 务必使用 Lua 脚本,杜绝非原子性操作风险 |
后续实践建议
- 在测试环境模拟服务宕机场景,验证持久化机制的数据恢复能力;
- 持续监控 Redis 内存使用情况,结合实际负载设定合理的
阈值;maxmemory - 在高并发项目中落地分布式锁方案时,重点关注锁的粒度控制与超时时间设定;
- 可逐步引入 Redisson 来简化开发复杂度,但应在理解底层原理的基础上进行。
总结:Redis 的优势不仅体现在极致的性能上,更在于其稳定可靠的机制设计。只有深入理解其核心特性,才能真正发挥其潜力。
下期预告:《Redis 学习笔记(五):避雷!这些 Redis 错误,别再踩了》—— 将带你识别并规避常见却极易被忽视的使用误区。


雷达卡


京公网安备 11010802022788号







