Redis 除了缓存,还能干什么?

文章目录

很多人一提到 Redis,就只会说"缓存",好像它生来就只能当数据库的"保姆"。醒醒吧,Redis 本质是一个高性能内存数据结构服务器,缓存只是它最"低端"的用法。真正牛的玩法,是用它的原生数据结构和原子操作去碴掉一堆分布式系统难题。除了缓存,Redis 最值得干的几件事,以及为什么它们比缓存更"值回票价"。

本文将带你深入了解 Redis 在生产环境中常见的"非缓存"用法,并配以实际场景和实现思路,帮助你更好地发挥 Redis 的价值。

一、分布式锁

分布式锁 是一种在分布式系统中,用来确保多个节点(或进程)对共享资源的互斥访问的机制;
用 Redis 实现是因为它高性能、支持原子操作(如 SET NX EX)、自带过期机制防死锁,且部署简单、延迟低,适合高并发场景。

1.为什么需要分布式锁?

单机环境下,我们可以用 Java 的 synchronizedReentrantLock 轻松解决并发问题。但在分布式集群中,多个进程/实例运行在不同机器上,单机锁失效了。这时就需要一个所有实例都能访问的"公共锁"------分布式锁。

典型场景:

  • 秒杀库存扣减
  • 订单防重复提交
  • 定时任务防重执行
  • 缓存更新互斥
2.Redis 分布式锁的核心原理

Redis 分布式锁基于以下关键特性:

  1. 互斥性:同一时刻只有一个客户端持有锁
  2. 高性能:Redis 单机 QPS 10w+,加锁/解锁延迟极低
  3. 原子操作:SET NX EX 等命令是原子的
  4. 自动释放:通过过期时间防止死锁
3.最基础的实现
redis 复制代码
-- 加锁
SET lock_key "value" NX EX 30

-- 解锁(伪代码)
if redis.get(lock_key) == "value":
    redis.del(lock_key)

但是这样实现的分布式锁会有一些小问题,比如:

  • 加锁和设置过期时间不是原子操作,如果加锁后设置过期时间失败了,那么key会永远存在
  • 解锁时可能删除别人加的锁(A 加锁 → A 业务超时 → 锁过期 → B 加锁 → A 业务完成 → A 删除 B 的锁)
4.是否有更安全的实现方式呢?答案是肯定的

1)使用原子命令加锁

redis 复制代码
SET resource_lock unique_client_id NX EX 30
  • unique_client_id:可以用 UUID、线程ID、机器名+进程ID 等,确保全局唯一
  • EX 30:锁自动 30 秒后过期,防止业务异常导致死锁

2)使用原子命令解锁,保证是自己加的锁

使用 Lua 脚本保证判断+删除原子性:

lua 复制代码
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

为什么用 Lua 脚本

  • Redis 执行 Lua 脚本是原子的,避免了"判断后被别人抢锁再删除"的竞态条件
5.更优秀成熟的方案:使用 Redisson

Redisson 是基于 Redis 的分布式锁框架,解决了几乎所有手工实现的坑。

核心特性

  • 可重入锁(Reentrant Lock):同一个线程可多次加锁
  • 自动续期(Watch Dog):持有锁的线程会自动延长锁过期时间,防止业务时间长导致锁被意外释放
  • 公平锁、读写锁、多锁等高级功能
  • RedLock 算法支持(多 Redis 节点高可用)

简单使用示例

java 复制代码
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");

RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("order:12345");

try {
    // 尝试加锁,最多等待10秒,锁自动30秒释放
    boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
    if (acquired) {
        // 业务处理...
    }
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();  // 自动判断是否自己持有
    }
}

Watch Dog 机制

  • 默认开启:只要锁被持有,Redisson 后台线程每 10 秒(默认)续期一次,将过期时间重新设为 30 秒
  • 防止 GC 或网络问题导致客户端宕机后锁无法释放
6.高可用方案:RedLock 算法

单 Redis 实例存在单点故障风险。

RedLock 原理

  • 使用多个独立的 Redis 节点(建议 5 个以上)
  • 加锁:向 N 个节点尝试加锁,成功超过 N/2 + 1 个节点才认为加锁成功
  • 解锁:向所有节点发送解锁命令
  • 每个节点仍设置过期时间,防止少数节点异常

