redis集群实现(四) 数据的和槽位的分配

来源:互联网 发布:打条码软件 编辑:程序博客网 时间:2024/06/05 16:15

不知道有没有人思考过Redis是如何把数据分配到集群中的每一个节点的,可能有人会说,把集群中的每一个节点编号,先放第一个节点,放满了就放第二个节点,以此类推。。如果真的是这样的话,服务器的利用率和性能就太低了,因为先放第一个,其他的服务器节点就闲置下来了,单个节点的压力就会非常的大,其实就相当于退化成为了单机服务器,从而违背了集群发挥每一个节点的性能的初衷。

redis官方给出的集群方案中,数据的分配是按照槽位来进行分配的,每一个数据的键被哈希函数映射到一个槽位,redis-3.0.0规定一共有16384个槽位,当然这个可以根据用户的喜好进行配置。当用户put或者是get一个数据的时候,首先会查找这个数据对应的槽位是多少,然后查找对应的节点,然后才把数据放入这个节点。这样就做到了把数据均匀的分配到集群中的每一个节点上,从而做到了每一个节点的负载均衡,充分发挥了集群的威力。

redis中,把一个key-value键值对放入的最简单的方式就是set key value,如下所示:

[cpp] view plain copy
  1. 127.0.0.1:7000> set key value  
  2. -> Redirected to slot [12539] located at 192.168.39.153:7002  
  3. OK  
  4. 192.168.39.153:7002> get key  
  5. "value"  
  6. 192.168.39.153:7002>   

可以看出,当我们把key的值设置成为value的时候,客户端被重定向到了另一个节点192.168.39.153:7002,这是因为key对应的槽位是12359,所以我们的key-value就被放到了槽12359对应的节点,192.168.39.153:7002了。接下来,我们来看看redis是怎么把一个key-value键值对映射成槽,然后又如何存放进集群中的。

首先在redis.c文件里定义了客户端命令和函数的对应关系,

[cpp] view plain copy
  1. struct redisCommand redisCommandTable[] = {  
  2. ————————————————————————  
  3. {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},  
  4. ————————————————————————  

可以看出,set命令会执行setCommand函数进行解析,继续进入setCommand函数查看

[cpp] view plain copy
  1. void setCommand(redisClient *c) {                                                
  2.     int j;                                                                       
  3.     robj *expire = NULL;                                                         
  4.     int unit = UNIT_SECONDS;                                                     
  5.     int flags = REDIS_SET_NO_FLAGS;                                              
  6.                                                                  
  7.     ————————————————————————                  
  8.                             
  9.     // 对value编码                                                      
  10.     c->argv[2] = tryObjectEncoding(c->argv[2]);   
  11.     //真正执行set命令的地方                                                                                                                                                       
  12.     setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);      
  13. }       

继续进入setGenericCommand函数

[cpp] view plain copy
  1. void setGenericCommand(redisClient *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {  
  2.     //参数检查和过期时间的检查  
  3.     ————————————————————————      
  4.     //在数据库里设置key  value      
  5.     setKey(c->db,key,val);  
  6.     //设置完成以后的时间通知  
  7.     ————————————————————————          
  8. }  

接着看数据库的setKey函数

[cpp] view plain copy
  1. void setKey(redisDb *db, robj *key, robj *val) {  
  2.   
  3.     // 添加或覆写数据库中的键值对  
  4.     if (lookupKeyWrite(db,key) == NULL) {  
  5.         dbAdd(db,key,val);  
  6.     } else {  
  7.         dbOverwrite(db,key,val);  
  8.     }  
  9. ------------------------------------------------------------------------------------------  

当没有在数据库中发现key的时候,我们需要执行dbAdd函数把key-value添加到数据库里。

[cpp] view plain copy
  1. void dbAdd(redisDb *db, robj *key, robj *val) {  
  2.   
  3.     // 赋值key的名字  
  4.     sds copy = sdsdup(key->ptr);  
  5.   
  6.     // 添加键值对到字典中  
  7.     int retval = dictAdd(db->dict, copy, val);  
  8.   
  9.     // 如果键已经存在,那么停止  
  10.     redisAssertWithInfo(NULL,key,retval == REDIS_OK);  
  11.   
  12.     // 如果开启了集群模式,就把键保存到槽里面  
  13.     if (server.cluster_enabled) slotToKeyAdd(key);  
  14.  }  

继续进入slotToKeyAdd函数

[cpp] view plain copy
  1. //把键key添加到槽里边  
  2. void slotToKeyAdd(robj *key) {  
  3.   
  4.     // 通过字符串key计算出键对应的槽  
  5.     unsigned int hashslot = keyHashSlot(key->ptr,sdslen(key->ptr));  
  6.   
  7.     // 将槽 slot 作为分数,键作为成员,添加到 slots_to_keys 跳跃表里面  
  8.     zslInsert(server.cluster->slots_to_keys,hashslot,key);                                                                                                     
  9.     incrRefCount(key);  
  10. }  

keyHashSlot是一个哈希函数,通过key映射到一个0-16384的整数,我们来看一下实现

[cpp] view plain copy
  1. unsigned int keyHashSlot(char *key, int keylen) {                                                                                                             
  2.     //start 和end  
  3.     int s, e;   
  4.   
  5.     for (s = 0; s < keylen; s++)   
  6.         if (key[s] == '{'break;  
  7.   
  8.     /* 没有发现和{对应的},就直接哈希整个字符串 */  
  9.     if (s == keylen) return crc16(key,keylen) & 0x3FFF;  
  10.   
  11.     /* 如果发现了{,看看是不是又}匹配 */  
  12.     for (e = s+1; e < keylen; e++)   
  13.         if (key[e] == '}'break;  
  14.   
  15.     /* 如果没有发现},哈希函数就计算整个字符串. */  
  16.     if (e == keylen || e == s+1) return crc16(key,keylen) & 0x3FFF;  
  17.   
  18.     /*如果{}在我们的两边,哈希中间的字符 */  
  19.     return crc16(key+s+1,e-s-1) & 0x3FFF;  
  20. }  

计算key字符串对应的映射值,redis采用了crc16函数然后与0x3FFF取低16位的方法。crc16以及md5都是比较常用的根据key均匀的分配的函数,就这样,用户传入的一个key我们就映射到一个槽上,然后经过gossip协议,周期性的和集群中的其他节点交换信息,最终整个集群都会知道key在哪一个槽上。

原创粉丝点击