Redis Hash 类型
1. Hash 类型概述
Section titled “1. Hash 类型概述”Hash 是 Redis 中存储字段-值映射的数据类型,结构上类似 Java 的 HashMap。一个 Hash key 下可以存储多个 field-value 对,非常适合存储对象的各个属性。
Hash 结构示意:
key = "user:1001"├── field: "name" → value: "Alice"├── field: "age" → value: "25"├── field: "email" → value: "alice@example.com"└── field: "city" → value: "Shanghai"| 特点 | 说明 |
|---|---|
| 字段独立存储 | 可单独读写某个字段,无需操作整个对象 |
| 内存高效 | 字段数量少时使用 listpack 压缩存储 |
| 无嵌套 | field 的 value 只能是字符串,不支持嵌套结构 |
| 上限 | 单个 Hash 最多 2^32 - 1 个字段 |
2. 常用命令
Section titled “2. 常用命令”2.1. 基础读写
Section titled “2.1. 基础读写”# 写入HSET user:1001 name Alice # 设置单个字段HSET user:1001 name Alice age 25 city Shanghai # 设置多个字段(Redis 4.0+)HSETNX user:1001 email "a@b.com" # 字段不存在才设置
# 读取HGET user:1001 name # 获取单个字段 → "Alice"HMGET user:1001 name age city # 批量获取 → ["Alice", "25", "Shanghai"]HGETALL user:1001 # 获取所有字段和值HKEYS user:1001 # 获取所有字段名HVALS user:1001 # 获取所有值HLEN user:1001 # 获取字段数量
# 删除HDEL user:1001 city # 删除指定字段(可批量)
# 判断HEXISTS user:1001 name # 字段是否存在 → 1/02.2. 数值操作
Section titled “2.2. 数值操作”HSET product:001 stock 100 price 99
HINCRBY product:001 stock -1 # 库存减 1(原子操作)HINCRBYFLOAT product:001 price 10.5 # 价格加 10.52.3. 命令速查
Section titled “2.3. 命令速查”| 命令 | 说明 | 复杂度 |
|---|---|---|
HSET key field value | 设置字段 | O(1) |
HGET key field | 获取字段值 | O(1) |
HMGET key f1 f2 | 批量获取 | O(N) |
HGETALL key | 获取全部 | O(N) |
HDEL key field | 删除字段 | O(1) |
HEXISTS key field | 字段是否存在 | O(1) |
HLEN key | 字段数量 | O(1) |
HINCRBY key field n | 字段原子自增 | O(1) |
3. 底层编码
Section titled “3. 底层编码”Hash 底层编码├── listpack(紧凑列表)│ → 字段数 ≤ 128 且所有值长度 ≤ 64 字节时使用│ → 内存紧凑,适合小对象│└── hashtable(哈希表) → 超过上述阈值自动升级 → 查找 O(1),但内存占用更大HSET small name Alice age 25OBJECT ENCODING small # → listpack
# 添加一个超长字段后自动升级HSET small bio "This is a very long bio exceeding 64 bytes limit, triggers upgrade..."OBJECT ENCODING small # → hashtable4. SpringBoot 实战
Section titled “4. SpringBoot 实战”4.1. 基础操作
Section titled “4.1. 基础操作”@Autowiredprivate StringRedisTemplate stringRedisTemplate;
HashOperations<String, String, String> hashOps = stringRedisTemplate.opsForHash();
// 设置字段hashOps.put("user:1001", "name", "Alice");hashOps.put("user:1001", "age", "25");
// 批量设置hashOps.putAll("user:1001", Map.of( "name", "Alice", "age", "25", "city", "Shanghai"));
// 读取String name = hashOps.get("user:1001", "name");
// 批量读取List<String> values = hashOps.multiGet("user:1001", List.of("name", "age"));
// 获取全部Map<String, String> all = hashOps.entries("user:1001");
// 删除字段hashOps.delete("user:1001", "city");
// 原子自增hashOps.increment("product:001", "stock", -1L); // 库存减 14.2. 封装用户缓存服务
Section titled “4.2. 封装用户缓存服务”@Service@RequiredArgsConstructorpublic class UserCacheService {
private final StringRedisTemplate redisTemplate; private static final String KEY_PREFIX = "user:"; private static final long EXPIRE_HOURS = 2;
// 存储用户对象(各字段独立存入 Hash) public void cacheUser(User user) { String key = KEY_PREFIX + user.getId(); Map<String, String> map = Map.of( "id", String.valueOf(user.getId()), "name", user.getName(), "age", String.valueOf(user.getAge()), "email", user.getEmail() ); redisTemplate.opsForHash().putAll(key, map); redisTemplate.expire(key, EXPIRE_HOURS, TimeUnit.HOURS); }
// 读取整个用户对象 public User getUser(Long userId) { String key = KEY_PREFIX + userId; Map<Object, Object> map = redisTemplate.opsForHash().entries(key); if (map.isEmpty()) return null; return User.builder() .id(Long.parseLong((String) map.get("id"))) .name((String) map.get("name")) .age(Integer.parseInt((String) map.get("age"))) .email((String) map.get("email")) .build(); }
// 只更新单个字段(无需读写整个对象) public void updateEmail(Long userId, String newEmail) { String key = KEY_PREFIX + userId; redisTemplate.opsForHash().put(key, "email", newEmail); }}5. 典型业务场景
Section titled “5. 典型业务场景”5.1. 购物车
Section titled “5.1. 购物车”// key 格式:cart:{userId},field:商品 ID,value:数量
public void addToCart(Long userId, Long productId, int quantity) { redisTemplate.opsForHash() .increment("cart:" + userId, String.valueOf(productId), quantity);}
public Map<Object, Object> getCart(Long userId) { return redisTemplate.opsForHash().entries("cart:" + userId);}
public void removeFromCart(Long userId, Long productId) { redisTemplate.opsForHash().delete("cart:" + userId, String.valueOf(productId));}
public void clearCart(Long userId) { redisTemplate.delete("cart:" + userId);}5.2. 多维度统计计数
Section titled “5.2. 多维度统计计数”// key = "stats:article:1001",field = "views" / "likes" / "comments"// 一个 key 聚合同一对象的多个计数器,比多个 String key 更紧凑
public void incrView(Long articleId) { redisTemplate.opsForHash() .increment("stats:article:" + articleId, "views", 1);}
public Map<Object, Object> getStats(Long articleId) { return redisTemplate.opsForHash().entries("stats:article:" + articleId);}6. Hash vs String 存储对象
Section titled “6. Hash vs String 存储对象”6.1. 存储结构对比
Section titled “6.1. 存储结构对比”String 存储对象(整体 JSON):
key = "user:1001" │ ▼value = '{"name":"Alice","age":25,"city":"Shanghai"}' └────────── 整体读写,无法单独操作某个字段 ──────────┘
修改 age 的代价:GET → JSON.parse → 改值 → JSON.stringify → SETHash 存储对象(字段独立):
key = "user:1001" │ ├── field: "name" → "Alice" ├── field: "age" → "25" ← HSET 直接改这一个字段,其余不动 └── field: "city" → "Shanghai"6.2. 多个独立 String Key vs 一个 Hash
Section titled “6.2. 多个独立 String Key vs 一个 Hash”# ❌ 劣质方案:每个字段独立一个 KeySET user:1001:name "Alice"SET user:1001:age "25"SET user:1001:email "alice@example.com"# 问题:3 个 Key 各自携带元数据(key名、TTL、LRU时钟),内存浪费# 3 次网络请求才能读完,无法原子更新多个字段
# ✅ 优质方案:Hash 聚合同一对象HSET user:1001 name "Alice" age "25" email "alice@example.com"# 1 个 Key,元数据只有一份,HSET 可一次原子更新多字段6.3. 综合对比
Section titled “6.3. 综合对比”| 对比项 | String(整体 JSON) | String(多 Key) | Hash |
|---|---|---|---|
| 读取整个对象 | GET 一次 ✅ | N 次 GET | HGETALL |
| 更新单个字段 | GET + 反序列化 + SET | SET 一次 | HSET 一次 ✅ |
| 原子更新多字段 | 需要事务 / Lua | 需要事务 / Lua | HSET 原生支持 ✅ |
| 内存占用 | 紧凑 ✅ | 最大(元数据重复)❌ | 字段少时紧凑 ✅ |
| 序列化开销 | 需要 JSON 序列化 | 无 | 无 ✅ |
| TTL 管理 | 整体一个 TTL | 每个 Key 独立 TTL | 整体一个 TTL |
| 推荐场景 | 整体读取为主 | ❌ 不推荐 | 频繁更新单字段 |