Redisson 原生支持 RedLock:

java 复制代码
config.useRedLock()
      .addNode("redis://node1:6379")
      .addNode("redis://node2:6379")
      // ...
RLock lock = redisson.getRedLock("critical_resource");

使用建议:相比于手动实现原子加锁原子解锁 ,我更推荐直接使用 Redisson 是因为它封装了分布式锁的复杂细节(如可重入、自动续期、安全释放、多种锁类型等),提供简单易用且生产可靠的 API,避免手动实现容易出错的问题。


二、 接口限流 / 流量控制

Redis 作为高性能内存数据库,非常适合实现限流,因为它支持原子操作、计数器和过期机制,能轻松处理高并发场景。

1.限流的基本概念

限流的核心是:在指定时间窗口内,限制请求次数。常见算法包括:

  • 固定窗口计数器:简单计数,窗口固定(如 1 分钟内限 100 次)。
  • 滑动窗口:更平滑,避免窗口边界爆发。
  • 令牌桶:允许突发流量,但整体速率受控。
  • 漏桶:强制平滑流量。
1. 固定窗口计数器算法

原理:用 Redis String 作为计数器,key 包含窗口时间戳。每次请求 INCR 计数,若 > 阈值则拒绝。窗口结束自动过期或重置。

伪代码示例(Java + Lettuce):

java 复制代码
public boolean isAllowed(String ip, int limit, int windowSeconds) {
    String key = "rate:limit:" + ip + ":" + (System.currentTimeMillis() / (windowSeconds * 1000));
    Long count = redisTemplate.opsForValue().increment(key);  // INCR
    if (count == 1) {
        redisTemplate.expire(key, windowSeconds, TimeUnit.SECONDS);  // 首次设置过期
    }
    return count <= limit;
}

优化:用 Pipeline 批量执行 INCR + EXPIRE,但非原子。更好用 Lua 脚本保证原子性。

Lua 脚本实现(原子计数 + 过期):

lua 复制代码
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire = tonumber(ARGV[2])

local count = redis.call('INCR', key)
if count == 1 then
    redis.call('EXPIRE', key, expire)
end
if count > limit then
    return 0  -- 限流
else
    return 1  -- 通过
end

调用 Lua

java 复制代码
RedisScript<Long> script = RedisScript.of(luaScript, Long.class);
List<String> keys = Collections.singletonList(key);
Object[] args = new Object[]{limit, windowSeconds};
Long result = redisTemplate.execute(script, keys, args);
return result == 1;

其它限流算法实现方式类似,不过多列举

Redis 限流适用于需要在分布式环境下按用户、IP 或资源等细粒度维度精确控制请求频率的场景,尤其当系统已有 Redis 支撑、流量规模适中且要求多节点共享限流状态时;但对于高吞吐入口流量或对可靠性要求极高的场景,通常优先采用网关或本地内存限流,Redis 仅作为补充。


三、Redis 实现排行榜

Redis 全内存操作 + 原生有序集合 ZSET,让实时排行榜的更新和查询变得极致高效,而传统数据库在高并发实时排序场景下容易成为瓶颈。

1.Redis ZSET 核心概念

Redis ZSet 是一种兼具哈希表与跳跃表(Skip List) 的复合数据结构:

  • 哈希表:O(1) 时间查成员分数
  • 跳跃表:O(log N) 时间插入、删除、范围排序

这使得 ZSet 天然支持:

  • 成员唯一,分数可重复
  • 按分数自动排序(升序/降序)
  • 高效获取 Top N、用户排名、分数区间等

简单实现:

java 复制代码
// 注入 RedisTemplate(String 类型键值)
@Autowired
private RedisTemplate<String, String> redisTemplate;

private ZSetOperations<String, String> zSet = redisTemplate.opsForZSet();

private static final String RANK_KEY = "game:global:score";

// 1. 给玩家加分(支持累计)
public Double addScore(String playerId, double increment) {
    return zSet.incrementScore(RANK_KEY, playerId, increment);
}

// 2. 获取玩家当前分数
public Double getScore(String playerId) {
    return zSet.score(RANK_KEY, playerId);
}

