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
相关推荐
William Dawson2 小时前
【实战分享】DTU设备高并发数据接入全流程(Redis + RabbitMQ + 数据库)
数据库·redis·rabbitmq
CodeMartain5 小时前
Redis为什么快?
数据库·redis·缓存
南汐以墨13 小时前
一个另类的数据库-Redis
数据库·redis·缓存
一个有温度的技术博主15 小时前
Redis AOF持久化:用“记账”的方式守护数据安全
redis·分布式·缓存
RATi GORI15 小时前
springBoot连接远程Redis连接失败(已解决)
spring boot·redis·后端
Zzxy16 小时前
Spring Boot 集成 Redisson 实现分布式锁
spring boot·redis
2402_8813193019 小时前
引入 Redis 分布式锁解决并发脏写 (Dirty Write)-AI模拟面试的构建rag部分
redis·分布式·面试
刘~浪地球20 小时前
Redis 从入门到精通(九):事务详解
数据库·redis·缓存
__土块__20 小时前
一次电商秒杀系统架构评审:从本地锁到分布式锁的演进与取舍
java·redis·高并发·分布式锁·redisson·架构设计·秒杀系统