性能优势
每秒可以处理超过 10 万次读写操作。
理论上 Redis 可以处理多达2^32的 keys,每个实例至少存放了2亿5千万的 keys。任何 list、set、和 sorted set 都可以放 2^32个元素。换句话说,Redis的存储极限是系统中的可用内存值。
特性
- 纯内存访问,这是快的根本原因;
- 定位算法优势,利用Hash检索key;
- 用C语言实现,更接近操作系统;
- 数据结构简单,数据操作简单;
- 采用单线程模型,避免并发竞争开销;
- 使用I/O多路复用模型,非阻塞I/O;
Redis VS Memcached
1. 数据类型:Memcached只支持简单的String数据类型;Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
2. 持久化:Memcached把数据全部存在内存之中;Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。
3. 集群模式:Memcached没有原生的集群模式;但是Redis原生支持集群模式。
4. 线程模型:Memcached使用多线程模型;Redis使用单线程模型。
5. 存储单元:Redis单个value 的最大限制是 1GB;Memcached只能保存 1MB 的数据
分析☆☆
1. 单线程模型
每次客户端调用都经历了发送命令、执行命令、返回结果三个过程。
因为Redis是单线程来处理命令,所以一条命令从客户端到达服务端不会立即被执行,所有的命令都会进入一个队列,然后逐个被执行。
所有即使是有先后顺序的几个命令到达服务端的执行顺序也是不确定的,因为中间有网络传输。但是可以肯定的是,不会有两条命令被同时执行。
这样就不会产生并发问题,这就是Redis单线程的基本模型。
但是,单线程会有一个问题:对于每个命令的执行事件是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务来说是致命的,所以Redis是面向快速执行场景的数据库。
2. 单线程的根本原因
传统RDBMS由于存在磁盘I/O,且表处理可能会很复杂,为了不让一个用户的操作阻塞后面的用户的操作,所以要用多线程。而Redis是纯内存I/O操作,一个用户的操作会很快执行完,如果多线程反而要处理复杂的同步问题,设计各种锁,而且还有上下文切换的代价,这可能才是真正的瓶颈,所以用单线程!
本质上存在一种博弈:磁盘或内存I/O的时间 数据处理的时间、多线程的上下文切换 同步竞争锁的时间损耗,最终目的是及时响应客户端。
3. 单线程的优点
这避免了并发带来的两方面的竞争消耗:
- 资源同步时锁的开销;
- 线程进程的上下文切换开销;
锁的实现机制在很多存储系统和编程语言内部都使用CAS机制,会有一定的CPU空转开销。
4. I/O多路复用
非阻塞的I/O多路复用:Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中连接、读写、关闭都转换为事件,不在网络I/O上浪费过多时间。
I/O多路复用模型可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。
这里“多路I/O”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络I/O的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存中的操作不会成为影响Redis性能的瓶颈。
参照上图, redis-client在操作的时候,会产生具有不同事件类型的socket。在服务端,有一段I/O多路复用程序,将其置入队列之中。然后,文件事件分派器,依次去队列中取,转发到不同的事件处理器中。
5. I/O多路复用原理
下面举一个例子,模拟一个tcp服务器处理30个客户socket。假设你是一个老师,让30个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:
1. 第一种选择:按顺序逐个检查,先检查A,然后是B,之后是C、D…这中间如果有一个学生卡主,全班都会被耽误。这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。
2. 第二种选择:你创建30个分身,每个分身检查一个学生的答案是否正确。这种类似于为每一个用户创建一个进程或者线程处理连接。
3. 第三种选择,你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A… 这种就是IO复用模型,Linux下的select、poll和epoll就是干这个的。将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。
这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor模式。
缓存缺陷☆☆
- 缓存和数据库:一致性问题
- 缓存的并发竞争问题
- 缓存雪崩问题
- 缓存击穿问题
- 缓存穿透问题
缓存穿透(猛查不存在值的键)
查询的数据在缓存中不存在,转而执行存储层查询,并且出于容错考虑,如果从存储层查不到数据则不将这个信息写入缓存,这将导致对这个不存在的数据的每次请求都要到存储层去查询。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
缓存击穿(猛查过期时候的键)
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
(1) 使用互斥锁(mutex key)
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。
(2) "提前"使用互斥锁(mutex key)
在value内部设置1个超时值(timeout1), timeout1比实际的memcachetimeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。伪代码如下:
(3) "永远不过期"
这里的“永远不过期”包含两层意思:
从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期。
(4) 采用netflix的hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。
缓存雪崩(键在同一时刻过期)
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
最后,对于缓存系统常见的缓存满了和数据丢失问题,需要根据具体业务分析,通常我们采用LRU策略处理溢出,Redis的RDB和AOF持久化策略来保证一定情况下的数据安全。
内存淘汰☆☆
比如你redis只能存5G数据,可是你写了10G,那会删内存中5G的数据,怎么删的?还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,思考过原因?
redis采用的是:定期删除 惰性删除 内存淘汰机制
为什么不用定时删除
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略。
定期删除 惰性删除
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
内存淘汰策略
如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
在redis.conf中有一行配置:# maxmemory-policynoeviction,该配置就是配内存淘汰策略。当内存不足以容纳新写入数据时,新写入操作会:
(1) noeviction:新写入操作报错。
(2) allkeys-lru:移除最近最少使用的key。
(3) allkeys-random:随机移除某个key。
(4) volatile-lru:在设置了过期时间的键空间中,移除最近最少使用的key。
(5) volatile-random:在设置了过期时间的键空间中,随机移除某个key。
(6) volatile-ttl:在设置了过期时间的键空间中,有更早过期时间的key优先移除。
写磁盘备份(持久化)
Redis写磁盘备份有两种方式:Snapshotting(快照),Append-only file(AOF)。
1. Snapshotting(快照)
将存储在内存的数据以快照的方式写入二进制文件中,如默认dump.rdb中,例如
(1) save 900 1 #900秒内如果超过1个Key被修改,则启动快照保存
(2) save 300 10 #300秒内如果超过10个Key被修改,则启动快照保存
功能核心函数rdbSave(内存到RDB文件)和rdbLoad(RDB文件到内存)两个函数:
2. Append-Only File(AOF)
使用AOF持久时,服务会将每个收到的写命令通过write函数追加到文件中,如默认appendonly.aof中,
(1) appendonly yes:开启AOF持久化存储方式(默认是关闭的)
(2) appendfsync always:收到写命令后就立即写入磁盘,效率最差,效果最好
(3) appendfsync everysec:每秒写入磁盘一次,效率与效果居中
(4) appendfsync no:完全依赖OS,效率最佳,效果没法保证
每当执行服务器(定时)任务或者函数时flushAppendOnlyFile函数都会被调用:
(1) WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件
(2) SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。
集群机制
Redis Cluster的设计考虑到了去中心化、去中间件(如果用zookeeper这种中间件,网络I/O就会成为Redis的瓶颈了,毕竟网络和内存的访问效率差很多个数量级),集群中的每个节点都是对等关系,每个节点都保存各自的数据和整个集群状态。每个节点都和其他所有节点连接,这样只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。
Redis集群采用哈希槽(hash slot)的方式来分配。Redis Cluster默认分配16384(2的14次方)个slot,当我们set一个key时,会用CRC16算法来取模得到所属的slot,然后将这个key分到该哈希槽所在节点上:
key -> CRC16(key)384 -> slot -> host
Redis集群会把数据存在一个master节点(读写),然后在这个master和其对应的slave之间进行数据同步(异步的,不保证强一致性,此乃内存数据库为了性能必然的选择)。当读取数据时,也根据哈希槽算法到对应的master(读写)/slave(只读)获取数据。
Redis Sentinal:只有当一个master 挂掉之后,才会启动一个对应的slave节点,充当master。
需要注意的是:必须要3个或以上的master节点,否则在创建集群时会失败,并且当存活的master节点数小于总节点数的一半时,整个集群就无法提供服务了。
Redis Cluster实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升。
Redis数据不一致问题有两个地方:
(1) master的数据更新通过异步复制到其它slave,不保证master和slave的数据强一致性。
(2) master挂掉后,日志未及时被更新或者网络原因挂掉后,原先的一些最近时间的数据更新操作会丢。
分片
按照某种规则去划分数据,分散存储在多个节点上。通过将数据分到多个 Redis 服务器上,来减轻单个 Redis 服务器的压力。
为什么要做Redis分区
分区可以让 Redis 管理更大的内存, Redis 将可以使用所有机器的内存。如果没有分区,你最多只能使用一台机器的内存。分区使 Redis 的计算能力通过简单地增加计算机得到成倍提升,Redis 的网络带宽也会随着计算机和网卡的增加而成倍增长。
Redis 分区有什么缺点
涉及多个 key 的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的 Redis 实例(实际上这种情况也有办法,但是不能直接使用交集指令)。
同时操作多个 key,则不能使用 Redis 事务.分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集。
当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的 Redis 实例和主机同时收集 RDB / AOF 文件。
分区时动态扩容或缩容可能非常复杂。Redis 集群在运行时增加或者删除 Redis 节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。
哨兵
Redis sentinel 是一个分布式系统中监控 redis 主从服务器,并在主服务器下线时自动进行故障转移,解决主从同步 Master 宕机后的主从切换问题。具有三个特性:
1. 监控(Monitoring):Sentinel会不断地检查你的主服务器和从服务器是否运作正常。
2. 提醒(Notification):被监控的某个Redis服务器出现问题时,Sentinel可以通过 API 向管理员或者其他应用程序发送通知。
3. 自动故障迁移(Automatic failover):当一个主服务器不能正常工作时,Sentinel会开始一次自动故障迁移操作。
性能调优
记录执行时间慢的操作:
(1) slowlog-log-slower-than,此配置项决定要对执行时间大于多少微秒(μs)的命令进行记录
(2) slowlog-max-len它决定slowlog最多能保存多少条日志
(3) 当发现redis性能下降的时候可以查看下是哪些命令导致的