Redis 数据操作:结构、TTL、事务与 Lua
Redis 数据结构、TTL 与原子性的选择
先了解各类型的特点与用途,请看基础理论中的 Redis 数据类型。本页侧重操作示例、TTL 与原子性的行为边界。
本轮仅全文静态审计,未安装依赖、启动服务、连接数据库或执行任何示例。下文实测、RED/GREEN及平台结果均为此前历史证据;本轮修改后的脚本未运行,命令与输出作为学习步骤和预期保留。
前置:独立服务与命令。下面命令在 docker exec showdocs-m4-redis-main redis-cli -e 后追加;全部仅修改 m4: 教学键。数据类型的选择决定可用命令、时间复杂度和业务不变量,不能只看值是否能序列化。
String 与计数器
String 保存字节串,可以是 UTF-8、整数文本或应用序列化后的 JSON。Redis 不会自动理解 JSON 内部字段;INCR 对整数文本原子加一,不能对 "1.5" 使用。金额若暂存在缓存中应先约定最小单位和上界,不能用浮点累加冒充精确账本。真实交易仍需持久存储的事务与审计约束。
SET m4:counter 0
INCR m4:counter
INCRBY m4:counter 3
GET m4:counter
DEL m4:counter预期 OK, 1, 4, "4", 1。INCR 是一个命令;先 GET、在应用加一、再 SET 是三个步骤,两个调用者会覆盖彼此结果。单个命令原子不自动扩展到调用者的业务流程。
Hash、List、Set 与 Sorted Set
本节操作预期要求这些m4:教学键原先不存在;重复练习前只清理自己已确认的这些键,否则RPUSH、SADD、ZADD的计数或队列顺序会受旧值影响。
Hash 适合按字段读取对象,List 适合两端操作的序列,Set 表达唯一成员,Sorted Set 在唯一成员上维护 score 排序。不要用一种容器包装所有关系;独立过期需求往往意味着应拆键,而非把所有用户放一个大 Hash。
HSET m4:user name Ada level 3
HGET m4:user name
RPUSH m4:queue a b
LPOP m4:queue
SADD m4:tags redis redis lua
SCARD m4:tags
ZADD m4:rank 10 alice 20 bob
ZRANGE m4:rank 0 -1 WITHSCORES
GET m4:user
DEL m4:user m4:queue m4:tags m4:rankHGET 返回 Ada,LPOP 返回 a(移除了它),SADD 返回 2,SCARD 返回 2,排名为 alice 10、bob 20。最后 GET Hash 返回 WRONGTYPE,原值不变。Set 无顺序保证,不能依赖 SMEMBERS 的输出排列;Sorted Set 相同 score 再按成员字节顺序排列,倒序命令和分页偏移要与产品排序规则一致。
List 的 LPOP 消费会立即移除:进程随后崩溃可能丢任务。需要确认、重新投递和消费组时应另学 Streams 或专用消息系统;本页没有把 List 演示声称为可靠队列。Hash 字段过期有版本差异,本课程使用整个键的 PEXPIRE,未演示的字段TTL不自动算已覆盖。
TTL、覆盖与缓存
TTL 返回剩余秒数,PTTL 返回毫秒;-1 表示键存在且没有过期时间,-2 表示不存在。普通 SET 覆盖会移除原 TTL;SET ... KEEPTTL 可保留。Hash 的 HSET 修改字段不会重置整个键的 TTL。过期时间不是必须立即回收内存的计时器:Redis 结合访问时检查与主动过期,读到已到期键按不存在处理。
SET m4:ttl value PX 30000
PTTL m4:ttl
SET m4:ttl replacement
TTL m4:ttl
PEXPIRE m4:ttl 30000
SET m4:ttl final KEEPTTL
PTTL m4:ttl
DEL m4:ttl
TTL m4:ttl预期第一次 PTTL 大于0且不超过30000,普通覆盖后 TTL=-1,KEEPTTL 后仍大于0,删除后 TTL=-2。网络和调度使剩余值变化,测试应断言区间而非等于30000。
Cache-aside 的基本流程是先读缓存,未命中读权威数据源,再 SET 加 TTL;更新时先提交数据源,再删除缓存。这仍可能在并发读回填时写入旧值,需要版本号、失效事件或其他协调策略。TTL 只限制陈旧时间,不保证强一致。对不存在对象可短暂缓存空值,热点回源可合并请求,TTL 加抖动可分散同时失效;这些策略都应有流量和失效需求依据。驱逐策略在内存不足时可能提前删除缓存;noeviction 则拒绝适用的写入。缓存可丢失,不能当资金账本。
Pipeline、MULTI 与 Lua 原子性
Pipeline 是批量发送减少网络往返,不提供多命令不可穿插。MULTI/EXEC 将队列中的命令连续执行;WATCH 提供乐观竞争检测,EXEC 返回空表示观察键变化,应重新读取并有界重试。执行阶段单条命令失败不会回滚其他成功命令。语法错误在排队阶段可能使整个事务被拒绝,两类错误不能混同。
Lua 脚本在服务器执行期间不被其他命令穿插,适合“读当前值→检查限额→写入并设过期”。这不是数据库式回滚:先 SET 再因 INCR 非整数失败,SET 仍然存在。先校验全部参数和类型,再写入,尽量把关联状态放同一个 Hash。脚本仍受 OOM、进程崩溃、故障切换及操作权限影响;客户端超时不能判断命令是否执行,盲目重试可能重复计数。
本课程完整限额项目用单个键、接受结果重放和窗口过期处理这些边界。脚本必须短小,长循环会阻塞其他客户端;所有键通过 KEYS 显式声明。Cluster 多键脚本要求同 slot;{subject} hash tag 仅帮助路由,不保证跨实例一致性。这里未验证集群。
练习与参考解法
提示:在两个命令之间画一个进程退出点,再观察原键的类型。
题目:为什么给计数器 INCR 后再 EXPIRE 不足以实现稳健限额?为什么 GET 字符串与 GET Hash 的失败应不同处理?
参考解法
两个独立命令之间进程可失败,留下无过期计数器;每次 EXPIRE 还可能无意延长窗口。把条件和首次过期写进 Lua 或使用合适的事务,仍须考虑运行错误不回滚。不存在字符串返回空是正常未命中;GET Hash 的 WRONGTYPE 是模式冲突,应该定位键所有者和类型,而非当未命中覆盖它。
一手资料:数据类型、EXPIRE、事务、Lua。国内云服务商对 Pipeline、事务的中文说明可见华为云 Redis 使用最佳实践,托管产品限制需另外对照,不能直接套到本机开源服务。
Redis 事务与脚本入门
单个 INCR 可以完成计数加一,但“读取 → 应用计算 → 写回”是多步操作,其他客户端可能在中间修改数据。需要根据目的选择批量发送、事务或脚本。
先区分四种能力
| 能力 | 解决的问题 | 主要边界 |
|---|---|---|
| Pipeline | 减少多次网络往返 | 不保证多条命令不会与其他客户端操作穿插 |
| MULTI / EXEC | 把排队的命令连续执行 | 执行时某条命令报错,不会回滚其他已成功命令 |
| WATCH | 发现读取后、提交前的键变化 | 冲突时事务不执行,应用需要重新读取并有限重试 |
| Lua | 在服务端完成读取、判断和写入 | 执行期间阻塞其他命令,脚本应短小并先校验再写入 |
一个简单事务
以下步骤用于已连接的独立实验服务,本次文档整理未运行:
MULTI
SET learn:transaction:count 0
INCR learn:transaction:count
EXEC
GET learn:transaction:count
DEL learn:transaction:countMULTI 后命令先排队,EXEC 返回每条命令的结果。没有其他客户端修改该教学键时,后续 GET 预期返回 1。DISCARD 可以取消尚未提交的队列。
什么时候使用 WATCH 或 Lua
如果需要客户端读取一个值后计算新值,可以先 WATCH 该键,再读取、计算和提交事务。键在提交前发生相关变化时,EXEC 会放弃执行;不能无限重试。
如果条件判断可以在服务端完成,可以使用 Lua。例如检查计数是否达到限额,再决定是否增加。脚本访问的键通过 KEYS 显式传入,普通参数通过 ARGV 传入。脚本原子执行不代表具备错误回滚;写入后再报错,先前写入可能已经生效。
客户端超时也不能证明服务端没有执行。涉及重试时,需要另外设计幂等性。
继续阅读:事务与 Lua 的边界、Lua 限额实践。参考:事务、Lua 脚本、Pipeline。
继续阅读
- 数据与原子性
- 数据与原子性
- 完整实践