咱们将先从Redis、Nginx+Lua等技术点出发,了解缓存应用的场景。经过使用缓存相关技术,解决高并发的业务场景案例,来深刻理解一套成熟的企业级缓存架构如何设计的。本文Redis部分总结于蒋德钧老师的《Redis核心技术与实战》。mysql
Redis是一个开源的使用ANSI C语言编写、遵照BSD协议、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。redis
它一般被称为数据结构服务器,由于值(value)能够是 字符串(String), 哈希(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。算法
Redis 与其余 key - value 缓存产品有如下三个特色:sql
优点mongodb
string 是 redis 最基本的类型,你能够理解成与 Memcached 如出一辙的类型,一个 key 对应一个 value。shell
string 类型是二进制安全的。意思是 redis 的 string 能够包含任何数据。好比jpg图片或者序列化的对象。数据库
string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。数组
redis 127.0.0.1:6379> SET runoob "laowang" OK redis 127.0.0.1:6379> GET runoob "laowang"
Redis hash 是一个键值(key=>value)对集合。缓存
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。安全
每一个 hash 能够存储 2^32 -1 键值对(40多亿)。
redis 127.0.0.1:6379> HMSET runoob field1 "Hello" field2 "World" "OK" redis 127.0.0.1:6379> HGET runoob field1 "Hello" redis 127.0.0.1:6379> HGET runoob field2 "World"
Redis 列表是简单的字符串列表,按照插入顺序排序。你能够添加一个元素到列表的头部(左边)或者尾部(右边)。
列表最多可存储 2^32 - 1 元素 (4294967295, 每一个列表可存储40多亿)。
redis 127.0.0.1:6379> lpush runoob redis (integer) 1 redis 127.0.0.1:6379> lpush runoob mongodb (integer) 2 redis 127.0.0.1:6379> lpush runoob rabitmq (integer) 3 redis 127.0.0.1:6379> lrange runoob 0 10 1) "rabitmq" 2) "mongodb" 3) "redis"
Redis 的 Set 是 string 类型的无序集合。
集合是经过哈希表实现的,因此添加,删除,查找的复杂度都是 O(1)。
sadd 命令 :添加一个 string 元素到 key 对应的 set 集合中,成功返回 1,若是元素已经在集合中返回 0。
集合中最大的成员数为 2^32 - 1(4294967295, 每一个集合可存储40多亿个成员)。
redis 127.0.0.1:6379> DEL runoob redis 127.0.0.1:6379> sadd runoob redis (integer) 1 redis 127.0.0.1:6379> sadd runoob mongodb (integer) 1 redis 127.0.0.1:6379> sadd runoob rabitmq (integer) 1 redis 127.0.0.1:6379> sadd runoob rabitmq (integer) 0 redis 127.0.0.1:6379> smembers runoob 1) "redis" 2) "rabitmq" 3) "mongodb"
Redis zset 和 set 同样也是string类型元素的集合,且不容许重复的成员。
不一样的是每一个元素都会关联一个double类型的分数。redis正是经过分数来为集合中的成员进行从小到大的排序。
zset的成员是惟一的,但分数(score)却能够重复。
zadd 命令 :添加元素到集合,元素在集合中存在则更新对应score
redis 127.0.0.1:6379> zadd runoob 0 redis (integer) 1 redis 127.0.0.1:6379> zadd runoob 0 mongodb (integer) 1 redis 127.0.0.1:6379> zadd runoob 0 rabitmq (integer) 1 redis 127.0.0.1:6379> zadd runoob 0 rabitmq (integer) 0 redis 127.0.0.1:6379> > ZRANGEBYSCORE runoob 0 1000 1) "mongodb" 2) "rabitmq" 3) "redis"
对这个问题的思考,将有助于咱们从总体架构上去学习Redis。
假设如今咱们已经设计好了一个KV数据库,首先若是咱们要使用,是否是得有入口,咱们是经过动态连接库仍是经过网络socket对外提供访问入口,这就涉及到了访问模块。Redis就是经过
经过访问模块访问KV数据库以后,咱们的数据存储在哪里?为了保证访问的高性能,咱们选在存储在内存中,这又须要有存储模块。存在内存中的数据,虽然访问速度快,但存在的的问题就是断电后,没法恢复数据,因此咱们还须要支持持久化操做。
有了存储模块,咱们还须要考虑,数据是以什么样的形式存储?怎样设计才能让数据操做更优,这就设计到了,数据类型的支持,索引模块。 索引的做用是让键值数据库根据 key 找到相应 value 的存储位置,进而执行操做。
有了以上模块的只是,咱们是否是要对数据进行操做了?好比往KV数据库中插入或更新一条数据,删除和查询,这就是须要有操做模块了。
至此咱们已经构造除了一个KV数据库的基本框架了,带着这些架构,咱们再深刻到每一个点中去探究,这样就会轻松不少,不会迷失在末枝细节中了。
咱们都知道Redis访问快,这是由于redis的操做都是在内存上的,内存的访问自己就很快,另外Redis底层的数据结构也对“快”起到了相当重要的做用。
咱们日常因此所说Redis的5种数据结构:String、Hash、Set、ZSet和List指的只是键值对中值的数据结构,而我这里所说的数据结构,指的是它们底层实现。
Redis的底层数据结构有:简单动态字符串、整数数组、压缩列表、跳表、hash表、双向列表6种。
简单动态数组:就是String的底层实现
其中整数数组、hash表、双向列表都是咱们常见的数据结构
压缩列表和跳表属于特殊的数据结构
压缩列表是Redis实现的特殊的数组:它本质就是一个数组,只不过,咱们常见的数组的每一个元素分配的空间大小是一致的,这样就会致使有多余的内存空间被浪费了。压缩列表就是为了解决这样的问题,他的每一个元素大小是按实际大小分配的,避免了内存的浪费,同时在压缩列表的表头还存了关于改列表的相关属性:用于记录列表个数zllen,表尾偏移量zltail和列表长度zlbytes。表尾还有一个zlend标记列表的结束。
跳表:有序链表查询元素只能逐一查询,跳表本质上就是链表的基础上加了多级索引,经过多级索引的几个跳转,快递定位到元素所在位置。
不一样数据结构的查询时间复杂度
上面从存储方面解释了,redis为何快.
逆向思惟能够说为何不用多线程,这个咱们得先看下多线程存在哪些问题?在正常应用操做中,使用多线程能够大大提升处理的时间。那是否是能够无限的加大线程数量,以获取更快的处理速度?实际试验后,发如今机器资源有限的状况下,不断增长线程处理时间,并无像咱们想象的那样成线性增加,而是到达必定阶段就趋于平衡,甚至有降低的趋势,这是为何呢?
其实主要有两个方面,咱们知道线程是CPU调度的最小单元,当线程多的时候,CPU须要不停的切换线程,线程切换是须要消耗时间的,当大量线程须要来回切换,那么CPU在这切换的损耗了不少时间。
另外当多个线程,须要对共享资源进行操做的时候,为了保证并发安全性,须要有额外的机制保证,好比加锁。这样就使得当多个线程再操做共享数据时,变成了串行。
因此为了不这些问题,Redis采用了单线程操做数据。
咱们知道Redis单线程操做的,可是只是指的Redis对外提供键值对存储服务是单线程的。Redis的其余功能并非,好比持久化,异步删除,集群同步等,都是由额外的线程去执行的。
除了上面说的,Redis的大部分操做都是在内存上完成的,加上高效的数据结构,是他实现高性能的一方面。另一方面Redis采用的多路复用机制,使其在网络IO操做中能并发处理大量的客户端请求。
在网络 IO 操做中,有潜在的阻塞点,分别是 accept() 和 recv()。当 Redis 监听到一个客户端有链接请求,但一直未能成功创建起链接时,会阻塞在 accept() 函数这里,致使其余客户端没法和 Redis 创建链接。相似的,当 Redis 经过 recv() 从一个客户端读取数据时,若是数据一直没有到达,Redis 也会一直阻塞在 recv()。 这就致使 Redis 整个线程阻塞,没法处理其余客户端请求,效率很低。不过,幸运的是,socket 网络模型自己支持非阻塞模式。
Socket 网络模型的非阻塞模式设置,主要体如今三个关键的函数调用上,若是想要使用 socket 非阻塞模式,就必需要了解这三个函数的调用返回类型和设置模式。接下来,咱们就重点学习下它们。在 socket 模型中,不一样操做调用后会返回不一样的套接字类型。socket() 方法会返回主动套接字,而后调用 listen() 方法,将主动套接字转化为监听套接字,此时,能够监听来自客户端的链接请求。最后,调用 accept() 方法接收到达的客户端链接,并返回已链接套接字。
针对监听套接字,咱们能够设置非阻塞模式:当 Redis 调用 accept() 但一直未有链接请求到达时,Redis 线程能够返回处理其余操做,而不用一直等待。可是,你要注意的是,调用 accept() 时,已经存在监听套接字了。
相似的,咱们也能够针对已链接套接字设置非阻塞模式:Redis 调用 recv() 后,若是已链接套接字上一直没有数据到达,Redis 线程一样能够返回处理其余操做。咱们也须要有机制继续监听该已链接套接字,并在有数据达到时通知 Redis。这样才能保证 Redis 线程,既不会像基本 IO 模型中一直在阻塞点等待,也不会致使 Redis 没法处理实际到达的链接请求或数据。
Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是咱们常常听到的 select/epoll 机制。简单来讲,在 Redis 只运行单线程的状况下,该机制容许内核中,同时存在多个监听套接字和已链接套接字。内核会一直监听这些套接字上的链接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不一样事件的发生,调用相应的处理函数。
由于Redis是操做是基于内存的,全部一点系统宕机存在内存中的数据就会丢失,为了实现数据的持久化,Redis中存在两个持久化机制AOF和RBD。
AOF的原理就是,经过记录下Redis的全部命令操做,在须要数据恢复的时候,再按照顺序把全部命令执行一次,从而恢复数据。
但跟数据库的写前日志不一样的,AOF采用的写后日志,也就是在Redis执行过操做以后,再写入AOF日志。之因此为何采用写后日志,能够避免由于写日志的占用redis调用的时间,另外为了保证Redis的高性能,在写aof日志的时候,不会作校验,若采用写前日志,若是命令是错误非法的,在恢复数据的时候就会出现异常。采用写后日志,只有命令执行成功的才会被保存。
AOF的执行策略有三种
all:每次写入/删除命令都会被写入日志文件中,保证了数据可靠性,可是写入日志,涉及到了磁盘的IO,必然会影响性能
everysec:每秒钟执行一第二天志写入,在一秒以内的命令操做会记录在aof内存缓冲区,每一秒会写回到日志文件中,相对于每次写入性能得以提高,可是在aof缓冲区没有来得及回写到日志文件中时,系统发生宕机就会丢失这部分数据。
no:内存缓冲区的命令记录不会不主动写回到日志文件中,而交给操做系统决定。这种策略性能最高,可是丢失数据的风险也最大。
可是AOF文件过大,会带来性能问题,全部AOF重写机制就登场了。
AOF重写的原理是,将多个命令对同一个key的操做合并成一个,由于数据恢复时,咱们只要关心数据最后的状态就能够了。
须要注意的是,与AOF日志由主线程写回不一样,重写过程是由后台子线程bgwriteaof来完成的,这个避免阻塞主线程,致使数据库性能降低。
每次 AOF 重写时,Redis 会先执行一个内存拷贝,用于重写;而后,使用两个日志保证在重写过程当中,新写入的数据不会丢失。并且,由于 Redis 采用额外的线程进行数据重写,因此,这个过程并不会阻塞主线程。
所谓内存快照,就是指内存中的数据在某一个时刻的状态记录。对 Redis 来讲,就是把某一时刻的状态以文件的形式写到磁盘上。
Redis执行RDB的策略是什么?
Redis进行快照的时候,是进行全量的快照,而且为了避免阻塞主线程,会默认使用bgsave命令建立一个子线程,专门用于写入RDB文件。
快照期间数据还能修改吗?
若是不能修改,那么在快照期间,这块数据就会只能读取不能修改,那么必然影响使用。若是能够修改,那么Redis是如何实现的?其实Redis是借助操做系统的写时复制,在执行快照期间,让修改的数据,会在内存中拷贝出一份副本,副本的数据能够被写入rdb文件中,而主线程仍然能够修改原数据。
多久执行一次呢?
跟aof一样的问题,若是快照频率低,那么在两次快照期间出现宕机,就会出现数据不完整的状况,若是快照频率过快,那么又会出现两个问题,一个是不停的对磁盘写出,增大磁盘压力,可能上一次写入还没完成,新的快照又来了,形成恶性循环.另外虽然执行快照是主线程fork出来的,可是不停的fork的过程是阻塞主线程的。
那么如何配置才合适呢?
其实咱们只须要第一次全量快照,后续只快照有数据变更的地方就能够大大下降快照的资源损耗了,那么如何记录这变更的数据呢,这里咱们能够想到aof具备这样的功能。Redis4.0就提使用RDB+AOF混合模式来完成Redis的持久化。简单来讲,内存快照以必定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的全部命令操做。
前面咱们经过Redis的持久化机制,来保证服务器宕机以后,经过回放日志和从新读取RDB文件恢复数据,减小数据丢失的风险。
可是在单台及其的状况下,机器发生宕机,就没法对外提供服务了。咱们所说的Redis具备高可靠性,指的一是,数据尽可能少丢失,以前持久化机制就解决了这一问题,另外一个是服务尽可能少中断,Redis的作法是增长副本冗余量。Redis提供的主从模式,主从库之间采用了读写分离的方式。
从库只读取,主库执行读与写,写的数据主库会同步给从库。之因此只让主库写,是由于,若是从库也写,那么当客户端对一个数据修改了3次,为了保证数据的正确性,就要设法让主从库对于写操做协同,这会带来巨额的开销。
主从库间如何进行第一次同步的?
当咱们启动多个 Redis 实例的时候,它们相互之间就能够经过 replicaof(Redis 5.0 以前使用 slaveof)命令造成主库和从库的关系,以后会按照三个阶段完成数据的第一次同步。
主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。
这里有个地方须要注意,FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前全部的数据都复制给从库。
在第二阶段,主库将全部数据同步给从库。从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。
具体来讲,主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,而后加载 RDB 文件。这是由于从库在经过 replicaof 命令开始和主库同步前,可能保存了其余数据。为了不以前数据的影响,从库须要先把当前数据库清空。
在主库将数据同步给从库的过程当中,主库不会被阻塞,仍然能够正常接收请求。不然,Redis 的服务就被中断了。可是,这些请求中的写操做并无记录到刚刚生成的 RDB 文件中。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的全部写操做。
最后,也就是第三个阶段,主库会把第二阶段执行过程当中新收到的写命令,再发送给从库。具体的操做是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操做发给从库,从库再从新执行这些操做。这样一来,主从库就实现同步了。
Redis在有了主从集群后,若是从库挂了,Redis对外提供服务不受影响,主库和其余从库,依然能够提供读写服务,可是当主库挂了以后,由于是读写分离的,若是此时有写的请求,那么就没法处理了。Redis是若是解决这样的问题的呢,这就要引入哨兵机制了。
当主库挂了,咱们须要从从库中选出一个当作主库,这样就能够正常对外提供服务了。哨兵的本质就是一个Redis示例,只不过它是运行在特殊模式下的Redis进程。它主要有三个做用:监控、选举、通知。
哨兵在监控到主库下线的时候,会从从库中经过必定的规则,选举出适合的从库当主库,并通知其余从库变动主库的信息,让他们执行replicaof命令,和新主库创建链接,并进行数据复制。那么具体每一步都是怎么作的呢?
监控:哨兵会周期性向主从库发送PING命令,检测主库是否正常运行,若是主从库没有在规定的时间内回应哨兵的PING命令,则会被断定为“下线状态”,若是是主库下线,则开始自动切换主库的流程。可是通常若是只有一个哨兵,那么它的判断可能不具备可靠性,因此通常哨兵都是采用集群模式部署,称为哨兵集群。单多个哨兵均判断该主库下线了,那么可能他就真的下线了,这是一个少数服从多数的规则。
选举: 哨兵选择新主库的过程称为“筛选 + 打分”。简单来讲,咱们在多个从库中,先按照必定的筛选条件,把不符合条件的从库去掉。而后,咱们再按照必定的规则,给剩下的从库逐个打分,将得分最高的从库选为新主库,以下图所示:
一、排除那些已经下线的从库,以及链接不稳定的从库。链接不稳定是经过配置项down-after-milliseconds,当主从链接超时达到必定阈值,就会被记录下来,好比设置的10次,那么就会标记该从库网络很差,不适合作为主库。
二、筛选出从库后,第二部就要开始打分了,主要从三方面打分,
1.从库优先级,这是能够经过slave-property设置的,设置的高,打分的就高,就会被选为主库,好比你能够给从库中内存带宽资源充足设置高优先级,当主库挂了以后被优先选举为主库。
2.从库与旧主库之间的复制进度,以前咱们知道主从之间增量复制,有个参数slave-repl-offset记录当前的复制进度。这个数值越大,说明与主库复制进度约靠近,打分也会越高。
3.每一个从库建立实例的时候,会随机生成一个id,id越小的得分越高。
通知:哨兵提高一个从库为新主库后,哨兵会把新主库的地址写入本身实例的pubsub(switch-master)中。客户端须要订阅这个pubsub,当这个pubsub有数据时,客户端就能感知到主库发生变动,同时能够拿到最新的主库地址,而后把写请求写到这个新主库便可,这种机制属于哨兵主动通知客户端。
若是客户端由于某些缘由错过了哨兵的通知,或者哨兵通知后客户端处理失败了,安全起见,客户端也须要支持主动去获取最新主从的地址进行访问。
因此,客户端须要访问主从库时,不能直接写死主从库的地址了,而是须要从哨兵集群中获取最新的地址(sentinel get-master-addr-by-name命令),这样当实例异常时,哨兵切换后或者客户端断开重连,均可以从哨兵集群中拿到最新的实例地址。
部署哨兵集群的时候,咱们知道只须要配置:sentinel monitor
Redis有提供了pub/sub机制,哨兵跟主库创建了链接以后,将本身的信息发布到 “sentinel:hello”频道上,其余哨兵发布并订阅了该频道,就能够获取其余哨兵的信息,那么哨兵之间就能够相互通讯了。
那么哨兵如何知道从库的链接信息呢,那是由于INFO命令,哨兵向主库发送该命令后,得到了全部从库的链接信息,就能分从库创建链接,并进行监控了。
从本质上说,哨兵就是一个运行在特定模式下的 Redis 实例,只不过它并不服务请求操做,只是完成监控、选主和通知的任务。因此,每一个哨兵实例也提供 pub/sub 机制,客户端能够从哨兵订阅消息。哨兵提供的消息订阅频道有不少,不一样频道包含了主从库切换过程当中的不一样关键事件。
与mysql同样,当一张表的数据很大时,查询耗时可能就会愈来愈大,咱们采起的措施是分表分库。一样的Redis也样,当数据量很大时,好比高达25G,在单分片下,咱们须要机器有32G的内存。可是咱们会发现,有时候redis响应会变的很慢,经过INFO查询Redis的latest_fork_usec指标,最近fork耗时,发现耗时很大,快到秒级别了,fork这个动做会阻塞主线程,因而就致使了Redis变慢了。
因而就有redis分片集群, 启动多个 Redis 实例组成一个集群,而后按照必定的规则,把收到的数据划分红多份,每一份用一个实例来保存。回到咱们刚刚的场景中,若是把 25GB 的数据平均分红 5 份(固然,也能够不作均分),使用 5 个实例来保存,每一个实例只须要保存 5GB 数据。
那么,在切片集群中,实例在为 5GB 数据生成 RDB 时,数据量就小了不少,fork 子进程通常不会给主线程带来较长时间的阻塞。采用多个实例保存数据切片后,咱们既能保存 25GB 数据,又避免了 fork 子进程阻塞主线程而致使的响应忽然变慢。
那么数据是如何决定存在在哪一个分片上的呢?
Redis Cluster 方案采用哈希槽(Hash Slot,接下来我会直接称之为 Slot),来处理数据和实例之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽相似于数据分区,每一个键值对都会根据它的 key,被映射到一个哈希槽中。具体的映射过程分为两大步:首先根据键值对的 key,按照CRC16 算法计算一个 16 bit 的值;而后,再用这个 16bit 值对 16384 取模,获得 0~16383 范围内的模数,每一个模数表明一个相应编号的哈希槽。
咱们在部署 Redis Cluster 方案时,可使用 cluster create 命令建立集群,此时,Redis 会自动把这些槽平均分布在集群实例上。例如,若是集群中有 N 个实例,那么,每一个实例上的槽个数为 16384/N 个。 也可使用 cluster meet 命令手动创建实例间的链接,造成集群,再使用 cluster addslots 命令,指定每一个实例上的哈希槽个数。