Redis简介
Redis是一个开源的key-value存储系统,它通常被称为一个数据结构服务器,因为keys可以包含strings,hashes,lists,sets和sorted sets。
你可以原子的操作这些类型,比如添加一个字符串,在hash中递增一个值,向list加入数据,计算set的交集,并集和差集,在sorted set中取得高范围的成员。
为了取得突出的性能,Redis数据集在内存中工作。如果想持久化保存数据可以时常转储数据集到磁盘上,或者添加每一条命令到日志。
Redis支持主从复制,第一次同步的时候是非阻塞的并且非常快,网络断开的时候自动重连接。
其他的功能包括简单的check-and-set机制,pub/sub和配置设置让Redis看起来像一个cache。
Redis是单线程服务器,不能从多CPU中获益。
Redis支持多种语言,本身是用ANSI C编写的,主要支持Linux系统。
Redis常被用来作为数据库,缓存,或者构建消息系统和队列系统。
Redis的特性
1. Pipelining
Redis是一个cs模式的tcp server,使用和http类似的请求响应协议。一个client可以通过一个socket连接发起多个请求命令。每个请求命令发出后client通常 会阻塞并等待redis服务处理,redis处理完后请求命令后会将结果通过响应报文返回给client。
Redis支持Pipelining,可以一次发送多个命令不用等待回复,最后一次读取所有的回复。
当客户端用Pipelining发送命令时,服务器端会在内存中使用队列存储所有的回复。如果客户端发送大量的命令,最好一次发送合理数量的命令,比如一次发送10k,然后读取回复,再发送另外的10k命令。这样可以保证每一次的速度大致相同,但是额外的内存用来存储保存着回复的队列。Pipelining不能使用的情形是在客户端调用写命令前先回复读取命令。
使用Redis scripting的一些案例使用了Pipelining有效的解决了使用脚本时在服务器端需要做的大量工作,可以用最小的延迟读取和写数据。
2. Pub/Sub
发布/订阅(Publish/subscribe 或pub/sub)是一种消息范式,消息的发送者(发布者)不是计划发送其消息给特定的接收者(订阅者)。而是发布的消息分为不同的类别,而不需要知道什么样的订阅者订阅。订阅者对一个或多个类别表达兴趣,于是只接收感兴趣的消息,而不需要知道什么样的发布者发布的消息。这种发布者和订阅者的解耦解耦可以允许更好的可扩放性和更为动态的网络拓扑。
Redis pub/sub的特性:
- 其他客户端发送到channel的消息会被Redis推向所有的订阅客户端
- 订阅了一个或多个channel的客户端不能发布命令,尽管它可以订阅和退订其它的channel。
- SUBSCRIBE和UNSUBSCRIBE的回复是以message的形式发送的,客户端只能读取message的类型。
message的格式:
message由3个元素组成的Multi-bulk回复。
第一个元素是message的类型:
- subscribe:表示我们成功的订阅了由第二个元素命名的channel,第三个元素表示当前订阅channel的数量
- unsubscribe:表示我们成功的退订了由第二个元素命名的channel,第三个元素表示当前订阅channel的数量。当最后一个元素为0时,表示不再订阅任何channel,客户端可以发布任何Redis命令了,就像不再处于 Pub/Sub 状态。
- message:作为其它客户端发布PUBLISH命令后收到的消息结果。第二个元素表示源channel,第三个元素表示实际消息内容。
支持模式匹配
这时返回的message的格式与前面的不同。是pmessage:第二个元素是原始模式匹配,第三个元素是源channel,最后一个元素是消息内容。
因为所有接收到的message包含源订阅导致客户端使用hash table绑定源订阅作为回调来传递消息。
通过传递消息到注册回调可以实现接受到的消息查找为O(1)。
3. 内存优化
Redis实现内存优化的措施:
1. 对小聚合数据类型的特殊编码
许多数据类型在一定规模内能被优化使用较少的空间。hash,list,整数构成的set,sorted set,当小于给定元素数量和达到一个最大元素的大小时,会被一种更节约内存的方式编码。
如果一个特殊编码的值超过配置的最大大小,Redis会自动的把它转换为正常的编码。转换在较小的值之间速度会很快,如果要对更大的聚合类型使用特殊编码值需要一些基准测试并测试转换所花费的时间,根据这些测试结果来判断是否可行。
2. 可以将Redis编译为32位,这样会让每个key少用一些内存,因为指针变小了,但是这样一来Redis实列可以使用的最大内存只能达到4GB。Redis用32位编译,RDB和AOF文件兼容32位和64位,可以在32位和64位之间互相转换。
3. 字节和位操作
GETRANGE, SETRANGE, GETBIT, SETBIT.这些命令可以把Redis string类型当成随机数组访问
4. 使用hash
小的hash能被编码为使用很小的空间,所以可以尽可能的使用hash表示你的数据。使用Redis保存string类型的简单key-value模型,比光使用Redis保存许多简单key和使用memcached来保存更能更有效的利用内存。多个key会比一个包含有许多field的hash的key占用更多的内存。为了保证查找在一个常数时间内,当hash很小的时候,可以将它编码为O(N)的数据结构,比如线性数组(在CPU cache中有很好的效果)。但当hash包含的元素数量增长太多的时候会转换为真正的hash表。HSET和HGET的时间一直为O(1)。
但缺陷是hash的字段不能使用Redis对象,也没有一个关联时间表现为一个真正的key,只能使用string类型。
hash最多可以包含100个字段
每当hash的元素数量或者元素大小超过指定的元素数量和元素大小时,会被转换为真正的hash表,这样节约的内存也会被丢弃掉。
4. Expire key
Redis在key上设置一个超时时间,当时间过期后,key会被自动的删除。
超时时间只会在key被DEL移除时或者使用SET、GETSET重写时清除。使用PERSIST命令将key变为持久的key时,超时时间也会被清除。当使用RENAME重命名key时,超时时间被转移给新命名的key。使用RENAME重写key时,新的key会继承原来key的所有特性。
可以对一个已经带有生存时间的 key 执行 EXPIRE 命令,新指定的生存时间会取代旧的生存时间
key的expire信息是以绝对的Unix timestamps存储的。
Redis检测过期key的方式可以分为主动和被动。
- 主动方式是客户端试图访问一些key,然后发现key过期了。
- 被动方式是Redis定期的随机测试一些key的过期时间,删除过期key。直到过期的key数量低于25%,也就是说任何给定数量的过期key的最大使用内存等于每秒写操作的最大数量除以4.
当key过期时,为了不牺牲一致性而获得正确的行为,DEL操作会被AOF文件和所有从中获益的附属的slave合成。过期进程操作会集中在master实列中进行。
当slave连接到master的不是独立过期key,slave仍然会取走数据集存在的所有过期状态,因此当一个slave升级为master,它能够独立使key到期,完全充当master。
5. Transactions
MULTI, EXEC, DISCARD 和 WATCH是Redis中事务的基本操作。可以在一个步操作中执行一组命令,一个事务中的命令都是序列化和顺序执行的,事务执行是原子性的。因此,在事务的执行过程中不会为另一个客户端发起的请求提供服务。当AOF开启的时候,每个属于某个Redis事务的命令只要操作完成了的话都会记录在AOF中。
Redis事务的基本操作命令:
- 使用 MULTI 命令进入事务,将要执行的命令顺序的放入队列中。
- 使用EXEC命令执行队列中的命令。
- DISCARD命令取消执行队列中命令并退出事务。
事务中产生的错误:
1. 在EXEC被执行前产生的错误是一个命令被放入队列中失败。比如语法错误或者一些临界条件比如超出了内存限制。
2. 在执行EXEC后产生的错误,比如对一个key所对应的值执行错误的操作。
客户端可以通过检查队列命令的返回值发现第一种错误,如果返回值是QUEUED则说明队列操作正确,否则返回错误。发现错误后,客户端会终止事务。
而在服务器端会记住有一个错误,在使用EXEC时拒绝执行事务,并且原子的退出事务。
发生在EXEC后的错误没有特殊的处理方式,因为所有事务中的命令都会被执行即使一些命令执行失败。
Redis的事务不支持回滚,原因是:
1. 错误的产生只会是语法错误或者对key的值执行错误的操作,这些都是编程错误,可以在生产中发现。
2. 这也是Redis简单和快速的一个原因。
Optimistic locking using check-and-set
WATCH对Redis事务提供check-and-set(CAS)行为。
可以监视一个或多个key,如果在执行EXEC之前这些key发生变化,事务会被终止,并且EXEC命令返回 Null multi-bulk通知事务失败。
Redis为了预防冲突提供了乐观锁定。
乐观锁定使得执行EXEC命令受到了约束,我们要求Redis只在没有别的客户端修改任何一个被监视的键时执行事务。否则,根本就不会开始事务。(如果你监视(WATCH)一个瞬时的键,然后Redis在你监视了这个键后,将这个键过期(expire)了,EXEC仍将继续生效)
Redis script被定义为事务。
当执行完EXEC后,所有的key都变为UNWATCH。
6. Redis的大量插入
Redis使用redis-cli --pipe执行大量插入。普通的执行插入方式是发送一条命令给server并返回后在发送另一条命令,这会花费大量的来回时间。也可以使用pipelining,但是你在插入大量数据的时候,写新命令的同时读取回复也需要同样的快速。只有少部分的客户端支持无阻塞IO,也不是所有的客户端能够用最有效的方式分析回复。
大量插入的工作方式:
1. redis-cli --pipe快速的发送数据到server
2. 同时读取有用的数据,并分析。
3. 当从stdin中没有数据读取的时候,发送一个特殊的ECHO命令并含有20字节的随机字符串。说明这是发送的最后一个命令,Redis能够匹配检测回复通过检查是否收到了同样的20字节的字符串。
4. 当最后特殊的命令发送后,客户端收到回复后开始匹配20字节的字符串。如果匹配就表明成功。
7. 分区
Redis支持分区,将数据分到多个Redis实列,分区的好处:
1. 可以使用更大数据库,使用多个电脑的内存之和。
2. 扩展计算能力到多核和多台计算机上,也可以分享带宽到多台计算机和网络适配器。
分区的方法:
1. 范围分区,映射一定范围的对象到指定的Redis实例。缺点是需要管理一张表来映射对象,并且包含我们需要的所有对象类型
2. hash分区,使用hash函数将key的名字转换为数字,并存储到实例。
3. 一致性hash分区,普通hash的改进。
实现不同的分区:
1. 客户端分区,客户端直接选择正确的节点实现读写给定的key。
2. 代理辅助分区,客户端发送请求给代理(可以正确识别Redis通信协议),代理确保将请求传递给正确的Redis实例并发送回复消息给客户端。Redis和Memcached代理Twemproxy实现了代理辅助分区。
3. 查询路由,发送查询到一个随机产生的实例,这个实例将查询发送到正确的节点。Redis集群在客户端的帮助下实现一种高效的查询路由。
分区的缺点:
1. 不支持涉及多个key操作。
2. 不能使用涉及多个key的Redis事务
3. 分区的粒度是key,不可能有一个很大的key数据,就像一个非常大的排序集合。
4. 使用分区时,数据的处理非常复杂。比如需要处理多个RDB/AOF文件,为了备份数据,需要聚集多个实例和主机存留的文件。
5. 增加和移除容量很复杂。
存储数据还是作为缓存
- Redis作为存储数据时,需要确定给定的key总是被映射到同一个实例。
- Redis作为缓存时,给定的节点如果失效不会是问题,我们可以使用其它的节点。改变key-instance的映射是为了增强系统的可用性。
当选择的节点对于给定的key失效时,一致性hash用来切换节点。如果你增加一个新节点,部分新key会被存储在新节点上。
1. 如果Redis作为cache,使用一致性hash扩大或减少节点很容易。
2. 如果Redis作为存储,需要确定key和节点间的映射,并且节点的数量也是固定的。否则需要一个系统来当我们增加和删除节点时,平衡key和节点,目前只有Redis集群可以实现这个功能,但Redis集群还没有实现。