找回密码
 立即注册
首页 业界区 业界 Redis能抗住百万并发的秘密

Redis能抗住百万并发的秘密

怒鼓踊 5 小时前
前言

今天想和大家深入聊聊Redis为什么能够轻松抗住百万级别的并发请求。
有些小伙伴在工作中可能遇到过这样的场景:系统访问量一上来,数据库就扛不住了,这时候大家第一时间想到的就是Redis。
但你有没有想过,为什么Redis能够承受如此高的并发量?它的底层到底做了什么优化?
今天我们就从浅入深,一步步揭开Redis高性能的神秘面纱。
1. Redis高并发的核心架构

1.1 单线程模型的威力

有些小伙伴可能会疑惑:Redis是单线程的,为什么还能支持这么高的并发?
这里需要澄清一个概念,Redis的"单线程"指的是网络IO和键值对读写是由一个线程来完成的,但Redis的整个系统并不是只有一个线程。
1.png

为什么单线程反而更快?

  • 避免了线程切换的开销:多线程环境下,CPU需要在不同线程间切换,这个过程需要保存和恢复线程上下文,开销很大。
  • 避免了锁竞争:单线程模型下,不需要考虑线程安全问题,避免了各种锁的开销。
  • CPU缓存友好:单线程执行时,CPU缓存命中率更高,减少了内存访问延迟。
让我们看一个简单的对比:
  1. // 多线程模式下的伪代码
  2. public class MultiThreadRedis {
  3.     private final Object lock = new Object();
  4.     private Map<String, String> data = new HashMap<>();
  5.    
  6.     public String get(String key) {
  7.         synchronized(lock) {  // 需要加锁
  8.             return data.get(key);
  9.         }
  10.     }
  11.    
  12.     public void set(String key, String value) {
  13.         synchronized(lock) {  // 需要加锁
  14.             data.put(key, value);
  15.         }
  16.     }
  17. }
  18. // Redis单线程模式下的伪代码
  19. public class SingleThreadRedis {
  20.     private Map<String, String> data = new HashMap<>();
  21.    
  22.     public String get(String key) {
  23.         return data.get(key);  // 无需加锁
  24.     }
  25.    
  26.     public void set(String key, String value) {
  27.         data.put(key, value);  // 无需加锁
  28.     }
  29. }
复制代码
1.2 事件驱动模型

Redis采用了事件驱动的架构,基于Reactor模式实现。
这种模式的核心思想是:用一个线程来处理多个连接的IO事件。
2.png

事件驱动的优势:

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

2.1 高效的数据结构设计

Redis的高性能很大程度上得益于其精心设计的内存数据结构。
每种数据类型都有多种底层实现,Redis会根据数据的特点自动选择最优的存储方式。
3.png

让我们深入了解几个关键的数据结构:
2.1.1 SDS (Simple Dynamic String)

有些小伙伴可能不知道,Redis并没有直接使用C语言的字符串,而是自己实现了SDS。
  1. // Redis SDS结构
  2. struct sdshdr {
  3.     int len;        // 字符串长度
  4.     int free;       // 未使用空间长度
  5.     char buf[];     // 字符串内容
  6. };
复制代码
SDS的优势:

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

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

跳跃表的查找过程:
  1. // 跳跃表查找伪代码
  2. public Node search(int target) {
  3.     Node current = header;
  4.    
  5.     // 从最高层开始查找
  6.     for (int level = maxLevel; level >= 0; level--) {
  7.         // 在当前层向右移动,直到下一个节点大于目标值
  8.         while (current.forward[level] != null &&
  9.                current.forward[level].value < target) {
  10.             current = current.forward[level];
  11.         }
  12.     }
  13.    
  14.     // 移动到下一个节点
  15.     current = current.forward[0];
  16.    
  17.     if (current != null && current.value == target) {
  18.         return current;
  19.     }
  20.     return null;
  21. }
复制代码
2.2 内存优化策略

2.2.1 压缩列表 (ziplist)

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

压缩列表的优势:

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

