跳转到内容

Redis 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 个字段

Terminal window
# 写入
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/0
Terminal window
HSET product:001 stock 100 price 99
HINCRBY product:001 stock -1 # 库存减 1(原子操作)
HINCRBYFLOAT product:001 price 10.5 # 价格加 10.5
命令说明复杂度
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)

Hash 底层编码
├── listpack(紧凑列表)
│ → 字段数 ≤ 128 且所有值长度 ≤ 64 字节时使用
│ → 内存紧凑,适合小对象
└── hashtable(哈希表)
→ 超过上述阈值自动升级
→ 查找 O(1),但内存占用更大
Terminal window
HSET small name Alice age 25
OBJECT ENCODING small # → listpack
# 添加一个超长字段后自动升级
HSET small bio "This is a very long bio exceeding 64 bytes limit, triggers upgrade..."
OBJECT ENCODING small # → hashtable

@Autowired
private 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); // 库存减 1
@Service
@RequiredArgsConstructor
public 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);
}
}

// 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);
}
// 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);
}

String 存储对象(整体 JSON):

key = "user:1001"
value = '{"name":"Alice","age":25,"city":"Shanghai"}'
└────────── 整体读写,无法单独操作某个字段 ──────────┘
修改 age 的代价:GET → JSON.parse → 改值 → JSON.stringify → SET

Hash 存储对象(字段独立):

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”
Terminal window
# ❌ 劣质方案:每个字段独立一个 Key
SET 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 可一次原子更新多字段
对比项String(整体 JSON)String(多 Key)Hash
读取整个对象GET 一次 ✅N 次 GETHGETALL
更新单个字段GET + 反序列化 + SETSET 一次HSET 一次 ✅
原子更新多字段需要事务 / Lua需要事务 / LuaHSET 原生支持 ✅
内存占用紧凑 ✅最大(元数据重复)❌字段少时紧凑 ✅
序列化开销需要 JSON 序列化无 ✅
TTL 管理整体一个 TTL每个 Key 独立 TTL整体一个 TTL
推荐场景整体读取为主❌ 不推荐频繁更新单字段