当前位置: 首页 > news >正文

Redis的Hash解析

Redis Hash数据结构概述

Redis的Hash数据类型是一种键值对集合,特别适合用于存储对象。它将多个字段-值对(field-value pairs)存储在一个Redis键下,每个字段对应一个值。从外部使用视角看,Hash类似于编程语言中的字典或Map数据结构,可以将一个对象的所有属性作为字段存储在一个键中

在实际应用中,Redis Hash常用于以下几种场景:

  • 存储对象信息:如用户信息、商品信息、配置项等,可以将一个对象的所有属性存储在单个Hash中
  • 结构化数据缓存:相比将对象的每个属性存储为单独的String类型,Hash能更有效地组织和管理相关数据
  • 聚合统计数据:可以方便地对字段进行增量操作,如计数器、统计指标等

与将对象属性存储为多个String键相比,Hash具有以下显著优势

优势说明
内存效率小字段的Hash编码非常紧凑,比多个String键占用更少内存
操作原子性可以原子性地操作多个字段,而无需使用事务
数据局部性相关数据存储在同一个键中,可以一次性获取所有字段
访问效率不需要为每个属性单独建立键,减少网络往返时间

Redis Hash的底层实现

Redis Hash数据结构的内部采用两种编码方式:ziplist(压缩列表)和hashtable(哈希表)。Redis会根据一定的条件自动选择和使用这两种编码。

ziplist编码

ziplist是Redis为了节约内存而设计的一种特殊紧凑的连续内存数据结构。当Hash中的元素数量较少时,Redis会使用ziplist作为底层实现。

ziplist的结构如下图所示:

[zlbytes][zltail][zllen][entry1][entry2]...[entryN][zlend]
  • zlbytes:4字节,表示整个ziplist占用的内存字节数
  • zltail:4字节,记录到最后一个节点的偏移量,便于快速定位到尾部
  • zllen:2字节,表示ziplist中节点的数量
  • entryX:不固定,ziplist中的各个节点
  • zlend:1字节,固定值0xFF,表示ziplist的结束

每个entry(节点)的结构为:[previous_entry_length][encoding][content]。其中previous_entry_length记录前一个节点的长度,以实现从后向前的遍历;encoding表示内容编码;content是实际保存的数据。

在ziplist编码的Hash中,字段和值依次相邻存放。例如,执行HSET user:1 name Tom age 25后,ziplist中的布局为:[field1][value1][field2][value2]...,即字段名和字段值交替存储。

hashtable编码

当Hash规模较大时,Redis会自动转换为hashtable编码。hashtable是一种标准的哈希表实现,使用数组加链表来解决哈希冲突。

Redis的hashtable实现涉及三个核心结构体:

  • dict:表示字典,包含两个哈希表(用于渐进式rehash)和其他管理信息
  • dictht:哈希表结构,包含桶数组、大小、掩码和使用数量
  • dictEntry:哈希表节点,保存键值对及下一个节点的指针(形成链表)

hashtable的结构如下图所示:

+-----------------------+
|          dict         |
|  ht[0]   |   ht[1]   |
| rehashidx| ...       |
+-----------------------+|           |v           v
+----------+  +----------+
| dictht   |  | dictht   |
| table    |  | table    |
| size     |  | size     |
| sizemask |  | sizemask |
| used     |  | used     |
+----------+  +----------+|v
+------------------+
| dictEntry* array |
+------------------+|v
+------------------+    +------------------+
|   dictEntry      | -> |   dictEntry      |
| key: "name"      |    | key: "age"       |
| val: "Tom"       |    | val: "25"        |
| next: pointer    |    | next: NULL       |
+------------------+    +------------------+

转换条件

Redis根据以下两个条件自动决定使用ziplist还是hashtable编码:

  1. 元素数量:Hash中的字段数量小于hash-max-ziplist-entries配置值(默认512)
  2. 值大小:所有字段值和字段名的字符串长度都小于hash-max-ziplist-value配置值(默认64字节)

同时满足以上两个条件时,使用ziplist编码;只要任一条件不满足,则转换为hashtable编码。

示例

