深入理解Redis数据类型Hashes原理

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 本文深入分析了Redis中的hashes数据类型,这是一种用于存储行记录的数据结构,允许一个key下存储多条记录。

前言

Redis的hashes类型是用来存储行记录的数据类型,一个key可以存储多条记录。

一、基本使用

HSET key field value

1、HSET是新增数据语法

2、key 是存储的数据key

3、field 是hash表中的某条记录名称

4、value是hash表某条数据的值

HGET key field

1、 hget是获取行数据的语法

2、根据key和field获取某行记录值

二、使用特点

1、field和value都是字符串,一个key对应多个field和value

2、某个field不能单独设置过期时间,只能对某个key设置过期时间

3、存在数据量大的时候,数据分布不均匀的情况

三、存储实现

hash类型总共有两种编码类型,一种是ziplist,一种是hashtable。

什么情况下使用ziplist呢?redis提供了两个配置项,如果hash表中

小于512个元素或者元素值小于64字节时使用ziplist。

//小于512个元素hash-max-ziplist-entries 512//元素值小于64字节hash-max-ziplist-value 64

总结:元素个数小,元素值内容占用小,用ziplist。

四、ziplist存储结构

在源码ziplist.c中,说明了ziplist的整体结构,如下:

<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>

zlbytes:是一个32位无符号整数,用于保存 ziplist 占用的字节数。

zltail:是列表中最后一个fiedl的偏移量,32位无符号整数

zllen:field的数量,16位无符号整数

entry:具体的field内容

zllen:是一个特殊的条目,代表 ziplist 的结尾

具体field结构

<prevlen> <encoding> <entry-data>

prevlen:代表前一个元素长度,如果该长度小于 254 字节,它将仅消耗一个字节,表示该长度为未编码的8位整数。当长度大于等于254时,会消耗5个字节。第一个字节设置为 254 (FE) 以指示后面有更大的值。剩下的 4 个字节取前一个条目的长度作为值

encoding:是编码类型,字符串或者整型,编码属性和内容息息相关。

  • 当entry是字符串时,编码第一个字节的前2位会保存用于存储字符串长度的编码类型,后面是字符串的实际长度
  • 当条目是整数时,前2位都设置为1。接下来的2位用于指定将在此标头后存储哪种整数。

field的存储结构如下图,ziplist是一个字节数组,重复考虑每一个字节的使用,不浪费空间,​充分提高内存利用率。

五、hashtable存储结构

在redis的dict.h中定义了hashtable的结构,它和java中的hashmap类似,也是适应数组+链表实现

整体结构图,有两个hashtable,但是​只会有一个使用到,另外一个是扩容的时候使用。

  • 扩容条件分析

既然是hashtable,那他和hashmap一样也会存在扩容情况,在redis中由一个标记位和负载因子决定是否扩容

//是否需要扩容static int dict_can_resize = 1;//扩容负载因子static unsigned int dict_force_resize_ratio = 5;
​/* 扩容条件判断 Expand the hash table if needed */static int _dictExpandIfNeeded(dict *d){    /* Incremental rehashing already in progress. Return. */    if (dictIsRehashing(d)) return DICT_OK;​    /* 初始化If the hash table is empty expand it to the initial size. */    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);​    /* If we reached the 1:1 ratio, and we are allowed to resize the hash     * table (global setting) or we should avoid it but the ratio between     * elements/buckets is over the "safe" threshold, we resize doubling     * the number of buckets. */     //使用的空间大于分配的空间    if (d->ht[0].used >= d->ht[0].size &&    //扩容开关打开或者使用的空间大小大于5倍        (dict_can_resize ||         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))    {        return dictExpand(d, d->ht[0].used*2);    }    return DICT_OK;}

+   什么​情况下不能扩容呢?

