Redis(Remote Dictionary Server) 是⼀个使⽤ C 语⾔编写的,开源的(BSD许可)⾼性能⾮关系型(NoSQL)的键值对数据库。
Redis 可以存储键和五种不同类型的值之间的映射。键的类型只能为字符串,值⽀持五种数据类型:字符串、列
表、集合、散列表、有序集合。
与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度⾮常快,因此 redis 被⼴泛应⽤于缓存⽅向,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。另外,Redis 也经常⽤来做分布式锁。除此之外,Redis ⽀持事务 、持久化、LUA脚本、LRU驱动事件、多种集群⽅案。
优点
读写性能优异, Redis能读的速度是110000次/s,写的速度是81000次/s。
ps:⽣产环境⼤概也就是60000次/s左右
⽀持数据持久化,⽀持AOF和RDB两种持久化⽅式。
ps:AOF与RDB的区别是什么?
⻅后⾯⽀持事务,Redis的所有操作都是原⼦性的,同时Redis还⽀持对⼏个操作合并后的原⼦性执⾏。
ps:事务⽀持不是特别良好,单机⽀持事务,集群不⽀持数据结构丰富,除了⽀持string类型的value外还⽀持hash、set、zset、list等数据结构。
⽀持主从复制,主机会⾃动将数据同步到从机,可以进⾏读写分离。
缺点
数据库容量受到物理内存的限制,不能⽤作海量数据的⾼性能读写,因此Redis适合的场景主要局限在较⼩数据量的⾼性能操作和运算上。
Redis 不具备⾃动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者⼿动切换前端的IP才能恢复。
主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引⼊数据不⼀致的问题,降低了系统的可⽤性。
Redis 较难⽀持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这⼀问题,运维⼈员在系统上线时必须确保有⾜够的空间,这对资源造成了很⼤的浪费。
主要从“⾼性能”和“⾼并发”这两点来看待这个问题。
⾼性能:
假如⽤户第⼀次访问数据库中的某些数据。这个过程会⽐较慢,因为是从硬盘上读取的。将该⽤户访问的数据存在数缓存中,这样下⼀次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
直接操作缓存能够承受的请求是远远⼤于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样⽤户的⼀部分请求会直接到缓存这⾥⽽不⽤经过数据库。
缓存分为本地缓存和分布式缓存。以 Python 为例,使⽤⾃带的 字典 实现的是本地缓存,最主要的特点是轻量以及快速,⽣命周期随着程序 的销毁⽽结束,并且在多实例的情况下,每个实例都需要各⾃保存⼀份缓存,缓存不具有⼀致性。
使⽤ redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共⽤⼀份缓存数据,缓存具有⼀致性。缺点是需要保持 redis 或 memcached服务的⾼可⽤,整个程序架构上较为复杂。
五 Redis为什么这么快
1、完全基于内存,绝⼤部分请求是纯粹的内存操作,⾮常快速。数据存在内存中,类似于 HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专⻔进⾏设计的;
3、采⽤单线程,避免了不必要的上下⽂切换和竞争条件,也不存在多进程或者多线程导致的切换⽽消耗 CPU,不⽤去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁⽽导致的性能消耗;
4、使⽤多路 I/O 复⽤模型,⾮阻塞 IO;
5、使⽤底层模型不同,它们之间底层实现⽅式以及与客户端之间通信的应⽤协议不⼀样,Redis 直接⾃⼰构建了VM 机制 ,因为⼀般的系统调⽤系统函数的话,会浪费⼀定的时间去移动和请求;
Redis主要有5种数据类型,包括String,List,Set,Zset,Hash,满⾜⼤部分的使⽤要求
场景⼀
1 计数器
可以对 字符串 进⾏⾃增⾃减运算,从⽽实现计数器功能。Redis 这种内存型数据库的读写性能⾮常⾼,很适合存储频繁读写的计数量。
2 缓存
将热点数据放到内存中,设置内存的最⼤使⽤量以及淘汰策略来保证缓存的命中率。
3 会话缓存(session)
可以使⽤ Redis 来统⼀存储多台应⽤服务器的会话信息。当应⽤服务器不再存储⽤户的会话信息,也就不再具有状态,⼀个⽤户可以请求任意⼀个应⽤服务器,从⽽更容易实现⾼可⽤性以及可伸缩性。
4 全⻚缓存
除基本的会话token之外,Redis还提供很简便的FPC平台。以Django缓存为例,Django缓存可以将整个⻚⾯缓存到Redis中,可以以最快速度加载你曾浏览过的⻚⾯。
5 查找表
例如 DNS 记录就很适合使⽤ Redis 进⾏存储。查找表和缓存类似,也是利⽤了 Redis 快速的查找特性。但是查找表的内容不能失效,⽽缓存的内容可以失效,因为缓存不作为可靠的数据来源。
6 消息队列(发布/订阅功能)
List 是⼀个双向链表,可以通过 lpush 和 rpop 写⼊和读取消息。不过最好使⽤ Kafka、RabbitMQ 等消息中间件。
7 分布式锁实现
在分布式场景下,⽆法使⽤单机环境下的锁来对多个节点上的进程进⾏同步。可以使⽤ Redis ⾃带的 SETNX 命令实现分布式锁,除此之外,还可以使⽤官⽅提供的 RedLock 分布式锁实现。
8 其它Set 可以实现交集、并集等操作,从⽽实现共同好友等功能。ZSet 可以实现有序性操作,从⽽实现排⾏榜等功能。
场景⼆
Redis相⽐其他缓存,有⼀个⾮常⼤的优势,就是⽀持多种数据类型。
数据类型说明string字符串,最简单的k-v存储hashhash格式,value为field和value,适合ID-Detail这样的场景。list简单的list,顺序列表,⽀持⾸位或者末尾插⼊数据set⽆序list,查找速度快,适合交集、并集、差集处理sorted set有序的set其实,通过上⾯的数据类型的特性,基本就能想到合适的应⽤场景了。
string——适合最简单的k-v存储,类似于memcached的存储结构,短信验证码,配置信息等,就⽤这种类型来存储。
hash——⼀般key为ID或者唯⼀标示,value对应的就是详情了。如商品详情,个⼈信息详情,新闻详情等。
list——因为list是有序的,⽐较适合存储⼀些有序且数据相对固定的数据。如省市区表、字典表等。因为list是有序的,适合根据写⼊的时间来排序,如:最新的***,消息队列等。
set——可以简单的理解为ID-List的模式,如微博中⼀个⼈有哪些好友,set最⽜的地⽅在于,可以对两个set提供交集、并集、差集操作。例如:查找两个⼈共同的好友等。
Sorted Set——是set的增强版本,增加了⼀个score参数,⾃动会根据score的值进⾏排序。⽐较适合类似于top 10等不根据插⼊的时间来排序的数据。
如上所述,虽然Redis不像关系数据库那么复杂的数据结构,但是,也能适合很多场景,⽐⼀般的缓存数据结构要多。了解每种数据结构适合的业务场景,不仅有利于提升开发效率,也能有效利⽤Redis的性能。
持久化就是把内存的数据写到磁盘中去,防⽌服务宕机了内存数据丢失。
Redis 提供两种持久化机制 RDB(默认) 和 AOF 机制:
RDB:是Redis DataBase缩写快照RDB是Redis默认的持久化⽅式。按照⼀定的时间将内存的数据以快照的形式保存到硬盘中,对应产⽣的数据⽂件为dump.rdb。通过配置⽂件中的save参数来定义快照的周期。
1、只有⼀个⽂件 dump.rdb,⽅便持久化。
2、容灾性好,⼀个⽂件可以保存到安全的磁盘。
3、性能最⼤化,fork ⼦进程来完成写操作,让主进程继续处理命令,所以是 IO 最⼤化。使⽤单独⼦进程来进⾏持久化,主进程不会进⾏任何 IO 操作,保证了 redis 的⾼性能
4.相对于数据集⼤时,⽐ AOF 的启动效率更⾼。
缺点:
1、数据安全性低。RDB 是间隔⼀段时间进⾏持久化,如果持久化之间 redis 发⽣故障,会发⽣数据丢失。所以这种⽅式更适合数据要求不严谨的时候)
2、AOF(Append-only file)持久化⽅式: 是指所有的命令⾏记录以 redis 命令请 求协议的格式完全持久化存储)保存为 aof ⽂件。
AOF:持久化
AOF持久化(即Append Only File持久化),则是将Redis执⾏的每次写命令记录到单独的⽇志⽂件中,当重启Redis会重新将持久化的⽇志中⽂件恢复数据。
当两种⽅式同时开启时,数据恢复Redis会优先选择AOF恢复。
优点:
1、数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进⾏⼀次 命令操作就记录到 aof ⽂件中⼀次。
2、通过 append 模式写⽂件,即使中途服务器宕机,可以通过 redis-check-aof ⼯具解决数据⼀致性问题。
3、AOF 机制的 rewrite 模式。AOF ⽂件没被 rewrite 之前(⽂件过⼤时会对命令 进⾏合并重写),可以删除其中的某些命令(⽐如误操作的 flushall))
缺点:
1、AOF ⽂件⽐ RDB ⽂件⼤,且恢复速度慢。
2、数据集⼤的时候,⽐ rdb 启动效率低。
优缺点是什么?
AOF⽂件⽐RDB更新频率⾼,优先使⽤AOF还原数据。
AOF⽐RDB更安全也更⼤
RDB性能⽐AOF好
如果两个都配了优先加载AOF
⼀般来说, 如果想达到⾜以媲美MySQL的数据安全性,你应该同时使⽤两种持久化功能。在这种情况下,当Redis 重启的时候会优先载⼊AOF⽂件来恢复原始的数据,因为在通常情况下AOF⽂件保存的数据集要⽐RDB⽂件保存的数据集要完整。
如果你⾮常关⼼你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以只使⽤RDB持久化。有很多⽤户都只使⽤AOF持久化,但并不推荐这种⽅式,因为定时⽣成RDB快照(snapshot)⾮常便于进⾏数据库备份, 并且 RDB 恢复数据集的速度也要⽐AOF恢复的速度要快,除此之外,使⽤RDB还可以避免AOF程序的bug。
如果你只希望你的数据在服务器运⾏的时候存在,你也可以不使⽤任何持久化⽅式。
如果Redis被当做缓存使⽤,使⽤⼀致性哈希实现动态扩容缩容。
如果Redis被当做⼀个持久化存储使⽤,必须使⽤固定的keys-to-nodes映射关系,节点的数量⼀旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使⽤可以在运⾏时进⾏数据再平衡的⼀套系统,⽽当前只有Redis集群可以做到这样。
我们都知道,Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间。Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理。
过期策略通常有以下三种:
定时过期:每个设置过期时间的key都需要创建⼀个定时器,到过期时间就会⽴即清除。该策略可以⽴即清除过期的数据,对内存很友好;但是会占⽤⼤量的CPU资源去处理过期的数据,从⽽影响缓存的响应时间和吞吐量。
惰性过期:只有当访问⼀个key时,才会判断该key是否已过期,过期则清除。该策略可以最⼤化地节省CPU资源,却对内存⾮常不友好。极端情况可能出现⼤量的过期key没有再次被访问,从⽽不会被清除,占⽤⼤量内存。
定期过期:每隔⼀定的时间,会扫描⼀定数量的数据库的expires字典中⼀定数量的key,并清除其中已过期的key。该策略是前两者的⼀个折中⽅案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis中同时使⽤了惰性过期和定期过期两种过期策略。
EXPIRE和PERSIST命令。
除了缓存服务器⾃带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进⾏⾃定义的缓存淘汰,常⻅的策略有两种:
1. 定时去清理过期的缓存;
2. 当有⽤户请求过来时,再判断这个请求所⽤到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第⼀种的缺点是维护⼤量缓存的key是⽐较麻烦的,第⼆种的缺点就是每次⽤户请求过来都要判断缓存失效,逻辑相对⽐较复杂!具体⽤哪种⽅案,⼤家可以根据⾃⼰的应⽤场景来权衡。
redis内存数据集⼤⼩上升到⼀定⼤⼩的时候,就会施⾏数据淘汰策略。
Redis的内存淘汰策略是指在Redis的⽤于缓存的内存不⾜时,怎么处理需要新写⼊且需要申请额外空间的数据。
全局的键空间选择性移除
noeviction:当内存不⾜以容纳新写⼊数据时,新写⼊操作会报错。
allkeys-lru:当内存不⾜以容纳新写⼊数据时,在键空间中,移除最近最少使⽤的key。(这个是最常⽤的)
allkeys-random:当内存不⾜以容纳新写⼊数据时,在键空间中,随机移除某个key。
设置过期时间的键空间选择性移除
volatile-lru:当内存不⾜以容纳新写⼊数据时,在设置了过期时间的键空间中,移除最近最少使⽤的key。
volatile-random:当内存不⾜以容纳新写⼊数据时,在设置了过期时间的键空间中,随机移除某个key。
volatile-ttl:当内存不⾜以容纳新写⼊数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
总结
Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略⽤于处理内存不⾜时的需要申请额外空间的数据;过期策略⽤于处理过期的缓存数据。
内存。
如果达到设置的上限,Redis的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以配置内存淘汰机制,当Redis达到内存上限时会冲刷掉旧的内容。
可以好好利⽤Hash,list,sorted set,set等集合类型数据,因为通常情况下很多⼩的Key-Value可以⽤更紧凑的⽅式存放到⼀起。尽可能使⽤散列表(hashes),散列表(是说散列表⾥⾯存储的数少)使⽤的内存⾮常⼩,所以你应该尽可能的将你的数据模型抽象到⼀个散列表⾥⾯。⽐如你的web系统中有⼀个⽤户对象,不要为这个⽤户的名称,姓⽒,邮箱,密码设置单独的key,⽽是应该把这个⽤户的所有信息存储到⼀张散列表⾥⾯
Redis基于Reactor模式开发了⽹络事件处理器,这个处理器被称为⽂件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复⽤程序、⽂件事件分派器、事件处理器。因为⽂件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
⽂件事件处理器使⽤ I/O 多路复⽤(multiplexing)程序来同时监听多个套接字, 并根据套接字⽬前执⾏的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执⾏连接应答(accept)、读取(read)、写⼊(write)、关闭(close)等操作时, 与操作相对应的⽂件事件就会产⽣, 这时⽂件事件处理器就会调⽤套接字之前关联好的事件处理器来处理这些事件。
虽然⽂件事件处理器以单线程⽅式运⾏, 但通过使⽤ I/O 多路复⽤程序来监听多个套接字, ⽂件事件处理器既实现了⾼性能的⽹络通信模型, ⼜可以很好地与 redis 服务器中其他同样以单线程⽅式运⾏的模块进⾏对接, 这保持了 Redis 内部单线程设计的简单性。
事务是⼀个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执⾏。事务在执⾏的过程中,不会被其他客
户端发送来的命令请求所打断。
事务是⼀个原⼦操作:事务中的命令要么全部被执⾏,要么全部都不执⾏。
Redis 事务的本质是通过MULTI、EXEC、WATCH等⼀组命令的集合。事务⽀持⼀次执⾏多个命令,⼀个事务中所有命令都会被序列化。在事务执⾏过程,会按照顺序串⾏化执⾏队列中的命令,其他客户端提交的命令请求不会插⼊到事务执⾏命令序列中。
总结说:redis事务就是⼀次性、顺序性、排他性的执⾏⼀个队列中的⼀系列命令。
1. 事务开始 MULTI
2. 命令⼊队
3. 事务执⾏ EXEC
事务执⾏过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放⼊队列中排队
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的Redis会将⼀个事务中的所有命令序列化,然后按顺序执⾏。
1. redis 不⽀持回滚,“Redis 在事务失败时不进⾏回滚,⽽是继续执⾏余下的命令”, 所以 Redis 的内部可以保持简单且快速。
2. 如果在⼀个事务中的命令出现错误,那么所有的命令都不会执⾏;
3. 如果在⼀个事务中出现运⾏错误,那么正确的命令会被执⾏。
WATCH 命令是⼀个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)⾏为。 可以监控⼀个或多个键,⼀旦其中有⼀个键被修改(或删除),之后的事务就不会执⾏,监控⼀直持续到EXEC命令。MULTI命令⽤于开启⼀个事务,它总是返回OK。 MULTI执⾏之后,客户端可以继续向服务器发送任意多条命令,这些命令不会⽴即被执⾏,⽽是被放到⼀个队列中,当EXEC命令被调⽤时,所有队列中的命令才会被执⾏。EXEC:执⾏所有事务块内的命令。返回事务块内所有命令的返回值,按命令执⾏的先后顺序排列。 当操作被打断时,返回空值 nil 。通过调⽤DISCARD,客户端可以清空事务队列,并放弃执⾏事务, 并且客户端会从事务状态中退出。UNWATCH命令可以取消watch对所有key的监控。
原⼦性(Atomicity)
原⼦性是指事务是⼀个不可分割的⼯作单位,事务中的操作要么都发⽣,要么都不发⽣。
⼀致性(Consistency)
事务前后数据的完整性必须保持⼀致。隔离性(Isolation)
多个事务并发执⾏时,⼀个事务的执⾏不应影响其他事务的执⾏
持久性(Durability)
持久性是指⼀个事务⼀旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发⽣故障也不应该对其有任何影响Redis的事务总是具有ACID中的⼀致性和隔离性,其他特性是不⽀持的。当服务器运⾏在AOF持久化模式下,并且appendfsync选项的值为always时,事务也具有耐久性。
7.6 Redis事务⽀持隔离性吗
Redis 是单进程程序,并且它保证在执⾏事务时,不会对事务进⾏中断,事务可以运⾏直到执⾏完所有事务队列中的命令为⽌。因此,Redis 的事务是总是带有隔离性的。
7.7 Redis事务保证原⼦性吗,⽀持回滚吗
Redis中,单条命令是原⼦性执⾏的,但事务不保证原⼦性,且没有回滚。事务中任意命令执⾏失败,其余的命令仍会被执⾏。
7.8 Redis事务其他实现
基于Lua脚本,Redis可以保证脚本内的命令⼀次性、按顺序地执⾏,其同时也不提供事务运⾏错误的回滚,执⾏过程中如果部分命令运⾏错误,剩下的命令还是会继续运⾏完基于中间标记变量,通过另外的标记变量来标识事务是否执⾏完成,读取数据时先读取该标记变量判断是否事务执⾏完成。但这样会需要额外写代码实现,⽐较繁琐
sentinel,中⽂名是哨兵。哨兵是 redis 集群机构中⾮常重要的⼀个组件,主要有以下功能:
集群监控:负责监控 redis master 和 slave 进程是否正常⼯作。
消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
故障转移:如果 master node 挂掉了,会⾃动转移到 slave node 上。
配置中⼼:如果故障转移发⽣了,通知 client 客户端新的 master 地址。
哨兵⽤于实现 redis 集群的⾼可⽤,本身也是分布式的,作为⼀个哨兵集群去运⾏,互相协同⼯作。
故障转移时,判断⼀个 master node 是否宕机了,需要⼤部分的哨兵都同意才⾏,涉及到了分布式选举的问题。
即使部分哨兵节点挂掉了,哨兵集群还是能正常⼯作的,因为如果⼀个作为⾼可⽤机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。
哨兵的核⼼知识
哨兵⾄少需要 3 个实例,来保证⾃⼰的健壮性。
哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的⾼可⽤性。
对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和⽣产环境,都进⾏充⾜的测试和演练。
redis 集群模式的⼯作原理能说⼀下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解⼀致性 hash 算法吗?
简介
Redis Cluster是⼀种服务端Sharding技术,3.0版本开始正式提供。Redis Cluster并没有使⽤⼀致性hash,⽽是采⽤slot(槽)的概念,⼀共分成16384个槽。将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执⾏
⽅案说明
1. 通过哈希的⽅式,将数据分⽚,每个节点均分存储⼀定哈希槽(哈希值)区间的数据,默认分配了16384 个槽位
2. 每份数据分⽚会存储在多个互为主从的多节点上
3. 数据写⼊先写主节点,再同步到从节点(⽀持配置为阻塞同步)
4. 同⼀分⽚多个节点间的数据不保持⼀致性
5. 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
6. 扩容时时需要需要把旧节点的数据迁移⼀部分到新节点在 redis cluster 架构下,每个 redis 要放开两个端⼝号,⽐如⼀个是 6379,另外⼀个就是 加1w 的端⼝号,⽐如16379。
16379 端⼝号是⽤来进⾏节点间通信的,也就是 cluster bus 的东⻄,cluster bus 的通信,⽤来进⾏故障检测、配置更新、故障转移授权。cluster bus ⽤了另外⼀种⼆进制的协议, gossip 协议,⽤于节点间进⾏⾼效的数据交换,占⽤更少的⽹络带宽和处理时间。
节点间的内部通信机制
基本通信原理
集群元数据的维护有两种⽅式:集中式、Gossip 协议。redis cluster 节点间采⽤ gossip 协议进⾏通信。
分布式寻址算法
hash 算法(⼤量缓存重建)
⼀致性 hash 算法(⾃动缓存迁移)+ 虚拟节点(⾃动负载均衡)
redis cluster 的 hash slot 算法
优点
⽆中⼼架构,⽀持动态扩容,对业务透明具备Sentinel的监控和⾃动Failover(故障转移)能⼒
客户端不需要连接集群所有节点,连接集群中任何⼀个可⽤节点即可
⾼性能,客户端直连redis服务,免去了proxy代理的损耗
缺点
运维也很复杂,数据迁移需要⼈⼯⼲预
只能使⽤0号数据库
不⽀持批量操作(pipeline管道操作)
分布式逻辑和存储模块耦合等
简介
Redis Sharding是Redis Cluster出来之前,业界普遍使⽤的多Redis实例集群⽅法。其主要思想是采⽤哈希算法将Redis数据的key进⾏散列,通过hash函数,特定的key会映射到特定的Redis节点上。
优点
优势在于⾮常简单,服务端的Redis实例彼此独⽴,相互⽆关联,每个Redis实例像单服务器⼀样运⾏,⾮常容易线性扩展,系统的灵活性很强
缺点
由于sharding处理放到客户端,规模进⼀步扩⼤时给运维带来挑战。
客户端sharding不⽀持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应⽤规模增⼤时,资源浪费制约优化
简介
客户端发送请求到⼀个代理组件,代理解析客户端的数据,并将请求转发⾄正确的节点,最后将结果回复给客户端
特征
透明接⼊,业务程序不⽤关⼼后端Redis实例,切换成本低
Proxy 的逻辑和存储的逻辑是隔离的
代理层多了⼀次转发,性能有所损耗
业界开源⽅案
Twtter开源的Twemproxy
豌⾖荚开源的Codis
单机的 redis,能够承载的 QPS ⼤概就在上万到⼏万不等。对于缓存来说,⼀般都是⽤来⽀撑读⾼并发的。因此架构做成主从(master-slave)架构,⼀主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部⾛从节点。这样也可以很轻松实现⽔平扩容,⽀撑读⾼并发。
redis replication -> 主从架构 -> 读写分离 -> ⽔平扩容⽀撑读⾼并发
redis replication 的核⼼机制redis 采⽤异步⽅式复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确认⾃⼰每次复制的数据量;
⼀个 master node 是可以配置多个 slave node 的;
slave node 也可以连接其他的 slave node;
slave node 做复制的时候,不会 block master node 的正常⼯作;
slave node 在做复制的时候,也不会 block 对⾃⼰的查询操作,它会⽤旧的数据集来提供服务;
但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;
slave node 主要⽤来进⾏横向扩容,做读写分离,扩容的 slave node 可以提⾼读的吞吐量。
注意,如果采⽤了主从架构,那么建议必须开启 master node 的持久化,不建议⽤ slave node 作为 master node的数据热备,因为那样的话,如果你关掉 master 的持久化,可能在 master 宕机重启的时候数据是空的,然后可能⼀经过复制, slave node 的数据也丢了。另外,master 的各种备份⽅案,也需要做。万⼀本地的所有⽂件丢失了,从备份中挑选⼀份 rdb 去恢复 master,这样才能确保启动的时候,是有数据的,即使采⽤了后续讲解的⾼可⽤机制,slave node 可以⾃动接管 master node,但也可能 sentinel 还没检测到 master failure,master node 就⾃动重启了,还是可能导致上⾯所有的 slave node 数据被清空。
redis 主从复制的核⼼原理
当启动⼀个 slave node 的时候,它会发送⼀个 PSYNC 命令给 master node。
如果这是 slave node 初次连接到 master node,那么会触发⼀次 full resynchronization 全量复制。此时master 会启动⼀个后台线程,开始⽣成⼀份 RDB 快照⽂件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。 RDB ⽂件⽣成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写⼊本地磁盘,然后再从本地磁盘加载到内存中,接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。slave node 如果跟 master node 有⽹络故障,断开了连接,会⾃动重连,连接之后 master node 仅会复制给 slave部分缺少的数据。
过程原理
1. 当从库和主库建⽴MS关系后,会向主数据库发送SYNC命令
2. 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来
3. 当快照完成后,主Redis会将快照⽂件和所有缓存的写命令发送给从Redis
4. 从Redis接收到后,会载⼊快照⽂件并且执⾏收到的缓存的命令
5. 之后,主Redis每当接收到写命令时就会将命令发送从Redis,从⽽保证数据的⼀致
缺点
所有的slave节点数据的复制和同步都由master节点来处理,会照成master节点压⼒太⼤,使⽤主从从结构来解决
为了使在部分节点失败或者⼤部分节点⽆法通信的情况下集群仍然可⽤,所以集群使⽤了主从复制模型,每个节点都会有N-1个复制品
redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了⼀个从实例,5 个节点对外提供读写服务,每个节点的读写⾼峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。
机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,⼀般线上⽣产环境,redis的内存尽量不要超过 10g,超过 10g 可能会有问题。
5 台机器对外提供读写,⼀共有 50g 内存。
因为每个主实例都挂了⼀个从实例,所以是⾼可⽤的,任何⼀个主实例宕机,都会⾃动故障迁移,redis 从实例会⾃动变成主实例继续提供读写服务。
你往内存⾥写的是什么数据?每条数据的⼤⼩是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占⽤内存是 20g,仅仅不到总内存的 50%。⽬前⾼峰期每秒就是3500 左右的请求量。
其实⼤型的公司,会有基础架构的 team 负责缓存集群的运维。
Redis集群没有使⽤⼀致性hash,⽽是引⼊了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验
后对16384取模来决定放置哪个槽,集群的每个节点负责⼀部分hash槽。
Redis并不能保证数据的强⼀致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。
异步复制
16384个
Redis集群⽬前⽆法做数据库选择,默认在0数据库。
可以在同⼀个服务器部署多个Redis的实例,并把他们当作不同的服务器来使⽤,在某些时候,⽆论如何⼀个服务器是不够的, 所以,如果你想使⽤多个CPU,你可以考虑⼀下分⽚(shard)。
分区可以让Redis管理更⼤的内存,Redis将可以使⽤所有机器的内存。如果没有分区,你最多只能使⽤⼀台机器的内存。分区使Redis的计算能⼒通过简单地增加计算机得到成倍提升,Redis的⽹络带宽也会随着计算机和⽹卡的增加⽽成倍增⻓。
客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。⼤多数客户端已经实现了客户端分区。
代理分区 意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis和memcached的⼀种代理实现就是Twemproxy查询路由(Query routing) 的意思是客户端随机地请求任意⼀个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了⼀种混合形式的查询路由,但并不是直接将请求从⼀个redis节点转发到另⼀个redis节点,⽽是在客户端的帮助下直接redirected到正确的redis节点。
涉及多个key的操作通常不会被⽀持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使⽤交集指令)。
同时操作多个key,则不能使⽤Redis事务。分区使⽤的粒度是key,不能使⽤⼀个⾮常⻓的排序key存储⼀个数据集(The partitioning granularity is the key, so it is not possible to shard a dataset with a single huge key like a very big sorted set)
当使⽤分区的时候,数据处理会⾮常复杂,例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF⽂件。
分区时动态扩容或缩容可能⾮常复杂。Redis集群在运⾏时增加或者删除Redis节点,能做到最⼤程度对⽤户透明地数据再平衡,但其他⼀些客户端分区或者代理分区⽅法则不⽀持这种特性。然⽽,有⼀种预分⽚的技术也可以较好的解决这个问题。
Redis为单进程单线程模式,采⽤队列模式将并发访问变成串⾏访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使⽤SETNX命令实现分布式锁。
当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则 SETNX 不做任何动作SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
返回值:设置成功,返回 1 。设置失败,返回 0 。
使⽤SETNX完成同步锁的流程及事项如下:
使⽤SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功
为了防⽌获取锁后程序出现异常,导致其他线程/进程调⽤SETNX命令总是返回0⽽进⼊死锁状态,需要为该key设置⼀个“合理”的过期时间释放锁,使⽤DEL命令将锁数据删除
所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对⼀个 key 进⾏操作,但是最后执⾏的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
推荐⼀种⽅案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使⽤分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。⼤致思想为:每个客户端对某个⽅法加锁时,在zookeeper上的与该⽅法对应的指定节点的⽬录下,⽣成⼀个唯⼀的瞬时有序节点。 判断是否获取锁的⽅式很简单,只需要判断有序节点中序号最⼩的⼀个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁⽆法释放,⽽产⽣的死锁问题。完成业务流程后,删除对应的⼦节点释放锁。
既然Redis是如此的轻量(单实例只使⽤1M内存),为防⽌以后的扩容,最好的办法就是⼀开始就启动较多实例。
即便你只有⼀台服务器,你也可以⼀开始就让Redis以分布式的⽅式运⾏,使⽤分区,在同⼀台服务器上启动多个实例。
⼀开始就多设置⼏个Redis实例,例如32或者64个实例,对⼤多数⽤户来说这操作起来可能⽐较麻烦,但是从⻓久来看做这点牺牲是值得的。
这样的话,当你的数据不断增⻓,需要更多的Redis服务器时,你需要做的就是仅仅将Redis实例从⼀台服务迁移到另外⼀台服务器⽽已(⽽不⽤考虑重新分区的问题)。⼀旦你添加了另⼀台服务器,你需要将你⼀半的Redis实例从第⼀台机器迁移到第⼆台机器。
Redis 官⽅站提出了⼀种权威的基于 Redis 实现分布式锁的⽅式名叫 Redlock,此种⽅式⽐原先的单节点的⽅法更
安全。它可以保证以下特性:
1. 安全特性:互斥访问,即永远只有⼀个 client 能拿到锁
2. 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了⽹络分区
3. 容错性:只要⼤部分 Redis 节点存活就可以正常提供服务
缓存雪崩是指缓存同⼀时间⼤⾯积的失效,所以,后⾯的请求都会落到数据库上,造成数据库短时间内承受⼤量请求⽽崩掉。
解决⽅案
1. 缓存数据的过期时间设置随机,防⽌同⼀时间⼤量数据过期现象发⽣。
2. ⼀般并发量不是特别多的时候,使⽤最多的解决⽅案是加锁排队。
3. 给每⼀个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存。
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受⼤量请求⽽崩掉。
解决⽅案
1. 接⼝层增加校验,如⽤户鉴权校验,id做基础校验,id<=0的直接拦截;
2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太⻓会导致正常情况也没法使⽤)。这样可以防⽌攻击⽤户反复⽤同⼀个id暴⼒攻击
3. 采⽤布隆过滤器,将所有可能存在的数据哈希到⼀个⾜够⼤的 bitmap 中,⼀个⼀定不存在的数据会被这个bitmap 拦截掉,从⽽避免了对底层存储系统的查询压⼒
附加
对于空间的利⽤到达了⼀种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。
Bitmap: 典型的就是哈希表
缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,恐怕只能靠牺牲更多的空间、时间来完成了。
布隆过滤器(推荐)
就是引⼊了k(k>1)k(k>1)个相互独⽴的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。
它的优点是空间效率和查询时间都远远超过⼀般的算法,缺点是有⼀定的误识别率和删除困难。
Bloom-Filter算法的核⼼思想就是利⽤多个不同的Hash函数来解决“冲突”。
Hash存在⼀个冲突(碰撞)的问题,⽤同⼀个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引⼊⼏个Hash,如果通过其中的⼀个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。
Bloom-Filter⼀般⽤于在⼤数据量的集合中判定某元素是否存在。
缓存击穿是指缓存中没有但数据库中有的数据(⼀般是缓存时间到期),这时由于并发⽤户特别多,同时读缓存没读到数据,⼜同时去数据库去取数据,引起数据库压⼒瞬间增⼤,造成过⼤压⼒。和缓存雪崩不同的是,缓存击穿指并发查同⼀条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从⽽查数据库。
解决⽅案
1. 设置热点数据永远不过期。
2. 加互斥锁,互斥锁
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在⽤户请求的时候,先查询数据库,然后再将数据缓存的问题!⽤户直接查询事先被预热的缓存数据!
解决⽅案
1. 直接写个缓存刷新⻚⾯,上线时⼿⼯操作⼀下;
2. 数据量不⼤,可以在项⽬启动的时候⾃动进⾏加载;
3. 定时刷新缓存;
11.5 缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或⾮核⼼服务影响到核⼼流程的性能时,仍然需要保证服务还是可⽤的,即使是有损服务。系统可以根据⼀些关键数据进⾏⾃动降级,也可以配置开关实现⼈⼯降级。
缓存降级的最终⽬的是保证核⼼服务可⽤,即使是有损的。⽽且有些服务是⽆法降级的(如加⼊购物⻋、结算)。
在进⾏降级之前要对系统进⾏梳理,看看系统是不是可以丢卒保帅;从⽽梳理出哪些必须誓死保护,哪些可降级;
⽐如可以参考⽇志级别设置预案:
1. ⼀般:⽐如有些服务偶尔因为⽹络抖动或者服务正在上线⽽超时,可以⾃动降级;
2. 警告:有些服务在⼀段时间内成功率有波动(如在95~100%之间),可以⾃动降级或⼈⼯降级,并发送告警;
3. 错误:⽐如可⽤率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最⼤阀值,此时可以根据情况⾃动降级或者⼈⼯降级;
4. 严重错误:⽐如因为特殊原因数据错误了,此时需要紧急⼈⼯降级。
服务降级的⽬的,是为了防⽌Redis服务故障,导致数据库跟着⼀起发⽣雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如⼀个⽐较常⻅的做法就是,Redis出现问题,不去数据库查询,⽽是直接返回默认值给⽤户。
11.6 热点数据和冷数据
热点数据,缓存才有价值
对于冷数据⽽⾔,⼤部分数据可能还没有再次访问到就已经被挤出内存,不仅占⽤内存,⽽且价值不⼤。频繁修改的数据,看情况考虑使⽤缓存对于热点数据,⽐如我们的某IM产品,⽣⽇祝福模块,当天的寿星列表,缓存以后可能读取数⼗万次。再举个例⼦,某导航产品,我们将导航信息,缓存以后可能读取数百万次。
数据更新前⾄少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作⽤就失效了,那就没有太⼤价值了。
那存不存在,修改频率很⾼,但是⼜不得不考虑缓存的场景呢?有!⽐如,这个读取接⼝对数据库的压⼒很⼤,但是⼜是热点数据,这个时候就需要考虑通过缓存⼿段,减少数据库的压⼒,⽐如我们的某助⼿产品的,点赞数,收藏数,分享数等是⾮常典型的热点数据,但是⼜不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库
压⼒。
11.7 缓存热点key
缓存中的⼀个Key(⽐如⼀个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有⼤量的并发请求过来,这些请求发现缓存过期⼀般都会从后端DB加载数据并回设到缓存,这个时候⼤并发的请求可能会瞬间把后端DB压垮。
解决⽅案
对缓存查询加锁,如果KEY不存在,就加锁,然后查DB⼊缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进⼊DB查询
Redis⽀持的Python客户端都有哪些?官⽅推荐⽤哪个?
# redis
# pip3 install redis
from redis import Redis
conn=Redis()
conn.set('name','zs')
conn.close()
import redis
from redis.sentinel import Sentinel
# 连接哨兵服务器(主机名也可以⽤域名)
# 10.0.0.101:26379
sentinel = Sentinel([('10.0.0.101', 26379),
('10.0.0.101', 26378),
('10.0.0.101', 26377)
],
socket_timeout=5)
print(sentinel)
# 获取主服务器地址
master = sentinel.discover_master('mymaster')
print(master)
# 获取从服务器地址
slave = sentinel.discover_slaves('mymaster')
print(slave)
##### 读写分离
# 获取主服务器进⾏写⼊
# master = sentinel.master_for('mymaster', socket_timeout=0.5)
# w_ret = master.set('foo', 'bar')
# slave = sentinel.slave_for('mymaster', socket_timeout=0.5)
# r_ret = slave.get('foo')
# print(r_ret)
# rediscluster
# pip3 install redis-py-cluster
from rediscluster import RedisCluster
startup_nodes = [{"host":"127.0.0.1", "port": "7000"},{"host":"127.0.0.1", "port":
"7001"},{"host":"127.0.0.1", "port": "7002"}]
# rc = RedisCluster(startup_nodes=startup_nodes,decode_responses=True)
rc = RedisCluster(startup_nodes=startup_nodes)
rc.set("foo", "bar")
print(rc.get("foo"))
两者都是⾮关系型内存键值数据库,现在公司⼀般都是⽤ Redis 来实现缓存,⽽且 Redis ⾃身也越来越强⼤了!
你只要⽤缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就⼀定会有数据⼀致性的问题,那么你如何解决⼀致性问题?
⼀般来说,就是如果你的系统不是严格要求缓存+数据库必须⼀致性的话,缓存可以稍微的跟数据库偶尔有不⼀致的情况,最好不要做这个⽅案,读请求和写请求串⾏化,串到⼀个内存队列⾥去,这样就可以保证⼀定不会出现不⼀致的情况串⾏化之后,就会导致系统的吞吐量会⼤幅度的降低,⽤⽐正常情况下多⼏倍的机器去⽀撑线上的⼀个请求。
还有⼀种⽅式就是可能会暂时产⽣不⼀致的情况,但是发⽣的⼏率特别⼩,就是先更新数据库,然后再删除缓存。
1. Master最好不要做任何持久化⼯作,包括内存快照和AOF⽇志⽂件,特别是不要启⽤内存快照做持久化。
2. 如果数据⽐较关键,某个Slave开启AOF备份数据,策略为每秒同步⼀次。
3. 为了主从复制的速度和连接的稳定性,Slave和Master最好在同⼀个局域⽹内。
4. 尽量避免在压⼒较⼤的主库上增加从库
5. Master调⽤BGREWRITEAOF重写AOF⽂件,AOF在重写的时候会占⼤量的CPU和内存资源,导致服务load过⾼,出现短暂服务暂停现象。
6. 为了Master的稳定性,主从复制不要⽤图状结构,⽤单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也⽅便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以⽴⻢启⽤Slave1做Master,其他不变。
因为⽬前Linux版本已经相当稳定,⽽且⽤户量很⼤,⽆需开发windows版本,反⽽会带来兼容性等问题。但是有专⻔的团队维护windows版本,最新只到3.x的版本
512M
Redis2.6开始redis-cli⽀持⼀种新的被称之为pipe mode的新模式⽤于执⾏⼤量数据插⼊⼯作。
使⽤keys指令可以扫出指定模式的key列表。
对⽅接着追问:如果这个redis正在给线上的业务提供服务,那使⽤keys指令会有什么问题?
这个时候你要回答redis关键的⼀个特性:redis的单线程的。keys指令会导致线程阻塞⼀段时间,线上服务会停顿,直到指令执⾏完毕,服务才能恢复。这个时候可以使⽤scan指令,scan指令可以⽆阻塞的提取出指定模式的key列表,但是会有⼀定的重复概率,在客户端做⼀次去重就可以了,但是整体所花费的时间会⽐直接⽤keys指令⻓。
使⽤list类型保存数据信息,rpush⽣产消息,lpop消费消息,当lpop没有消息,
可以sleep⼀段时间,然后再检查有没有信息,如果不想sleep的话,可以使⽤blpop,
在没有信息的时候,会⼀直阻塞,直到信息的到来。redis可以通过pub/sub主题订阅模式
实现⼀个⽣产者,多个消费者,当然也存在⼀定的缺点,当消费者下线时,⽣产的消息会丢失。
使⽤sortedset,使⽤时间戳做score, 消息内容作为key,调⽤zadd来⽣产消息消费者使⽤zrangbyscore获取n秒之前的数据做轮询处理。
1. ⼀个客户端运⾏了新的命令,添加了新的数据。
2. Redis检查内存使⽤情况,如果⼤于maxmemory的限制, 则根据设定好的策略进⾏回收。
3. ⼀个新的命令被执⾏,等等。
4. 所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。
如果⼀个命令的结果导致⼤量内存被使⽤(例如很⼤的集合的交集保存到⼀个新的键)不⽤多久内存限制就会被这个内存使⽤量超越。
LRU算法