Redis
[TOC]
基础篇
数据类型
- String (字符串)
- List (列表)
- Hash (字典)
- Set (集合)
- ZSet (有序集合)
String(字符串)
- String 类型是二进制安全的,意思是 Redis 的 string 可以包含任何数据,比如图片或者序列化的对象;
- 一个 redis 中字符串 value 最多可以是 512M;
- 特殊的Key-Value操作即INCR/DECR,可以利用Redis自动帮助我们对一个Key对应的Value进行加减;
List(列表)
- 简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部(左边)或者尾部(右边),它的底层实际上是个链表;
Hash(字典)
- hash 是一个键值对集合,是一个 string 类型的 key和 value 的映射表,key 还是key,但是value是一个键值对(key-value)。类比于 Java里面Map<String,Map<String,Object>> 集合;
- 哈希对象的编码可以是 zipList 或者 hashTable;
- 当使用ziplist,也就是压缩列表作为底层实现时,新增的键值对是保存到压缩列表的表尾;
- 当同时满足下面两个条件时,使用zipList(压缩列表)编码:
- 列表保存元素个数小于512个;
- 每个元素长度小于64字节;
-
不能满足这两个条件的时候使用 hashTable 编码。第一个条件可以通过配置文件中的 set-max-intset-entries 进行修改;
Set(集合)
- 集合对象 set 是 string 类型(整数也会转换成string类型进行存储)的无序集合;
- 意集合和列表的区别:
- 集合中的元素是无序的,因此不能通过索引来操作元素;
- 集合中的元素不能有重复;
- 集合对象的编码可以是 intSet 或者 hashTable;
- intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中;
- hashtable 编码的集合对象使用 字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值则全部设置为 null。这里可以类比Java集合中HashSet 集合的实现,HashSet 集合是由 HashMap 来实现的,集合中的元素就是 HashMap 的key,而 HashMap 的值都设为 null;
ZSet(有序集合)
- 有序集合对象是有序的;
- 与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据;
- 有序集合的编码可以是 ziplist 或者 skiplist:
-
ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。
skiplist 编码的有序集合对象使用 zet 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表:
字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值。
两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费;
-
typedef struct zset {
//跳跃表
zskiplist *zsl;
//字典
dict *dice;
} zset;
其实有序集合单独使用字典或跳跃表其中一种数据结构都可以实现,但是这里使用两种数据结构组合起来,原因是假如我们单独使用 字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是查找操作有 O(1)的复杂度变为了O(logN)。因此Redis使用了两种数据结构来共同实现有序集合;
-
编码转换,当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:
- 保存的元素数量小于128;
- 保存的所有元素长度都小于64字节;
- 不能满足上面两个条件的使用 skiplist 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改;
五大数据类型的应用场景
- 对于string 数据类型,因为string 类型是二进制安全的,可以用来存放图片,视频等内容,另外由于Redis的高性能读写功能,而string类型的value也可以是数字,可以用作计数器(INCR,DECR),比如分布式环境中统计系统的在线人数,秒杀等;
- 对于 hash 数据类型,value 存放的是键值对,比如可以做单点登录存放用户信息;
- 对于 list 数据类型,可以实现简单的消息队列,另外可以利用lrange命令,做基于redis的分页功能;
- 对于 set 数据类型,由于底层是字典实现的,查找元素特别快,另外set 数据类型不允许重复,利用这两个特性我们可以进行全局去重,比如在用户注册模块,判断用户名是否注册;另外就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能;
- 对于 zset 数据类型,有序的集合,可以做范围查找,排行榜应用,取TopN操作等;
进阶篇
内存回收和内存共享
内存回收
- Redis 的每个对象都是由 redisObject 结构表示;
typedef struct redisObject{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层数据结构的指针
void *ptr;
//引用计数
int refcount;
//记录最后一次被程序访问的时间
unsigned lru:22;
} robj
- 创建一个新对象,属性 refcount 初始化为1;
- C 语言不具备自动回收内存功能,那么该如何回收内存呢?于是 Redis自己构建了一个内存回收机制,通过在 redisObject 结构中的 refcount 属性实现。这个属性会随着对象的使用状态而不断变化;
- 创建一个新对象,属性 refcount 初始化为1;
- 对象被一个新程序使用,属性refcount加1;
- 对象不再被一个程序使用,属性refcount减1;
-
当对象的引用计数值变为 0 时,对象所占用的内存就会被释放;
内存共享
refcount 属性除了能实现内存回收以外,还能用于内存共享;
-
比如通过如下命令 set k1 100,创建一个键为 k1,值为100的字符串对象,接着通过如下命令 set k2 100 ,创建一个键为 k2,值为100 的字符串对象
- 将数据库键的值指针指向一个现有值的对象;
- 将被共享的值对象引用refcount 加 1:
Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为O(1);对于普通字符串,判断复杂度为O(n);而对于哈希、列表、集合和有序集合,判断的复杂度为O(n^2);
共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用);
一致性Hash
Redis效率
Redis 为什么这么快
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
- 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
- 响应队列:Redis 同样也会为每个客户端套接字关联一个响应队列。Redis 服务器通过响应队列来将指令的返回结果回复给客户端。 如果队列为空,那么意味着连接暂时处于空闲状态,不需要去获取写事件,也就是可以将当前的客户端描述符从write_fds里面移出来。等到队列有数据了,再将描述符放进去。避免select系统调用立即返回写事件,结果发现没什么数据可以写。出这种情况的线程会飙高 CPU;
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- 使用多路I/O复用模型,非阻塞IO;
- 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
单进程多线程模型
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
多路 I/O 复用模型
-
Redis 是跑在单线程中的,所有的操作都是按照顺序线性执行的,但是由于读写操作等待用户输入或输出都是阻塞的,所以 I/O 操作在一般情况下往往不能直接返回,这会导致某一文件的 I/O 阻塞导致整个进程无法对其它客户提供服务,而 I/O 多路复用就是为了解决这个问题而出现的;
-
redis的io模型主要是基于epoll实现的,不过它也提供了 select和kqueue的实现,默认采用epoll;
- epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大 ,具体数目可以 cat /proc/sys/fs/file-max 察看;
- 效率提升, Epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关,因此在实际的网络环境中, Epoll 的效率就会远远高于 select 和 poll;
- 内存拷贝, Epoll 在这点上使用了“共享内存 ”,这个内存拷贝也省略了
多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作;
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量;
-
概括:
- 大量请求进入Redis,(读,写操作);
- 分析大量的请求并分类(按读写类型);
- 监察多个流的 I/O 事件的能力(准备读,准备写,准备连接);
- 把所有的请求根据读写类型分发至各个I/O流线程;
- 批量请求,并等待所有请求完成后返回;
- 每个用户请求获取对应的请求返回数据;
- 将所有的用户请求汇聚在一起并根据请求类型所需I/O连接分类,一个I/Oq请求批量批量执行多个用户请求(减少网络 IO 的时间消耗),达到多线程的效果;
-
扩展
数据存储结构
部署方式
单机模式
- 单机版三个问题
- 内存容量有限;
- 处理能力有限;
- 无法高可用;
多机版模式
-
特性
- 复制(Replication);
- 哨兵 (Sentinel);
- 集群 (Cluster);
-
Redis多机版部署特性功能
- 复制:扩展系统对于读的能力;
- 哨兵:为服务器提供高可用特性,减少故障停机出现;
- 集群:扩展内存容量,增加机器,提高性能读写能力和存储以及提高可用特性;
复制模式
Redis的复制(replication)功能允许用户根据一个Redis 服务器来创建任意多个该服务器的复制品,其中被复制的服务器为主服务器(master),而通过复制创建出来的服务器复制品,则为从服务器(slave),master负责读写,slave一般只负责读。只要主从服务器之间的网络连接正常,主从服务器两者会具有相同的数据,主服务器就会一直将发生在自己身上的数据更新同步给从服务器,从而一直保证主从服务器的数据相同。
- 特点:
- master/slave 角色;
- master/slave 数据相同;
- 降低master读的压力,在转交从库
- 问题
- 无法保证高可用;
- 没有解决master写的压力
哨兵模式
Redis sentinel 是一个分布式系统中监控redis主从服务器,并在主服务器下线时自动进行故障转移,其中三个特性:
监控(Monitoring):Sentinel 会不断的检查你的主服务器和从服务器是否运作正常;
提醒(Notification):当被监控的某个Redis服务器出现问题时,Sentinel可以通过API向管理员或者其他应用程序发送通知;
自动故障迁移(Automatic failover):当一个主服务器不能正常工作时,Sentinel会开始一次自动故障前一操作;
- 特点:
- 保证高可用;
- 监控各个节点;
- 自动故障迁移
- 缺点:
- 主从模式,切换需要时间丢数据;
- 没有解决master写的压力
集群proxy模式
TwemproxyTwemproxy 是一个Twitter开源的一个redis和memcache 快速/轻量级代理服务器。Twemproxy是一个快速的单线程代理程序,支持Memcached ASCII协议和redis协议。
- 特点:
- 多种hash算法:MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkis;
- 支持失败节点自动删除;
- 后端Sharding分片逻辑对业务透明,业务方的读写方式和操作单个Redis一致;
- 缺点:
- 增加了新的proxy,需要维护其高可用;
- failover逻辑需要自己实现,其本身不能支持故障的自动转移;
- 可扩展性差,进行扩缩容都需要手动干预;
直连版(三主三从)(cluster)单节点模式
- 特点:
- 无中心架构(不存在哪个节点影响性能瓶颈),少了proxy层;
- 数据按照slot存储分布在多个节点,节点时间数据共享,可动态调用整数分布;
- 可扩展性,可线性扩展到1000个节点,节点可动态添加或删除;
- 高可用性,部分节点不可用时,集群仍可用,通过增加Slave做备份数据副本;
- 实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升;
- 缺点:
- 资源隔离性较差,容易出现相互影响的情况;
- 数据通过异步复制,不保证数据的强一致性;