```javascript
void updateDictResizePolicy(void) {//rdb,aof子进程正在进行备份数据,禁止扩容    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)        dictEnableResize();    else        dictDisableResize();}  void dictEnableResize(void) {    dict_can_resize = 1;}  void dictDisableResize(void) {    dict_can_resize = 0;}
  • 扩容是否会影响吞吐量和性能?

答案是不会的,redis采用了渐近性的扩容方式,每次add,del,get操作都会对1个数组下标元素进行迁移,直到所有元素迁移完,将dict1指向dict0,重新分配内存给dict0。

例如新增一个元素:

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing){
   
       long index;    dictEntry *entry;    dictht *ht;​    //尝试帮助扩容一个数组下标位置    if (dictIsRehashing(d)) _dictRehashStep(d);/* Get the index of the new element, or -1 if     * the element already exists. */    //获取下标,并尝试扩容    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)        return NULL;/* Allocate the memory and store the new entry.     * Insert the element in top, with the assumption that in a database     * system it is more likely that recently added entries are accessed     * more frequently. */     //如果在扩容中,则新元素加到第2个hashtable数组里    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];    entry = zmalloc(sizeof(*entry));    entry->next = ht->table[index];    ht->table[index] = entry;    ht->used++;/* Set the hash entry fields. */    dictSetKey(d, entry, key);    return entry;}
  • hashtable总结

通过渐近性扩容方式,每个新增,删除,查询操作都协助迁移一个元素从第一个hash表到第2个hahs表。

对于新增,修改,删除,查询操作都不会影响,因为有标记,判断是否在扩容中,扩容中则操作第2个hash表,不在扩容中则操作第1个hash表。

六、hash类型总结

hashes存储类型是用来存储多行记录的存储类型,底层使用了两种编码类型来存储hash类型的数据,在设计ziplist时候,充分利用了每一个字节的作用,也是为了精细化的设计这个结构,提升hash结构的存储性能。

hashtable从扩容方式上对性能有充分的考量,基于分而治之的思想,扩容操作分担给多个客户端操作(多个线程),避免长时间的扩容等待。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1天前
|
存储 缓存 NoSQL
解决Redis缓存数据类型丢失问题
解决Redis缓存数据类型丢失问题
109 85
|
17天前
|
存储 NoSQL Redis
redis常见数据类型
Redis 是一种基于内存的键值存储数据库,支持字符串、哈希表、列表、集合及有序集合等多种数据类型,每种类型均有特定用途与适用场景,提供丰富的命令操作,适用于高速数据访问与处理。
31 5
|
1月前
|
存储 消息中间件 NoSQL
使用Java操作Redis数据类型的详解指南
通过使用Jedis库,可以在Java中方便地操作Redis的各种数据类型。本文详细介绍了字符串、哈希、列表、集合和有序集合的基本操作及其对应的Java实现。这些示例展示了如何使用Java与Redis进行交互,为开发高效的Redis客户端应用程序提供了基础。希望本文的指南能帮助您更好地理解和使用Redis,提升应用程序的性能和可靠性。
38 1
|
2月前
|
存储 消息中间件 NoSQL
Redis 数据类型
10月更文挑战第15天
43 1
|
2月前
|
设计模式 NoSQL 网络协议
大数据-48 Redis 通信协议原理RESP 事件处理机制原理 文件事件 时间事件 Reactor多路复用
大数据-48 Redis 通信协议原理RESP 事件处理机制原理 文件事件 时间事件 Reactor多路复用
44 2
|
2月前
|
存储 缓存 NoSQL
大数据-46 Redis 持久化 RDB AOF 配置参数 混合模式 具体原理 触发方式 优点与缺点
大数据-46 Redis 持久化 RDB AOF 配置参数 混合模式 具体原理 触发方式 优点与缺点
71 1
|
2月前
|
NoSQL 关系型数据库 MySQL
Redis 事务特性、原理、具体命令操作全方位诠释 —— 零基础可学习
本文全面阐述了Redis事务的特性、原理、具体命令操作,指出Redis事务具有原子性但不保证一致性、持久性和隔离性,并解释了Redis事务的适用场景和WATCH命令的乐观锁机制。
373 0
Redis 事务特性、原理、具体命令操作全方位诠释 —— 零基础可学习
|
3月前
|
存储 消息中间件 缓存
深入探析Redis常见数据类型及应用场景
深入探析Redis常见数据类型及应用场景
66 2
|
3月前
|
存储 缓存 NoSQL
redis的原理(四)
redis的原理(四)
|
2月前
|
消息中间件 NoSQL Kafka
大数据-116 - Flink DataStream Sink 原理、概念、常见Sink类型 配置与使用 附带案例1:消费Kafka写到Redis
大数据-116 - Flink DataStream Sink 原理、概念、常见Sink类型 配置与使用 附带案例1:消费Kafka写到Redis
192 0
下一篇
DataWorks