笔记又长又硬,做知识的分享者。
1. redis为什么这么快
纯内存结构
KV结构的内存数据库,时间复杂度O(1).
单线程
优点:
- 没有创建线程、销毁线程带来的消耗;
- 避免了上下文切换导致cpu的消耗;
- 避免了线程之间带来的竞争问题;
多路复用
多路复用处理并发连接,是异步非阻塞I/O。
1.1 虚拟内存
虚拟内存:可以提供更大的地址空间,并且地址空间是连续的,使得程序编写,链接更加简单。并且可以对物理内存进行隔离,不同的进程操作互不影响。还可以通过把同一块物理内存映射到不同的虚拟地址空间实现内存共享。
1.2 用户空间
用户空间存放的是用户程序的代码和数据。在用户空间只能执行简单的运算,不能直接调用系统资源,必须通过系统接口才能想内核发出指令。
1.3 内核空间
内核是操作系统的核心,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的权限。内核空间中存放的是内核代码和数据。
不管是内核空间还是用户空间,它们都是处于虚拟空间中,都是对物理地址的映射。在linux系统中,内核进程和用户进程所占的虚拟内存比例是1:3。
1.4 进程的阻塞
正在运行的线程提出系统服务请求,但是在某种原因的情况下,未能得到操作系统的立即响应,该进程只能把自己变成阻塞状态,等待相应的事件出现后才被唤醒,进程在阻塞状态时时是不占用cpu资源的。
1.5 文件描述符(FD)
linux系统将所有的设备都当作文件来处理,而linux用文件描述符来标识每个文件对象。文件描述符是内核为了高效管理已被打开的文件所创建的索引,用于指向被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来完成的。文件描述符是一个简单的非负整数,用来表明每个被进程打开的文件。
1.6 传统I/O数据拷贝
已读操作为例:
当应用程序执行read系统调用读取文件描述符的时候,如果这块数据已经存在于用户进程的页内存中,就直接从内存中读取数据,如果数据不存在,则先将数据从磁盘加载数据到内核缓冲区中,再从内核缓冲区拷贝到用户进程的页内存中。就是是说两次拷贝,两次的用户空间和内核空间的上下文切换。
1.7 Blocking I/O
当使用read或write对某个文件描述符进行过读写时,如果当前文件描述符不可读,系统就不会对其他的操作作出相应。从设备复制数据到内核缓冲区是阻塞的,从内核缓冲区拷贝到用户空间也是阻塞的,直到拷贝完成,内核返回结果,用户进程才解除block的状态。
解决阻塞问题的方法思路:
- 在服务端创建多个线程或者使用线程池,但是在高并发的情况下需要的线程会很多,系统无法承受,而且创建和释放线程都需要消耗资源。
- 由请求方定期轮询,在数据准备完毕后再从内核缓冲区复制到用户空间(非阻塞I/O),这种方式会存在一定的延迟。
1.8 I/O多路复用
这里的I/O指的是网络I/O,多路指的是多个TCP连接(socket或channel),复用指的是复用一个或多个线程。
基本原理就是不再由应用程序自己监视连接,而是由内核替应用程序监视文件描述符。客户端再操作的时候,会产生具有不同事件类型的socket,在服务端,I/O多路复用程序会把消息放入队列中,然后通过文件事件分派器(File event Dispatcher),转发到不同的事件处理器中。
多路复用有很多的实现,以select为例,当用户进程调用了多路复用器,进程会被阻塞,内核会监视多路复用器负责的所有socket,当任何一个socket的数据准备好了,多路复用器就会返回,这时候用户进程再调用read操作,把数据从内核缓冲区拷贝到用户空间。所以I/O多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪(readable)状态,select()函数就可以返回。redis的多路复用,提供了select、epoll、evport、kqueue几种选择,在编译的时候来选择一种。
- evport:是Solaris系统内核提供支持的;
- epoll:是linux系统内核提供支持的;
- kqueue:是mac系统提供支持的;
- select :是posix提供的,一般的操作系统都有支撑(兜底方案);
2. 内存回收
redis所有的数据都是存储在内存中的,在某些情况下需要对占用的内存空间进行回收,内存回收分为两类,一类是key过期,一类是内存使用达到上限触发内存淘汰。
2.1 过期策略
定期过期(主动)
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除,该策略可以立即清除过期的数据,对内存很友好,但是会占用大量的cpu资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期(被动)
只有当访问一个key时,才会判断该key是否过期,过期则删除。改策略可以最大化地节省cpu资源,却对内存影响非常大,极端情况可能出现大量的过期key没有在次被访问,导致这些key不会被清除,占用大量内存。
2.2 淘汰策略
redis的内存淘汰策略,是指当内存使用到最大内存极限时,需要使用淘汰算法来决定要清理掉那些数据,来保证新数据的存入。
参数配置(redis.conf)
maxmemory <bytes>
如果不设置maxmemory或者设置为0,64位系统不限制内存,32位系统最多使用3GB内存。
动态修改:
redis> config set maxmemory
2.2.1 淘汰策略
LRU:Least Recently Used,最近最少使用,判断最近被使用的时间,目前最远的数据优先被淘汰。
LFU:Least Frequently Used,最不常用。(4.0)版本新增。
volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够内存为止,如果没有可以删除的键对象,回退到noeviction策略。
alleys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够的内存为止。
volatile-lfu:在带有过期时间的键中选择最不常用的。
alleys-lfu:在所有的键中选择最不常用的,不管数据有没有设置超时属性。
volatile-random:在带有过期时间属性的键中随机选择。
alleys-random:随机删除所有键,直到腾出足够内存为止。
volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。
noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息。
建议使用volatile-lru,在保证正常服务的情况下,优先删除最近最少使用的key。
3. 发布订阅模式
3.1 订阅频道
- channel:频道,可以简单理解成queue,订阅者可以订阅一个或者多个频道,消息的发布者(生产者)可以给指定的频道发布消息,只要有消息到达频道,所有订阅了这个频道的订阅者都会收到这条消息。需要注意的是,发出去的消息不会被持久化,因为它已经从队列里面移除了,所以消费者只能收到它开始订阅这个频道之后发布的消息。
简单使用方法:
订阅者:
subscribe channel-1 channel-2 channel-3
发布者:(不支持一次向多个频道发送消息)
publish channel-1 key
取消订阅:(不能在订阅状态下使用)
unsubscribe channel-1
3.2 按照规则订阅频道(Pattern)
支持?和*占位符,
- ?代表一个字符;
- *代表0个或者多个字符;
4. redis事务
常用命令:
multi(开启事务)、exec(执行事务)、discard(取消事务)、watch(监视)
4.1 特点
- 按照进入队列的顺序执行;
- 不会受到其他客户端的请求的影响;
- redis事务之间是不能嵌套的;
4.2 watch命令
redis事务提供cas乐观锁行为,也就是多个线程更新变量的时候,会跟原值比较,只要它没有被其他的线程修改的情况下,才更新成新的值。
可以使用watch监控一个或多个key,如果开启事务之后,至少有一个被监视key在exec执行之前被改了,那么整个事务都会被取消(key提前过期除外)。可以用unwatch取消。
4.3 事务存在问题
4.3.1 执行exec之前
比如入队的命令存在语法错误(编译器错误)(例如:参数数量、参数名称等)。在这种情况下事务会被拒绝执行,也就是队列中所有的命令都不会得到执行。
4.3.2 执行exec之后
比如,类型错误,或者对string使用了hash命令,运行期间的错误,在这种情况下,只有错误的命令没有被执行,正确的命令不会受到影响。这种情况不符合我们对原子性的定义,也就是说我们没有办法用redis的这种事务机制来实现原子性,也就不能保证数据的一致。
4.3.3 LUA脚本
Lua是一种轻量级脚本语言,它是用c语言编写的,和数据库的存储过程有点类似,
- 好处
- 一次可以发送多个命令,减少网络开销。
- redis会将整个脚本作为一个整体去执行,不会被其他的请求打断,保证原子性。
- 对于复杂的组合命令,lua脚本可以放到文件中,然后实现程序之间的命令集复用。
5. 持久化机制
5.1 RDB
RDB是redis的默认持久化方案,当满足一定条件的时候,会把当前内存中的数据写入磁盘,生成一个快照文件dump.rdb,redis重启会通过加载dump.rdb文件恢复数据。
5.1.1 触发机制
自动触发
- 配置规则触发:redis.conf中SNAPSHOTTING,其中定了触发把数据保存到磁盘的触发频率。
如果不需要RDB方案,注释save或配置成空字符串“”;
save 900 1 #900秒内至少有一个key被修改(包括添加)
save 300 10 #300秒内至少有10个key被修改
save 60 10000 #60秒内至少有10000个key被修改以上配置是不冲突的,只要满足任意一个都会触发。
- RDB两种触发方式
- shutdown触发,保证服务器正常关闭。
- flushall。
手动触发
- save:save在生成快照的时候会阻塞当前redis服务器,redis不能处理其他命令,如果内存中的数据比较多,会造成redis长时间的阻塞,生产环境不建议使用这个命令。
- bgsave:执行bgsave,redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体的操作是redis进程执行fork操作创建子进程(copy-on-write),RBD持久化过程有子进程负责,完成后自动结束。它不会记录fork之后的命令,阻塞只发生在fork阶段,一般时间很短。
5.1.2 RBD文件的优缺点
- 优点
- RDB是一个非常紧凑的文件,它保存了redis在某个时间点上的数据集。这种文件非常适合用于进行备份和灾备恢复。
- 生产RDB文件的时候,redis主进程会fork一个子线程来处理所有保存的工作,主进程不需要进行任何磁盘IO操作。
- RBD在恢复大数据集时的速度比AOF的恢复速度快。
- 缺点
- RDB方式数据没办法做到实时持久化/秒级持久化,因为bgsave每次运行都要执行fork操作创建子进程,频繁执行成本过高。
- 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失最后一次快照之后的所有数据。
5.2 AOF(Append Only File)
redis默认不开启。AOF采用日志的形式来记录每个写操作,并追加到文件中。开启后执行更改redis的数据命令时,就会把命令写入到AOF文件中。redis重启时会根据日志文件的内容把写值令从前到后执行一次以完成数据恢复的工作。
由于操作系统的缓存机制,AOF数据并没有真正地写入硬盘,而是进入了系统的硬盘缓存。
5.2.1 AOF持久化策略
参数:appendfsync everysec
AOF默认持久化策略everysec
- no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快,但是不安全;
- always表示每次写入都执行fsync,已保证数据同步到磁盘,效率很低;
- everysec表示美秒执行一次fsync,可能会导致丢失这1s数据,通常选择everysec兼顾安全和效率;
由于AOF持久化是redis不断将写命令记录到AOF文件中,随着redis不断的进行,AOF的文件会越来越大,占用的服务器内存越大,所以AOF恢复要求的时间就越长。
redis新增了重写机制来解决这个问题,当AOF文件的大小超过所设定的阈值时,redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。可以使用bgrewriteaof,AOF文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的AOF文件。
5.2.3 AOF数据恢复
重启redis之后就会进行AOF文件的恢复。
5.2.4 AOF优点和缺点
- 优点
AOF持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,redis最多丢失1秒的数据。
- 缺点
- 对于具有相同数据的redis,AOF文件通常会比RDB文件要大。
- 虽然AOF提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。在高并发的情况下,RDB比AOF具有更好的新能保证。
5.3 RDB和AOF比较
那么对于AOF和RDB两种持久化方式,如果可以接受一小段时间内数据的丢失,使用RDB是最好的,定时生成RBD快照非常便于进行数据库的备份,并且RDB恢复数据集的速度也要比AOF恢复的速度要快。
但在一般情况下建议不要单独使用某一种持久化机制,而是两种方式结合使用,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据要比RDB文件保存的数据要完整。