目录
一、redis是单线程还是多线程
二、Redis 为什么用单线程
三、redis是单线程为什么还这么快
1、内存数据库
2、简单的数据结构
3、单线程
4、IO多路复用
四、redis是如何使用跳表来存储的
五、redis没有设置过期时间,为什么redis自动删除了
六、删除redis的key会阻塞redis么,如何删除一个大key
一、redis是单线程还是多线程
通常我们看到redis介绍的时候都会说redis是单线程的,但是当我们深入学习的时候,我们使用一些命令对redis进行持久化操作的时候,比如bgsave命令,它的作用就是在后台异步启动一个线程保存当前数据库的数据到磁盘。
其实通常说的redis是单线程指的是redis在对外提供键值对存储服务的主要流程,即网路IO和键值对读写是由单个线程来完成的。除此外redis的其他功能,比如:redis的持久化、异步删除、集群数据同步等,是由额外的线程来完成,因此redis严格意义上并不是完全的单线程。
多线程是 Redis6.0 推出的一个新特性。正如上面所说 Redis 是核心线程负责网络 IO ,命令处理以及写数据到缓冲,而随着网络硬件的性能提升,单个主线程处理络请求的速度跟不上底层络硬件的速度,导致网络 IO 的处理成为了 Redis 的性能瓶颈。
而 Redis6.0 就是从单线程处理网络请求到多线程处理,通过多个 IO 线程并处理网络操作提升实例的整体处理性能。需要注意的是对于读写命令,Redis 仍然使单线程来处理,这是因为继续使单线程执行命令操作,就不为了保证 Lua 脚本、事务的原性,额外开发多线程互斥机制了。
需要注意的是在 Redis6.0 中,多线程机制默认是关闭的,需要在 redis.conf 中完成以下两个设置才能启用多线程。
设置 io-thread-do-reads 配置项为 yes,表示启用多线程。
设置线程个数。一般来说,线程个数要小于 Redis 实例所在机器的 CPU 核数, 例如,对于个 8 核的机器来说,Redis 官建议配置 6 个 IO 线程。
io-threads-do-reads yes
io-threads 6
二、Redis 为什么用单线程
Redis 为什么用单线程?在回答这个问题前,先来看大家都很熟悉的数据库 MySQL,它使用的就是多线程。MySQL 不会每有一个连接就创建一个线程,因为线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等,同时也会降低计算机的整体性能。这个正是多线程会遇到的难点。
此外多线程系统中通常会存在被多线程同时访问的共享资源,比如一个共享的数据结构,当有多个进程要修改这个共享资源时,为了保证共享资源的正确性,就需要有额外的机制进行保证,而这个额外的机制,也会带来额外的开销。还是以 MySQL 举例,MySQL 引入了锁机制来解决这个问题。
从上面不难看出,多线程开发中并发访问控制是个难点,需要精细的设计才能处理。如果只是简单地处理,比如简单地采个粗粒度互斥锁,只会出现不理想的结果。即便增加了线程,系统吞吐率也不会随着线程的增加而增加,因为大部分线程还在等待获取访问共享资源的互斥锁。而且,大部分采用多线程开发引入的同步原语保护共享资源的并发访问,也会降低系统代码的易调试性和可维护性。
而正是以上这些问题,才让 Redis 采了单线程模式。
三、redis是单线程为什么还这么快
- 内存型数据库
- 简单的数据结果
- 单线程
- IO多路复用
1、内存数据库
redis完全是基于内存的,绝大部分请求是纯粹的内存操作,所以非常快速。
2、简单的数据结构
redis目前支持5种数据类型(string、list、hash、set、zset),数据结构相对简单,操作起来也相对快速。
对于string来说,redis采用SDS方式来组织数据
redis的有序集合,采用的跳跃表的数据结构,通过层来加快访问其他节点
3、单线程
redis采用单线程模型,单线程的好处在于避免了多线程对数据竞争的问题、加锁的问题、上下文切换的问题。
4、IO多路复用
redis采用了非阻塞的IO多路复用技术。redis本身就是一个事件驱动程序,redis把socket抽象成文件事件。这里说的IO多路复用就是文件事件处理器以单线程的方式,来监听相关的套接字(accept、read、write、close)。
由于IO多路复用程序是一个单线程,那么当多个socket到来时,肯定要排队,它们总是以队列的方式顺序地处理。
C10K问题
在没有IO多路复用的时候,假设现在有10000个客户端连接(fd1-10000),但是只有1个客户端有发数据,然而计算机并不知道哪个fd有数据,只能遍历10000次,每次都要陷入内核,开销比较大,而且实际上9999次都是浪费的。
IO多路复用
IO多路复用的意思就是多个网路IO即为多个TCP连接 复用一个进程或者线程,这种模型最大的好处就是不用为每个连接创建一个进程或者线程。比较经典的模型就是 select、poll、epoll。
- select:select(fds),一次性把fds交给内核,然后内核告诉哪些fd可读可写(内核自己遍历,而不用用户遍历,将多次的系统调用变成1次系统调用)。fds最大是1024,这也决定了select模型最大并发是1024。
- poll:和select差不多,只不过并发不止1024了,可以更多
- epoll: select和poll的缺点是内核遍历的时间复杂度是O(n),虽然用户态不用遍历了,减少了陷入内核的次数,但是内核还是要遍历的。epoll的优点就是内核也不需要遍历了,当用户把fds传给内核时,然后依赖硬件中断,比如当网卡有数据到来时,就会中断告诉cpu,cpu就知道哪个fd有数据到达了。
redis默认采用epoll,除非系统不支持。
四、redis是如何使用跳表来存储的
redis的有序集合zset就使用到了跳表这个概念。
跳表:跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。
跳表图例:出自文章https://news.ycombinator.com/item?id=1171423
- 单链表:查询时间复杂度O(n)
- level-2单链表:每隔一个节点为一个level-2节点,每个level-2节点有2个后继指针,分别指向单链表中的下一个节点和下一个level-2节点。查询时间复杂度为O(n/2)
- level-3单链表:每隔一个节点为一个level-2节点,每隔4个节点为一个level-3节点,查询时间复杂度O(n/4)
- 指数式单链表:每2^i个节点的level为i+1,查询时间复杂度为O(log2N)
- 跳跃表:各个level的节点个数同指数式单链表,但出现的位置随机,查询复杂度是O(logN)
采用跳表不采用红黑树的原因:
可以看到redis选择跳跃表而非红黑树作为有序集合实现方式的原因并非是基于并发上的考虑,因为redis是单线程的,选用跳跃表的原因仅仅是因为跳跃表的实现相较于红黑树更加简洁。
五、redis没有设置过期时间,为什么redis自动删除了
原因:当redis的已用内存大于maxMemory的时候,触发主动清理策略。
主动清理策略在Redis4.0之前有6种,在4.0之后又8种
- volatile-ttl:在筛选时,会针对设置的过期时间的键值对,根据过期时间的先后顺序进行删除,越早过期的越先删除。
- volatile-random:在设置了过期时间的key中,进行随机删除。
- volatile-lru:会使用LRU算法进行删除 LRU(Least Recently Used) 就是最少使用的key中,时间维度
- volatile-lfu:会使用LFU算法进行删除 LFU(Least Frequently Used,最不经常使用),通过使用频率这个维度来删除
- allkeys-ttl:不管key是否设置了过期,淘汰即将过期的key
- allkeys-random:不管key是否设置了过期,随机淘汰
- allkeys-lru:不管key是否设置了过期,淘汰最近最少访问的key
- noeviction:不淘汰任何key,满容后再写入直接报错。
六、删除redis的key会阻塞redis么,如何删除一个大key
关于redis大健,我们主要从来个维度来说
- 时间复杂度 一个包含1000W个数据的list 我们通过删除这个list会阻塞redis十几秒甚至几十秒。
- 空间复杂度 比如一个string 内存空间占用100MB 。
因为内存空间复杂性处理耗时都非常小,测试 del 100MB String键耗时约0.5毫秒,而删除一个含有1000w个数据的List,却会阻塞Redis进程数十秒。
说明:Redis是单线程处理。执行一个耗时过大的命令,导致阻塞其他命令,容易引起应用程序雪崩或Redis集群发生故障切换。所以避免在生产环境中使用耗时过大命令是非常关键的。
那如果线上环境需要处理大key的删除,我们应该怎么处理呢?
使用分批删除的思想
- hash key:通过hscan命令,每次获取500个字段,再用hdel命令;
- set key:使用sscan命令,每次扫描集合中500个元素,再用srem命令每次删除一个元素;
- list key:删除大的List键,未使用scan命令; 通过ltrim命令每次删除少量元素。
- sorted set key:删除大的有序集合键,和List类似,使用sortedset自带的zremrangebyrank命令,每次删除top 100个元素。