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

Redis Hash 介绍

Redis Hash 介绍

从基础命令、内部编码和使用场景三个维度分析如下:


一、基础命令

Redis Hash 提供了丰富的操作命令,适用于字段(field)级别的增删改查:

  1. 设置与修改

    • HSET:设置单个字段值(HSET key field value​)。

      # 存储用户信息(用户ID=1001)
      HSET user:1001 name "Alice" age 28 email "alice@example.com"  
      # 返回3(表示成功设置3个字段)
      
    • HSETNX:仅当字段不存在时设置值,避免覆盖。

      # 防止覆盖已存在的字段  
      HSETNX user:1001 email "new@example.com"  # 返回0(字段已存在,未修改)  
      HSETNX user:1001 address "New York"       # 返回1(新增字段)  
      
    • HINCRBY:对数值型字段进行增减操作(如计数器场景)。

      场景:计数器(库存、销量、点击量),支持原子增减,避免并发问题。

      # 商品ID=2001的库存增减  
      HINCRBY product:2001 stock -5  # 减少库存5(返回新值,如95)  
      HINCRBY product:2001 sales 10   # 增加销量10(返回新值,如150)  
      
  2. 查询与检索

    • HGET:获取单个字段值。

      HGET user:1001 name  # 返回"Alice"  
      
    • HMGET:批量获取多个字段值。

      HMGET user:1001 name age email  # 返回["Alice", "28", "alice@example.com"]  
      
    • HGETALL:获取所有字段及值(需谨慎使用,大数据量可能阻塞服务)。

      HGETALL user:1001  # 返回所有字段和值(适用于小规模数据)  
      
    • HKEYS/HVALS:分别获取所有字段名或值。

      HKEYS user:1001  # 返回["name", "age", "email", "address"]  
      HVALS user:1001  # 返回["Alice", "28", "alice@example.com", "New York"]  
      
  3. 删除与判断

    • HDEL:删除指定字段。

      场景:清理过期字段,例如用户注销后删除敏感信息。

    • HEXISTS:判断字段是否存在。

      场景:检查字段是否存在,例如权限校验或防止重复操作。

    • HLEN:统计字段数量。

      统计对象属性数量,例如监控购物车商品种类数。


二、内部编码

