从Redis 6.0开始,很多人升级后的目光都被多线程IO、ACL、客户端缓存这几个新特性吸走了,反而忽略了一个每天都在用、但经常被用变形的数据类型——hash。Redis的hash类型说简单也简单,就是一个field-value结构,但它在实际项目中承担的角色远比“另一个map”要重得多:用户信息缓存、购物车计数、订单明细、分布式锁重入、甚至很多高并发下的热点数据治理都能靠它实现。我用了这么多年Redis,回头看踩过的坑里,有很大一部分是和hash的使用姿势有关的。这篇我就结合Redis 6的实际环境,把hash的底层原理、常用命令、实战设计、性能排查完整捋一遍,希望能帮你少走一些弯路。
1. hash是什么,为什么说它是对象存储的天然载体
1.1 从底层编码看hash的两种形态
Redis的hash类型在官方文档里的定义是string类型的field和value的映射表,适合存储对象。这个定义很准确,但只说了一半。另一半在底层实现:Redis 6里,一个hash键根据数据规模会使用两种不同的内部编码方式,一种是紧凑的listpack(Redis 6.0以前叫ziplist),一种是标准的hashtable。
listpack本质上是一段连续内存,把字段名、字段值、长度信息按顺序紧凑排列。它的优点是内存占用低,读取时顺序扫描即可,在字段数量少、值很小的情况下性能并不差。但缺点是字段多了、值大了以后,顺序扫描的开销会明显上升。所以Redis设置了一个自动转换阈值:当hash中的字段数量超过hash-max-listpack-entries,或者某个字段值长度超过hash-max-listpack-value时,这个key会自动从listpack转为hashtable。hashtable就是典型的哈希表结构,字段多的时候查询复杂度是O(1),但每个节点有额外指针和内存分配开销。
我们可以通过object encoding命令查看当前hash键的编码类型。
127.0.0.1:6379> hset user:1001 name "tom" age 18 (integer) 2 127.0.0.1:6379> object encoding user:1001 "listpack"Redis 6中默认的hash-max-listpack-entries是128,hash-max-listpack-value是64。也就是说,如果字段数量在128以内,且每个字段和值的长度都不超过64字节,就保持listpack;一旦超了,就转为hashtable,这个转换是不可逆的。很多线上排查的“内存突然变大”问题,往往就是某些hash键字段值长度超过了阈值,编码发生切换导致内存翻倍。这一点在后面的性能章节我再详细展开。
理解了底层编码,就明白为什么hash适合做对象存储了:它天然具备“按字段名取属性值”的能力,不像一个大的JSON字符串需要整体读出来再解析。对象本身往往就是一组有名字的字段,hash把这个对应关系原封不动地表达出来。
1.2 为什么对象缓存不使用String直接存JSON
很多同学在刚接触缓存的时候,习惯把所有对象都用set user:1001 '{"name":"tom","age":18}'这样的方式存成JSON字符串。这种做法没有错,在对象整体更新、整体读取、很少修改单个字段的场景下,String存JSON反而简单粗暴、通用性最强。但如果你的业务是“每次只改年龄”或“每次只取名字”,String方案就会变得很别扭:你需要先把整个JSON取出来,反序列化成对象,修改字段后再序列化,再写回去。这个读改写的过程在高并发下很容易丢更新,还浪费了网络带宽。
hash的方式就不一样了,字段就是Redis hash里的field,你可以只hset user:1001 age 19,只hget user:1001 name。Redis本身保证这是原子的,不需要你在应用层做任何加锁或CAS。从网络角度说,传输的数据量也少得多,大对象场景下这个优势会被放得更大。
我把这两种方式的关键差异整理成一个表,方便你心里有个数。
| 维度 | String存JSON | hash存字段 |
|---|---|---|
| 整体写入性能 | 每次写全量JSON,性能一般 | 只写变更字段,性能更高 |
| 单字段读取性能 | 需要读出全量再解析 | 直接读单个field |
| 单字段原子更新 | 需要读改写,非原子 | HINCRBY/HSET原生原子 |
| 过期控制 | key级过期,简单 | key级过期,无法对field单独设置 |
| 内存占用 | 偶尔JSON冗余,整体略高 | listpack编码下非常紧凑 |
| 实现复杂度 | 需要序列化框架 | 需要field与对象属性的映射 |
从我的实践经验看,如果你的对象字段超过三五个、且存在单字段高频读写或按字段更新的需求,hash是更优解。如果对象是一次性写入、经常整体读取、很少修改,String JSON反而更省事。没有绝对的好坏,只是适用场景不同。Redis热词里经常出现序列化相关的坑,很多其实都发生在hash这种字段化存储上,比如用Jackson/Gson把对象序列化成hash时,嵌套对象该怎么处理,后面我也会聊到。
2. 和hash打交道的核心命令,这些足够覆盖日常开发
2.1 基础增删改查,必须搞清楚的写操作语义
hash的命令入门门槛不高,但有几个细节容易被忽略。先来看最基础的增删改查,我在命令行里直接演示。
# 设置单个字段 127.0.0.1:6379> hset user:1001 name "tom" (integer) 1 # 同时设置多个字段 127.0.0.1:6379> hmset user:1001 age 18 city "beijing" OK # 获取单个字段 127.0.0.1:6379> hget user:1001 name "tom" # 获取多个字段 127.0.0.1:6379> hmget user:1001 name age 1) "tom" 2) "18" # 判断字段是否存在 127.0.0.1:6379> hexists user:1001 name (integer) 1 # 删除字段 127.0.0.1:6379> hdel user:1001 city (integer) 1这里有几个要点需要特别注意。hset在对已存在的字段设置值时,返回0,表示没有新增字段而是覆盖了旧值;如果字段不存在则返回1。这个返回值常被用来判断“是不是第一次写入”,实现类似MySQL的INSERT ON DUPLICATE KEY UPDATE的效果,但要注意Redis内部并没有upsert这种概念,只是旧字段被覆盖了而已。hmset是hset的批量版本,但从Redis 4开始hset已经支持多个field-value参数,因此hmset在业务代码里完全可以被hset替代,官方也慢慢弱化它的存在感。
紧接着还有一个容易踩的坑:hgetall会把一个key下的所有字段和值都拉出来。字段少的时候没问题,一旦hash变成了大key,比如一个用户对象挂了上百个字段,hgetall就成了性能杀手。原因在于Redis是单线程模型,执行hgetall期间会阻塞整个Redis实例,字段越多,阻塞时间越长。所以我在生产环境里会明确要求:禁止使用hgetall获取大hash的全部字段,必须明确业务需要哪些字段,用hmget按需取;非要全量遍历时,用hscan增量取。
还有hscan,很多人把它和hgetall混为一谈,实际上hscan是游标式遍历,可以分批返回字段,可以在遍历过程中对key进行修改,具体用法后面讲大key排查时会详细展开。对于字段很少的小hash,hgetall完全没问题,但你需要知道这行命令背后有风险。
2.2 数字字段的原子操作,是计数场景的常青树
hash里还有一个其他数据类型羡慕的能力,就是对字段值做原子自增自减。假设你有一个商品库存hash:
127.0.0.1:6379> hset sku:2001 stock 100 (integer) 1 127.0.0.1:6379> hincrby sku:2001 stock -1 (integer) 99hincrby会把字段stock的值减1,整个过程是原子的,不需要担心并发覆盖。除了整数,hincrbyfloat还能对浮点数做自增。
127.0.0.1:6379> hset account:2001 balance 100.5 (integer) 1 127.0.0.1:6379> hincrbyfloat account:2001 balance 0.5 "101"这两个命令特别适合做购物车数量变更、积分累计、库存扣减、实时统计等场景。尤其配合Lua脚本时,可以实现“在hash里扣减库存并返回是否成功”这种原子操作。因为hash里的字段天然就是一个命名空间,用不同的field存储不同商品,可以极大减少key数量。比如购物车场景,常规方案是每个用户一个key,每个商品ID一个field,field的值是数量,这样整个购物车只有一个hash key而不是每个商品一个key,无论是内存开销还是Redis的key管理压力都会小很多。这个场景我在第三部分会详细写。
2.3 命名和写入细节,容易被忽视的隐性成本
hash的命令使用上,有几个隐性成本常年被忽视。第一个是field的命名长度,hash里的field和value一样都是字符串,field越长,listpack编码下占用的连续内存就越大,hashtable编码下需要计算的hash和节点存储也就越多。所以field设计要尽可能短,但短到失去可读性就得不偿失。一般我会建议在业务上下文明确的情况下,用简短的业务缩写,比如商品ID直接用sku:1001,而不是product_id_1001。
第二是批量命令的切片问题。hmset/hmget一次可以传很多字段,但Redis命令本身有网络包大小限制,而且一次命令执行时间也不能太长。如果字段数量特别大,比如一次hmget上千个字段,Redis处理时间会显著上升。我们要学会通过mget/hmget的切片控制,分多批跑,或者用pipeline合并。关于pipeline,我后面会专门说。
第三是过期时间的问题。hash没有field级别的过期功能,整个key只有一个TTL。如果你需要让某个字段自动过期,常规做法是用一个独立的hash记录写入时间,配合定时任务扫描清理,或者干脆用String类型存这个字段。很多人一上来就想让hash里的field自动过期,这是不现实的,设计上就得规避。
第四是空hash问题。当hash里最后一个字段被hdel删除后,这个hash key会自动消失。这是Redis的回收机制,正常情况下没有问题,但如果你依赖key存在性判断对象是否存在,要注意:可能你只是想清空部分字段,结果key没了,后续hget返回nil。这个也符合预期,但容易让测试人员误以为是bug。
3. 从项目实战看hash的高频应用场景
3.1 对象缓存,hash最经典的主场
对象缓存是hash最经典的应用,没有之一。假设我们要缓存用户信息,用户对象有uid、name、level、score四个字段。用hash存,就是hset user:1001 name tom level 1 score 2000。查询时,如果业务只需要展示用户名,可以只hget user:1001 name,不需要把整个用户对象拉出来反序列化。
但这里有一个对象设计上的问题,很多新手会卡住:如果用户对象里有嵌套结构,比如profile字段本身又是一个JSON对象,该不该用hash的field去存这个嵌套对象?我的经验是嵌套对象单独存成一个hash,或者把该字段序列化成JSON字符串存进hash的value。原因是hash本身是两层结构,field这一层没有支持嵌套的“二级field”,强行把一个对象压成字符串存进value,虽然能work,但这部分就无法利用hash的字段级操作了。比如用户信息里的收货地址也是个JSON结构,你存入user:1001的fieldaddress里,想修改地址的城市名,就只能读出整个地址字符串再解析再写回,性能和原子性都不理想。更合理的方式是单独给地址建一个hash,用address:1001当key,城市、街道、手机号当field,然后把user:1001里的address字段只存一个引用标识,或者干脆不存。
在缓存读取的时序上,我强烈建议设计一个统一的对象缓存工具类,封装hash的读写,对上层业务屏蔽命令细节。这个工具要做三件事:读取时判断字段是否存在,为空则加载DB并回填缓存;更新时先更新DB再更新hash中的对应字段;删除时直接把整个key删掉。这种封装能减少团队里到处写Redis命令导致的脏代码,也方便统一加监控埋点。实际操作中,我还会在写入时额外维护一个类似updatedAt的字段,便于后续排查缓存和DB不一致的时间窗口。
3.2 购物车和计数器,一个hash顶一片String key
购物车是一个非常典型的hash使用场景。对每个用户维护一个hash key,比如cart:1001,商品ID作为field,商品数量作为value。用户加购商品时就是hincrby cart:1001 2002 1,减少就是hincrby cart:1001 2002 -1,查询购物车明细用hgetall cart:1001(需要注意大key问题,一般购物车字段数量不会太多)或hscan分页。相比给每个用户每个商品建立一个String key,hash的优势非常明显:key数量少,内存占用低,一个购物车对象可以用del一次清空,也方便统计某用户购物车里的商品总数。
同样,计数器场景也能用hash达到类似的效果。比如一个活动页面,需要记录每个用户每天的点击次数,可以用incr给每个用户建一个key,但更好的方案是hincrby user:1001 click:20250601 1,把用户ID当key,日期当field,这样同一个用户的多天计数都在一个hash里,后续查最近几天点击情况只要hmget几个field就行。当然,如果用户量极大且每个用户只维护少量天数,也可以反过来,把日期当key,userID当field。设计哪种从性能角度都可行,关键是结合你的主要查询路径:是“查某用户的历史记录”还是“查某天的全量用户”,它们对应的key-field设计方向刚好是反的。
hash做计数还有一个隐藏好处:hincrby天然支持负数,减库存时不会出现负数,因为它只做自增运算,能否出现负数完全由业务控制。如果业务要求“不允许扣成负数”,我们可以搭配Lua脚本,先hget再判断再hincrby,在脚本里保证原子性。这比用String型incr再额外判断返回值要清晰得多。
3.3 分布式锁里的hash身影,重入锁和锁续期
很多文章讲Redis分布式锁,张口就是setnx加过期时间,这是最基础的版本。但如果你需要支持可重入锁,或者需要给锁增加一些元信息,hash就派上用场了。比如Redisson的可重入锁实现,就用了hash结构,key是锁的名称,field是持有锁的线程标识,value是重入次数。
具体做法是:每次加锁,先判断锁是否存在;不存在则用hset lock:order:1001 threadId 1并设置过期时间;如果存在但field相同,则hincrby lock:order:1001 threadId 1,表示重入一次;解锁时则hincrby lock:order:1001 threadId -1,当计数值降到0时del整个key。这个过程配合Lua脚本,可以在Redis服务端原子完成,避免并发问题。这种方式比单纯的String型锁多了一个“计数维度”,应用场景很有价值。
我自己在项目里也遇到过类似需求:同一个请求线程在多个方法里尝试获取同一把锁,如果锁不支持重入,就可能造成自死锁。用hash实现重入锁后,锁的持有次数由hash的value字段表达,既避免了String锁的非重入问题,也能清楚看到当前锁被谁持有、重入了几次。需要提醒的是,分布式锁的过期时间续期也很重要,如果业务执行时间超过锁的TTL,锁会被自动释放,这时候可以通过Lua脚本读取hash里的线程标识,如果还是当前线程持有就pexpire续期。这就是所谓的看门狗机制,核心逻辑都离不开hash里的field信息。
3.4 缓存治理与序列化,Java客户端里最容易翻车的地方
讲hash就不得不提序列化。在Redis命令行里,所有field和value都是字节序列,但到了Java、Go这些客户端里,我们通常都会配置序列化器。热词里反复出现“redis序列化”和“redis序列化 乱码”,其实大多数都和hash有关。
以Spring Data Redis为例,如果全局为hash的field和value配置了JDK序列化器,那么你用redisTemplate.opsForHash().put("user:1001", "name", "tom"),最终写入Redis的命令不是hset user:1001 name tom,而是hset user:1001 \xAC\xAC... \xED\x00...,一长串二进制乱码。这样不仅浪费内存,还会导致其他语言客户端完全读不懂这个key。正确的做法是给hash的field和value单独配置String序列化器,通常就是StringRedisSerializer;如果value想要存对象,可以选择JSON序列化器,但要注意此时对象需要能正确反序列化成目标类型。
我在实践中确认过,最省心的方案是:key使用String序列化器,hash的field使用String序列化器,value根据业务自行决定,如果是简单对象就用JSON字符串。如果确实需要将一个Java对象整体序列化到hash的某个字段中,建议单独使用GenericJackson2JsonRedisSerializer或自定义的序列化工具,但必须提前约定好类型的存储方式,不然反序列化时容易撞上类型信息丢失的问题。序列化这个环节,建议在项目初期就通过redis-cli --raw检查一下Redis里实际存的字节内容,别等到线上查数据发现都是乱码才后悔。
4. hash的性能、内存与高并发注意事项
4.1 编码选择,为什么listpack转hashtable后内存会翻倍
前面提到hash底层有listpack和hashtable两种编码。listpack编码下,所有数据都挤在一块连续内存里,没有多余指针,内存利用率极高;hashtable编码下,每个键值对都是一个节点,每个节点还要额外存储链表指针和哈希表结构,内存开销会明显增加。因此当编码从listpack切到hashtable,内存可能直接翻倍甚至更多,尤其是在value值都很小的情况下,额外指针的占比会非常大。
Redis 6里,控制这个动作的配置项是hash-max-listpack-entries和hash-max-listpack-value。在生产环境,如果业务上的hash字段数量或value长度不会大幅波动,我会根据实际情况调整这两个参数。比如我知道某个hash字段数量最多50个、值都比较短,那完全可以把hash-max-listpack-entries保持在默认值128,没必要改。但如果业务上hash字段数量可能长期在几百个,那就不能为了省内存硬把阈值调高,否则listpack变成巨大的连续块,每次更新都可能触发内存复制,得不偿失。更合适的做法是接受hashtable,并使用合理的field命名长度来降低内存。
要查看当前key的编码方式,用object encoding命令;要估算一个key的内存占用,Redis 4.0以后提供了memory usage命令:
127.0.0.1:6379> memory usage user:1001 (integer) 73这个数值可以帮助我们对比不同编码下的内存变化,但要注意它不是绝对精确,而且对一个包含大量field的hash执行时也可能带来一定开销,生产环境慎用。另外,redis-cli --bigkeys会扫描整个实例,按数据类型找出“大key”样例,hash类型会展示较大field数量的key,这对巡检很有帮助。
4.2 大key,hash最大的敌人
hash字段数量膨胀到一定程度,就会变成大key。这也是Redis运维中非常常见的问题:一个用户hash字段几十个,没问题;但如果把一周的登录记录、一天的用户行为都塞进同一个hash,光字段数量可能就几十万,操作时延迟飙升、内存分配频繁、主从同步压力大,甚至BGSAVE都可能变慢。
大key的解决方案,常规思路是分片或拆分。先说拆,把不相关的字段按业务维度拆成多个hash key,比如用户基础信息和用户行为记录分开,每个key只保留一类字段。比如一个订单hash,可能包括订单状态、支付时间、物流公司、物流单号等,但如果业务上经常只更新物流相关字段,就应该拆成order:1001:base和order:1001:logistics两个hash,避免所有更新都独占整个key的写锁。
另一个思路是hash分片,也叫hash slot。当一个key的field数量即使按业务维度拆分后仍可能很大,比如某个活动的参与用户计数,一个key下可能几十万field,这种时候可以把原来的一个hash拆成多个分片hash,比如根据用户ID的hash值取模,分成16个分片,每个分片key负责部分field。查询时先定位分片再操作。分片会增加客户端复杂度,但能显著降低单key压力。在Redis cluster模式下,分片还有助于让数据分布更均匀,避免单节点热点。
还有一个常见的做法是限制hash的field数量,比如约定一个hash最多1000个field,超过就新建一个key,key名里带上序号。这个方法适合日志、流水、事件这类持续增长的数据,查询时需要知道具体在哪个分片。
4.3 高并发读写,hash和缓存一致性怎么平衡
高并发场景下,hash做缓存最核心的矛盾在于:业务需要先更新数据库,再更新缓存,还是先更新缓存,再更新数据库?我的建议是:缓存里存的是数据库的投影,必须把数据库当作数据源,hash的更新应该在数据库事务成功后再执行。但这个时序不能保证绝对一致,因为缓存更新也可能失败。
更稳妥的方案是cache aside:读的时候先读hash,miss了就从DB加载并回填;写的时候先写DB,再删除缓存key,让下次读的时候回填。这个删除动作要留意“先删缓存再写DB”和“先写DB再删缓存”的坑,比较推荐的是“先写DB再删缓存”,虽然是两个步骤,中间有一条极短的窗口可能读到旧值,但至少不会出现长期脏数据。如果对一致性要求更高,可以使用canal这类binlog监听工具,把数据库变更异步同步到Redis的hash中,这样业务代码里连更新缓存的动作都可以省略。
高并发下,还有一个hash专门优势值得提:因为hash可以在不整体读出的情况下更新单字段,所以缓存更新的网络包非常小、操作时间短,对Redis单线程模型特别友好。如果是String存JSON,一个用户对象更新一个字段就得写整个对象,持久化文件、网络传输、命令执行的时间都会显著增加。因此高并发系统里,把热点对象缓存在hash里反而能降低Redis的CPU和带宽消耗,当然前提是field设计合理。
我自己在实际压测中遇到过这样的场景:一个商品详情页,每秒几万次请求,每次请求需要读商品名称、价格、库存三个字段。如果用String JSON,每次请求都要把整个商品JSON拉到客户端再解析;改用hash后,一次hmget三个字段只返回三个小字符串,响应体和RT都下降了一个量级。这类优化不需要换架构,只是把数据类型换掉,收益却非常直接。
5. 常见问题排查与进阶建议
5.1 典型问题速查表,照着排查能省不少事
我在维护Redis的过程中,把遇到过的一些hash相关的高频问题整理成了表格,方便你在故障时直接对照。
| 问题现象 | 常见原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Redis内存突然翻倍 | listpack转hashtable,字段值长度超阈值 | object encoding key查看编码 | 调整value长度或字段数量,避免反复转换 |
| 单个命令延迟飙升 | hgetall拉取大hash所有字段 | redis-cli --bigkeys扫大key | 改用hmget按需取,或用hscan分页 |
| 客户端看到二进制乱码 | hash field/value使用了JDK默认序列化器 | redis-cli --raw查看原始数据 | 使用StringRedisSerializer或JSON序列化器 |
| 并发下库存或计数不准确 | 读取后修改不是原子操作,使用了get+set | 检查是否用了hincrby | 改为hincrby或Lua原子操作 |
| hash key意外消失 | 最后一个字段被hdel删除,key自动回收 | 检查业务代码是否有删除动作 | 如需要保留空key,单独维护占位字段 |
| 锁无法重入或提前释放 | 使用了不支持重入的String型锁,或没有续期 | 查看锁实现代码 | 使用hash存储线程标识和重入次数 |
| 大key导致主从延迟 | hash字段数量过大,同步时RDB/AOF压力大 | 观察慢日志和同步延迟 | 按业务拆分或分片hash |
这张表是我个人的使用经验总结,不代表所有场景都适用,但排查思路是通用的。只要问题定位到hash类型,先确认编码、字段数量、value长度、序列化配置这四个维度,大部分问题都能找到答案。
5.2 实测经验,hash字段数量和内存怎么评估
从内存评估的角度,我通常会给新项目一个默认约束:单个hash的field数量不要超过1000,单个field和value尽量不要超过200字节。这样即使保持listpack编码,也能在一个合理的内存范围之内。如果业务必须超过这个值,就要评估是否需要拆分。具体内存占用,除了memory usage命令,也可以参考Redis官方文档中的估算公式,但公式会随版本变化,实际最好通过实验数据验证。
我做过一个小实验,创建一个10万字段的hash,每个field和value长度都约8字节,内存占用大约在15MB左右;而同样的数据拆成100个hash,每个1000字段,总体内存反而会高一些,因为每个key的开销和hashtable的元数据会重复。所以拆key并不总是能省内存,它主要是为了降低单key操作延迟和避免阻塞。内存和性能是两回事,设计时要分清楚当前瓶颈是哪一个。
在field数量的监控上,我建议定时任务扫描所有hash size比较大的key,比如用hscan或hstrlen批量统计,一旦发现field数量超阈值就告警。这个做法比等--bigkeys跑一遍再人工看要主动得多。另外,在日志里也可以加上hash的field数量埋点,比如每次写hash后异步上报一个样本,这样后续排查某个key是什么时候开始变大的,会容易很多。
5.3 让hash操作更高效的三个小习惯
最后分享几个我在代码里沉淀下来的小习惯,它们很轻量,但能让hash操作更高效。
第一是批量操作尽量走pipeline。如果业务需要一次更新多个hash的多个字段,或者读取多个key中的几个field,用pipeline把多个命令打包一起发,能极大减少RTT。比如购物车结算时,需要同时读取多个购物车hash,如果循环一个个hgetall或hmget,慢得让人心慌。用pipeline后,整个批量读取只消耗一次网络往返。这在高并发服务里效果非常明显。
第二是原子性要求高的场景用Lua脚本。Redis 6里Lua脚本的编写和使用都很成熟,可以在一次脚本里完成多个hash操作。比如扣减库存、校验状态、记录日志,一个Lua脚本搞定,整个脚本原子执行,不会有并发进程插进来。热词里也专门出现过“redis lua”,说明这个话题确实是大家关心的。实际编码时要注意Lua脚本里调的key要提前用KEYS数组传进去,不要在脚本里用字符串拼key,否则Redis Cluster下会报跨slot错误,线上运行也会有问题。
第三是设计field时预留扩展位。有人在设计hash时会犯一个错:field只表达当前需求,完全不考虑后续扩展。比如一开始只存价格,field就叫price,过两天要存一个原价、一个成交价,就又得加字段。其实hash加字段成本很低,field命名不用吝啬,短但语义清晰最好。可能的话,用前后缀区分业务维度,比如cnt_20250601表示某天计数,比直接20250601更不容易和其他字段混淆。hash的好处是加字段不会影响已有字段,所以设计时可以稍微大胆一点,但别太臃肿。
我这里最后想特别提一下,刚开始用hash的时候,很容易把组织和规范方面的力气省掉,觉得Redis就是KV,随手怎么存都行。但一旦对象字段多起来、访问频次高起来,脏数据、大key、序列化问题都会找上门。多花一点时间在hash的设计上,后面运维和开发都能轻松很多。
另外,Redis 6之后官方对hash的优化虽然没有翻天覆地的新命令,但稳定性已经很好了,我们在生产环境大规模使用的经验也验证了这一点。如果你在项目里已经遇到了String JSON存储的痛点,不妨试着把高频读写的对象切到hash上,很可能会有眼前一亮的感觉。