Redis能抗住百万并发的秘密

今天想和大家深入聊聊Redis为什么能够轻松抗住百万级别的并发请求。

有些小伙伴在工作中可能遇到过这样的场景:系统访问量一上来,数据库就扛不住了,这时候大家第一时间想到的就是Redis。

但你有没有想过,为什么Redis能够承受如此高的并发量?它的底层到底做了什么优化?

今天我们就从浅入深,一步步揭开Redis高性能的神秘面纱。

1. Redis高并发的核心架构

1.1 单线程模型的威力

有些小伙伴可能会疑惑:Redis是单线程的,为什么还能支持这么高的并发?

这里需要澄清一个概念,Redis的"单线程"指的是网络IO和键值对读写是由一个线程来完成的,但Redis的整个系统并不是只有一个线程。

图片

为什么单线程反而更快?

避免了线程切换的开销:多线程环境下,CPU需要在不同线程间切换,这个过程需要保存和恢复线程上下文,开销很大。避免了锁竞争:单线程模型下,不需要考虑线程安全问题,避免了各种锁的开销。CPU缓存友好:单线程执行时,CPU缓存命中率更高,减少了内存访问延迟。

让我们看一个简单的对比:

复制
// 多线程模式下的伪代码 public class MultiThreadRedis { private final Object lock = new Object(); private Map<String, String> data = new HashMap<>(); public String get(String key) { synchronized(lock) { // 需要加锁 return data.get(key); } } public void set(String key, String value) { synchronized(lock) { // 需要加锁 data.put(key, value); } } } // Redis单线程模式下的伪代码 public class SingleThreadRedis { private Map<String, String> data = new HashMap<>(); public String get(String key) { return data.get(key); // 无需加锁 } public void set(String key, String value) { data.put(key, value); // 无需加锁 } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.

1.2 事件驱动模型

Redis采用了事件驱动的架构,基于Reactor模式实现。

这种模式的核心思想是:用一个线程来处理多个连接的IO事件。

图片

事件驱动的优势:

高效的IO多路复用:一个线程可以同时监听多个socket连接非阻塞IO:不会因为某个连接的IO操作而阻塞整个程序内存占用少:相比多线程模型,节省了大量线程栈空间

2. 内存数据结构的极致优化

2.1 高效的数据结构设计

Redis的高性能很大程度上得益于其精心设计的内存数据结构。

每种数据类型都有多种底层实现,Redis会根据数据的特点自动选择最优的存储方式。

让我们深入了解几个关键的数据结构:

2.1.1 SDS (Simple Dynamic String)

有些小伙伴可能不知道,Redis并没有直接使用C语言的字符串,而是自己实现了SDS。

复制
// Redis SDS结构 struct sdshdr { int len; // 字符串长度 int free; // 未使用空间长度 char buf[]; // 字符串内容 };1.2.3.4.5.6.

SDS的优势:

O(1)时间复杂度获取长度:直接读取len字段预分配空间:减少内存重新分配次数二进制安全:可以存储任意二进制数据兼容C字符串函数:以空字符结尾2.1.2 跳跃表 (Skip List)

跳跃表是Redis中有序集合的核心数据结构,它的查找效率可以达到O(log N)。

图片

跳跃表的查找过程:

复制
// 跳跃表查找伪代码 public Node search(int target) { Node current = header; // 从最高层开始查找 for (int level = maxLevel; level >= 0; level--) { // 在当前层向右移动,直到下一个节点大于目标值 while (current.forward[level] != null && current.forward[level].value < target) { current = current.forward[level]; } } // 移动到下一个节点 current = current.forward[0]; if (current != null && current.value == target) { return current; } returnnull; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.

2.2 内存优化策略

2.2.1 压缩列表 (ziplist)

当Hash、List、ZSet的元素较少时,Redis会使用压缩列表来节省内存。

图片

压缩列表的优势:

内存紧凑:所有元素连续存储,减少内存碎片缓存友好:连续内存访问,CPU缓存命中率高节省指针开销:不需要存储指向下一个元素的指针2.2.2 整数集合 (intset)