当Set中只包含整数元素时,Redis使用整数集合来存储。
  1. // 整数集合结构
  2. typedef struct intset {
  3.     uint32_t encoding;  // 编码方式
  4.     uint32_t length;    // 元素数量
  5.     int8_t contents[];  // 元素数组
  6. } intset;
复制代码
编码方式自动升级:
  1. // 整数集合编码升级示例
  2. public class IntSetExample {
  3.     // 初始状态:所有元素都是16位整数
  4.     // encoding = INTSET_ENC_INT16
  5.     // contents = [1, 2, 3, 4, 5]
  6.    
  7.     // 添加一个32位整数
  8.     public void addLargeNumber() {
  9.         // 自动升级为32位编码
  10.         // encoding = INTSET_ENC_INT32
  11.         // 重新分配内存,转换所有现有元素
  12.     }
  13. }
复制代码
3. 网络IO优化

3.1 IO多路复用技术

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

  • Linux: epoll
  • macOS/FreeBSD: kqueue
  • Windows: select
6.png

epoll的优势:

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

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

缓冲区配置示例:
  1. # redis.conf配置
  2. # 普通客户端缓冲区限制
  3. client-output-buffer-limit normal 0 0 0
  4. # 从服务器缓冲区限制  
  5. client-output-buffer-limit replica 256mb 64mb 60
  6. # 发布订阅客户端缓冲区限制
  7. client-output-buffer-limit pubsub 32mb 8mb 60
复制代码
4. 内存管理优化

4.1 内存分配器选择

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

4.2 过期键删除策略

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

定期删除算法:
  1. // Redis定期删除伪代码
  2. public void activeExpireCycle() {
  3.     int maxIterations = 16; // 最大检查数据库数
  4.     int maxChecks = 20;     // 每个数据库最大检查键数
  5.    
  6.     for (int i = 0; i < maxIterations; i++) {
  7.         RedisDb db = server.db[i];
  8.         int expired = 0;
  9.         
  10.         for (int j = 0; j < maxChecks; j++) {
  11.             String key = db.expires.randomKey();
  12.             if (key != null && isExpired(key)) {
  13.                 deleteKey(key);
  14.                 expired++;
  15.             }
  16.         }
  17.         
  18.         // 如果过期键比例小于25%,跳出循环
  19.         if (expired < maxChecks / 4) {
  20.             break;
  21.         }
  22.     }
  23. }
复制代码
5. 持久化优化

5.1 RDB持久化

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

RDB的优势:

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

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

AOF重写优化:
  1. // AOF重写示例
  2. public class AOFRewrite {
  3.     // 原始AOF文件可能包含:
  4.     // SET key1 value1
  5.     // SET key1 value2  
  6.     // SET key1 value3
  7.     // DEL key2
  8.     // SET key2 newvalue
  9.     // LPUSH list a
  10.     // LPUSH list b
  11.     // LPUSH list c
  12.    
  13.     // 重写后的AOF文件:
  14.     // SET key1 value3
  15.     // SET key2 newvalue  
  16.     // LPUSH list c b a
  17.    
  18.     public void rewriteAOF() {
  19.         // 遍历所有数据库
  20.         for (RedisDb db : server.databases) {
  21.             // 遍历所有键
  22.             for (String key : db.dict.keys()) {
  23.                 Object value = db.dict.get(key);
  24.                 // 根据值的类型生成对应的命令
  25.                 generateCommand(key, value);
  26.             }
  27.         }
  28.     }
  29. }
复制代码
6. 集群和分片优化

6.1 Redis Cluster

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

