文章目录
-
- 一、分布式锁
-
-
- 1.为什么需要分布式锁?
- [2.Redis 分布式锁的核心原理](#2.Redis 分布式锁的核心原理)
- 3.最基础的实现
- 4.是否有更安全的实现方式呢?答案是肯定的
- [5.更优秀成熟的方案:使用 Redisson](#5.更优秀成熟的方案:使用 Redisson)
- [6.高可用方案:RedLock 算法](#6.高可用方案:RedLock 算法)
-
- [二、 接口限流 / 流量控制](#二、 接口限流 / 流量控制)
-
-
- 1.限流的基本概念
- [1. 固定窗口计数器算法](#1. 固定窗口计数器算法)
-
- [三、Redis 实现排行榜](#三、Redis 实现排行榜)
-
-
- [1.Redis ZSET 核心概念](#1.Redis ZSET 核心概念)
-
- [四、Redis 作为消息队列](#四、Redis 作为消息队列)
-
-
- [Redis 提供的两种消息队列方案](#Redis 提供的两种消息队列方案)
- 和专业消息队列一比,差距立现
-
- 五、总结:
很多人一提到 Redis,就只会说"缓存",好像它生来就只能当数据库的"保姆"。醒醒吧,Redis 本质是一个高性能内存数据结构服务器,缓存只是它最"低端"的用法。真正牛的玩法,是用它的原生数据结构和原子操作去碴掉一堆分布式系统难题。除了缓存,Redis 最值得干的几件事,以及为什么它们比缓存更"值回票价"。
本文将带你深入了解 Redis 在生产环境中常见的"非缓存"用法,并配以实际场景和实现思路,帮助你更好地发挥 Redis 的价值。
一、分布式锁
分布式锁 是一种在分布式系统中,用来确保多个节点(或进程)对共享资源的互斥访问的机制;
用 Redis 实现是因为它高性能、支持原子操作(如 SET NX EX)、自带过期机制防死锁,且部署简单、延迟低,适合高并发场景。
1.为什么需要分布式锁?
单机环境下,我们可以用 Java 的 synchronized 或 ReentrantLock 轻松解决并发问题。但在分布式集群中,多个进程/实例运行在不同机器上,单机锁失效了。这时就需要一个所有实例都能访问的"公共锁"------分布式锁。
典型场景:
- 秒杀库存扣减
- 订单防重复提交
- 定时任务防重执行
- 缓存更新互斥
2.Redis 分布式锁的核心原理
Redis 分布式锁基于以下关键特性:
- 互斥性:同一时刻只有一个客户端持有锁
- 高性能:Redis 单机 QPS 10w+,加锁/解锁延迟极低
- 原子操作:SET NX EX 等命令是原子的
- 自动释放:通过过期时间防止死锁
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 提供的两种消息队列方案
-
List 结构(LPUSH + BRPOP)
最经典的早期做法,实现简单,阻塞弹出支持消费。
但问题很大:没有 ACK 机制,消费者宕机或处理失败消息就丢了;不支持消费者组;无法查看历史消息;消费进度完全不可靠。
-
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得了吧