Redis从入门到放弃系列(二) Hash

Redis从入门到放弃系列(二) Hash

本文例子基于:5.0.4 Hash是Redis中一种比较常见的数据结构,其实现为hashtable/ziplist,默认创建时为ziplist,当到达一定量级时,redis会将ziplist转化为hashtable

Redis从入门到放弃系列(一) String

首先让我们来看一下该如何在redis里面使用Hash类型

1//将hash表中key的域field的值设为value 2//如果key不存在,一个新的哈希表被创建并进行HSET操作 3//如果域field已经存在于哈希表中,旧值将被覆盖 4hset key field value 5

代码示例:

1//创建不存在的field 2>hset user:1 id 1 3(integer) 1 4//覆盖原先的field 5>hset user:1 id 2 6(integer) 0 7>hget user:1 id 8"2" 9//获取不存在的field 10>hget user:1 not_exist 11(nil) 12---------------------------------- 13// hsetnx key field value 14//当不存在该field 设置成功返回1 ,否则返回0 15> hsetnx user:1 id 1 16(integer) 1 17> hsetnx user:1 id 1 18(integer) 0 19> hget user:1 id 20"1" 21---------------------------------- 22// hmset key field value [field value ....] 23//批量设置多个键值对 24>HMSET user:1 id 1 name "黑搜丶D" wechat "black-search" 25OK 26---------------------------------- 27//hget key field 28//获取hash表key中给定的field的值 29>hget user:1 id 30"1" 31---------------------------------- 32// hmget key field[field...] 33//按照我们输入的field的顺序返回 34>hmget user:1 name wechat id not_exist 351) "黑搜丶D" 362) "black-search" 373) "1" 384) (nil) 39---------------------------------- 40// hdel key field 删除返回被成功移除的域的数量 41> hgetall user:1 421) "id" 432) "1" 443) "name" 454) "black-search" 46> HDEL user:1 name 47(integer) 1 48> HDEL user:1 name 49(integer) 0 50---------------------------------- 51// HINCRBY key field increment 52// 为hash表某个整数类型的field增加increment ,返回增加increment之后的大小 53> hset user:1 wechat "black-search" 54(integer) 1 55> HINCRBY user:1 wechat 2 56(error) ERR hash value is not an integer 57> HINCRBY user:1 id 21 58(integer) 22 59> hget user:1 id 60"22" 61

至此,redis hash的用法先告一段落.


debug object key

本文开头的时候讲默认创建为ziplist,当达到一定的量级转化为hashtable,那么具体是在什么时候才会转化成hashtable呢?

1# Hashes are encoded using a memory efficient data structure when they have a 2# small number of entries, and the biggest entry does not exceed a given 3# threshold. These thresholds can be configured using the following directives. 4hash-max-ziplist-entries 512 5hash-max-ziplist-value 64 6

从上文我们可以知道,只有当我们满足以下两个条件会将ziplist转化为hashtable结构

  1. 保存的所有键值对个数小于 512个 (这个限制是由 hash-max-ziplist-entries 参数控制,默认 512)

  2. 保存的所有键值对的长度都小于 64 字节(这个限制是由 hash-max-ziplist-value 参数控制,默认 64)

    // 这里测试当键值对小于等于512时,hash的类型 @RequestMapping("/") public void test(){ List<Long> list = redisTemplate.executePipelined(new RedisCallback<Long>() { @Override public Long doInRedis(RedisConnection redisConnection) throws DataAccessException { redisConnection.openPipeline(); for (int i=0;i<512;i++){ redisConnection.hSet("key".getBytes(),("field"+i).getBytes(),"value".getBytes()); } return null; } }); System.out.println("结束"); } //我们发现这里hash的类型就是ziplist

    debug object key Value at:0xbc6f80 refcount:1 encoding:ziplist serializedlength:2603 lru:14344435 lru_seconds_idle:17 //让我们调大一下循环的次数,改为513,我们发现 debug object key Value at:0xbc6f80 refcount:1 encoding:hashtable serializedlength:7587 lru:14344656 lru_seconds_idle:4

源码解析

1//首先我们来看一下dict的结构 2typedef struct dict { 3 dictType *type; 4 void *privdata; 5 dictht ht[2]; 6 long rehashidx; /* rehashing not in progress if rehashidx == -1 */ 7 unsigned long iterators; /* number of iterators currently running */ 8} dict; 9typedef struct dictType { 10 uint64_t (*hashFunction)(const void *key); 11 void *(*keyDup)(void *privdata, const void *key); 12 void *(*valDup)(void *privdata, const void *obj); 13 int (*keyCompare)(void *privdata, const void *key1, const void *key2); 14 void (*keyDestructor)(void *privdata, void *key); 15 void (*valDestructor)(void *privdata, void *obj); 16} dictType; 17/* This is our hash table structure. Every dictionary has two of this as we 18 * implement incremental rehashing, for the old to the new table. */ 19typedef struct dictht { 20 dictEntry **table; 21 unsigned long size; 22 unsigned long sizemask; 23 unsigned long used; 24} dictht; 25typedef struct dictEntry { 26 void *key; 27 union { 28 void *val; 29 uint64_t u64; 30 int64_t s64; 31 double d; 32 } v; 33 struct dictEntry *next; 34} dictEntry; 35

