一、从单线程模型到I/O多路复用的Redis性能原理剖析
1.1 Redis为什么使用单线程?
核心原因
-
避免上下文切换:多线程的线程切换需要保存/恢复寄存器、程序计数器等,开销大。
-
无锁竞争:无需考虑共享资源的并发访问问题,自然线程安全。
-
内存操作极快:Redis瓶颈通常在网络I/O或内存带宽,而非CPU。
-
历史原因:Redis早期作者认为单线程足够简单高效。
1.2 I/O多路复用详解
什么是I/O多路复用?
-
一个线程通过记录每个Socket的文件描述符(FD),监听多个Socket的状态变化(可读、可写)。
-
当某个Socket就绪时,通知Redis线程处理。
底层实现(按优先级)
| 系统调用 | 适用系统 | 特点 |
|---|---|---|
| epoll | Linux | 边缘触发,高效,支持百万连接 |
| kqueue | BSD/macOS | 类似epoll |
| select | 通用 | 有FD上限(1024),效率低 |
| poll | 通用 | 无上限,但需要遍历所有FD |
Redis中的事件循环模型
c
// 伪代码
while (true) {
aeProcessEvents(); // 调用epoll_wait等待事件
// 处理文件事件(命令请求)
// 处理时间事件(定时任务如serverCron)
}
1.3 单线程如何实现高并发?
关键点
-
非阻塞I/O:Redis使用非阻塞Socket,读写不会阻塞线程。
-
管道化(Pipeline):客户端可以批量发送命令,减少RTT。
-
快速内存操作:所有数据都在内存中,命令执行时间通常是微秒级。
-
多路复用:一个线程处理成千上万个连接。
性能数据
-
单节点Redis在普通服务器上可达 10万+ QPS(GET/SET)。
-
使用Pipeline时可达 100万+ QPS。
1.4 常见面试题与坑
面试题
问 :Redis单线程为什么还能这么快?
答:①纯内存操作 ②I/O多路复用 ③避免多线程开销 ④高效的数据结构实现。
问 :Redis 6.0为什么引入多线程?
答:多线程只用于网络I/O读写,命令执行仍是单线程,提升大包传输性能。
常见坑
-
误以为Redis所有操作都是单线程(实际上
BGSAVE会fork子进程,UNLINK会异步删除)。 -
单线程意味着单个慢查询(如
KEYS *、大Key删除)会阻塞所有请求。
二、生产环境String、List、Hash等数据结构底层实现剖析
2.1 String(字符串)
底层:SDS(Simple Dynamic String)
c
struct sdshdr {
int len; // 已使用长度
int free; // 未使用长度
char buf[]; // 字节数组
};
优点
-
O(1)获取长度
-
预分配空间减少内存重分配
-
二进制安全(可存储图片、序列化对象)
内存优化
-
小字符串(<44字节)使用
embstr编码,与RedisObject一起分配内存。 -
大字符串使用
raw编码。
2.2 List(列表)
底层演进
| Redis版本 | 底层实现 | 切换条件 |
|---|---|---|
| <3.2 | ziplist + 双向链表 | 元素数<512且每个元素<64字节用ziplist |
| >=3.2 | quicklist | 统一使用quicklist(ziplist的分段链表) |
quicklist结构
text
quicklist
└─ quicklistNode (ziplist)
└─ quicklistNode (ziplist)
└─ ...
实战建议
-
避免大List(超过1万元素),删除中间元素会导致O(n)复杂度。
-
使用
LPUSH+LTRIM实现定长队列。
2.3 Hash(哈希表)
底层编码
-
ziplist:小Hash使用,内存紧凑。
-
hashtable(dict):大Hash使用,O(1)读写。
转换阈值
conf
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
实战坑
-
大Hash的
HGETALL会导致慢查询。 -
使用
HSCAN替代HGETALL分批获取。
2.4 Set(集合)
底层编码
-
intset :所有元素都是整数且数量小于
set-max-intset-entries(默认512)。 -
hashtable:其他情况。
2.5 ZSet(有序集合)
底层编码
-
ziplist:元素数<128且每个元素大小<64字节。
-
skiplist + dict:跳表实现有序,字典实现快速查找。
跳表结构
-
多层索引,平均O(log N)查找。
-
每个节点有多个前向指针(level1、level2...)。
2.6 面试题示例
问 :Redis的Hash类型什么时候用ziplist?
答:当field-value对数量<512且每个field或value长度<64字节时。
问 :ZSet的底层为什么用跳表而不是红黑树?
答:跳表实现更简单,支持范围查询效率高,且并发友好(Redis单线程无影响)。
三、线上RDB快照与AOF日志混合持久化方案实战
3.1 RDB(Redis Database)
原理
-
fork()子进程,子进程将内存数据写入临时RDB文件。 -
完成后替换旧文件。
触发方式
-
手动:
SAVE(阻塞)、BGSAVE(异步) -
自动:
save 900 1(900秒内至少1次修改)
优缺点
| 优点 | 缺点 |
|---|---|
| 文件小,恢复快 | 可能丢失最后一次快照后的数据 |
| 适合备份 | 大内存时fork耗时较长(可能几秒到几十秒) |
3.2 AOF(Append Only File)
原理
-
每执行一条写命令,就追加到AOF缓冲区。
-
根据策略刷盘:
-
always:每次命令都刷盘,最安全但慢 -
everysec:每秒刷一次(默认),最多丢1秒数据 -
no:由操作系统决定
-
AOF重写(BGREWRITEAOF)
-
去除冗余命令,压缩AOF文件。
-
类似RDB的fork子进程方式。
优缺点
| 优点 | 缺点 |
|---|---|
| 数据更安全(最多丢1秒) | 文件大,恢复慢 |
| 可读性好 | 大量写时性能稍差 |
3.3 混合持久化(Redis 4.0+)
原理
-
AOF重写时,将内存数据以RDB格式写入AOF文件头部。
-
后续增量命令以AOF格式追加。
配置
conf
aof-use-rdb-preamble yes
优点
-
恢复快(加载RDB部分)
-
数据安全(AOF部分)
3.4 生产环境实战方案
推荐配置
conf
# 开启混合持久化
aof-use-rdb-preamble yes
# AOF刷盘策略
appendfsync everysec
# 自动重写条件
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# RDB备份(可选,作为冷备)
save 900 1
save 300 10
主从架构中的持久化
-
主节点:开启AOF(混合模式)
-
从节点:可以只开启RDB,或关闭持久化(纯复制)
-
定期从节点
BGSAVE做全量备份到云存储
故障恢复流程
-
优先加载AOF(如果开启)
-
否则加载RDB
-
两者都无则启动空实例
3.5 常见坑
-
fork耗时过长导致Redis暂停服务 → 控制内存大小(<20GB)或使用diskless replication。 -
AOF文件损坏 → 使用
redis-check-aof --fix修复。 -
混合持久化中RDB部分损坏 → 无法恢复,需定期验证备份文件。
四、高并发场景缓存穿透、击穿、雪崩三大问题实战解决
4.1 缓存穿透
定义
查询一个不存在的数据,请求直接打到数据库。
攻击场景
-
恶意请求不存在的主键ID(如负数ID)
-
自动化脚本遍历所有可能Key
解决方案
方案1:缓存空对象
java
Object value = redis.get(key);
if (value == null) {
value = db.get(key);
if (value == null) {
redis.set(key, "null", 60); // 缓存空值,过期时间短
return null;
}
}
return value;
方案2:布隆过滤器(Bloom Filter)
java
// 使用Redisson
RBloomFilter<String> filter = redisson.getBloomFilter("userFilter");
filter.tryInit(100000L, 0.03); // 预计10万元素,误判率3%
// 添加所有存在的数据
for (String id : allUserIds) {
filter.add(id);
}
// 查询前置过滤
if (!filter.contains(requestId)) {
return null; // 一定不存在
}
// 否则查询Redis/DB
优缺点对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 缓存空对象 | 简单 | 浪费内存,可能被恶意填充 |
| 布隆过滤器 | 内存占用极小 | 有误判率,无法删除元素 |
4.2 缓存击穿
定义
某个热点Key过期瞬间,大量并发请求同时查询该Key,全部穿透到数据库。
解决方案
方案1:互斥锁(分布式锁)
java
public String getHotData(String key) {
String value = redis.get(key);
if (value != null) {
return value;
}
// 尝试获取锁
String lockKey = "lock:" + key;
boolean locked = redis.setnx(lockKey, "1", 10); // 10秒过期
if (locked) {
try {
// 双重检查
value = redis.get(key);
if (value == null) {
value = db.get(key);
redis.set(key, value, 3600);
}
} finally {
redis.del(lockKey);
}
} else {
// 等待100ms后重试
Thread.sleep(100);
return getHotData(key);
}
return value;
}
方案2:逻辑过期(不设置物理过期时间)
java
// 存储时带逻辑过期时间
class CacheValue {
Object data;
long expireTime; // 逻辑过期时间戳
}
// 查询时
CacheValue cv = redis.get(key);
if (cv.expireTime > System.currentTimeMillis()) {
return cv.data; // 未过期
}
// 已过期,尝试获取锁去更新
if (tryLock(key)) {
// 异步更新缓存
threadPool.submit(() -> {
Object newData = db.get(key);
redis.set(key, new CacheValue(newData, now + 3600*1000));
unlock(key);
});
}
// 返回旧数据
return cv.data;
4.3 缓存雪崩
定义
大量Key在同一时间过期,或Redis实例宕机,导致所有请求打到数据库。
解决方案
方案1:过期时间打散
java
int baseExpire = 3600;
int randomExpire = new Random().nextInt(300); // 0-300秒随机
redis.set(key, value, baseExpire + randomExpire);
方案2:多级缓存
text
本地缓存(Caffeine) → Redis → 数据库
-
本地缓存设置较短过期时间(如5分钟)
-
Redis设置较长过期时间(如1小时)
方案3:Redis高可用
-
主从 + 哨兵 / Cluster集群
-
避免单点故障
方案4:熔断降级(Hystrix/Sentinel)
- 当Redis异常时,直接返回默认值或错误码,不查询DB。
五、基于SETNX+Lua脚本的分布式锁完整实现与踩坑
5.1 正确实现方式
加锁(Redis 2.6.12+)
redis
SET lock_key unique_value NX EX 30
-
NX:不存在才设置 -
EX 30:30秒自动过期 -
unique_value:客户端唯一标识(如UUID)
释放锁(Lua脚本保证原子性)
lua
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
Java代码示例(Jedis)
java
public class RedisLock {
private Jedis jedis;
public boolean tryLock(String key, String value, int expireSeconds) {
String result = jedis.set(key, value, "NX", "EX", expireSeconds);
return "OK".equals(result);
}
public void unlock(String key, String value) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
}
}
5.2 常见踩坑与解决方案
| 坑 | 错误写法 | 正确做法 |
|---|---|---|
| 忘记设过期时间 | SETNX lock val |
SET lock val NX EX 30 |
| 释放别人的锁 | del lock |
先get判断value再del(Lua) |
| 业务执行超时锁自动释放 | 无 | 看门狗续期(Redisson) |
| 加锁后未释放(异常) | 无finally | try{...}finally{unlock} |
| 不可重入 | 无 | 使用Redisson或自己实现计数器 |
5.3 Redisson看门狗机制(第10点会详述)
java
RLock lock = redisson.getLock("myLock");
lock.lock(); // 默认30秒,每10秒自动续期
try {
// 长时间业务逻辑
} finally {
lock.unlock();
}
六、Redis惰性删除与LRU/LFU内存淘汰策略选型
6.1 过期删除策略
三种策略结合
| 策略 | 描述 | 时机 |
|---|---|---|
| 惰性删除 | 访问时检查过期,过期则删除 | 每次get/set等操作 |
| 定时删除 | 每秒抽查10次,每次随机20个Key | 后台定时任务 |
| 主动删除 | 内存不足时触发淘汰策略 | 内存达到maxmemory |
惰性删除源码逻辑
c
expireIfNeeded(db, key) {
if (key未过期) return 0;
// 删除Key
deleteKey(db, key);
// 传播DEL命令到从库
propagateExpire(db, key);
return 1;
}
6.2 内存淘汰策略(maxmemory-policy)
策略全览
| 策略 | 说明 | 适用场景 |
|---|---|---|
| noeviction | 内存满时拒绝写入,返回错误 | 不允许丢数据的场景 |
| allkeys-lru | 所有Key中淘汰最近最少使用 | 热点数据均匀 |
| volatile-lru | 有过期时间的Key中淘汰LRU | 临时数据缓存 |
| allkeys-random | 随机淘汰 | 数据访问无热点 |
| volatile-random | 随机淘汰有过期时间的Key | 同上 |
| volatile-ttl | 淘汰剩余生存时间最短的Key | 希望尽快淘汰旧数据 |
| allkeys-lfu | 淘汰最不经常使用(Redis 4.0+) | 长期热点数据 |
| volatile-lfu | 有过期时间的Key中淘汰LFU | 同上 |
LRU vs LFU
-
LRU(Least Recently Used):淘汰最近最少使用,可能淘汰冷门但刚被访问的数据。
-
LFU(Least Frequently Used):淘汰访问频率最低,更适合长期热点。
配置示例
conf
maxmemory 4gb
maxmemory-policy allkeys-lru
6.3 生产选型建议
| 业务场景 | 推荐策略 |
|---|---|
| 热点数据缓存(如商品详情) | allkeys-lru |
| 会话数据(有过期时间) | volatile-lru |
| 排行榜、计数器 | noeviction + 监控告警 |
| 日志、临时数据 | volatile-ttl |
6.4 面试题
问 :Redis的LRU是完美的吗?
答:不是,Redis采用近似LRU,随机采样5个Key淘汰其中一个,性能更好。
七、主从复制到哨兵模式再到Cluster集群架构演进
7.1 主从复制
架构
text
Master (写)
├─ Slave1 (读)
├─ Slave2 (读)
└─ Slave3 (读)
复制流程
-
从节点发送
PSYNC命令 -
主节点执行
BGSAVE生成RDB -
主节点发送RDB给从节点
-
从节点加载RDB
-
主节点将后续写命令同步给从节点(增量复制)
配置
conf
# 从节点配置
replicaof 192.168.1.1 6379
masterauth <password>
缺点
-
手动故障转移
-
主节点写瓶颈
7.2 哨兵模式(Sentinel)
架构
text
Sentinel1, Sentinel2, Sentinel3 (监控+选举)
Master
├─ Slave1
└─ Slave2
核心功能
-
监控:定期检查主从健康状态
-
通知:故障时通知管理员
-
自动故障转移:选举新主,修改其他从的配置
仲裁机制
-
quorum:至少几个哨兵同意下线 -
主客观下线判断:
-
主观下线:单个哨兵ping不通
-
客观下线:
quorum个哨兵都认为主观下线
-
配置示例(sentinel.conf)
conf
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel auth-pass mymaster mypass
sentinel down-after-milliseconds mymaster 30000
sentinel failover-timeout mymaster 180000
缺点
- 每个节点存储全量数据,无法水平扩展写能力
7.3 Cluster集群
架构
text
Slot 0-5460: Master1 + Slave1
Slot 5461-10922: Master2 + Slave2
Slot 10923-16383: Master3 + Slave3
核心概念
-
哈希槽:16384个槽,CRC16(key) % 16384
-
分片:每个Master负责一部分槽
-
节点间通信:Gossip协议(PING/PONG)
-
MOVED/ASK重定向:客户端需跟随
客户端路由
java
// Jedis Cluster自动处理重定向
JedisCluster cluster = new JedisCluster(hosts);
cluster.set("key", "value");
优点
-
水平扩展(最多1000节点)
-
高可用(主从自动切换)
缺点
-
不支持跨节点事务
-
批量操作(mget)可能跨节点
7.4 架构演进决策树
text
单机 < 10万QPS → 主从(读多写少) → 哨兵(需要高可用) → Cluster(写瓶颈)
八、生产环境热key打散与大key拆分性能优化实践
8.1 热Key问题
定义
某个Key的访问频率远高于其他Key(如明星微博、秒杀商品)。
危害
-
单节点CPU飙高
-
网络带宽打满
-
可能引发缓存击穿
检测方法
-
redis-cli --hotkeys(需要maxmemory-policy lru) -
监控节点CPU/网络
-
客户端埋点统计
解决方案:热Key打散
方案1:随机前缀 + 本地缓存
java
// 写入时拆分为N个子Key
int N = 100;
String hotKey = "product:123";
for (int i = 0; i < N; i++) {
redis.set(hotKey + "_" + i, value);
}
// 读取时随机选一个子Key
int index = ThreadLocalRandom.current().nextInt(N);
String value = redis.get(hotKey + "_" + index);
// 本地缓存(Caffeine)减少Redis压力
方案2:代理层(如Twitter Twemproxy)
- 自动打散热点
方案3:读写分离 + 副本读
java
// 从多个从节点读取
List<String> replicas = getReplicas();
String value = redis.get(replicas.get(random.nextInt(replicas.size())), key);
8.2 大Key问题
定义
String类型value > 10KB,或集合类型元素数 > 5000。
危害
-
慢查询(
HGETALL、SMEMBERS) -
网络阻塞
-
集群中数据倾斜(某个节点内存过高)
检测方法
bash
redis-cli --bigkeys
解决方案:大Key拆分
String大Value
-
压缩:使用Snappy、LZ4
-
拆分:分段存储(如key_part1, key_part2)
Hash大Key
java
// 原Hash:user:1000 有100万个field
// 拆分为1000个小Hash:user:1000:0 ~ user:1000:999
int shard = Math.abs(field.hashCode() % 1000);
String newKey = "user:1000:" + shard;
redis.hset(newKey, field, value);
List大Key
-
使用
LRANGE分批读取 -
避免一次性
LPOP所有元素
删除大Key
redis
# 异步删除(Redis 4.0+)
UNLINK bigkey
# 对于Hash,分批删除
HSCAN key 0 COUNT 100
HDEL key field1 field2...
8.3 生产案例
案例:电商商品详情页
-
原方案:一个Hash存储商品所有信息(200个field,大Key)
-
优化后:拆分为基本信息(Hash)、库存(String)、评价(List)
-
效果:
HGETALL耗时从50ms降到1ms
九、京东二面:单节点10万QPS到百万级QPS架构设计
9.1 目标分析
-
当前:单节点Redis 10万QPS
-
目标:100万QPS(10倍提升)
9.2 架构设计图(文字版)
text
客户端层
↓
Nginx负载均衡(LVS + Keepalived)
↓
多级缓存层
├─ 本地缓存(Caffeine,命中率80%)
└─ Redis Cluster(20%流量,10个分片,每分片5万QPS = 50万)
↓
数据库层(兜底)
9.3 关键技术点
1. 水平扩展:Redis Cluster
-
10个Master分片,每个分片处理10万QPS → 100万QPS
-
每个Master带1个Slave,保证高可用
2. 多级缓存
java
@Cacheable(cacheNames = "product", cacheManager = "caffeineRedisCacheManager")
public Product getProduct(Long id) {
return db.get(id);
}
-
Caffeine(本地):过期时间5分钟,最大1000条目
-
Redis:过期时间1小时
-
命中率:本地80% + Redis15% = 95%缓存命中
3. 读写分离
-
写请求:Master
-
读请求:Slave + 本地缓存
4. Pipeline批量操作
java
// 批量获取100个商品
List<String> keys = getKeys();
Pipeline p = jedis.pipelined();
for (String key : keys) {
p.get(key);
}
List<Object> values = p.syncAndReturnAll();
5. 异步处理 + 消息队列
-
写操作先写MQ,异步更新Redis
-
削峰填谷,避免瞬间高并发打垮Redis
6. 热Key处理
-
本地缓存兜底
-
随机前缀打散
9.4 性能估算
| 组件 | QPS | 说明 |
|---|---|---|
| 本地缓存 | 80万 | 80%命中率,仅20万打到Redis |
| Redis Cluster | 20万 | 10个分片,每分片2万QPS(远低于10万极限) |
| 数据库 | <1万 | 缓存未命中 |
9.5 面试回答模板
问 :如何从单节点10万QPS设计到百万级QPS?
答:
使用Redis Cluster水平扩展,10个分片即可达到100万QPS。
引入多级缓存(Caffeine + Redis),本地缓存命中80%流量,大幅降低Redis压力。
针对热Key进行打散,避免单节点瓶颈。
使用Pipeline批量操作减少网络开销。
异步写MQ + 批量写Redis,削峰填谷。
最终,实际打到Redis的QPS只有20万左右,每个分片仅2万QPS,远低于单节点极限。
十、基于Redisson实现看门狗机制的分布式锁续期方案
10.1 为什么需要看门狗?
问题场景
-
业务执行时间超过锁的过期时间(如30秒)
-
锁自动释放,其他线程获得锁
-
原线程执行完业务后释放了别人的锁(错误)
解决方案
- 自动续期:锁过期前,如果业务未完成,自动延长锁时间
10.2 Redisson实现原理
加锁流程
-
发送
EVAL命令执行Lua脚本:lua
if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]); -
默认过期时间30秒
-
启动看门狗线程(每10秒续期一次,续到30秒)
续期Lua脚本
lua
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('pexpire', KEYS[1], ARGV[1]);
return 1;
end;
return 0;
10.3 代码示例
自动续期(不指定leaseTimeout)
java
RLock lock = redisson.getLock("myLock");
lock.lock(); // 默认30秒,每10秒续期
try {
// 业务逻辑,可能执行1分钟
doSomething();
} finally {
lock.unlock();
}
手动控制过期时间(不续期)
java
lock.lock(10, TimeUnit.SECONDS); // 10秒后自动释放,不续期
10.4 看门狗配置
java
Config config = new Config();
config.setLockWatchdogTimeout(30000L); // 默认30秒,可修改
RedissonClient redisson = Redisson.create(config);
10.5 注意事项
-
看门狗只在未指定leaseTimeout时生效
-
不要手动调用
expire命令,会破坏续期机制 -
看门狗线程会在unlock后自动停止
-
如果业务执行时间超过30秒,看门狗会持续续期,直到业务完成或进程崩溃
10.6 面试题
问 :Redisson看门狗的实现原理?
答:加锁成功后,启动一个定时任务,每(过期时间/3)秒执行一次续期Lua脚本,将锁的过期时间重置。当unlock时,取消定时任务。
十一、Pipeline批量操作与Lua脚本减少网络开销实战
11.1 Pipeline
原理
-
将多个命令打包一次性发送给Redis
-
Redis依次执行后,一次性返回所有结果
-
减少RTT(Round-Trip Time)
性能对比
| 操作 | 1000条命令(网络RTT 1ms) | 耗时 |
|---|---|---|
| 普通 | 1000次请求 | 1000ms + 执行时间 |
| Pipeline | 1次请求 | 1ms + 执行时间 |
代码示例
java
// Jedis Pipeline
Pipeline p = jedis.pipelined();
for (int i = 0; i < 1000; i++) {
p.set("key_" + i, "value_" + i);
}
List<Object> results = p.syncAndReturnAll();
// RedisTemplate Pipeline
List<Object> results = redisTemplate.executePipelined(
(RedisCallback<Object>) connection -> {
for (int i = 0; i < 1000; i++) {
connection.set(("key_" + i).getBytes(), ("value_" + i).getBytes());
}
return null;
}
);
注意事项
-
Pipeline中的命令无原子性(中途可能穿插其他客户端命令)
-
大量命令时需分批(如每次1000条)
11.2 Lua脚本
原理
-
将多个命令以Lua脚本形式发送
-
Redis原子执行整个脚本
-
减少网络开销,且保证原子性
典型场景
-
限流
-
扣减库存
-
分布式锁释放
-
条件更新
限流Lua脚本
lua
-- 固定窗口限流
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire = tonumber(ARGV[2])
local current = redis.call('incr', key)
if current == 1 then
redis.call('expire', key, expire)
end
if current > limit then
return 0
else
return 1
end
java
String script =
"local current = redis.call('incr', KEYS[1])\n" +
"if current == 1 then redis.call('expire', KEYS[1], ARGV[2]) end\n" +
"if current > tonumber(ARGV[1]) then return 0 else return 1 end";
Long result = (Long) jedis.eval(script,
Collections.singletonList("rate_limit:user:123"),
Arrays.asList("10", "60")); // 每分钟最多10次
if (result == 1L) {
// 允许访问
} else {
// 限流
}
库存扣减Lua脚本
lua
local stock_key = KEYS[1]
local order_key = KEYS[2]
local user_id = ARGV[1]
local stock = redis.call('get', stock_key)
if not stock or tonumber(stock) <= 0 then
return 0
end
redis.call('decr', stock_key)
redis.call('sadd', order_key, user_id)
return 1
注意事项
-
脚本要保证幂等性(可重复执行)
-
避免在脚本中使用随机函数(导致主从数据不一致)
-
脚本复杂度不能太高(默认5秒超时)
11.3 Pipeline vs Lua对比
| 特性 | Pipeline | Lua |
|---|---|---|
| 原子性 | 无 | 有 |
| 依赖前一个命令结果 | 不支持 | 支持 |
| 网络开销 | 减少 | 减少 |
| 可读性 | 高 | 中 |
| 适用场景 | 批量无依赖操作 | 原子条件操作 |
十二、如何将Redis高并发优化经验写进简历的项目亮点里
12.1 简历亮点撰写公式
text
【项目名称】+【技术栈】+【量化指标】+【具体技术手段】
12.2 项目亮点模板(可直接复制修改)
模板1:电商秒杀系统
项目 :双11秒杀系统
技术栈 :Spring Boot + Redis Cluster + Redisson + RocketMQ
个人贡献:
设计多级缓存架构(Caffeine本地缓存 + Redis Cluster),将热点商品详情页QPS从2万提升至80万,缓存命中率达95%
基于Redisson分布式锁+Lua脚本实现原子库存扣减,保证秒杀不超卖,TPS提升3倍
采用布隆过滤器解决缓存穿透问题,拦截90%无效请求,DB压力降低70%
使用Pipeline批量写入订单数据,网络IO减少90%,写入性能提升5倍
设计热Key自动打散机制(随机前缀+副本读),避免单节点CPU飙高,系统峰值CPU从85%降至45%
模板2:社交Feed流系统
项目 :微博Feed流
技术栈 :Redis Cluster + ZSet + Pipeline
个人贡献:
使用Redis ZSet存储用户关注列表的时间线,通过
ZREVRANGEBYSCORE分页拉取Feed,单次查询<10ms针对千万级粉丝的大V,采用「写扩散 + 读扩散」混合模式,减少大Key问题
通过Pipeline批量拉取多个用户的Feed聚合,减少RTT,接口响应时间从200ms降至50ms
模板3:配置中心/权限系统
项目 :统一配置中心
技术栈 :Redis Sentinel + Lua + AOF
个人贡献:
基于Redis Sentinel实现配置数据的高可用存储,故障自动转移时间<30秒
使用Lua脚本实现配置的原子性更新与版本号CAS检查,防止并发覆盖
开启混合持久化(RDB+AOF),保证配置数据最多丢失1秒,且重启恢复速度提升80%
12.3 关键词提炼(用于简历技能栏)
text
Redis:主从复制、哨兵模式、Cluster集群、Pipeline、Lua脚本、持久化(RDB/AOF/混合)
分布式锁:Redisson、看门狗、原子性释放
高并发:多级缓存、热Key打散、大Key拆分、布隆过滤器
性能优化:QPS从10万提升至100万、缓存命中率95%、网络开销降低90%
12.4 面试中如何讲述(STAR法则)
-
S(Situation):项目背景,如"双11期间商品详情页QPS峰值达到50万,原Redis单节点无法支撑"
-
T(Task):任务目标,如"需要将系统吞吐量提升至100万QPS,同时保证99.9%可用性"
-
A(Action):具体行动,如"引入Redis Cluster 10分片 + Caffeine本地缓存 + 热Key打散"
-
R(Result):量化结果,如"最终峰值QPS达120万,缓存命中率96%,单机CPU从90%降至30%"
附录:学习与复习建议
学习路径
-
理解原理:单线程模型、数据结构底层、持久化机制
-
掌握实战:分布式锁、缓存三大问题、热Key大Key处理
-
架构设计:主从→哨兵→Cluster演进
-
性能优化:Pipeline、Lua、多级缓存
-
简历提炼:量化指标 + 技术亮点
推荐阅读
-
《Redis设计与实现》(黄健宏)
-
Redis官方文档:https://redis.io/docs/
-
Redisson GitHub:https://github.com/redisson/redisson
常用命令速查
| 用途 | 命令 |
|---|---|
| 查看内存 | INFO memory |
| 查看慢查询 | SLOWLOG get 10 |
| 查看大Key | redis-cli --bigkeys |
| 查看热Key | redis-cli --hotkeys |
| 集群信息 | CLUSTER INFO |