Redis 哈希把多个字段名及其字节值放在一个键下,适合按属性独立读取或修改的对象,例如拥有姓名和积分的用户资料。HGET 与 HSET 各自都只需要一条命令,并不会因为多了一层字段组织,就天然比字符串多一次网络往返。类型选择的依据是实际访问粒度,而不是把内部结构想象成额外的远程查询。
只读取和更新目标字段
HGET 获取一个字段,HMGET 按请求顺序获取指定字段,HGETALL 则返回所有字段和值。Redis 不会自动把某个字段值解释为嵌套对象,不会把带点的字段名当作对象路径,也不会自动为任意属性组合建立查询索引。字段如何命名、值如何序列化,仍然由应用负责。
HSET 可以在一条命令里写多个字段,未指定的字段不会被替换。它的整数返回值统计新增字段数,而不是所有发生变化的值:更新已有字段即使成功,也可以返回零。HDEL 返回实际删除的字段数;最后一个字段删除后,整个哈希键也会消失。因此,不能用一个没有字段的哈希作为长期存在的占位对象。
字段缺失和空值是两种状态。redis-py 关闭自动解码时,HMGET 对缺失字段返回 None,对已存在但内容为空的字段返回空字节串。直接用真假值判断会混淆两者,应按明确的返回值区分。HGETALL 对小对象很方便,但处理量和响应大小会随整个哈希增长;调用方只需要几个属性时,应该直接读取这些字段,避免每次搬运全部数据。
过期时间分为两层
键级过期会删除整个哈希,HSET 会保留该键已有的截止时间。从 Redis 7.4 开始,还可以通过 HEXPIRE 和 HPEXPIRE 给单个字段设置期限。普通 HSET 覆盖字段时会清除这个字段自己的过期时间,HINCRBY 则保留被修改字段的已有期限。字段 TTL 返回 -1,只表示它没有独立的字段期限,不代表它能逃过父键到期后整体删除。
Redis 8.0 及更新版本提供 HSETEX,可以把字段值与字段期限放在同一条命令里写入;其 KEEPTTL 选项能在替换字段值时保留已有字段截止时间。先 HSET、再 HPEXPIRE 仍然是两条操作,中间失败会产生值已经变更、期限尚未附加的窗口。应根据希望保留、重置还是新建期限来选命令,并在采用字段过期能力前核对实际部署版本,不能把键级与字段级期限混为一谈。
坏掉的样子
局部更新可以避免无意覆盖其他属性,却不能让 HGET、本地计算和 HSET 自动合并成一个原子操作。两个客户端可能先读到相同积分,各自加一后写回同一个结果,于是实际少计了一次。HINCRBY 把整数变更放在一条命令内完成,并拒绝非法整数内容。如果业务要求多个字段共同满足某个条件,仍需明确的原子操作协议,单纯把数据放入哈希并不能提供这种保证。
序列化的 JSON 字符串仍是字符串,对它执行 HGET 会报 WRONGTYPE。另一方面,单字段很大或字段数量不受控,也会增加传输和操作成本。一个哈希仍然是一个 Redis 键,不会自动成为跨节点拆分的数据方案。应用始终整块交换载荷时,可以使用不透明字符串;只有确实按属性独立访问,哈希带来的局部读写能力才有实际价值。
快速验证
原有实验记录使用 macOS、Python 3.14.4、Redis 8.10.1 和 redis-py 8.1.0;本轮审阅没有重新执行服务实验,下面的结果说明保留原有观察范围。准备 POSIX 环境、C 编译器、make、curl、tar 与 Python 3.10 或更新版本。安装步骤仅用 libc 构建 Redis 核心,并把客户端装入临时虚拟环境。
redis_lab=$(mktemp -d)
curl --fail --location --output "$redis_lab/redis.tar.gz" https://download.redis.io/releases/redis-8.10.1.tar.gz
tar -xzf "$redis_lab/redis.tar.gz" -C "$redis_lab"
make -C "$redis_lab/redis-8.10.1" build redis -j2 MALLOC=libc
export REDIS_SERVER="$redis_lab/redis-8.10.1/src/redis-server"
python3 -m venv "$redis_lab/venv"
"$redis_lab/venv/bin/python" -m pip install redis==8.1.0
在同一个终端使用 "$redis_lab/venv/bin/python" 执行下面的 Python 代码。临时 Redis 仅通过 /tmp 下的私有 Unix socket 通信,不开放 TCP,不安排快照,也不启用追加日志。程序退出时关闭自己的客户端,终止自己创建的进程,再移除临时运行目录。下载依赖需要网络,实验不会访问默认本机 Redis,也不会清理其他实例的数据。
from contextlib import contextmanager
import os
from pathlib import Path
import subprocess
from tempfile import TemporaryDirectory
import time
import redis
from redis.backoff import NoBackoff
from redis.retry import Retry
@contextmanager
def instance():
with TemporaryDirectory(prefix="redis-lab-", dir="/tmp") as directory:
endpoint = str(Path(directory) / "redis.sock")
process = subprocess.Popen([
os.environ["REDIS_SERVER"], "--port", "0",
"--unixsocket", endpoint, "--unixsocketperm", "700",
"--save", "", "--appendonly", "no", "--dir", directory,
"--maxmemory-policy", "noeviction",
], stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
client = redis.Redis(unix_socket_path=endpoint, decode_responses=False,
socket_timeout=2, retry=Retry(NoBackoff(), 0))
try:
deadline = time.monotonic() + 5
while True:
assert process.poll() is None, "Redis exited during startup"
try:
if client.ping():
break
except redis.ConnectionError:
if time.monotonic() >= deadline:
raise
time.sleep(0.02)
yield client
finally:
client.close()
if process.poll() is None:
process.terminate()
try:
_, errors = process.communicate(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.communicate()
raise
assert process.returncode == 0, errors.decode()
with instance() as client:
assert client.hset("profile", mapping={
"name": b"Ada", "score": b"0", "empty": b"",
}) == 3
assert client.hset("profile", "name", b"Ada L") == 0
assert client.hmget("profile", ["name", "empty", "missing"]) == [
b"Ada L", b"", None,
]
assert client.hlen("profile") == 3
print("HSET reports added fields, not updates; empty bytes differ from None")
assert client.pexpire("profile", 60000)
key_deadline = client.pexpiretime("profile")
assert client.hpexpire("profile", 30000, "name", "score") == [1, 1]
field_deadlines = client.hpexpiretime("profile", "name", "score")
assert all(value > 0 for value in field_deadlines)
assert client.hset("profile", "name", b"Grace") == 0
assert client.pexpiretime("profile") == key_deadline
assert client.hpttl("profile", "name") == [-1]
assert client.hincrby("profile", "score", 1) == 1
assert client.hpexpiretime("profile", "score") == [field_deadlines[1]]
print("HSET keeps key TTL but clears its field TTL; HINCRBY keeps field TTL")
assert client.hsetex("session", mapping={"token": b"A"}, px=60000) == 1
token_deadline = client.hpexpiretime("session", "token")
assert token_deadline[0] > 0
assert client.hsetex("session", mapping={"token": b"B"}, keepttl=True) == 1
assert client.hget("session", "token") == b"B"
assert client.hpexpiretime("session", "token") == token_deadline
print("HSETEX writes field value and TTL together; KEEPTTL preserves its deadline")
client.hset("race", "score", 0)
a = int(client.hget("race", "score"))
b = int(client.hget("race", "score"))
client.hset("race", "score", a + 1)
client.hset("race", "score", b + 1)
assert client.hget("race", "score") == b"1"
client.hset("race", "score", 0)
assert client.hincrby("race", "score", 1) == 1
assert client.hincrby("race", "score", 1) == 2
print("controlled stale-read schedule loses an update; two HINCRBY calls yield 2")
client.hset("bad", "score", b"3.5")
try:
client.hincrby("bad", "score", 1)
except redis.ResponseError:
pass
else:
raise AssertionError("HINCRBY requires an integer")
assert client.hget("bad", "score") == b"3.5"
client.set("blob", b'{"name": "Ada"}')
try:
client.hget("blob", "name")
except redis.ResponseError as error:
assert "WRONGTYPE" in str(error)
else:
raise AssertionError("a serialized string is not a Redis hash")
print("invalid counters and wrong key types are rejected")
client.hset("single", "field", b"value")
assert client.hdel("single", "missing") == 0
assert client.hdel("single", "field") == 1
assert client.exists("single") == 0
print("HDEL of the last field removes the key")
第一次 HSET 新增三个字段,返回三;第二次成功替换姓名,返回零。HMGET 的结果保留了空字节串与缺失值的区别。比较绝对截止时间可以看到,修改姓名保留了整键期限,却移除了姓名字段的独立期限;积分自增仍保留原字段期限。HSETEX 则验证了字段值和期限一次写入,以及随后在替换值时保留相同截止时间。
丢失更新部分刻意安排了两个假想客户端的读写交错,是可确定复现的操作顺序,不是多线程并发压测。两次先读后写最终只得到一,两条 HINCRBY 得到二。非法计数值和错误键类型均被拒绝;删除唯一字段后,对应键不再存在。这些断言证明的是命令行为,不是内存效率、吞吐量、集群扩展效果,也不能保证网络失败后重试不会重复计数。
来源
- Redis 哈希,字段式数据建模与命令行为,已核验 2026-09-08。
- Redis HSET,局部更新与新增字段计数,已核验 2026-09-08。
- Redis HEXPIRE,字段期限与覆盖行为,已核验 2026-09-08。
- Redis HSETEX,字段值和过期时间的原子更新及 KEEPTTL,已核验 2026-09-08。
- Redis HDEL,删除计数及最后一个字段的清理,已核验 2026-09-08。
- Redis 8.10.1 构建说明,只编译核心及分配器选择,已核验 2026-09-08。