// 3. 获取玩家排名(从1开始)
public Long getRank(String playerId) {
    Long rank = zSet.reverseRank(RANK_KEY, playerId);
    return rank == null ? null : rank + 1;
}

// 4. 获取前 N 名(带分数和排名)
public List<PlayerRank> getTopN(int n) {
    Set<ZSetOperations.TypedTuple<String>> tuples = 
        zSet.reverseRangeWithScores(RANK_KEY, 0, n - 1);
    
    List<PlayerRank> result = new ArrayList<>();
    if (tuples != null) {
        int rank = 1;
        for (TypedTuple<String> tuple : tuples) {
            result.add(new PlayerRank(
                tuple.getValue(),
                tuple.getScore().longValue(),
                rank++
            ));
        }
    }
    return result;
}

四、Redis 作为消息队列

Redis 可以实现消息队列的功能,但它本质上不是一个合格的生产级消息队列

Redis 提供的两种消息队列方案
  1. List 结构(LPUSH + BRPOP)

    最经典的早期做法,实现简单,阻塞弹出支持消费。

    但问题很大:没有 ACK 机制,消费者宕机或处理失败消息就丢了;不支持消费者组;无法查看历史消息;消费进度完全不可靠。

  2. Stream 结构(Redis 5.0+)

    Redis 官方后来意识到大家拿它当队列用,于是加了 Stream 类型,带来了消费者组、消息 ID、手动 ACK、持久化、阻塞读取等特性,看起来"挺像回事"。

    实际一用你就发现:消息积压能力受内存限制,积压几百万条就容易 OOM;ACK 机制原始,消费者异常退出容易漏消费或重复消费;没有原生的延迟队列、死信队列、重试机制;高可用场景下主从切换仍可能丢消息。

和专业消息队列一比,差距立现
能力项 Redis Stream Kafka / RocketMQ / RabbitMQ
消息可靠性 依赖 AOF,弱保证 至少一次 / 精确一次
积压能力 内存级,百万即顶 磁盘级,TB 级没问题
消费进度管理 PEL 原始易出错 Offset 成熟可靠
延迟/死信/重试 无原生支持 大多内置或插件完善
吞吐与扩展 单机 10w+ TPS 集群百万级 TPS

可以用 Redis 做消息队列的场景

  • 轻量异步任务(发邮件、推送、埋点上传)
  • 对偶尔丢消息不敏感
  • 数据量小、实时性要求极高
  • 系统架构简单,不想额外引入中间件

五、总结:

Redis 虽然能干这个能干那个,但是实际上没那么好用。除了缓存,它还被强行当作消息队列、分布式锁、计数器、会话存储、甚至轻量级数据库来用。这些场景虽能跑通,却往往掩盖了其本质缺陷:

  • 拿 Redis 做消息队列?丢消息、无 ACK、堆积难处理;
  • 当主数据库?来两个复杂sql试试,存个token得了吧
相关推荐
千寻技术帮2 小时前
10356_基于Springboot的老年人管理系统
java·spring boot·后端·vue·老年人
u0131635512 小时前
Oracle 报错:PLS-00201: 必须声明标识符‘DBMS_LOCK‘的解决方法
数据库·oracle
崎岖Qiu2 小时前
【设计模式笔记24】:JDK源码分析-Comparator中的「策略模式」
java·笔记·设计模式·jdk·策略模式
萧曵 丶2 小时前
Java 安全的单例模式详解
java·开发语言·单例模式
Qiuner2 小时前
Spring Boot 全局异常处理策略设计(一):异常不只是 try-catch
java·spring boot·后端
Awkwardx2 小时前
MySQL数据库—MySQL数据类型
数据库·mysql
superman超哥2 小时前
Rust 错误处理模式:Result、?运算符与 anyhow 的最佳实践
开发语言·后端·rust·运算符·anyhow·rust 错误处理
郑泰科技2 小时前
hbase 避坑F:\hbase\hadoop\sbin>start-dfs.cmd 系统找不到文件 hadoop。
大数据·数据库·hadoop·hdfs·hbase
强子感冒了2 小时前
Java List学习笔记:ArrayList与LinkedList的实现源码分析
java·笔记·学习