哈希槽分配算法:
  1. public class RedisClusterSlot {
  2.     private static final int CLUSTER_SLOTS = 16384;
  3.    
  4.     public int calculateSlot(String key) {
  5.         // 检查是否有哈希标签
  6.         int start = key.indexOf('{');
  7.         if (start != -1) {
  8.             int end = key.indexOf('}', start + 1);
  9.             if (end != -1 && end != start + 1) {
  10.                 key = key.substring(start + 1, end);
  11.             }
  12.         }
  13.         
  14.         // 计算CRC16校验和
  15.         int crc = crc16(key.getBytes());
  16.         return crc % CLUSTER_SLOTS;
  17.     }
  18.    
  19.     // CRC16算法实现
  20.     private int crc16(byte[] data) {
  21.         int crc = 0x0000;
  22.         for (byte b : data) {
  23.             crc ^= (b & 0xFF);
  24.             for (int i = 0; i < 8; i++) {
  25.                 if ((crc & 0x0001) != 0) {
  26.                     crc = (crc >> 1) ^ 0xA001;
  27.                 } else {
  28.                     crc = crc >> 1;
  29.                 }
  30.             }
  31.         }
  32.         return crc & 0xFFFF;
  33.     }
  34. }
复制代码
6.2 分片策略

有些小伙伴在设计分片策略时,可能会遇到数据倾斜的问题。
Redis提供了多种分片方式:
13.png

7. 性能监控和调优

7.1 关键性能指标

14.png

性能监控命令:
  1. # 查看Redis信息
  2. INFO all
  3. # 监控实时命令
  4. MONITOR
  5. # 查看慢查询日志
  6. SLOWLOG GET 10
  7. # 查看客户端连接
  8. CLIENT LIST
  9. # 查看内存使用情况
  10. MEMORY USAGE keyname
  11. # 查看延迟统计
  12. LATENCY LATEST
复制代码
7.2 性能调优建议

内存优化:
  1. # redis.conf优化配置
  2. # 启用内存压缩
  3. hash-max-ziplist-entries 512
  4. hash-max-ziplist-value 64
  5. list-max-ziplist-size -2
  6. list-compress-depth 0
  7. set-max-intset-entries 512
  8. zset-max-ziplist-entries 128
  9. zset-max-ziplist-value 64
  10. # 内存淘汰策略
  11. maxmemory-policy allkeys-lru
  12. # 启用内存压缩
  13. rdbcompression yes
复制代码
网络优化:
  1. # TCP相关优化
  2. tcp-keepalive 300
  3. tcp-backlog 511
  4. # 客户端超时
  5. timeout 0
  6. # 输出缓冲区限制
  7. client-output-buffer-limit normal 0 0 0
  8. client-output-buffer-limit replica 256mb 64mb 60
  9. client-output-buffer-limit pubsub 32mb 8mb 60
复制代码
8. 故障处理和高可用

8.1 故障检测机制

15.png

8.2 数据一致性保证

主从复制机制:
  1. // Redis主从复制流程
  2. public class RedisReplication {
  3.    
  4.     // 全量同步
  5.     public void fullResync() {
  6.         // 1. 从服务器发送PSYNC命令
  7.         // 2. 主服务器执行BGSAVE生成RDB文件
  8.         // 3. 主服务器将RDB文件发送给从服务器
  9.         // 4. 从服务器载入RDB文件
  10.         // 5. 主服务器将缓冲区的写命令发送给从服务器
  11.     }
  12.    
  13.     // 增量同步
  14.     public void partialResync() {
  15.         // 1. 从服务器发送PSYNC runid offset
  16.         // 2. 主服务器检查复制偏移量
  17.         // 3. 如果偏移量在复制积压缓冲区内,执行增量同步
  18.         // 4. 主服务器将缓冲区中的数据发送给从服务器
  19.     }
  20. }
复制代码
总结

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


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


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


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


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


  • 水平扩展:通过分片支持更大规模
  • 高可用:主从复制和故障转移
  • 负载均衡:智能的数据分布算法
有些小伙伴在工作中可能会问:"既然Redis这么强大,是不是可以完全替代数据库?"答案是否定的。
Redis更适合作为缓存和高速数据存储,而不是主要的数据存储。
正确的做法是将Redis与传统数据库结合使用,发挥各自的优势。
最后,要想真正发挥Redis的性能,不仅要了解其原理,更要在实际项目中不断实践和优化。
希望这篇文章能够帮助大家更好地理解和使用Redis。
最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
本文收录于我的技术网站:http://www.susan.net.cn

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册