Redis Hash 根据数据规模和配置动态选择底层数据结构,以平衡性能与内存效率:

  1. ziplist(压缩列表)

    • 条件:当字段数 ≤ hash-max-ziplist-entries​(默认 512)且所有字段值 ≤ hash-max-ziplist-value​(默认 64 字节)时启用。
    • 特点:连续内存存储,无指针开销,内存利用率高,但插入/删除效率随数据量增长而下降。
  2. hashtable(哈希表)

    • 触发条件:超出 ziplist 配置限制时自动转换。
    • 特点:通过哈希表实现 O(1) 复杂度的读写操作,适合大规模数据,但内存占用较高。
  3. Redis 7.0 及以上版本已弃用 ziplist,改用 listpack 作为默认的紧凑数据结构。

    1. 数据结构设计对比

      特性ziplistlistpack
      存储结构连续内存块,元素通过prevlen​字段记录前一个元素的长度,形成隐式链表连续内存块,每个元素独立存储自身长度信息,无前后依赖。
      连锁更新风险存在:插入/删除元素时,可能触发后续元素的prevlen​字段更新,导致级联内存重分配。不存在:元素长度独立编码,无需依赖前驱元素,彻底避免连锁更新。
      内存布局通过prevlen​和encoding​字段实现变长编码。通过element-total-len​字段统一记录元素总长度,结构更简单。
      安全性易因prevlen​计算错误导致内存越界(历史漏洞)。长度信息自包含,访问更安全,减少内存越界风险。
    2. 性能与内存效率

      维度ziplistlistpack
      插入/删除效率平均 O(n),可能触发连锁更新(最坏 O(n^2))。平均 O(n),无连锁更新,性能更稳定。
      随机访问速度O(n),需遍历查找元素。O(n),与 ziplist 相同,但遍历更高效(长度解析更快)。
      内存占用较低(但存在prevlen​字段冗余)。与 ziplist 接近,甚至更优(更紧凑的长度编码)。
      并发操作适应性高并发写入时,连锁更新可能导致性能抖动。无连锁更新,高并发下性能更稳定。
    3. ziplist 的缺陷

      • 连锁更新问题
        在中间位置插入元素时,若后续元素的 prevlen​ 字段因长度变化需要扩展(例如从 1 字节变为 5 字节),会触发后续元素的连续内存重分配,导致性能骤降。
        示例:插入一个长度超过 254 字节的元素,导致后续元素的 prevlen​ 从 0xFE​(1 字节)变为 0xFF + 4字节长度​(5 字节)。
      • 安全性风险
        prevlen​ 解析错误可能引发内存越界(如 Redis 早期版本中的 CVE-2018-11213 漏洞)。
    4. listpack 的改进

      • 自包含长度信息
        每个元素头部使用 element-total-len​ 字段记录自身总长度(包含编码类型、数据长度等),解析时无需依赖前驱元素。

      • 编码优化
        长度字段采用更紧凑的变长编码(类似 UTF-8),例如:

        • 长度 ≤ 127:1 字节。
        • 长度 ≤ 16383:2 字节。
        • 更大长度:5 字节(1 字节标记 + 4 字节长度)。
    5. 使用场景对比

      场景推荐结构原因
      小规模只读数据ziplist/listpack内存紧凑,适合存储用户基础信息、配置项等(Redis 7.0+ 优先使用 listpack)。
      高频写入/随机修改listpack无连锁更新问题,适合计数器、动态元数据等场景(Redis 7.0+ 默认使用 listpack)。
      大规模数据存储hashtable数据量大时 O(1) 哈希表更高效(无论底层是 ziplist 还是 listpack,超阈值后均转为 hashtable)。
      高并发写入场景listpack避免 ziplist 的连锁更新抖动,性能更稳定。
  4. 数据结构特性对比

    特性ziplist(压缩列表)hashtable(哈希表)
    存储方式连续内存块存储,无指针开销,通过偏移量访问数据。散列桶结构,每个键值对通过哈希函数分配到桶中,通过指针链接。
    内存占用内存紧凑,无额外指针开销,内存利用率高。内存占用较高(存储指针、哈希表扩容时的冗余空间)。
    查询复杂度O(n)(需要遍历字段查找O(1)(哈希表直接定位)
    插入/删除效率平均 O(n),数据量大时效率低(需移动后续元素,可能触发连锁更新)。O(1)(哈希表直接操作,扩容时可能触发 rehash 耗时)。
    扩容机制不可动态扩容,超出阈值后直接转换为 hashtable。动态扩容(负载因子超过阈值时翻倍扩容,缩容时按需减少)。
    适用数据规模字段数少(默认 ≤512)且字段值小(默认 ≤64 字节)。字段数多或字段值大。
  5. 适用场景对比

    场景推荐编码原因
    小型对象存储ziplist字段数少且值小(如用户基础信息),内存利用率高。
    高频写入/随机访问hashtable避免 ziplist 的遍历开销和连锁更新问题(如频繁修改的计数器)。
    只读或低频修改数据ziplist利用内存紧凑特性减少内存占用(如静态配置数据)。
    大规模数据存储hashtable数据量大时 O(1) 查询效率显著优于 ziplist(如购物车、社交关系链)。
    需要遍历所有字段ziplist(小数据)连续内存布局遍历更快;大数据量时 hashtable 的HSCAN​更安全(避免HGETALL​阻塞)。
  6. 配置调优建议

    hash-max-ziplist-entries 512  # 字段数阈值,默认 512
    hash-max-ziplist-value 64     # 单个字段值最大字节数,默认 64
    
    • 降低阈值若需优先保障读写性能,可调小阈值(如 entries 128​),让更多 Hash 使用 hashtable。(空间)
    • 提高阈值:若内存紧张且数据规模可控,可增大阈值(如 entries 1024​),延长 ziplist 的使用。(时间)

三、使用场景

案例所需依赖 :

 <dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>4.3.1</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.15.2</version></dependency></dependencies>
  1. 对象属性存储

    • 将对象的多个属性(如用户信息、商品详情)聚合存储为一个 Hash,键为对象 ID,字段为属性名,简化键管理并减少内存碎片。

      package com.example.redis.hash;/*** 描述: 将对象的多个属性(如用户信息、商品详情)聚合存储为一个 Hash,键为对象 ID,字段为属性名,简化键管理并减少内存碎片** @author ZHOUXIAOYUE* @date 2025/4/17 09:52*/
      import redis.clients.jedis.Jedis;
      import com.fasterxml.jackson.core.type.TypeReference;
      import com.fasterxml.jackson.databind.ObjectMapper;
      import java.util.Map;
      class User {private String id;private String name;private String age;private String email;private String phone;// 构造器、getter、setter 略public User(String id, String name, String age, String email, String phone) {this.id = id;this.name = name;this.age = age;this.email = email;this.phone = phone;}public String getId() { return id; }public String getName() { return name; }public String getAge() { return age; }public String getEmail() { return email; }public String getPhone() { return phone; }
      }
      public class RedisHashExample {public static void main(String[] args) throws Exception {//创建链接Jedis jedis = new Jedis("localhost", 6379);// 定义对象User user = new User("user:1001", "Alice", "30", "alice@example.com", "1234567890");// 使用 Jackson 将对象转换为 Map<String, String>ObjectMapper objectMapper = new ObjectMapper();Map<String, String> userMap = objectMapper.convertValue(user, new TypeReference<Map<String, String>>() {});// 从 Map 中移除 id 字段(如果不需要存储到 Hash 中)userMap.remove("id");// 存储到 Redis 中jedis.hmset(user.getId(), userMap);System.out.println("存储用户信息成功,查询结果为:");Map<String, String> storedUserInfo = jedis.hgetAll(user.getId());storedUserInfo.forEach((field, value) -> System.out.println(field + ": " + value));jedis.close();}
      }
      
  2. 部分更新与原子操作

    • 支持单个字段的原子更新(如用户年龄修改),避免序列化整个对象。

      package com.example.redis.hash;/*** 描述: 支持单个字段的原子更新(如用户年龄修改),避免序列化整个对象。** @author ZHOUXIAOYUE* @date 2025/4/17 10:12*/
      import redis.clients.jedis.Jedis;
      import java.util.Map;
      import java.util.HashMap;
      public class RedisHashUpdateExample {public static void main(String[] args) {// 创建 Jedis 连接,连接到本地 Redis 服务Jedis jedis = new Jedis("localhost", 6379);// 假设已经存储了用户信息,键为 "user:1001"String userId = "user:1001";// 如果未事先存储用户数据,可以先初始化对象(仅供测试)if (!jedis.exists(userId)) {Map<String, String> userInfo = new HashMap<>();userInfo.put("name", "Alice");userInfo.put("age", "30");userInfo.put("email", "alice@example.com");userInfo.put("phone", "1234567890");jedis.hmset(userId, userInfo);System.out.println("初始化用户信息成功。");}// 查询更新前的年龄String oldAge = jedis.hget(userId, "age");System.out.println("更新前用户年龄: " + oldAge);// 对单个字段进行原子更新,这里更新年龄为 31jedis.hset(userId, "age", "31");// 查询更新后的年龄String newAge = jedis.hget(userId, "age");System.out.println("更新后用户年龄: " + newAge);// 关闭 Jedis 连接jedis.close();}
      }
      
  3. 计数器与统计

    • 利用 HINCRBY​ 实现实时计数(如商品销量、页面访问量)。

      package com.example.redis.hash;/*** 描述: 利用 HINCRBY 实现实时计数(如商品销量、页面访问量)** @author ZHOUXIAOYUE* @date 2025/4/17 10:15*/
      import redis.clients.jedis.Jedis;
      public class RedisCounterExample {public static void main(String[] args) {// 连接 Redis 服务器Jedis jedis = new Jedis("localhost", 6379);// 示例1:统计商品销量// 商品 ID 为 product:2001,对应的销量字段为 "sales"String productKey = "product:2001";// 初始化销量字段,注意如果该字段不存在,则 hincrby 会默认其值为 0// jedis.hset(productKey, "sales", "0");// 模拟一次商品销售,使用 HINCRBY 将销量加 1long salesCount = jedis.hincrBy(productKey, "sales", 1);System.out.println("商品 " + productKey + " 当前销量为:" + salesCount);// 示例2:统计页面访问量// 页面标识为 page:homepage,对应的访问量字段为 "views"String pageKey = "page:homepage";// 初始化访问量字段,若不存在则 hincrby 默认从 0 开始计数// jedis.hset(pageKey, "views", "0");// 模拟页面访问,使用 HINCRBY 将访问量加上 5(比如一次批量更新)long viewCount = jedis.hincrBy(pageKey, "views", 5);System.out.println("页面 " + pageKey + " 当前访问量为:" + viewCount);// 关闭 Jedis 连接jedis.close();}
      }
      
  4. 购物车实现

    • 以用户 ID 为键,商品 ID 为字段,数量为值,便于动态增删商品。

      package com.example.redis.hash;/*** 描述: 以用户 ID 为键,商品 ID 为字段,数量为值,便于动态增删商品。** @author ZHOUXIAOYUE* @date 2025/4/17 10:18*/
      import redis.clients.jedis.Jedis;
      import java.util.Map;
      public class ShoppingCartDemo {public static void main(String[] args) {// 创建 Jedis 连接,连接到本地 Redis 服务,默认端口 6379Jedis jedis = new Jedis("localhost", 6379);// 以用户ID构建购物车的 key(建议加上 cart 前缀以示区分)String cartKey = "cart:user:1001";// -------------------------------// 示例1:添加商品到购物车// -------------------------------// 将商品 product:2001 添加至购物车,数量 2jedis.hset(cartKey, "product:2001", "2");System.out.println("添加商品 product:2001 数量 2 到购物车 " + cartKey);// 添加另一个商品 product:2002,数量 3jedis.hset(cartKey, "product:2002", "3");System.out.println("添加商品 product:2002 数量 3 到购物车 " + cartKey);printCart(jedis, cartKey);// -------------------------------// 示例2:动态增加已有商品的数量// -------------------------------// 将商品 product:2001 的数量原子性增加 1jedis.hincrBy(cartKey, "product:2001", 1);System.out.println("将商品 product:2001 的数量增加 1");printCart(jedis, cartKey);// -------------------------------// 示例3:删除商品// -------------------------------// 从购物车中删除商品 product:2002jedis.hdel(cartKey, "product:2002");System.out.println("从购物车中删除商品 product:2002");printCart(jedis, cartKey);// 关闭 Jedis 连接jedis.close();}// 打印购物车中所有商品信息private static void printCart(Jedis jedis, String cartKey) {System.out.println("购物车 " + cartKey + " 当前内容:");Map<String, String> cartItems = jedis.hgetAll(cartKey);if(cartItems.isEmpty()){System.out.println("购物车为空。");} else {cartItems.forEach((productId, quantity) ->System.out.println("商品ID:" + productId + ", 数量:" + quantity));}System.out.println("--------------------------------------------------");}
      }
      
  5. 分布式锁与缓存

    • 通过 HSETNX​ 实现轻量级锁,或缓存计算结果减少重复计算。

      package com.example.redis.hash;/*** 描述: 通过 HSETNX 实现轻量级锁,或缓存计算结果减少重复计算* 连接 Redis 后,使用哈希 key "cache:calc" 保存缓存数据和锁信息。* 首先通过 hget 检查是否已经有计算结果存入缓存,如果存在则直接返回。* 如果未命中缓存,通过 HSETNX 尝试对字段 "lock" 加锁。如果返回 1,则表示当前线程获得锁,然后执行耗时计算,将结果存入 "result" 字段,最后释放锁(删除 "lock" 字段)。* 若 HSETNX 返回 0,则表示已有线程在计算,当前线程等待一段时间后轮询 "result" 字段,直到获取到已缓存的计算结果。\* @author ZHOUXIAOYUE* @date 2025/4/17 10:26*/
      import redis.clients.jedis.Jedis;
      public class CacheLockExample {// 模拟一个耗时计算,例如求 1~1000000 的和private static long doExpensiveComputation() {long sum = 0;for (int i = 1; i <= 1_000_000; i++) {sum += i;}// 模拟延迟try {Thread.sleep(1000);} catch (InterruptedException e) {// 忽略异常处理}return sum;}public static void main(String[] args) {// 连接 Redis 服务Jedis jedis = new Jedis("localhost", 6379);// 使用哈希结构保存缓存与锁信息String hashKey = "cache:calc";String lockField = "lock";String resultField = "result";// 判断缓存中是否已有计算结果String cachedResult = jedis.hget(hashKey, resultField);if (cachedResult != null) {System.out.println("直接从缓存中获取结果: " + cachedResult);jedis.close();return;}// 尝试通过 HSETNX 获取轻量级锁(lockField 不存在时设置锁)long lockAcquired = jedis.hsetnx(hashKey, lockField, String.valueOf(System.currentTimeMillis()));if (lockAcquired == 1) {// 获取锁成功,执行耗时计算System.out.println("获取锁成功,开始计算...");long computedValue = doExpensiveComputation();// 将计算结果缓存到 resultField 中jedis.hset(hashKey, resultField, String.valueOf(computedValue));// 释放锁(删除 lockField)jedis.hdel(hashKey, lockField);System.out.println("计算完成并缓存结果: " + computedValue);} else {// 获取锁失败,其他线程正在计算,等待缓存结果System.out.println("其他线程正在计算,等待获取结果...");int maxWaitTime = 10; // 最长等待时间(单位:循环次数,每次 100 毫秒)int waited = 0;while (waited < maxWaitTime) {try {Thread.sleep(100); // 每次等待100毫秒} catch (InterruptedException e) {// 忽略异常}cachedResult = jedis.hget(hashKey, resultField);if (cachedResult != null) {System.out.println("等待期间缓存中获取结果: " + cachedResult);break;}waited++;}if (cachedResult == null) {System.out.println("等待超时,还未获取到计算结果。");}}jedis.close();}
      }
      

总结

Redis Hash 通过灵活的字段操作、高效的内存管理和多样化的应用场景,成为存储结构化数据的理想选择。

相关文章:

  • HttpSessionListener 的用法笔记250417
  • Pikachu靶场-CSRF
  • DSO:牛津大学推出的物理一致性3D模型优化框架
  • ubuntu 查看现在服务使用的端口
  • 签到功能---实现签到接口
  • Unity基于屏幕空间的鼠标拖动,拖动物体旋转
  • 强化学习算法系列(五):最主流的算法框架——Actor-Critic算法框架
  • 论文阅读VACE: All-in-One Video Creation and Editing
  • 用Python Pandas高效操作数据库:从查询到写入的完整指南
  • 音视频相关协议和技术内容
  • 智能体开发的范式革命:Cangjie Magic全景解读与实践思考
  • 游戏盾和高防ip有什么区别
  • CSS进度条带斑马纹动画(有效果图)
  • 云转型(cloud transformation)——不仅仅是简单的基础设施迁移
  • Java字符串处理
  • IntelliJ IDEA 2025.1 发布 ,默认 K2 模式 | Android Studio 也将跟进
  • XC7K410T‑2FFG900I 赛灵思XilinxFPGA Kintex‑7
  • BUUCTF-Web(21-40)
  • 计算机视觉——JPEG AI 标准发布了图像压缩新突破与数字图像取证的挑战及应对策略
  • HTTP 3.0 协议的特点
  • 图集|俄罗斯举行纪念苏联伟大卫国战争胜利80周年阅兵式
  • 山东14家城商行中,仅剩枣庄银行年营业收入不足10亿
  • 欧洲承诺投资6亿欧元吸引外国科学家
  • 人民日报钟声:中方维护自身发展利益的决心不会改变
  • 习近平向“和平薪火 时代新章——纪念中国人民抗日战争和苏联伟大卫国战争胜利80周年中俄人文交流活动”致贺信
  • 保利发展前4个月销售额约876亿元,单月斥资128亿元获4个项目