从以上我们可以知道,dict里面包含了两个dictht(ps:hashtable),通常情况下只有一个dictht有值.但是当dict扩容/缩容的时候,需要分配新的dictht,然后渐进式搬迁,当迁移结束之后,旧的dictht被删除,只保留新的dictht dict如何解决hash冲突呢?其实原理跟Java的HashMap是一样的,采用数组+链表的方式去解决

渐进式rehash

我们知道,redis是单进程的,如果要将一个大的字典扩容是会比较耗时的,那么有可能就会将其他请求挂起。所以redis采用渐进式rehash来完成这一项艰巨任务~

1dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) 2{ 3 long index; 4 dictEntry *entry; 5 dictht *ht; 6 //这里每次都会进行搬迁~ 7 if (dictIsRehashing(d)) _dictRehashStep(d); 8 9 /* Get the index of the new element, or -1 if 10 * the element already exists. */ 11 if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1) 12 return NULL; 13 14 /* Allocate the memory and store the new entry. 15 * Insert the element in top, with the assumption that in a database 16 * system it is more likely that recently added entries are accessed 17 * more frequently. */ 18 //当字典处于搬迁中,将新添加的元素挂到新的数组下面 19 ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; 20 entry = zmalloc(sizeof(*entry)); 21 entry->next = ht->table[index]; 22 ht->table[index] = entry; 23 ht->used++; 24 25 /* Set the hash entry fields. */ 26 dictSetKey(d, entry, key); 27 return entry; 28} 29

这样,在客户端每次请求(hset/hdel等)都会去判断是否需要搬迁,那么当客户端不请求我们的时候,有可能没有完整的搬迁?no no no redis会在定时任务里面扫描处于rehash的dict,然后完成剩余的搬迁~代码如下

1/* This function handles 'background' operations we are required to do 2 * incrementally in Redis databases, such as active key expiring, resizing, 3 * rehashing. */ 4void databasesCron(void) { 5 /* Expire keys by random sampling. Not required for slaves 6 * as master will synthesize DELs for us. */ 7 if (server.active_expire_enabled) { 8 if (server.masterhost == NULL) { 9 activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW); 10 } else { 11 expireSlaveKeys(); 12 } 13 } 14 15 /* Defrag keys gradually. */ 16 if (server.active_defrag_enabled) 17 activeDefragCycle(); 18 19 /* Perform hash tables rehashing if needed, but only if there are no 20 * other processes saving the DB on disk. Otherwise rehashing is bad 21 * as will cause a lot of copy-on-write of memory pages. */ 22 if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) { 23 /* We use global counters so if we stop the computation at a given 24 * DB we'll be able to start from the successive in the next 25 * cron loop iteration. */ 26 static unsigned int resize_db = 0; 27 static unsigned int rehash_db = 0; 28 int dbs_per_call = CRON_DBS_PER_CALL; 29 int j; 30 31 /* Don't test more DBs than we have. */ 32 if (dbs_per_call > server.dbnum) dbs_per_call = server.dbnum; 33 34 /* Resize */ 35 for (j = 0; j < dbs_per_call; j++) { 36 tryResizeHashTables(resize_db % server.dbnum); 37 resize_db++; 38 } 39 40 /* Rehash */ 41 //重点在这里rehash 42 if (server.activerehashing) { 43 for (j = 0; j < dbs_per_call; j++) { 44 int work_done = incrementallyRehash(rehash_db); 45 if (work_done) { 46 /* If the function did some work, stop here, we'll do 47 * more at the next cron loop. */ 48 break; 49 } else { 50 /* If this db didn't need rehash, we'll try the next one. */ 51 rehash_db++; 52 rehash_db %= server.dbnum; 53 } 54 } 55 } 56 } 57} 58

应用场景

储存业务数据,我们发现其实hset的用法很简单,回顾上一讲最后的应用场景

1//上一讲使用string 2>set user:1 '{"id":1,"name":"黑搜丶D","wechat":"black-search"}' 3//让我们使用hash来实现相似的做法 4> HMSET user:1 id 1 name "黑搜丶D" wechat "black-search" 5OK 6//获取key的某个field的值 7>hget user:1 wechat 8"black-search" 9//获取到key的所有 field:value组合 10> HGETALL user:1 111) "id" 122) "1" 133) "name" 144) "\xe9\xbb\x91\xe6\x90\x9c\xe4\xb8\xb6D" 155) "wechat" 166) "black-search" 17

相对于string的用法,我们使用hash get某个field或者set某个field会省很多带宽~

黑搜丶D

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

Redis从入门到放弃系列(四) Set

Redis从入门到放弃系列(四)Set本文例子基于:5.0.4Set是Redis中一种比较常见的数据结构,当存储的member为十进制64位有符号整数范围内的整数的字符串的时候其实现为intset,其他为hashtableRedis从入门到放弃系列(三)List(https://www.osch

Redis从入门到放弃系列(十) Cluster

Redis从入门到放弃系列(十)Cluster本文例子基于:5.0.4RedisCluster集群高可用方案,去中心化,最基本三主多从,主从切换类似Sentinel,关于Sentinel内容可以查看编者另外一篇【Redis从入门到放弃系列(九)Sentinel(https://www.o

Redis从入门到放弃系列(一) String

Redis从入门到放弃系列(一)String本文例子基于:5.0.4字符串是Redis中最常见的数据结构,底层是采用SDS,是可以修改的字符串,类似ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配。首先让我们来看一下该如何在redis里面使用字符串这种类型//将字符