全部版块 我的主页
论坛 经济学论坛 三区 教育经济学
49 0
2025-11-21

Redis 学习笔记(四):揭秘 Redis 的三大核心特性

掌握基本命令后,是否以为 Redis 的强大之处仅此而已?

其实不然!Redis 能在众多数据库中脱颖而出,除了依赖其内存操作的极致性能外,更关键的是它背后一系列保障系统高可用、高性能与高可靠的核心机制。

本文将深入解析 Redis 三大隐藏功能:

  • 持久化机制:断电也不丢数据;
  • 过期与淘汰策略:智能控制内存使用,避免“爆仓”;
  • 分布式锁:在高并发场景下确保资源安全。

我们将不仅说明“是什么”,还会剖析“为什么需要”,并通过 Spring Boot + StringRedisTemplate 实战代码演示,帮助你真正理解并掌握这些高级特性的应用方式。

一、持久化机制:为数据加装“保险箱”

为何必须做持久化?

Redis 是基于内存的数据存储系统,读写速度极快。但一旦服务器宕机或断电,内存中的所有数据都会消失。对于缓存类应用这或许可接受,但如果 Redis 承担了核心业务数据的存储任务(例如秒杀库存、用户会话等),数据丢失将带来严重后果。

为此,Redis 提供了持久化(Persistence)功能,能够将内存数据定期或实时地保存到磁盘,重启时自动恢复,相当于给数据上了“保险”。

目前主流的持久化方式有:RDBAOF,以及从 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

三、分布式锁:并发控制的“协调者”

为何需要分布式锁?

在单机应用中,可通过 synchronizedReentrantLock 实现线程级别的互斥访问。

synchronized
ReentrantLock

但在分布式环境下,多个服务实例并行运行,本地锁无法跨进程生效。此时必须依赖外部协调机制。

例如,在“秒杀”场景中,1000 名用户争抢一件商品库存,必须确保只有一个请求能成功扣减库存,避免超卖问题。

得益于其单线程模型原子性操作能力,Redis 成为实现分布式锁的理想工具。

正确实现分布式锁的三大关键要素

  1. 互斥性:任意时刻,仅有一个客户端可以持有锁;
  2. 防死锁:锁必须设置超时时间,防止客户端异常崩溃导致锁无法释放;
  3. 防误删:每个客户端只能释放自己持有的锁,不可删除他人创建的锁。

推荐实现方案

自 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 混合模式 可通过
execute
触发原生命令(如 BGSAVE)
过期 & 淘汰策略 有效控制内存占用
volatile-lru
配合合理的 TTL 设置
利用
set(key, value, duration)
原子化设置键值与过期时间
分布式锁 确保高并发下的数据一致性 采用 Lua 脚本实现 SETNX + EXPIRE(SET NX EX),并安全释放锁 务必使用 Lua 脚本,杜绝非原子性操作风险

后续实践建议

  • 在测试环境模拟服务宕机场景,验证持久化机制的数据恢复能力;
  • 持续监控 Redis 内存使用情况,结合实际负载设定合理的
    maxmemory
    阈值;
  • 在高并发项目中落地分布式锁方案时,重点关注锁的粒度控制与超时时间设定;
  • 可逐步引入 Redisson 来简化开发复杂度,但应在理解底层原理的基础上进行。

总结:Redis 的优势不仅体现在极致的性能上,更在于其稳定可靠的机制设计。只有深入理解其核心特性,才能真正发挥其潜力。


下期预告:《Redis 学习笔记(五):避雷!这些 Redis 错误,别再踩了》—— 将带你识别并规避常见却极易被忽视的使用误区。

二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

栏目导航
热门文章
推荐文章

说点什么

分享

扫码加好友,拉您进群
各岗位、行业、专业交流群