# 初始时,所有字段和值都小于64字节,使用ziplist编码
127.0.0.1:6379> HSET user:1 name "Tom" age 25 career "Programmer"
(integer) 3
127.0.0.1:6379> OBJECT encoding user:1
"ziplist"# 当添加一个长字符串字段(超过64字节)后,自动转换为hashtable编码
127.0.0.1:6379> HSET user:1 desc "Programmer 11111112121v121kl lldklakdkalgam fsfdslkgkskgsklgklsklgklsklgsdkgkskgdsklmvm,,vm,vm,,maafaklglkaklsfakslkf"
(integer) 1
127.0.0.1:6379> OBJECT encoding user:1
"hashtable"

注意
从ziplist转换为hashtable的过程是不可逆的。即使之后删除了导致转换的大字段,Hash也不会恢复为ziplist编码。

Redis Hash操作命令

Redis提供了丰富的命令来操作Hash数据类型,这些命令可以分为以下几类:

基本操作命令

  • HSET key field value:设置Hash中指定字段的值

    127.0.0.1:6379> HSET user:1 name "Tom"
    (integer) 1
    
  • HGET key field:获取Hash中指定字段的值

    127.0.0.1:6379> HGET user:1 name
    "Tom"
    
  • HDEL key field [field …]:删除Hash中的一个或多个字段

    127.0.0.1:6379> HDEL user:1 age
    (integer) 1
    
  • HEXISTS key field:判断指定字段是否存在于Hash中

    127.0.0.1:6379> HEXISTS user:1 name
    (integer) 1
    
  • HLEN key:获取Hash中字段的数量

    127.0.0.1:6379> HLEN user:1
    (integer) 2
    

批量操作命令

  • HMSET key field value [field value …]:同时设置多个字段-值对

    127.0.0.1:6379> HMSET user:1 name "Tom" age 25 city "Shanghai"
    OK
    
  • HMGET key field [field …]:获取多个字段的值

    127.0.0.1:6379> HMGET user:1 name age city
    1) "Tom"
    2) "25"
    3) "Shanghai"
    
  • HGETALL key:获取Hash中所有字段和值

    127.0.0.1:6379> HGETALL user:1
    1) "name"
    2) "Tom"
    3) "age"
    4) "25"
    5) "city"
    6) "Shanghai"
    
  • HKEYS key:获取Hash中的所有字段名

    127.0.0.1:6379> HKEYS user:1
    1) "name"
    2) "age"
    3) "city"
    
  • HVALS key:获取Hash中的所有值

    127.0.0.1:6379> HVALS user:1
    1) "Tom"
    2) "25"
    3) "Shanghai"
    

数字操作命令

  • HINCRBY key field increment:将指定字段的值增加整数增量

    127.0.0.1:6379> HINCRBY user:1 age 1
    (integer) 26
    
  • HINCRBYFLOAT key field increment:将指定字段的值增加浮点数增量

    127.0.0.1:6379> HINCRBYFLOAT user:1 score 0.5
    "0.5"
    

高级操作命令

  • HSCAN key cursor [MATCH pattern] [COUNT count]:增量式迭代Hash中的字段

    127.0.0.1:6379> HSCAN user:1 0 MATCH "n*"
    1) "0"
    2) 1) "name"2) "Tom"
    
  • HSETNX key field value:只在字段不存在时设置其值

    127.0.0.1:6379> HSETNX user:1 name "Mike"
    (integer) 0  # 返回0表示字段已存在,设置失败
    
  • HSTRLEN key field:获取指定字段值的字符串长度(Redis 3.2+)

    127.0.0.1:6379> HSTRLEN user:1 name
    (integer) 3
    

Java操作Redis Hash

StringRedisTemplate与RedisTemplate区别

  • StringRedisTemplate:专门用于处理字符串类型的数据,使用String序列化器,键和值都序列化为可读的字符串
  • RedisTemplate:可以处理任意类型的对象,默认使用JDK序列化器,在Redis中存储为二进制数据

基本配置

首先需要配置StringRedisTemplate Bean:

