Redis hash 在一个键下组织可以分别访问的字段。

Redis hash 用一个键装多个字段

Redis hash 是一个键下面可按字段访问的记录,不是自动校验模式的对象,也不是天然支持嵌套路径的文档。当调用方只需要部分属性、又不想替换整条记录时,它很有用。但是否划算取决于访问模式、字段大小和返回值语义,而不是只要选择 hash 类型,就自动得到廉价读写或完整业务一致性。 只读取调用方真正需要的字段 HGET 获取一个字段,HMGET 按请求顺序获取选定字段,HGETALL 则返回整个映射。在 redis-py 的二进制响应模式下,缺失字段表示为 None,存在但为空的值表示为空字节串。直接判断真假会把这两种情况错误地合并;对不存在的键执行 HMGET,同样得到对应位置的缺失值,并不会自动创建 hash。 字段名和字段值本质上都是字节。名字中含点号仍是字面名称,Redis 不会把 name.first 解释为进入嵌套对象再找 first。同样,包含 JSON 文本的字符串键还是字符串,HGET 不会替你解析 JSON。序列化和内容校验属于应用职责,“没有提供属性”和“明确写入空值”也应保留区别。 更新操作有精确的返回语义 HSET 只修改指定字段,未指定的字段保持不变。一次多字段 HSET 是一个命令,但其整数回复统计新增字段数量,而不是成功修改的总数。替换已有 name 完全可能成功返回零;如果把零视为失败,就会制造多余重试、错误告警,甚至触发不必要的补偿动作。 HINCRBY 在一个命令内完成整数递增,字段不存在时从零开始,拒绝非整数值以及超出有符号六十四位范围的结果。它避免的是这一项递增的读改写竞争,并不把外围的业务检查也变为原子操作。分别执行 HGET、在客户端计算、再执行 HSET,仍可能覆盖其他客户端已完成的修改。 HDEL 的返回值统计实际删除的字段,删除最后一个字段会让整个键消失。因此,不能靠空 hash 长期占位,表示某条实体存在。键级过期同样会删除其字段;当前 Redis 还支持字段级截止时间,不能沿用“hash 字段绝对不能单独过期”的旧结论。下面的实验没有设置任何过期时间,也不把结果作为生命周期验证。 ...

二月 24, 2028 · 3 分钟 · XB Field Notes
传输分块可能停在 UTF-8 序列中间,解码器需要保留尚未完整的字节。

字节边界与解码规则一致,UTF-8 才能完整保留文本