当Set中只包含整数元素时,Redis使用整数集合来存储。

复制
// 整数集合结构 typedef struct intset { uint32_t encoding; // 编码方式 uint32_t length; // 元素数量 int8_t contents[]; // 元素数组 } intset;1.2.3.4.5.6.

编码方式自动升级:

复制
// 整数集合编码升级示例 public class IntSetExample { // 初始状态:所有元素都是16位整数 // encoding = INTSET_ENC_INT16 // contents = [1, 2, 3, 4, 5] // 添加一个32位整数 public void addLargeNumber() { // 自动升级为32位编码 // encoding = INTSET_ENC_INT32 // 重新分配内存,转换所有现有元素 } }1.2.3.4.5.6.7.8.9.10.11.12.13.

3. 网络IO优化

3.1 IO多路复用技术

Redis在不同操作系统上使用不同的IO多路复用技术:

Linux: epollmacOS/FreeBSD: kqueueWindows: select

图片

epoll的优势:

事件驱动:只有当socket有事件时才会通知应用程序高效轮询:不需要遍历所有文件描述符支持边缘触发:减少系统调用次数

3.2 客户端输出缓冲区

Redis为每个客户端维护输出缓冲区,避免慢客户端影响整体性能。

图片

缓冲区配置示例:

复制
# redis.conf配置 # 普通客户端缓冲区限制 client-output-buffer-limit normal 0 0 0 # 从服务器缓冲区限制 client-output-buffer-limit replica 256mb 64mb 60 # 发布订阅客户端缓冲区限制 client-output-buffer-limit pubsub 32mb 8mb 601.2.3.4.5.6.7.8.9.

4. 内存管理优化

4.1 内存分配器选择

Redis支持多种内存分配器,默认使用jemalloc,这是一个专门为多线程应用优化的内存分配器。

图片

4.2 过期键删除策略

Redis采用惰性删除和定期删除相结合的策略来处理过期键。

图片

定期删除算法:

复制
// Redis定期删除伪代码 public void activeExpireCycle() { int maxIterations = 16; // 最大检查数据库数 int maxChecks = 20; // 每个数据库最大检查键数 for (int i = 0; i < maxIterations; i++) { RedisDb db = server.db[i]; int expired = 0; for (int j = 0; j < maxChecks; j++) { String key = db.expires.randomKey(); if (key != null && isExpired(key)) { deleteKey(key); expired++; } } // 如果过期键比例小于25%,跳出循环 if (expired < maxChecks / 4) { break; } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.

5. 持久化优化

5.1 RDB持久化

RDB是Redis的默认持久化方式,它会在指定的时间间隔内生成数据集的时点快照。

图片

RDB的优势:

紧凑的文件格式:适合备份和灾难恢复快速重启:恢复速度比AOF快对性能影响小:使用子进程进行持久化

5.2 AOF持久化

AOF通过记录服务器执行的所有写操作命令来实现持久化。

图片

AOF重写优化:

复制
// AOF重写示例 public class AOFRewrite { // 原始AOF文件可能包含: // SET key1 value1 // SET key1 value2 // SET key1 value3 // DEL key2 // SET key2 newvalue // LPUSH list a // LPUSH list b // LPUSH list c // 重写后的AOF文件: // SET key1 value3 // SET key2 newvalue // LPUSH list c b a public void rewriteAOF() { // 遍历所有数据库 for (RedisDb db : server.databases) { // 遍历所有键 for (String key : db.dict.keys()) { Object value = db.dict.get(key); // 根据值的类型生成对应的命令 generateCommand(key, value); } } } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.

6. 集群和分片优化

6.1 Redis Cluster

Redis Cluster是Redis的官方集群解决方案,采用无中心化的架构。

图片

哈希槽分配算法:

复制
public class RedisClusterSlot { private static final int CLUSTER_SLOTS = 16384; public int calculateSlot(String key) { // 检查是否有哈希标签 int start = key.indexOf({); if (start != -1) { int end = key.indexOf(}, start + 1); if (end != -1 && end != start + 1) { key = key.substring(start + 1, end); } } // 计算CRC16校验和 int crc = crc16(key.getBytes()); return crc % CLUSTER_SLOTS; } // CRC16算法实现 private int crc16(byte[] data) { int crc = 0x0000; for (byte b : data) { crc ^= (b & 0xFF); for (int i = 0; i < 8; i++) { if ((crc & 0x0001) != 0) { crc = (crc >> 1) ^ 0xA001; } else { crc = crc >> 1; } } } return crc & 0xFFFF; } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.

6.2 分片策略

有些小伙伴在设计分片策略时,可能会遇到数据倾斜的问题。

Redis提供了多种分片方式:

图片

7. 性能监控和调优

7.1 关键性能指标

图片

性能监控命令:

复制
# 查看Redis信息 INFO all # 监控实时命令 MONITOR # 查看慢查询日志 SLOWLOG GET 10 # 查看客户端连接 CLIENT LIST # 查看内存使用情况 MEMORY USAGE keyname # 查看延迟统计 LATENCY LATEST1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.

7.2 性能调优建议

内存优化:

复制
# redis.conf优化配置 # 启用内存压缩 hash-max-ziplist-entries 512 hash-max-ziplist-value 64 list-max-ziplist-size -2 list-compress-depth 0 set-max-intset-entries 512 zset-max-ziplist-entries 128 zset-max-ziplist-value 64 # 内存淘汰策略 maxmemory-policy allkeys-lru # 启用内存压缩 rdbcompression yes1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.

网络优化:

复制
# TCP相关优化 tcp-keepalive 300 tcp-backlog 511 # 客户端超时 timeout 0 # 输出缓冲区限制 client-output-buffer-limit normal 0 0 0 client-output-buffer-limit replica 256mb 64mb 60 client-output-buffer-limit pubsub 32mb 8mb 601.2.3.4.5.6.7.8.9.10.11.

8. 故障处理和高可用

8.1 故障检测机制

8.2 数据一致性保证

主从复制机制:

复制
// Redis主从复制流程 public class RedisReplication { // 全量同步 public void fullResync() { // 1. 从服务器发送PSYNC命令 // 2. 主服务器执行BGSAVE生成RDB文件 // 3. 主服务器将RDB文件发送给从服务器 // 4. 从服务器载入RDB文件 // 5. 主服务器将缓冲区的写命令发送给从服务器 } // 增量同步 public void partialResync() { // 1. 从服务器发送PSYNC runid offset // 2. 主服务器检查复制偏移量 // 3. 如果偏移量在复制积压缓冲区内,执行增量同步 // 4. 主服务器将缓冲区中的数据发送给从服务器 } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.

总结

通过以上深入分析,我们可以看到Redis能够抗住10万并发的核心原因包括:

架构层面

单线程模型:避免了线程切换和锁竞争的开销事件驱动:基于epoll的IO多路复用,高效处理大量连接内存存储:所有数据存储在内存中,访问速度极快

数据结构层面

高效的数据结构:针对不同场景优化的数据结构内存优化:压缩列表、整数集合等节省内存的设计智能编码:根据数据特点自动选择最优存储方式

网络层面

IO多路复用:单线程处理多个连接客户端缓冲区:避免慢客户端影响整体性能协议优化:简单高效的RESP协议

持久化层面

异步持久化:不阻塞主线程的持久化机制多种策略:RDB和AOF满足不同场景需求增量同步:高效的主从复制机制

集群层面

水平扩展:通过分片支持更大规模高可用:主从复制和故障转移负载均衡:智能的数据分布算法

有些小伙伴在工作中可能会问:"既然Redis这么强大,是不是可以完全替代数据库?"答案是否定的。

Redis更适合作为缓存和高速数据存储,而不是主要的数据存储。

正确的做法是将Redis与传统数据库结合使用,发挥各自的优势。

最后,要想真正发挥Redis的性能,不仅要了解其原理,更要在实际项目中不断实践和优化。

希望这篇文章能够帮助大家更好地理解和使用Redis。

阅读剩余
THE END