欢迎来到DIVCSS5查找CSS资料与学习DIV CSS布局技术!
  从此Redis是路人
 
  序言:Redis(RemoteDIctionaryServer)作为一个开源/C实现/高性能/基于内存的key-value存储系统,相信做Java的小伙伴都不会陌生。Redis常用于缓存、分布式锁、队列(或有序集合)等场景,追求技术的小伙伴们肯定不只满足于Redis的使用上,肯定也想了解Redis背后的设计思想及对应的开发实践,话不多少,上车吧~
 
  ps:文章较长,小伙伴可以搬个小板凳慢慢阅读,也可以结合自身情况进行选择阅读:)
 
  由于文章内容较多,下面就按照Redis对象、事件处理机制、持久化机制、事务机制、主备模型、集群机制等内容进行分析讨论,中间可能穿插着相关内容的扩展和思考。Let'sgo....
 
  Redis对象
 
  Redis主要有5种不同类型的对象,分别是字符串、列表、哈希表、集合、有序集合。这些对象都是基于Redis基础数据结构来构建的,并且每种对象都用到了至少一种基础数据结构。Redis对象还实现了引用计数的内存回收技术,当不再使用某个对象时,可以及时释放其内存;通过引用计数实现了对象共享机制,节约内存(Redis只对包含整数值的字符串对象进行共享);Redis的对象带有访问时间戳,可用于计算该对象空转时间,启用maxmemroy功能时,空转时间较长的键优先被删除。
 
  Redis用到的底层数据结构有:简单动态字符串、双端链表、字典、压缩列表、整数集合、跳跃表等,Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些基础数据结构创建了一个对象系统,这写对象包括字符串对象、列表对象、哈希对象、集合对象和有序集合对象等。
 
  Redis中使用对象表示键和值,当新建一个键值对时,Redis至少创建2个对象,一个是键对象,另一个是值对象。
 
  typedefstructredisObject{
 
  unsignedtype:4;
 
  unsignedencoding:4;
 
  unsignedlru:REDIS_LRU_BITS;/*lrutime(relativetoserver.lruclock)*/
 
  intrefcount;
 
  void*ptr;
 
  }robj;
 
  type表示对象类型(有REDIS_STRING/REDIS_LIST/REDIS_HASH/REDIS_SET/REDIS_ZSET几种),对于Redis键值对来说,键永远都是字符串,值可以是字符串、列表、哈希表、集合、有序集合中的一种。encoding表示对象编码,也就是该对象使用什么底层数据结构实现。ptr指向对象的底层数据结构。
 
  Redis对象:
 
  字符串:字符串对象底层可以是int和raw编码方式,如setnum1后执行appendnumhello,则会导致编码方式由int到raw转换。
 
  127.0.0.1:6379>setnum1
 
  OK
 
  127.0.0.1:6379>objectencodingnum
 
  "int"
 
  127.0.0.1:6379>appendnumhello
 
  (integer)6
 
  127.0.0.1:6379>objectencodingnum
 
  "raw"
 
  列表:列表对象的编码可以是ziplist、linkedlist和quicklist(新版本Redis使用quicklist作为列表底层数据结构了)。ziplist使用功能压缩列表作为底层实现,每个压缩列表节点保存一个列表元素。
 
  127.0.0.1:6379>rpushnums1"tow"3
 
  127.0.0.1:6379>objectencodingnums
 
  "quicklist"
 
  集合:set容器,集合对象的编码可以是intset和hashtable。intset编码的集合对象使用整数集合作为底层实现,所有元素都保存在整数集合中。另一方面,使用hashtable的集合对象使用字典作为底层实现,字典中每个键都是一个字符串对象,即一个集合元素,而字典的值都是NULL。
 
  127.0.0.1:6379>saddmans1
 
  127.0.0.1:6379>saddmans2
 
  127.0.0.1:6379>objectencodingmans
 
  "intset"
 
  127.0.0.1:6379>saddmansluoxn28
 
  127.0.0.1:6379>objectencodingmans
 
  "hashtable"
 
  有序集合:有序集合对象的编码可以是ziplist和skiplist。ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨着的压缩列表节点保存,第一个保存集合元素,第二个保存集合元素对应的分值。压缩列表内集合元素按照分值大小进行排序,分值较小的在前,分值大的在后;skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表,通过字典提高获取单个元素效率,通过skiplist提高获取范围查询能力,二者各取所长。
 
  哈希表:哈希对象的编码可以是ziplist和hashtable。hashtable编码的哈希对象使用字典作为底层实现,则哈希对象中的每个键值对都是字典键值对来保存,hashtable为数组+链表的分离连接法实现。
 
  事件处理机制
 
  Redis的事件类型分为时间事件和文件事件,文件事件也就是IO事件。时间事件的处理是在epoll_wait返回处理文件事件后处理的,每次epoll_wait的超时时间都是Redis最近的一个定时器时间。Redis对epoll进行了简单封装,不像memcached直接使用libevent作为网络通信组件。
 
  Redis在进行事件处理前,首先会进行初始化,初始化的主要逻辑在main/initServer函数中。初始化流程主要做的工作如下:
 
  设置回调函数;
 
  创建事件循环机制,即调用epoll_create;
 
  创建服务监听端口,创建定时事件,并将这些事件添加到事件机制中。
 
  voidinitServer(void){
 
  //设置信号对应的处理函数
 
  signal(SIGHUP,SIG_IGN);
 
  signal(SIGPIPE,SIG_IGN);
 
  setupSignalHandlers();
 
  ...
 
  createSharedObjects();
 
  adjustOpenFilesLimit();
 
  //创建事件循环机制,及调用epoll_create创建epollfd用于事件监听
 
  server.el=aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
 
  server.db=zmalloc(sizeof(redisDb)*server.dbnum);
 
  /*OpentheTCPlisteningsocketfortheusercommands.*/
 
  //创建监听服务端口,socket/bind/listen
 
  if(server.port!=0&&
 
  listenToPort(server.port,server.ipfd,&server.ipfd_count)==C_ERR)
 
  exit(1);
 
  ...
 
  /*CreatetheRedisdatabases,andinitializeotherinternalstate.*/
 
  for(j=0;j<server.dbnum;j++){
 
  server.db[j].dict=dictCreate(&dbDictType,NULL);
 
  server.db[j].expires=dictCreate(&keyptrDictType,NULL);
 
  server.db[j].blocking_keys=dictCreate(&keylistDictType,NULL);
 
  server.db[j].ready_keys=dictCreate(&setDictType,NULL);
 
  server.db[j].watched_keys=dictCreate(&keylistDictType,NULL);
 
  server.db[j].eviction_pool=evictionPoolAlloc();
 
  server.db[j].id=j;
 
  server.db[j].avg_ttl=0;
 
  }
 
  ...
 
  /*CreatetheserverCron()timeevent,that'sourmainwaytoprocess
 
  *backgroundoperations.创建定时事件*/
 
  if(aeCreateTimeEvent(server.el,1,serverCron,NULL,NULL)==AE_ERR){
 
  serverPanic("Can'tcreatetheserverCrontimeevent.");
 
  exit(1);
 
  }
 
  /*CreateaneventhandlerforacceptingnewconnectionsinTCPandUnix
 
  *domainsockets.*/
 
  for(j=0;j<server.ipfd_count;j++){
 
  if(aeCreateFileEvent(server.el,server.ipfd[j],AE_READABLE,
 
  acceptTcpHandler,NULL)==AE_ERR)
 
  {
 
  serverPanic("Unrecoverableerrorcreatingserver.ipfdfileevent.");
 
  }
 
  }
 
  //将事件加入到事件机制中,调用链为aeCreateFileEvent/aeApiAddEvent/epoll_ctl
 
  if(server.sofd>0&&aeCreateFileEvent(server.el,server.sofd,AE_READABLE,
 
  acceptUnixHandler,NULL)==AE_ERR)
 
  /*OpentheAOFfileifneeded.*/
 
  if(server.aof_state==AOF_ON){
 
  server.aof_fd=open(server.aof_filename,
 
  O_WRONLY|O_APPEND|O_CREAT,0644);
 
  if(server.aof_fd==-1){
 
  exit(1);
 
  }
 
  }
 
  ...
 
  }
 
  IO事件的处理:
 
  Redis监听端口的事件回调函数链是:acceptTcpHandler/acceptCommonHandler/createClient/aeCreateFileEvent/aeApiAddEvent/epoll_ctl。在Reids监听事件处理流程中,会将客户端的连接fd添加到事件机制中,并设置其回调函数为readQueryFromClient,该函数负责处理客户端的命令请求。
 
  命令处理流程:
 
  命令处理流程链是:readQueryFromClient/processInputBuffer/processCommand/call/对应命令的回调函数(c->cmd->proc),比如getkey命令的处理回调函数为getCommand。getCommand的执行流程是先到client对应的数据库字典中根据key来查找数据,然后根据响应消息格式将查询结果填充到响应消息中。
 
  定时事件的处理:
 
  Redis的定时时间是通过堆来管理的,按照延时时间作为优先级排序,当进行epoll_wait时,选择堆中最小延时时间作为epoll_wait的timeout:
 
  typedefstructaeTimeEvent{
 
  longlongid;/*timeeventidentifier.*/
 
  longwhen_sec;/*seconds*/
 
  longwhen_ms;/*milliseconds*/
 
  aeTimeProc*timeProc;
 
  aeEventFinalizerProc*finalizerProc;
 
  void*clientData;
 
  structaeTimeEvent*next;
 
  }aeTimeEvent;
 
  注意:定时事件是在IO事件处理完成之后才进行的,这样保证了优先响应client端的请求操作。
 
  持久化机制
 
  Redis作为内存数据库,它将自己的数据库状态保存在内存中,试想一下,如果此时服务器突然崩溃,那么数据库的状态就无法恢复了。所以,Reids提供了持久化机制,将Redis数据库状态持久化到磁盘,Redis的持久化机制分为2种:RDB和AOF。
 
  RDB
 
  RDB持久化既可以手动执行,也可以定期执行,该功能就是将某个时间点的数据库状态保存到一个RDB文件。有2个命令用于生成RDB文件,save和bgsave命令。二者不同的是,前者会阻塞Redis服务器进程,直到RDB文件创建完成为止;后者会fork一个子进程,然后由子进程来负责创建RDB文件,而父进程可以继续处理请求命令。
 
  和使用save和bgsave命令创建RDB文件不同,RDB文件的启动是在Redis服务器启动时自动执行的,所以Redis未提供用于加载RDB文件的命令。注意:因为AOF文件的更新频率通常比RDB文件要高,所以如果Redis开启了AOF功能,则会优先使用AOF文件来还原数据;只有在AOF未开启情况下,Redis才会使用RDB文件完成数据库还原操作。
 
  自动RDB策略:
 
  save9001
 
  save30010
 
  save6010000
 
  以上配置表示当900s内有一次更新操作,或者300s内有10次更新操作,或者60s内有10000次更新操作,就会触发RDB持久化。RDB文件是按照RDB格式存储的,经过压缩的二进制文件,故数据恢复速度较快。当RDB机制被触发时,会fork子进程,扫描所有数据库的所有键值对,然后将其按照固定格式写入到RDB文件中,扫描完毕后写入磁盘,这时可能会进行重写文件名操作。
 
  AOF
 
  与RDB通过保存数据库中键值对来记录数据状态不同,AOF持久化是通过保存Redis服务器所执行的写命令来记录数据状态的。AOF持久化的实现:在AOF使能后,Redis在执行完一个写命令后,会以协议格式将写命令追加到服务器状态的aof_buf缓冲区末尾。
 
  Redis进程就是一个事件循环,事件分为IO事件和时间事件,IO事件就是处理与客户端的通信,时间事件就是负责执行像serverCron函数这样需要定时执行的函数。文件事件可能会涉及到写命令,所以Redis在每次结束一个事件循环前,都会调用flushAppendOnlyFile函数,考虑是否将aof_buf缓冲区的内容写入到AOF文件。flushAppendOnlyFile的行为有appendfsync配置项来决定:
 
  always:每次写命令都会同步到aof文件
 
  everysec:每秒同步一次(默认)
 
  no:不主动进行同步,何时同步由操作系统决定
 
  AOF重写
 
  因为AOF文件保存写命令来记录数据库状态,所以随着服务器的运行,AOF文件记录内容会越来越多,不加控制的话AOF会占用过多磁盘空间。因此,AOF增加了重写功能,也就是Redis将生成AOF文件替换旧AOF文件,虽然名字是AOF重写,但是AOF重写并不是对现有的AOF文件进行任何读取修改操作,而是读取数据库当前状态来实现的,生成能够还原当前数据库记录的最小写命令集合。
 
  Redis使用子进程来完成AOF重写操作,这样Redis服务器可以继续处理客户端请求,子进程使用父进程的数据副本,使用子进程而不是线程,避免使用锁的同时也保证数据安全性。不过还有一个问题需要解决,如果在子进程进行AOF重写时,父进程又处理一部分写命令,从而使得服务器当前数据库状态和重写后的AOF文件所保存的数据库状态不一致。Redis又是如何处理该问题的呢?
 
  AOF缓冲区内容会定期被写入和同步到AOF文件,对现有AOF文件的处理工作照常执行。从创建子进程开始,服务器所执行的写命令都会被记录到AOF重写缓冲区中。在子进程执行完AOF重写后,会向父进程发送一个信号,然后父进程会进行以下操作:
 
  将AOF重写缓冲区内容写入到新的AOF文件中,这是新的AOF文件所保存的数据库状态和服务器当前状态一致。
 
  对新的AOF文件更改名字,原子覆盖现有的AOF文件,完成新旧两个文件的替换。
 
  既然有AOF和RDB两种方式,那么哪一种更好呢?总的来说,二者都有优缺点,最好是根据业务场景来做(只作为缓存的场景理论上也可以不要持久化策略),同样的数据集AOF文件要比RDB文件大很多,但是AOF策略数据持久化更实时可靠一些。因为二者各有所长,所以Redis4.0中引入了混合持久化机制。
 
  混合持久化
 
  在Redis4.0中引入了混合持久化,将rdb文件的内容和增量的AOF日志文件存在一起。这里的AOF日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量AOF日志,通常这部分AOF日志很小。

如需转载,请注明文章出处和来源网址:http://www.divcss5.com/html/h56835.shtml