UTF-8 用一到四字节编码 Unicode 标量值。ASCII 字符保持熟悉的单字节表示,许多其他字符则需要更长序列。它适合文本交换,却不会让任意字节自动成为有效文本,也不会让每个字节偏移都成为字符边界。正确处理需要编码约定、符合契约的有效性检查,以及对原始字节和已经解码文本的明确区分。 坏掉的样子 文件或网络读取可以停在多字节序列中间。每块独立解码,会拒绝原本完整且有效的整个字节流;如果每块都立即替换错误,还可能破坏本应与下一块组成字符的字节。流式解码器需要保留未完成输入,等后续数据到来再处理,并在流结束时检查是否仍有残缺序列。分块边界是传输细节,不是文本天然边界。 UTF-8 的起始与续字节具有可识别模式,但这不使任意切片安全。直接截断编码后的字节串,可能把一个序列切成两半。使用另一种编码成功解码,也不能证明解释正确。Latin-1 能映射每个字节,因此把 UTF-8 误解为 Latin-1,仍会得到合法 Unicode 字符串,却可能已经成为乱码。有效字符串和正确文本不是同一结论。 编码有效性还不同于文本等价与显示宽度。一个看起来带重音的字母,可以由单个码点表示,也可以由基础字母和组合标记表示。规范化能够让选定形式便于比较,但 UTF-8 解码不会自动完成规范化。同样,字节顺序标记可能成为字符串开头的实际字符,除非选定编解码器明确消费它。不能把这些处理都笼统称为“转换编码”。 快速验证 将脚本保存为 check-utf8.py,准备 Docker 后执行 python3 check-utf8.py。固定 Python 3.11.16 容器在无网络环境下运行标准库编码与 Unicode 操作,输入都是内存中明确给出的字节和字符串。实验不迁移数据库,也不转换现有文件;断言依赖字节和码点,不要求终端能够正确绘制某个表情图形: import json,subprocess,uuid image="python@sha256:528257d48c1da0dcecc2e725d1ae34498d60c965f1241e39cd6a85a8859bdf84" program=r""" import codecs,json,unicodedata text='A中🙂';encoded=text.encode('utf-8');assert len(text)==3 and len(encoded)==8 assert encoded.hex()=='41e4b8adf09f9982' and encoded.decode('utf-8')==text pieces=[encoded[:2],encoded[2:5],encoded[5:]] split_errors=0 for piece in pieces: try:piece.decode('utf-8') except UnicodeDecodeError:split_errors+=1 assert split_errors==3 decoder=codecs.getincrementaldecoder('utf-8')(errors='strict');decoded=[] for piece in pieces:decoded.append(decoder.decode(piece,final=False)) decoded.append(decoder.decode(b'',final=True));assert ''.join(decoded)==text rejected={} for label,raw in {'overlong':bytes.fromhex('c0af'),'surrogate':bytes.fromhex('eda080'), 'too_large':bytes.fromhex('f4908080'),'truncated':bytes.fromhex('e4b8')}.items(): try:raw.decode('utf-8');raise AssertionError('invalid UTF-8 accepted') except UnicodeDecodeError as error:rejected[label]=error.reason bad_tail=codecs.getincrementaldecoder('utf-8')();assert bad_tail.decode(bytes.fromhex('e4b8'),final=False)=='' try:bad_tail.decode(b'',final=True);raise AssertionError('incomplete final sequence accepted') except UnicodeDecodeError:final_rejected=True mojibake=encoded.decode('latin-1');assert mojibake!=text and mojibake.encode('latin-1')==encoded decomposed='e\u0301';composed='é';assert decomposed!=composed and unicodedata.normalize('NFC',decomposed)==composed bom=b'\xef\xbb\xbf'+encoded;assert bom.decode('utf-8').startswith('\ufeff') and bom.decode('utf-8-sig')==text replacement=bytes.fromhex('ff41').decode('utf-8',errors='replace');assert replacement=='\ufffdA' print(json.dumps({'text':text,'code_points':len(text),'bytes':len(encoded),'hex':encoded.hex(), 'chunk_hex':[x.hex() for x in pieces],'independent_chunk_failures':split_errors,'incremental_output':decoded, 'invalid_rejections':rejected,'final_incomplete_rejected':final_rejected,'latin1_code_points':[hex(ord(c)) for c in mojibake], 'normalization_lengths':[len(decomposed),len(composed)],'bom_utf8_first':hex(ord(bom.decode('utf-8')[0])), 'utf8_sig_strips_bom':bom.decode('utf-8-sig')==text,'replacement':replacement},ensure_ascii=False)) """ name='xbcatm-socket-'+uuid.uuid4().hex[:10] try: result=subprocess.run(['docker','run','--rm','-i','--name',name,'--network','none', '--memory','128m','--pids-limit','64',image,'python3','-'],input=program, capture_output=True,text=True,timeout=60) assert result.returncode==0,result.stderr print(json.dumps(json.loads(result.stdout),ensure_ascii=False,sort_keys=True)) finally: subprocess.run(['docker','rm','-f',name],capture_output=True,text=True,timeout=10) 字符串 A中🙂 在 Python 中具有三个对应码点的元素,编码后为八字节,十六进制是 41e4b8adf09f9982。ASCII 占一字节,汉字占三字节,表情占四字节;重新解码返回原文。编码字节数和这些码点数量回答不同问题,也都不能自动等同于屏幕列宽。限制字段长度前,必须先明确限制使用哪种单位。 ...

十二月 2, 2026 · 1 分钟 · XB Field Notes