Redis核心点

一、从单线程模型到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做全量备份到云存储

故障恢复流程

  1. 优先加载AOF(如果开启)

  2. 否则加载RDB

  3. 两者都无则启动空实例

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 (读)

复制流程

  1. 从节点发送PSYNC命令

  2. 主节点执行BGSAVE生成RDB

  3. 主节点发送RDB给从节点

  4. 从节点加载RDB

  5. 主节点将后续写命令同步给从节点(增量复制)

配置

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。

危害

  • 慢查询(HGETALLSMEMBERS

  • 网络阻塞

  • 集群中数据倾斜(某个节点内存过高)

检测方法

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?

  1. 使用Redis Cluster水平扩展,10个分片即可达到100万QPS。

  2. 引入多级缓存(Caffeine + Redis),本地缓存命中80%流量,大幅降低Redis压力。

  3. 针对热Key进行打散,避免单节点瓶颈。

  4. 使用Pipeline批量操作减少网络开销。

  5. 异步写MQ + 批量写Redis,削峰填谷。

    最终,实际打到Redis的QPS只有20万左右,每个分片仅2万QPS,远低于单节点极限。


十、基于Redisson实现看门狗机制的分布式锁续期方案

10.1 为什么需要看门狗?

问题场景

  • 业务执行时间超过锁的过期时间(如30秒)

  • 锁自动释放,其他线程获得锁

  • 原线程执行完业务后释放了别人的锁(错误)

解决方案

  • 自动续期:锁过期前,如果业务未完成,自动延长锁时间

10.2 Redisson实现原理

加锁流程

  1. 发送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]);
  2. 默认过期时间30秒

  3. 启动看门狗线程(每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%"


附录:学习与复习建议

学习路径

  1. 理解原理:单线程模型、数据结构底层、持久化机制

  2. 掌握实战:分布式锁、缓存三大问题、热Key大Key处理

  3. 架构设计:主从→哨兵→Cluster演进

  4. 性能优化:Pipeline、Lua、多级缓存

  5. 简历提炼:量化指标 + 技术亮点

推荐阅读

常用命令速查

用途 命令
查看内存 INFO memory
查看慢查询 SLOWLOG get 10
查看大Key redis-cli --bigkeys
查看热Key redis-cli --hotkeys
集群信息 CLUSTER INFO
相关推荐
二宝哥12 小时前
CentOS 7.9 系统下 Redis 5.0.4 单机安装与源码编译安装指南
redis·centos
ShiXZ21318 小时前
Redis docker-compose 部署指南
redis·docker·eureka
青山木21 小时前
Redis 缓存一致性全解:从 Cache Aside 到延迟双删、Canal+MQ 的进阶之路
java·数据库·redis·后端·缓存
YDS8291 天前
大营销平台 —— 活动SKU库存扣减业务及其一致性处理
java·spring boot·redis·rabbitmq·ddd
爬也要爬着前进2 天前
redis主从搭建
数据库·redis·缓存
用户095367515832 天前
在系统的学习 redis 前的疑惑
redis·go
吴声子夜歌2 天前
Redis 3.x——集群故障转移
java·数据库·redis·集群
明豆2 天前
Redis 分布式缓存生产实战
redis·分布式·缓存
海兰2 天前
【高速缓存】RedisVL 指南:从向量搜索到 AI 应用落地
人工智能·redis
weixin_727535622 天前
双Token认证体系深度拆解:Spring Security + JWT + Redis
redis·spring·wpf