@Configuration
public class RedisConfig {@Beanpublic StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {StringRedisTemplate template = new StringRedisTemplate();template.setConnectionFactory(redisConnectionFactory);// 设置Hash值的序列化器GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();template.setHashValueSerializer(jackson2JsonRedisSerializer);return template;}
}

代码示例

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import java.util.HashMap;
import java.util.Map;@Component
public class UserHashService {@Autowiredprivate StringRedisTemplate stringRedisTemplate;// 获取Hash操作接口private HashOperations<String, String, Object> hashOps() {return stringRedisTemplate.opsForHash();}// 添加单个字段public void addUserField(String userId, String field, String value) {hashOps().put(userKey(userId), field, value);}// 添加整个用户对象public void addUser(String userId, Map<String, Object> userData) {hashOps().putAll(userKey(userId), userData);}// 获取用户字段public Object getUserField(String userId, String field) {return hashOps().get(userKey(userId), field);}// 获取整个用户信息public Map<String, Object> getUser(String userId) {return hashOps().entries(userKey(userId));}// 更新用户年龄(数字操作)public long incrementUserAge(String userId, long increment) {return hashOps().increment(userKey(userId), "age", increment);}// 删除用户字段public void deleteUserField(String userId, String field) {hashOps().delete(userKey(userId), field);}// 检查字段是否存在public boolean hasUserField(String userId, String field) {return hashOps().hasKey(userKey(userId), field);}// 获取用户所有字段名public Set<String> getUserFields(String userId) {return hashOps().keys(userKey(userId));}// 获取用户字段数量public long getUserFieldCount(String userId) {return hashOps().size(userKey(userId));}private String userKey(String userId) {return "user:" + userId;}// 使用示例public void userOperationsExample() {String userId = "123";// 添加用户Map<String, Object> userData = new HashMap<>();userData.put("name", "Tom");userData.put("age", 25);userData.put("city", "Shanghai");addUser(userId, userData);// 获取用户姓名String name = (String) getUserField(userId, "name");System.out.println("User name: " + name);// 增加年龄incrementUserAge(userId, 1);// 获取完整用户信息Map<String, Object> user = getUser(userId);System.out.println("User: " + user);}
}

序列化策略

在Redis中存储对象时,选择合适的序列化策略很重要:

  • StringRedisSerializer:用于键和字段名的序列化,保证可读性
  • Jackson2JsonRedisSerializer:用于值的序列化,将对象序列化为JSON格式,便于不同语言间共享数据
  • JdkSerializationRedisSerializer:默认序列化器,将对象序列化为二进制,但不同语言间不兼容

对于Hash值的序列化,可以针对不同的需求选择不同的策略:

@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(redisConnectionFactory);// 设置键的序列化器template.setKeySerializer(new StringRedisSerializer());template.setHashKeySerializer(new StringRedisSerializer());// 设置值的序列化器Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);ObjectMapper objectMapper = new ObjectMapper();objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);objectMapper.activateDefaultTyping(LazySerializationizationParser.DefaultTyping.NON_FINAL);jsonSerializer.setObjectMapper(objectMapper);template.setValueSerializer(jsonSerializer);template.setHashValueSerializer(jsonSerializer);return template;}
}

Redis Hash的渐进式扩容机制

需要渐进式Rehash的原因

Redis的哈希表在数据量增加时需要扩容,以避免哈希冲突导致的性能下降。传统的Rehash操作需要一次性将所有键从旧表迁移到新表,当哈希表很大时,这会阻塞Redis主线程较长时间,影响服务可用性。

为了解决这个问题,Redis设计了渐进式Rehash机制,将大规模的数据迁移分多次、渐进式地完成,避免长时间阻塞。

触发Rehash的条件

Rehash操作在以下两种情况下触发:

扩容条件

  • 当哈希表的负载因子(元素数量/哈希桶数量)超过1,且Redis没有执行BGSAVE或BGREWRITEAOF命令时
  • 当负载因子超过5,无论是否在执行持久化操作,都会强制扩容

缩容条件

  • 当哈希表的负载因子低于0.1时,触发缩容以节省内存

需要注意的是,在Redis执行BGSAVE或BGREWRITEAOF时,正常情况下会尽量避免扩容以减少内存页的过多分离(Copy On Write),但如果负载因子超过5,说明冲突已经很严重,会强制扩容。

执行流程

渐进式Rehash的整个过程可以分为以下几个步骤:

  1. 准备阶段

    • ht[1]分配空间:
      • 扩容时:大小为第一个大于等于ht[0].used * 2的2的n次幂
      • 缩容时:大小为第一个大于等于ht[0].used的2的n次幂
    • 设置rehashidx = 0,表示Rehash正式开始
  2. 渐进迁移阶段

    • 每次对字典执行增删改查操作时,Redis除了执行指定操作外,还会将ht[0]在rehashidx索引上的整个桶中的所有键值对迁移到ht[1]
    • 迁移完成后,rehashidx值加1
    • 在Rehash期间,新添加的键值对会直接保存到ht[1],而ht[0]不再进行任何添加操作
  3. 完成阶段

    • ht[0]的所有桶都迁移完成后,rehashidx设置为-1
    • 释放ht[0]的空间,将ht[1]设置为新的ht[0],并在ht[1]创建一个新的空哈希表为下一次Rehash做准备

