主题
Redis 数据结构
把一个商品放进 Redis,最省事的做法是序列化成 JSON,再用 SET 保存。问题通常出现在下一步:只想改库存,却要读改写整个 JSON;要按销量排序,必须把所有商品读出来;要统计独立访客,保存每个访客又太贵。这时需要按操作选择数据结构,而不只是按数据长什么样选择。
本文讲知识地图列出的九种结构:String、Hash、List、Set、ZSet、Bitmap、HyperLogLog、Stream、Geo。目标是知道常用命令、底层如何存储,以及为什么同一份数据换一种组织方式,内存和延迟会变化。
编码结论固定在 Redis 8.2.2。 本文使用官方源码包,在 Linux/amd64 上以 libc 分配器编译,所有标为“实测”的结果来自临时单机实例。它是可复现的教学版本,不是当前生产升级建议;其他版本、配置、分配器下的编码和字节数需要重测。文末有完整实验,不需要 Go 客户端,直接验证 Redis 服务端。
先选操作,再选类型
| 需要做什么 | 首选结构 | 常用入口 | 主要代价或限制 |
|---|---|---|---|
| 保存一段内容、计数、带 TTL 的结果 | String | SET、GET、INCR、MGET | 修改 JSON 中一个字段通常要重写整个值 |
| 按字段读写一个对象 | Hash | HSET、HGET、HMGET、HINCRBY | 不能按任意字段值自动检索其他对象 |
| 保持插入顺序、从两端取任务 | List | LPUSH、RPUSH、LPOP、BLMOVE | 没有消费者组和自动确认账本 |
| 精确去重、判断成员、做交并差 | Set | SADD、SISMEMBER、SINTER、SCARD | 保存所有成员,内存随基数增长 |
| 排行榜、分数区间、到期顺序 | ZSet | ZADD、ZINCRBY、ZRANGE、ZRANK | 成员唯一;要明确相同分数的顺序 |
| 连续整数编号的布尔状态 | Bitmap | SETBIT、GETBIT、BITCOUNT、BITOP | 空间由最大编号决定,稀疏编号可能浪费巨大 |
| 只需要近似去重人数 | HyperLogLog | PFADD、PFCOUNT、PFMERGE | 不能列出成员,也不能判断某个成员是否存在 |
| 保留事件、分别消费、确认重试 | Stream | XADD、XREADGROUP、XACK、XAUTOCLAIM | 必须设计保留、积压和重复处理策略 |
| 找附近的点 | Geo | GEOADD、GEOSEARCH、GEODIST | 不是道路导航或任意几何数据库 |
这些是数据建模入口,不是“某结构永远最快”的排名。比如 Set 和 HyperLogLog 都能回答人数,但只有 Set 能回答“具体是谁”,因此不能只拿二者内存大小做替代决定。数据类型总览
类型是接口,编码是实现
TYPE key 回答客户端能对它做哪些操作;OBJECT ENCODING key 回答此时内部怎么存。一个 Hash 从紧凑数组变成哈希表,HGET 的接口不会变。
正在绘制图示…
查看图源代码
flowchart LR
K["键 user:7"] --> E["键空间中的对象"]
E --> T["逻辑类型 Hash"]
E --> V["当前编码 listpack"]
V --> P["连续存储 field,value,field,value"]
V -->|"字段太多或字段/值太长"| H["转换为 hashtable"]
H --> D["按字段哈希定位 value"]
T -. "HGET/HSET 接口保持一致" .-> D
listpack 把许多小元素紧凑放在连续内存里,少了大量独立分配和指针。代价是查找可能要扫描、插入可能要移动后面的字节。哈希表、跳表和链式节点增加管理开销,却避免大集合一直做线性扫描。因此 Redis 在小对象上选紧凑编码,超过阈值再转换。这是 CPU 与内存的取舍,不是压缩得越紧越好。8.2.2 listpack 实现
在本次默认配置下,主要边界如下。表中只列本文涉及的内建结构;带字段过期的 Hash 另有 listpackex,不能拿普通 Hash 的两种编码概括全部情况。
| 类型 | Redis 8.2.2 主要编码 | 本文要记住的转换条件 |
|---|---|---|
| String | int、embstr、raw | 是否能表示为整数;短字符串还受键与对象布局影响 |
| Hash | listpack、hashtable;字段过期可用 listpackex | 普通 Hash 默认最多 512 个字段,字段名和值各不超过 64 字节 |
| List | listpack、quicklist | 小列表可单个 listpack;超过节点限制转 quicklist,默认 list-max-listpack-size -2 对应约 8 KiB 的常规节点目标 |
| Set | intset、listpack、hashtable | 纯整数 intset 默认上限 512;小字符串 listpack 默认上限 128 个且每个最多 64 字节 |
| ZSet | listpack、skiplist | 默认最多 128 个成员且成员最多 64 字节时可紧凑存储 |
| Bitmap | String 的字符串存储 | 按位操作改变 String 中的字节 |
| HyperLogLog | 外层 String;内部稀疏或稠密 HLL 表示 | 内部格式与 OBJECT ENCODING 报告的外层编码是两层 |
| Stream | stream,内部 radix tree 与 listpack | 消息块、消费者组和待确认记录分别有管理成本 |
| Geo | ZSet 的 listpack 或 skiplist | 坐标编码为分数,成员为位置名 |
阈值来自 8.2.2 redis.conf,转换逻辑来自各类型源码。不要推导“删到阈值以下一定恢复紧凑编码”;转换方向和触发时机属于具体实现,应检查实际对象。CONFIG GET 能看到当前阈值,OBJECT ENCODING 能验证当前结果。
String:看起来只有一个值,内部仍有三种存法
String 保存二进制安全的字节序列,可以是文本、图片片段、序列化对象或整数文本。GET 不理解 JSON;INCR 则会把值解释为可增减的整数,并在同一条命令中完成读改写。自己先 GET 再 SET 会把原子操作拆开,两个客户端可能覆盖对方的更新。String 命令实现
int 直接在对象里表示整数,embstr 把对象和短字符串放在一块分配里,raw 使用单独的动态字符串 SDS。SDS 记录长度,所以中间可以含 \0,读取长度也不需要扫描到终止符。object.c、sds.h
很多资料把 String 编码总结成“44 字节以内 embstr”。对本次版本的键空间对象,这句话不够准确。实验先写 42、44 个 x、45 个 x,然后查看编码:
python
call('SET', 'lab08:n', '42')
call('SET', 'lab08:s44', 'x' * 44)
call('SET', 'lab08:s45', 'x' * 45)
print('string:', encoding('n'), encoding('s44'), encoding('s45'))实际输出:
text
string: int raw raw44 字节那项也是 raw。8.2.2 的 createStringObject 确实有 44 字节限制,但进入键空间时还经过 kvobjSet:它把键也嵌入对象,并检查整体大小是否能放进一个 64 字节缓存行。只看值的创建函数,漏掉后续存入键空间的步骤,就会得出错误结论。
换成只有一个字节的键,不设过期,实测如下:
text
string-boundary: a 40 False embstr
string-boundary: b 41 False raw这两个数字对应本文的创建路径和机器布局,不应成为新的跨版本口诀。键长、过期元数据、分配器可用空间和修改路径都值得核对。APPEND 也可能让原来的紧凑对象换成可修改表示;判断应以 OBJECT ENCODING 与固定版本代码为准。8.2.2 kvobjSet
Hash:适合局部字段更新,别把“一个 key”当成小对象
HSET user:7 name Ada age 37 把两个字段放在一个 Hash。HGET user:7 name 只取一个字段,HINCRBY user:7 visits 1 原子增量更新一个整数属性,避免整段 JSON 的传输和读改写冲突。它没有二级索引:想找所有 age=37 的用户,需要另建索引或交给有查询能力的数据系统。
小 Hash 的 listpack 按字段和值成对存放;大 Hash 用哈希表。下面的实验让变化发生在眼前:
python
call('HSET', 'lab08:h', 'name', 'Ada', 'age', '37')
print('hash:', encoding('h'))
call('HSET', 'lab08:h', 'bio', 'x' * 65)
print('hash-long:', encoding('h'))text
hash: listpack
hash-long: hashtable
hash-fields: 512 listpack
hash-fields: 513 hashtable第二次写入超过默认 64 字节单值阈值,所以即使只有三个字段,也转换成哈希表。另两行用短字段名和短值验证了字段数边界。盲目调大阈值会延长紧凑结构的扫描与搬移时间,应该用真实字段分布测量。Hash 实现与字段过期扩展
HGETALL 会取回所有字段。一个 Hash 下放几百万条记录,虽然键数只有 1,返回量仍然巨大。大对象分批遍历用 HSCAN,但扫描不是一致性快照,期间的修改需要业务容忍;“分批”也不等于每批严格返回指定条数。SCAN 语义
List:两端队列很顺手,任务可靠性还要另外设计
List 中的元素允许重复,按插入顺序排列。LPUSH/RPUSH 选择从哪一端加入,LPOP/RPOP 选择从哪一端取出;同侧压入弹出是栈,异侧是队列。LRANGE 的结束下标包含在结果里,-1 表示最后一个元素。
text
list: listpack a ['b', 'c']
list-long: quicklist这里先 RPUSH a b c,再 LPOP,取出 a,剩下 b,c。随后添加一个 9000 字节的元素,编码从 listpack 变成 quicklist。quicklist 是串起多个节点的链表,常规节点内部仍用 listpack;特大元素还可以单独作为 plain 节点。因此“List 底层永远是双向链表”或“永远是 quicklist”都漏掉了版本与大小条件。t_list.c、quicklist.c
端点操作不需要从头找位置,中间下标和范围操作则可能遍历一段列表;LRANGE 0 -1 还要传输全部元素。选择它应基于两端访问为主,而不是把它当随机访问数组。
任务直接 LPOP 后,消费者若崩溃,任务已经从队列消失。LMOVE/BLMOVE 可以把任务原子移到处理中列表,成功后再移除,并由另外的逻辑找回超时任务。它提供的是可靠队列的基础动作,重试、重复处理和死信管理仍需自己实现。LMOVE
Set 与 ZSet:是否需要顺序,是关键区别
Set 保存唯一成员,SADD 对已存在成员不会再添一份,SISMEMBER 判断存在,SINTER/SUNION/SDIFF 做集合运算。遍历顺序没有业务保证。纯整数成员可能用排序的紧凑 intset;短小字符串集合可以用 listpack;较大的集合转换成 hashtable。Set 源码
text
sets: intset listpack
set-long: hashtable第一行分别来自 {1,2,3} 与 {red,blue}。给后者加入 65 字节成员后,变成 hashtable。Set 的常用成员查找在哈希表表示下平均 O(1),但集合交集、全量返回等命令仍有集合规模成本,不能把“Set O(1)”套到所有命令。
ZSet 在唯一成员之外维护浮点分数。修改同一成员的分数是更新位置,不是新增一个同名成员。ZADD 写分数,ZINCRBY 增加分数,ZRANGE 取排名或分数范围,ZRANK 取从零开始的排名。
python
call('ZADD', 'lab08:z', 10, 'b', 10, 'a', 20, 'c')
print('zset:', encoding('z'), call('ZRANGE', 'lab08:z', 0, -1))text
zset: listpack ['a', 'b', 'c']
zset-long: skiplist相同分数时按成员字节序排序,所以后写入的 a 在 b 前面。若需求是同分按时间先后,就要明确增加次序字段或设计可比较的成员,不能假设保留插入顺序。ZADD
大 ZSet 的 skiplist 编码实际还配合字典:字典按成员找到分数,跳表按分数与成员维护有序路径。插入、删除通常涉及 O(log N) 的有序结构工作,范围读取还要加返回 M 个成员的成本。给实验 ZSet 加一个 65 字节成员后便转换成此表示。ZSet 实现
分数是 double,不适合把任意 64 位订单号直接塞进去并期待逐整数精确。整数绝对值超过 2^53 后可能丢失相邻整数的区别。排行榜常用计分、延迟队列常用毫秒时间,仍要核对范围和比较规则。
Bitmap 与 HyperLogLog:都是省空间,但省掉的东西不同
Bitmap 保存每个位置的一位
给第 9 位设为 1:
python
call('SETBIT', 'lab08:bits', 9, 1)
print('bitmap:', call('TYPE', 'lab08:bits'),
call('STRLEN', 'lab08:bits'), call('BITCOUNT', 'lab08:bits'))text
bitmap: string 2 1位偏移从零开始,第 9 位位于第二个字节,因此总长度为 2。它没有独立的键类型,TYPE 返回 string。对稠密用户编号做签到标记很合适;但如果编号是很大的随机数,SETBIT 需要把中间未使用字节也补出来。空间近似为 最大偏移/8,不是 实际签到人数/8。可以按日期与用户编号区间分片,或改用 Set。SETBIT
BITCOUNT 要扫描涉及的字节,BITOP 需要读取输入并生成结果。Bitmap 节省成员标识的空间,却不让扫描变成零成本。
HyperLogLog 只保留估算基数所需的摘要
HLL 用哈希后位模式的统计特征估计不同元素数。PFADD 加入观测,PFCOUNT 估算人数,PFMERGE 合并多个观测集合的摘要。重复输入不会像简单计数器那样把人数一直加上去。
text
hll: string raw 2实验输入 a,b,a,估算结果恰好是 2。这个小样本不能证明所有输入都精确。Redis 的实现使用 16384 个寄存器,稠密寄存器区约 12 KiB,外加头部及 Redis 对象开销,标准误差约 0.81%;它不是“每次最多错 0.81%”的硬边界。少量数据可用稀疏表示进一步省空间。8.2.2 HyperLogLog 实现
HLL 丢掉了成员本身,因此无法返回访客名单、精确删除某个访客,或者用它判断一个用户是否存在。需要这些能力就用精确集合或数据库索引。Bitmap 保留了每个位置的状态,HLL 则只保留整体统计信息,这是二者最重要的区别。
Stream:消息还在,与消息已确认,是两件事
Stream 是带 ID 的追加式事件记录。XADD 写事件;XRANGE 按 ID 范围读;XREAD 从某个 ID 后继续读。消费者组把消费进度和待确认记录加在事件日志上,XREADGROUP 分配消息,XACK 确认处理完成。8.2.2 Stream 实现
正在绘制图示…
查看图源代码
sequenceDiagram
participant P as 生产者
participant S as Stream
participant C as 消费者
participant G as 组的待确认记录 PEL
P->>S: XADD 1-0 event created
C->>S: XREADGROUP GROUP workers c1 STREAMS key >
S->>G: 记录 1-0 已分给 c1,等待确认
S-->>C: 返回事件 1-0
C->>C: 执行业务,可能失败或重复
C->>S: XACK key workers 1-0
S->>G: 移除待确认项
Note over S: 原始事件仍在,需另定删除/裁剪策略
实际创建一条事件、用消费者组领取,再确认:
text
stream: stream pending= 1
xack: 1 length= 1pending=1 是待确认数;XACK 返回确认了 1 条,但 XLEN 仍为 1。确认不会删除事件,也不代表业务副作用在 Redis 外部恰好发生一次。消费者若执行成功但未确认便崩溃,后续认领可能重复执行业务,因此通常需要业务幂等键。XACK
内部 radix tree 根据消息 ID 组织节点,每个节点存一个 listpack,可以共享字段信息、紧凑存多条记录。消费者组、消费者和 PEL 另有状态,积压的待确认记录同样占内存。用 XPENDING 看未确认消息,用 XAUTOCLAIM 认领超过空闲阈值的消息;空闲不等于原消费者已经永久死去,重复执行仍要处理。
保留策略要同时回答“生产者最多写多少”和“慢消费者落后多久仍能补读”。MAXLEN ~ 允许近似裁剪,不能当成严格条数上限;裁掉消费者尚需的内容,会造成无法补读的缺口。8.2 还增加了与消费者引用有关的裁剪选项,采用前应按该版本 XTRIM 文档逐项选定,而不是随手套用最新版示例。XTRIM
Geo:先缩小候选区域,再算距离
GEOADD 接收经度、纬度、成员名,写入一个 ZSet;score 保存坐标的空间编码。GEOSEARCH 按圆形或矩形搜索候选点,GEODIST 求两点距离。底层利用编码后的有序区间筛选候选,再做距离过滤,所以搜索区域很大、候选很多时仍然昂贵。geo.c
text
geo: zset listpack ['Palermo', 'Catania']本次在 Palermo 周围 200 km 查找,并指定 ASC 按距离排序。TYPE 是 zset,小集合编码是 listpack,不应把 Geo 当成第六种独立键类型。删除位置使用 ZREM,但不要随意用普通 ZADD 改它的 score,否则会破坏空间编码的含义。
输入顺序是经度在前、纬度在后。Redis 支持的经纬度有范围限制,坐标来自不同坐标系时也应先转换。GEOSEARCH COUNT 限制返回条数,ANY 可以让实现更早停止寻找足够结果,但不能同时把结果理解成完整搜索后严格最近的前 N 个。GEOSEARCH、GEOADD
Geo 适合“附近有哪些店”,不负责路网距离、行车时间和多边形拓扑。把结果当成业务候选,再交给真正需要的地理或路线计算系统。
衡量内存与延迟时,要把边界算完整
实验保存 1 字节内容:
text
memory: bytes= 1 usage= 40STRLEN 只量内容长度;MEMORY USAGE 估算这个键和值关联的内存,含管理结构与分配开销。它不等于整个进程 RSS,更不能简单把所有键的值长相加当作容量计划。键名、字典桶、过期信息、客户端缓冲、复制积压和分配器碎片都会影响实际占用。MEMORY USAGE
本次 40 字节属于 libc 构建和这个键的实测值,换分配器、键长、版本可能变化。嵌套结构默认会采样估算,需要更完整估算可以考虑 SAMPLES 0,但大对象的遍历成本会增加。
落地时可以依次做三件事:按业务操作选类型;用典型和极端对象确认编码与命令复杂度;再测真实请求的返回字节数、尾延迟和进程内存。只跑一个小对象的 GET,无法说明批量返回、编码转换或巨型对象删除的代价。
依据与验证范围
- Redis 8.2.2 官方源码:
object.c/db.c的键值布局;t_hash.c、t_list.c、t_set.c、t_zset.c、t_stream.c、hyperloglog.c、geo.c的具体实现;redis.conf的默认阈值。正文各节已链接具体文件。 - Redis 数据类型、内存优化:逻辑结构与小对象编码的整体关系。在线文档持续变化,本文内部编码以固定源码为准。
- 命令边界:正文所链
SCAN、ZADD、SETBIT、LMOVE、XACK、XTRIM、GEOSEARCH、MEMORY USAGE文档。 - 实测:Redis 8.2.2,Linux/amd64,libc 分配器;Python 标准库 RESP2 客户端;临时实例关闭 RDB/AOF,只绑定回环地址。正文输出均来自下方脚本。
- 未验证:生产负载性能、内存碎片长期变化、复制与集群故障、所有数据分布下的近似统计误差。本文没有把这些内容包装成实验结论。
完整复现实验
需要 Python 3、Bash,以及可运行的 Redis 8.2.2 redis-server、redis-cli。以下脚本只应指向临时教学实例;启动包装器会为本次测试创建空目录,并在结束时关闭自己启动的进程、删除该临时目录。测试命令只会在自己启动的实例上执行,端口被占用则退出。启动时会读取目标端口的进程标识以核对归属。
可从官方源码包构建。该归档官方 SHA-256 为 4e340e8e822a82114b6fb0f7ca581b749fa876e31e36e9fbcb75416bec9d0608,出处为 redis-hashes。示例固定的是测试版本,生产环境应另外检查安全修复。
bash
curl -fLO https://download.redis.io/releases/redis-8.2.2.tar.gz
printf '%s %s\n' 4e340e8e822a82114b6fb0f7ca581b749fa876e31e36e9fbcb75416bec9d0608 redis-8.2.2.tar.gz | sha256sum -c -
tar xzf redis-8.2.2.tar.gz
make -C redis-8.2.2 -j4 MALLOC=libc BUILD_TLS=no OPTIMIZATION=-O2 redis-server redis-cli
export REDIS_BIN="$PWD/redis-8.2.2/src"
"$REDIS_BIN/redis-server" --version
bash with-redis.sh python3 data.py保存下面三个文件在同一目录。resp.py 是只供本例使用、按 UTF-8 解码的最小 RESP2 客户端,不是通用二进制安全或生产客户端。输出中的 PID 和临时目录每次不同。实验中没有压测,也不修改系统的内存或网络设置。
with-redis.sh
bash
#!/usr/bin/env bash
set -euo pipefail
: "${REDIS_BIN:?Set REDIS_BIN to the directory containing redis-server and redis-cli}"
export REDIS_PORT=${REDIS_PORT:-16389}
work=$(mktemp -d)
log=${REDIS_RUN_LOG:-$work/server.log}
pid=
cleanup() {
if [[ -n "$pid" ]]; then
kill -TERM "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
fi
rm -rf -- "$work"
}
trap cleanup EXIT
"$REDIS_BIN/redis-server" --bind 127.0.0.1 --port "$REDIS_PORT" \
--protected-mode yes --save '' --appendonly no --dir "$work" \
--daemonize no > "$log" 2>&1 &
pid=$!
echo "redis_pid=$pid port=$REDIS_PORT temporary_dir=$work" >&2
ready=0
for ((i=0;i<100;i++)); do
kill -0 "$pid" 2>/dev/null || { cat "$log" >&2; exit 1; }
if "$REDIS_BIN/redis-cli" -h 127.0.0.1 -p "$REDIS_PORT" INFO server 2>/dev/null | tr -d '\r' | grep -qx "process_id:$pid"; then
ready=1; break
fi
sleep 0.05
done
[[ "$ready" == 1 ]] || { cat "$log" >&2; exit 1; }
"$@"resp.py
代码块 · 37 行 · 点击收起 / 展开
python
import os
import socket
class RedisError(Exception):
pass
class Redis:
def __init__(self):
self.sock = socket.create_connection(('127.0.0.1', int(os.environ.get('REDIS_PORT', '16389'))), timeout=3)
self.f = self.sock.makefile('rb')
def close(self):
self.f.close()
self.sock.close()
def read(self):
line = self.f.readline()
if not line.endswith(b'\r\n'):
raise EOFError('incomplete RESP line')
kind, body = line[:1], line[1:-2]
if kind == b'+': return body.decode()
if kind == b'-': return RedisError(body.decode())
if kind == b':': return int(body)
if kind == b'$':
n = int(body)
if n == -1: return None
data = self.f.read(n)
if len(data) != n or self.f.read(2) != b'\r\n': raise EOFError('incomplete bulk')
return data.decode()
if kind == b'*':
n = int(body)
return None if n == -1 else [self.read() for _ in range(n)]
raise ValueError('unsupported RESP type')
def call(self, *args):
vals = [str(a).encode() for a in args]
wire = b'*%d\r\n' % len(vals)
wire += b''.join(b'$%d\r\n' % len(v) + v + b'\r\n' for v in vals)
self.sock.sendall(wire)
return self.read()data.py
代码块 · 55 行 · 点击收起 / 展开
python
from resp import Redis
r = Redis()
try:
def call(*a):
v = r.call(*a)
if isinstance(v, Exception): raise v
return v
def encoding(k): return call('OBJECT', 'ENCODING', 'lab08:'+k)
for key, value in [('n','42'),('s44','x'*44),('s45','x'*45)]:
call('SET','lab08:'+key,value)
print('string:',encoding('n'),encoding('s44'),encoding('s45'))
for key, n, ttl in [('a',40,False),('b',41,False)]:
args=['SET',key,'x'*n]
if ttl: args += ['PX',60000]
call(*args)
print('string-boundary:',key,n,ttl,call('OBJECT','ENCODING',key))
call('APPEND','lab08:s44','y')
print('append:',encoding('s44'))
call('HSET','lab08:h','name','Ada','age','37')
print('hash:',encoding('h'))
call('HSET','lab08:h','bio','x'*65)
print('hash-long:',encoding('h'))
for n in [512,513]:
k='lab08:h'+str(n)
args=sum(([str(i),'v'] for i in range(n)),[])
call('HSET',k,*args)
print('hash-fields:',n,call('OBJECT','ENCODING',k))
call('RPUSH','lab08:l','a','b','c')
print('list:',encoding('l'),call('LPOP','lab08:l'),call('LRANGE','lab08:l',0,-1))
call('RPUSH','lab08:l','x'*9000)
print('list-long:',encoding('l'))
call('SADD','lab08:ints',1,2,3)
call('SADD','lab08:words','red','blue')
print('sets:',encoding('ints'),encoding('words'))
call('SADD','lab08:words','x'*65)
print('set-long:',encoding('words'))
call('ZADD','lab08:z',10,'b',10,'a',20,'c')
print('zset:',encoding('z'),call('ZRANGE','lab08:z',0,-1))
call('ZADD','lab08:z',30,'x'*65)
print('zset-long:',encoding('z'))
call('SETBIT','lab08:bits',9,1)
print('bitmap:',call('TYPE','lab08:bits'),call('STRLEN','lab08:bits'),call('BITCOUNT','lab08:bits'))
call('PFADD','lab08:hll','a','b','a')
print('hll:',call('TYPE','lab08:hll'),encoding('hll'),call('PFCOUNT','lab08:hll'))
call('XADD','lab08:stream','1-0','event','created')
call('XGROUP','CREATE','lab08:stream','workers','0')
call('XREADGROUP','GROUP','workers','c1','COUNT',1,'STREAMS','lab08:stream','>')
print('stream:',encoding('stream'),'pending=',call('XPENDING','lab08:stream','workers')[0])
print('xack:',call('XACK','lab08:stream','workers','1-0'),'length=',call('XLEN','lab08:stream'))
call('GEOADD','lab08:geo',13.361389,38.115556,'Palermo',15.087269,37.502669,'Catania')
print('geo:',call('TYPE','lab08:geo'),encoding('geo'),call('GEOSEARCH','lab08:geo','FROMMEMBER','Palermo','BYRADIUS',200,'km','ASC'))
call('SET','lab08:tiny','x')
print('memory: bytes=',call('STRLEN','lab08:tiny'),'usage=',call('MEMORY','USAGE','lab08:tiny'))
finally:
r.close()