Redis DECR 命令详细教程
DECR 用于将指定 Key 存储的数字值减 1,并返回递减后的新值。它是 Redis 原子计数操作家族的一员,与 INCR、DECRBY、INCRBY 共同构成无锁计数方案的核心。由于命令在服务端原子执行,多个客户端并发递减同一 Key 时不会出现丢失更新,这是它相比"读取后回写"流程的最大价值。
本文基于 Redis String 数据类型介绍 DECR。Redis 命令本身不区分大小写,因此 DECR、decr 和 Decr 的效果相同;文档统一使用大写形式。DECR 自 Redis 1.0.0 起可用。
资料合集:https://pan.quark.cn/s/10e98d308913、https://pan.quark.cn/s/f56bc69c5338
一、命令概览
1. 基本语法
DECR key
参数说明:
| key | 存储数字的目标 Key |
返回值:
- 返回递减操作完成后 Key 的新值(整数)。
- Key 不存在时,先按 0 处理再递减,返回 -1 并创建该 Key。
DECR 没有指定递减幅度的参数,固定减 1;需要减去任意整数时使用 DECRBY。
2. 最简单的示例
先写入一个数字值:
SET stock:1001 10
递减一次:
DECR stock:1001
返回:
(integer) 9
再递减一次:
DECR stock:1001
返回:
(integer) 8
3. 命令特性
- DECR 只作用于 String 类型的 Key,对其他类型返回 WRONGTYPE 错误。
- 值必须能被解析为 64 位有符号整数,否则报错。
- 时间复杂度为 O(1)。
- 命令是原子操作:并发调用不会丢失任何一次递减。
- 递减会保留 Key 已有的过期时间。
- ACL 类别为 @write、@string、@fast。
二、DECR 的返回结果
1. Key 存在且值合法
SET attempts:1001 3
DECR attempts:1001
返回:
(integer) 2
返回的是新值,不是旧值,也不是递减的幅度。
2. Key 不存在
DECR fresh-counter
返回:
(integer) -1
Key 不存在时按 0 处理,0 – 1 = -1,同时 Key 被创建且值为 -1:
GET fresh-counter
返回:
"-1"
这一点容易被忽略:对不存在的 Key 执行 DECR 会产生负数计数器,而不是报错或返回空值。
3. 连续递减
DECR fresh-counter
DECR fresh-counter
依次返回:
(integer) -2
(integer) -3
4. 值不是整数
SET bad-value "abc"
DECR bad-value
返回:
(error) ERR value is not an integer or out of range
含小数点、空白字符或字母的值都会触发该错误:
SET float-value "10.5"
DECR float-value
同样返回 value is not an integer or out of range 错误。浮点数计数应使用 INCRBYFLOAT(用负数增量实现递减)。
空字符串同样报错:
SET empty-value ""
DECR empty-value
Key 存在但值为空串,不等于 Key 不存在,仍会报整数解析错误。
5. Key 的类型不是 String
RPUSH mylist a b
DECR mylist
返回:
(error) WRONGTYPE Operation against a key holding the wrong kind of value
可用 TYPE 命令确认实际类型后再选择对应命令。
6. 已过期的 Key
SET temp-counter 5 EX 1
等待超过 1 秒后执行:
DECR temp-counter
返回:
(integer) -1
已过期的 Key 被视为不存在,DECR 按 0 处理并重建 Key,返回 -1。新 Key 没有过期时间。
7. 到达数值下界溢出
SET min-value -9223372036854775808
DECR min-value
返回:
(error) ERR increment or decrement would overflow
64 位有符号整数的最小值是 -9223372036854775808,再减 1 会溢出,Redis 拒绝执行且不修改原值。
三、数值边界与内部编码
1. 64 位有符号整数范围
DECR 操作的数值范围是 64 位有符号整数:
| 最小值 | -9223372036854775808(即 -2^63) |
| 最大值 | 9223372036854775807(即 2^63 – 1) |
DECR 只在下界溢出(详见第二章第 7 节);上界溢出只会发生在 INCR/INCRBY 场景。
2. int 编码
能表示为 long long 的字符串值在 Redis 内部使用 int 编码存储:
SET demo:counter 100
OBJECT ENCODING demo:counter
返回:
int
DECR 对 int 编码的值直接做整数运算,性能极高。值超出 long long 范围时会转为 embstr/raw 编码,此时再执行 DECR 会因无法解析为 64 位整数而报错。
3. 结果值的规范化
递减后的值以规范的整数文本存储。即使原值通过其他方式写入,只要能被解析为整数,DECR 之后的 GET 结果都是规范形式(无多余前缀)。
四、与相关命令的区别
1. DECR 与 INCR
两者方向相反,语法对称:
SET demo:counter 10
DECR demo:counter # 9
INCR demo:counter # 10
| 操作 | 减 1 | 加 1 |
| Key 不存在时 | 从 0 开始,返回 -1 | 从 0 开始,返回 1 |
| 溢出方向 | 下界(LLONG_MIN) | 上界(LLONG_MAX) |
2. DECR 与 DECRBY
DECRBY 减去指定幅度,DECR key 等价于 DECRBY key 1:
SET demo:points 100
DECRBY demo:points 10
DECR demo:points
依次返回 90、89。
| 递减幅度 | 固定 1 | 任意 64 位整数 |
| 语法 | DECR key | DECRBY key decrement |
| 负数幅度 | 不适用 | 相当于递增 |
注意 DECRBY 的幅度为负数时效果是加法:DECRBY key -5 等价于 INCRBY key 5。
3. DECR 与 INCRBY
INCRBY 增加指定幅度,用负数增量也能实现递减:
SET demo:counter 10
INCRBY demo:counter -3 # 7
INCRBY key -1 与 DECR key 效果相同。写代码时建议按语义选择:明确是"递减"就用 DECR/DECRBY,可读性更好。
4. DECR 与 INCRBYFLOAT
INCRBYFLOAT 操作浮点数,且只能"增"方向传参,递减靠负数增量:
SET demo:balance 10.5
INCRBYFLOAT demo:balance -1.5
返回 9。注意 INCRBYFLOAT 执行后值以浮点文本形式存储,之后再执行 DECR 会报"非整数"错误。整数计数与浮点计数不要混用于同一 Key。
5. DECR 与"GET 后 SET"
客户端自行实现的读取减法回写不是原子操作:
GET key -> 应用层减 1 -> SET key newvalue # 并发下互相覆盖,丢失更新
DECR key # 原子,无丢失
高并发计数场景必须使用 DECR 等服务端原子命令,或 Lua 脚本。
五、原子性、事务、Pipeline 与并发
1. 原子性
DECR 是单条命令,在 Redis 单线程模型中原子执行。任意时刻并发的 N 次 DECR,最终值恰好减少 N,任何一次都不会丢失。这是库存扣减、限流等场景信赖 Redis 计数的基础。
2. 在事务中使用
MULTI
DECR demo:counter
DECR demo:counter
EXEC
EXEC 返回各命令结果,如依次为 8、7(假设原值为 9)。事务内多条 DECR 按顺序执行。
需要"检查后再递减"的条件逻辑时,事务无法基于中间结果做分支判断,应使用 Lua 脚本(见下文)。
3. Pipeline 批量递减
对同一 Key 连续递减或对多个 Key 分别递减时,用 Pipeline 减少网络往返:
DECR quota:user:1
DECR quota:user:2
DECR quota:user:3
Pipeline 只负责批量传输,不改变各命令的原子性语义。
4. Lua 脚本实现有下界的递减
DECR 本身不限制结果为负。业务上通常要求计数不低于 0(如库存不为负),这时用 Lua 脚本把"递减 + 判断 + 回滚"变成原子操作:
EVAL "local v = redis.call('DECR', KEYS[1]) if v < 0 then redis.call('INCR', KEYS[1]) return -1 end return v" 1 stock:1001
脚本逻辑:先 DECR,若结果小于 0 则 INCR 还原并返回 -1 表示扣减失败;否则返回剩余值。这可以防止超卖、超扣等负数问题。
5. WATCH 乐观锁
除 Lua 外,也可用 WATCH 实现基于版本检查的并发控制,但计数场景下 Lua 脚本更简洁高效,WATCH 更适合多 Key 复杂条件的并发更新。
六、常见客户端用法
1. redis-cli
redis-cli SET stock:1001 10
redis-cli DECR stock:1001
redis-cli DECR stock:1001
两次 DECR 分别返回 9 和 8。
2. Python(redis-py)
import redis
client = redis.Redis(host="localhost", port=6379, decode_responses=True)
client.set("stock:1001", 10)
print(client.decr("stock:1001")) # 9
print(client.decr("stock:1001")) # 8
client.decr(key) 固定减 1;client.decrby(key, amount) 对应 DECRBY。
3. Node.js(node-redis)
import { createClient } from "redis";
const client = createClient();
await client.connect();
await client.set("stock:1001", 10);
console.log(await client.decr("stock:1001")); // 9
console.log(await client.decr("stock:1001")); // 8
await client.quit();
4. Java(Jedis)
import redis.clients.jedis.Jedis;
try (Jedis jedis = new Jedis("localhost", 6379)) {
jedis.set("stock:1001", "10");
System.out.println(jedis.decr("stock:1001")); // 9
System.out.println(jedis.decr("stock:1001")); // 8
}
各客户端的 decr 方法均返回递减后的新值(长整数)。
七、典型业务场景
1. 库存扣减
高并发抢购场景的库存余量扣减:
DECR stock:item:1001
配合第五章的 Lua 脚本可保证不超卖。注意单纯 DECR 允许减到负数。
2. 限流与令牌桶
单位时间窗口内的请求配额扣减:
DECR rate:user:1001
返回值大于等于 0 放行,小于 0 拒绝。配额重置由定时任务或 EXPIRE 完成。
3. 剩余次数控制
登录失败剩余次数、验证码重试次数:
DECR attempts:login:1001
减到 0 后锁定账户,比应用层数据库行锁更轻量。
4. 资源信号量
限制并发数的信号量获取:
DECR semaphore:pool
获取成功(结果 >= 0)则执行任务,完成后 INCR 归还。
5. 倒计时与配额追踪
活动名额、优惠券余量等单调递减指标的实时展示。
八、性能与使用建议
九、常见问题排查
问题 1:DECR 返回 -1
最常见原因是 Key 不存在(按 0 减 1):
EXISTS key
GET key
检查是否拼写错误、是否连接了错误的实例或逻辑数据库、Key 是否已过期。
问题 2:报 “value is not an integer or out of range”
Key 的当前值无法解析为 64 位整数。检查实际内容:
GET key
TYPE key
STRLEN key
常见原因:存了小数(10.5)、字符串(abc)、空字符串、含空白的值,或该 Key 曾被 INCRBYFLOAT 写成浮点文本。
问题 3:报 WRONGTYPE 错误
Key 是 List、Hash 等非 String 类型:
TYPE key
确认业务应改用对应类型的命令(如 HINCRBY),或先删除旧 Key 重建。
问题 4:计数出现负数
DECR 不做下界保护。需要非负语义时使用 Lua 脚本(见第五章第 4 节),或在应用层对返回值小于 0 的情况做补偿回滚(补偿不是原子操作,高并发下推荐脚本方案)。
问题 5:报 “increment or decrement would overflow”
值已是 64 位最小值 -9223372036854775808,再减会溢出。检查业务是否会把计数减到下界;正常业务不会触达该边界,出现时通常说明逻辑缺陷或恶意输入。
问题 6:Key 的过期时间消失了
DECR 本身保留 TTL。若发现 Key 变成永久,排查是否有不带过期选项的 SET 覆盖过该 Key(普通 SET 会清除 TTL),重新执行 EXPIRE 修正。
十、完整练习
下面的示例覆盖基本递减、Key 不存在、非法值、类型错误、溢出边界、TTL 保留与 Lua 防负数。请在测试数据库执行;FLUSHDB 会清空当前逻辑数据库中的全部 Key。
FLUSHDB
# 1. 基本用法
SET demo:counter 10
DECR demo:counter
DECR demo:counter
# 2. Key 不存在(按 0 处理)
DECR demo:fresh
GET demo:fresh
DECR demo:fresh
# 3. 非整数值报错
SET demo:bad "abc"
DECR demo:bad
SET demo:float "10.5"
DECR demo:float
SET demo:empty ""
DECR demo:empty
# 4. 类型错误
RPUSH demo:list a b
DECR demo:list
# 5. 下界溢出
SET demo:min -9223372036854775808
DECR demo:min
GET demo:min
# 6. TTL 保留
SET demo:quota 5 EX 100
TTL demo:quota
DECR demo:quota
TTL demo:quota
# 7. 过期后递减重建
SET demo:temp 5 EX 1
# 等待 1 秒以上后执行:
# DECR demo:temp
# TTL demo:temp
# 8. 与 DECRBY / INCRBY 对比
SET demo:points 100
DECRBY demo:points 10
DECR demo:points
INCRBY demo:points -3
GET demo:points
# 9. 与 INCR 对比
INCR demo:points
# 10. Lua 防负数递减
SET demo:stock 1
EVAL "local v = redis.call('DECR', KEYS[1]) if v < 0 then redis.call('INCR', KEYS[1]) return -1 end return v" 1 demo:stock
EVAL "local v = redis.call('DECR', KEYS[1]) if v < 0 then redis.call('INCR', KEYS[1]) return -1 end return v" 1 demo:stock
GET demo:stock
# 11. 事务中的连续递减
MULTI
DECR demo:counter
DECR demo:counter
EXEC
# 12. 内部编码
SET demo:encoding 42
OBJECT ENCODING demo:encoding
DECR demo:encoding
OBJECT ENCODING demo:encoding
预期结果:
- demo:counter 两次 DECR 依次返回 9、8。
- DECR demo:fresh 返回 -1,GET 返回 "-1",再次 DECR 返回 -2。
- demo:bad、demo:float、demo:empty 均返回 (error) ERR value is not an integer or out of range。
- 对 List 执行 DECR 返回 (error) WRONGTYPE …。
- demo:min 的 DECR 返回 (error) ERR increment or decrement would overflow,GET 确认值仍为 -9223372036854775808。
- demo:quota 递减前 TTL 约 100,递减返回 4,TTL 继续递减(如 97)。
- demo:temp 过期后 DECR 返回 -1,TTL 返回 -1。
- demo:points:DECRBY 10 返回 90,DECR 返回 89,INCRBY -3 返回 86,GET 为 "86"。
- INCR demo:points 返回 87。
- demo:stock:第一次 EVAL 返回 0;第二次返回 -1(扣减失败并回滚),GET 返回 "0"。
- 事务返回 7、6(基于步骤 1 后的值 8)。
- demo:encoding 的编码为 int,递减返回 41 后编码仍为 int。
十一、命令速查表
| 减 1 | DECR key |
| 减指定整数 | DECRBY key decrement |
| 加 1 | INCR key |
| 加指定整数 | INCRBY key increment |
| 浮点数增减 | INCRBYFLOAT key value |
| Hash 字段计数 | HINCRBY key field increment |
| 读取当前值 | GET key |
| 判断是否存在 | EXISTS key |
| 查询数据类型 | TYPE key |
| 设置过期时间 | EXPIRE key seconds |
| 原子条件递减 | Lua 脚本(EVAL) |
总结
DECR 的核心作用是将 Key 的整数值原子地减 1 并返回新值,基本语法为:
DECR key
使用时重点注意五点:
- Key 不存在时按 0 处理,返回 -1 并创建 Key,不会报错;业务需防止意外的负数起点。
- 值必须是可解析为 64 位有符号整数的文本,否则报 value is not an integer or out of range;浮点计数用 INCRBYFLOAT。
- 原子性是核心价值,任何并发计数都应使用 DECR/INCR 家族而非"GET 后 SET"。
- 递减保留已有 TTL,但新建的 Key 永不过期;需要非负下界(库存、配额)时用 Lua 脚本防止减成负数。
- 固定减 1,减任意幅度用 DECRBY;下界 -9223372036854775808 再减会溢出报错且不修改原值。