Rehash期间的访问规则

在渐进式Rehash期间,字典同时持有两个哈希表,访问规则如下:

  • 查找操作:先在ht[0]中查找,如果没找到再到ht[1]中查找
  • 插入操作:新键值对直接插入到ht[1]
  • 删除和更新操作:需要在ht[0]ht[1]上同时进行

以下是在Rehash期间查找操作的伪代码示例:

def get(key):if rehashing:# 先查ht[0]idx = dict_rehashidx(d)bucket = &d->ht[0].table[idx]while bucket->used > 0:if key matches:return valuebucket++# 如果ht[0]未找到,转向ht[1]return lookup_in_ht1(key)else:return lookup_in_ht0(key)

示例

为了更好地理解渐进式Rehash的过程,以下是一个简化的示例:

  1. 初始状态

    ht[0]: 大小为4,有3个元素
    ht[1]: 为空
    rehashidx: -1
    
  2. 开始Rehash

    ht[0]: 大小为4,有3个元素
    ht[1]: 大小为8(扩容为2倍)
    rehashidx: 0
    
  3. 第一次迁移后

    ht[0]: 索引0的桶已迁移,剩余2个元素
    ht[1]: 已接收索引0的桶中元素
    rehashidx: 1
    
  4. 完成Rehash

    ht[0]: 已全部迁移,将被释放
    ht[1]: 包含所有3个元素
    rehashidx: -1
    

优缺点

优点

  • 非阻塞:将庞大的Rehash操作分摊到多个请求上,避免长时间阻塞服务
  • 平滑过渡:在Rehash期间,Redis仍能正常处理读写请求,保证高可用性

缺点

  • 迁移期间性能略有下降:每个操作需要额外检查两个哈希表
  • 内存占用翻倍:在Rehash期间,需要同时存储两个哈希表,内存占用增加
http://www.dtcms.com/a/466670.html

相关文章:

  • 旅游业网站开发建设毕设做微课资源网站设计可以吗
  • 设计公司网站什么重要杭州工业设计
  • 【北京迅为】iTOP-4412精英版使用手册-第三十五章 WEB控制LED
  • 重庆seo整站优化报价福建建筑人才网官网
  • 教学信息化大赛网站建设作品永久免费国外ip代理
  • [嵌入式系统-93]: NVIDIA 正在从‘数据中心霸主’向‘端-边-云一体化AI平台’战略扩张。
  • 网站管理助手4.0域名备案查询管理系统
  • Oracle EBS ERP之报表开发—条件筛选按钮和组件开发
  • 济南网站建设与优化注册城乡规划师考试时间2023
  • 南通网站建设公司做品牌推广用什么网站
  • linux模拟压测CPU彪高到100%
  • 【2025全新】CDToolX专业圆二色谱数据处理软件下载安装教程(含最新版安装包)
  • 做网站做小程序推广中搜seo
  • Qiankun 微前端框架 start() 方法详解
  • 网站开发服务器多少钱个体户45万以下免个税
  • Autoware Universe 定位模块详解 | 第二节 深入研究定位模块数据流
  • 网站底部横条导航代码免费软件app下载大全
  • Java程序员如何深入学习JVM底层原理?
  • 送上门卤菜网站要怎么做软文代写发布网络
  • 有关网站建设的app安徽省建设工程质量协会网站
  • 【Liunx】高级IO
  • 「日拱一码」104 MOFs + AI
  • 学习笔记:Vue Router 路由匹配语法详解
  • 美妆网站建设外贸网站建设广告
  • 嵌入式开发基础知识补充—内存的种类和C语言变量的存储位置(非易失性存储和易失性存储)
  • 沈阳妇科排名前十的医生seo关键词排名优化
  • 读写分离架构:数据访问层的演进与实践
  • 应用最广网站建设技术wordpress被百度收录
  • Shell 编程1
  • conv([1 1], [1 2]) % 输出 [1 3 2](对应 (x+1)(x+2) = x²+3x+2)