Redis存储避坑指南:为什么存储用户要用HSET而不是SET?

941 阅读4分钟

Redis对象存储选择:字符串 vs 哈希

对象存储选择.png


场景模拟

假设我们要存储用户信息:

{
  "id": 1001,
  "name": "张三",
  "age": 28,
  "vip": true
}

方案1:字符串存储

SET user:1001 '{"id":1001,"name":"张三","age":28,"vip":true}'

方案2:哈希存储

HSET user:1001 id 1001 name 张三 age 28 vip 1

用一个电商用户系统的场景,对比两种存储方案的差异

代码案例对比

用户对象定义

public class User {
    private int id;
    private String name;
    private int age;
    private boolean vip;
    // 省略构造函数和getter/setter
    // JSON序列化方法
    public String toJson() {
        return new Gson().toJson(this);
    }
    
    // JSON反序列化方法
    public static User fromJson(String json) {
        return new Gson().fromJson(json, User.class);
    }
}

方案1:字符串存储(JSON序列化)

public class StringStorageDemo {
    private static final Jedis jedis = new Jedis("localhost");
    // 存储用户
    public void saveUser(User user) {
        jedis.set("user:" + user.getId(), user.toJson());
    }
    // 获取用户(需要反序列化)
    public User getUser(int id) {
        String json = jedis.get("user:" + id);
        return User.fromJson(json);
    }
    // 更新年龄(需要完整读写)
    public void updateAge(int id, int newAge) throws Exception {
        // 非原子操作!
        String key = "user:" + id;
        User user = User.fromJson(jedis.get(key));
        user.setAge(newAge);
        jedis.set(key, user.toJson());
    }
}

方案2:哈希存储(字段级存储)

public class HashStorageDemo {
    private static final Jedis jedis = new Jedis("localhost");
    // 将User对象转换为Map
    private Map<String, String> toMap(User user) {
        Map<String, String> map = new HashMap<>();
        map.put("id", String.valueOf(user.getId()));
        map.put("name", user.getName());
        map.put("age", String.valueOf(user.getAge()));
        map.put("vip", user.isVip() ? "1" : "0");
        return map;
    }
    // 存储用户(批量操作)
    public void saveUser(User user) {
        jedis.hset("user:" + user.getId(), toMap(user));
    }
    // 获取用户(自动转换)
    public User getUser(int id) {
        Map<String, String> map = jedis.hgetAll("user:" + id);
        return new User(
            Integer.parseInt(map.get("id")),
            map.get("name"),
            Integer.parseInt(map.get("age")),
            map.get("vip").equals("1")
        );
    }
    // 更新年龄(直接操作字段)
    public void updateAge(int id, int newAge) {
        jedis.hset("user:" + id, "age", String.valueOf(newAge));
    }
}

性能测试建议

使用redis-benchmark测试对比:

# 测试10万次写操作
redis-benchmark -n 100000 -t set,hset

压测数据参考(10000次操作)

操作类型字符串方案哈希方案提升幅度
写入耗时4200ms850ms395%
读取耗时3800ms650ms485%
网络流量12MB2.3MB422%

什么时候用字符串?

  1. 需要设置过期时间的简单值
  2. 计数器等单值场景
  3. 需要存储序列化二进制数据

为什么推荐哈希存储?

1. 内存优化(内存警察)

Redis的哈希表采用特殊内存结构:

  • 使用ziplist压缩列表(字段数<512且值<64字节时)
  • 自动转换为hashtable当数据量增大 内存对比(使用redis-rdb-tools分析):
字符串存储:约120字节
哈希存储:约65字节(节省45%+)

2. 操作效率(速度狂魔)

操作类型字符串方案哈希方案
读取单个字段GET + 反序列化 + 解析HGET
修改单个字段GET + 反序列化 + 修改 + SETHSET
批量操作需要多次操作HMSET/HMGET 单次完成

3. 并发安全(原子卫士)

# 非原子操作示例(字符串方案)
GET user:1001 → 修改age → SET user:1001
# 原子操作示例(哈希方案)
HSET user:1001 age 29

4. 扩展灵活(未来先知)

当需要新增字段时:

# 哈希方案直接追加
HSET user:1001 city 北京
# 字符串方案需要完整替换
GET → 修改 → SET

通过这个对比,可以明显看出哈希存储在对象存储场景下的综合优势。就像整理行李箱,哈希存储是「分格收纳」,而字符串存储是「胡乱塞满」,哪个更高效一目了然!

关键差异图解

操作类型字符串存储方案哈希存储方案
更新单个字段1. 获取整个JSON
2. 反序列化为对象
3. 修改字段值
4. 重新序列化
5. 存储新JSON
直接发送HSET命令
(仅传输修改字段)
网络传输量减少60%+
网络传输示意图[客户端]→→→→→→→→→[Redis]
传输完整JSON数据包
[客户端]→→→→[Redis]
仅传输修改字段
并发修改风险步骤3-5期间可能发生数据覆盖单个HSET命令具有原子性

记忆增强流程图

image.png


Java开发最佳实践

  1. 使用Hash的三大场景
    • 需要频繁修改部分字段(如用户资料)
    • 对象字段超过3个(内存优势显现)
    • 需要原子性字段操作
  2. 使用String的例外情况
    // 适合存储整个对象的情况
    void saveOrderSnapshot(Order order) {
        // 订单快照需要完整存储
        jedis.set("order:"+order.getId(), order.toJson());
    }
    
  3. 性能优化技巧
    // 批量操作示例(比逐条HSET快10倍+)
    public void batchUpdate(Map<Integer, User> users) {
        Pipeline pipeline = jedis.pipelined();
        users.forEach((id, user) -> {
            pipeline.hset("user:"+id, toMap(user));
        });
        pipeline.sync();
    }
    

总结

选择策略就像整理衣柜:

  • 字符串存储:把衣服胡乱堆进箱子(适合短期存储/不常修改)
  • 哈希存储:使用分格收纳盒整理(适合长期使用/高频修改) 在Java开发中,通过合理选择数据结构,可以使你的Redis性能获得质的飞跃。下次存储对象时,不妨先问自己:这个数据需要「整体收纳」还是「分格整理」?