Redis 8.8新增命令INCREX详解¶
本文有视频版本可供观看:bilibili.com/video/BV1hmEq66EVw
在最近发布的Redis 8.8版本中新增了INCREX命令,该命令整合了以下多个命令的功能,以此来在一次命令调用中同时执行“对键自增并处理可能出现的计算溢出”以及“为键设置过期时间”或“移除键的过期时间”等多项操作:
INCR/DECRINCRBY/DECRBYINCRBYFLOATEXPIRE/PEXPIRE/EXPIREAT/PEXPIREATPERSIST
INCREX命令的具体格式如下:
INCREX key
[<BYINT|BYFLOAT> increment]
[UBOUND upper-limit] [LBOUND lower-limit] [SATURATE]
[EX sec | PX ms | EXAT unixtime-sec | PXAT unixtime-ms | PERSIST]
[ENX]
本文接下来的多个小节将分别对INCREX命令的不同使用情形进行介绍,并在最后给出一个使用该命令实现时间窗口限速器的例子。
基本用法¶
在不使用任何可选项执行INCREX命令的情况下,命令将对给定键执行自增一操作,就像INCR命令一样:
INCREX key
命令在执行之后将返回一个包含两个元素的数组:
第一个元素为自增操作执行之后,键的当前值
第二元素为本次自增的实际增量
比如以下代码就对counter键反复执行了三次INCREX命令,将它的值(从键不存在时默认的)0自增为3,并在最后通过执行GET命令确认了这一点:
127.0.0.1:6379> INCREX counter
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> INCREX counter
1) (integer) 2
2) (integer) 1
127.0.0.1:6379> INCREX counter
1) (integer) 3
2) (integer) 1
127.0.0.1:6379> GET counter
"3"
命令返回数组的第二个元素表明这三次自增的实际增量都为1。
指定整数或浮点数增量¶
INCREX命令允许通过可选的BYINT或BYFLOAT选项来指定要执行的具体自增操作及其增量:
INCREX key
[<BYINT|BYFLOAT> increment]
当给定BYINT选项时,命令执行的是整数自增操作(效果类似INCRBY命令),而当给定BYFLOAT选项时,命令执行的是浮点数自增操作(效果类似INCRBYFLOAT命令)。
以下两个例子分别展示了这两种情况:
127.0.0.1:6379> INCREX float-counter BYFLOAT 3.14
1) "3.14"
2) "3.14"
127.0.0.1:6379> INCREX float-counter BYFLOAT 2.0
1) "5.14"
2) "2"
127.0.0.1:6379> INCREX int-counter BYINT 256
1) (integer) 256
2) (integer) 256
127.0.0.1:6379> INCREX int-counter BYINT 100
1) (integer) 356
2) (integer) 100
此外,虽然Redis没有提供与INCREX命令对应的DECREX命令,但只要将INCREX命令执行时的增量参数设置为负数即可实现对键的自减操作。
以下是使用INCREX命令执行整数自减操作的一个例子:
127.0.0.1:6379> SET minus-counter 100
OK
127.0.0.1:6379> INCREX minus-counter BYINT -32
1) (integer) 68
2) (integer) -32
限制边界¶
INCREX命令允许通过给定UBOUND和/或LBOUND可选项来为自增/自减操作设置上限值或下限值:
INCREX key
[<BYINT|BYFLOAT> increment]
[UBOUND upper-limit] [LBOUND lower-limit]
当命令检测到自增/自减操作将超过给定的限制值时,命令将放弃执行自增/自减操作,并返回键的当前值以及0作为本次增量/减量。
以下是一个使用UBOUND可选项来指定自增操作上限值的例子:
127.0.0.1:6379> INCREX ub-counter UBOUND 3
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> INCREX ub-counter UBOUND 3
1) (integer) 2
2) (integer) 1
127.0.0.1:6379> INCREX ub-counter UBOUND 3
1) (integer) 3
2) (integer) 1
127.0.0.1:6379> INCREX ub-counter UBOUND 3
1) (integer) 3
2) (integer) 0
可以看到,当命令的自增操作到达上限值3之后,它将不再对键值进行自增,只返回该值,还有0作为该操作的自增量。
提示
即使在未设置
UBOUND/LBOUND可选项的情况下,键值的默认取值范围上下界也会由键值类型(整数或浮点数)的最小值/最大值定义,因此也可以认为INCREX命令总是带有隐式的UBOUND和LBOUND可选项。
饱和¶
上一节提到,带有UBOUND/LBOUND可选项的INCREX命令在检测到自增/自减操作将会超过限制时将放弃执行操作,但如果命令在执行时给定了SATURATE可选项,那么命令将把键值自增/自减至给定的上限值/下限值,而超过限制的值则作为命令结果数组的第二个元素返回(第一个元素为键的当前值):
INCREX key
[<BYINT|BYFLOAT> increment]
[UBOUND upper-limit] [LBOUND lower-limit] [SATURATE]
以下是一个直观的对比示例,它展示了同样的超上限计算在不给定SATURATE和给定SATURATE两种情况下的区别:
127.0.0.1:6379> SET non-sat 50
OK
127.0.0.1:6379> INCREX non-sat BYINT 100 UBOUND 100
1) (integer) 50 -- 放弃执行自增操作
2) (integer) 0 -- 本次操作未增加任何增量
127.0.0.1:6379> SET sat 50
OK
127.0.0.1:6379> INCREX sat BYINT 100 UBOUND 100 SATURATE
1) (integer) 100 -- 键被自增为上限值
2) (integer) 50 -- 计算中超过上限的值
可以看到,在给定SATURATE的情况下,键值将被自增为上限100,而超过上限未被计算在内的值则为50。
提示
如果
INCREX命令在执行时给定了SATURATE可选项,但并未显式地给定UBOUND/LBOUND可选项,那么命令将根据键值类型的限界值进行饱和计算。
设置/移除过期时间¶
跟带有EX、PX、PERSIST等命令的SET命令类似,INCREX命令也可以通过同样的可选项,在执行自增/自减的同时,为键设置或移除过期时间:
INCREX key
[<BYINT|BYFLOAT> increment]
[UBOUND upper-limit] [LBOUND lower-limit] [SATURATE]
[EX sec | PX ms | EXAT unixtime-sec | PXAT unixtime-ms | PERSIST]
比如以下示例就分别通过EX可选项和PERSIST可选项,先为被自增的键设置了过期时间,然后又为被自增的键移除了过期时间:
127.0.0.1:6379> INCREX counter-with-ttl EX 1000
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> TTL counter-with-ttl
(integer) 996 -- 剩余生存时间
127.0.0.1:6379> INCREX counter-with-ttl PERSIST
1) (integer) 2
2) (integer) 1
127.0.0.1:6379> TTL counter-with-ttl
(integer) -1 -- 键的过期时间已被移除
条件性设置过期时间¶
如果被设置的键已经带有过期时间,那么带有EX等可选项的INCREX命令在默认情况下将直接使用新的过期时间去覆盖旧的过期时间,但如果命令在执行时也给定了ENX可选项,那么命令将仅在键未有过期时间的情况下为其设置过期时间:
INCREX key
[<BYINT|BYFLOAT> increment]
[UBOUND upper-limit] [LBOUND lower-limit] [SATURATE]
[EX sec | PX ms | EXAT unixtime-sec | PXAT unixtime-ms | PERSIST]
[ENX]
比如以下示例就展示了不带ENX可选项和带有ENX可选项在处理同样带有过期时间的键的区别,其中第二个INCREX命令将直接覆盖已有的过期时间,而第三个INCREX命令将放弃更新键的过期时间,仅对其执行自增操作:
127.0.0.1:6379> INCREX exp-counter EX 1000
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> TTL exp-counter
(integer) 997 -- 设置初始过期时间
127.0.0.1:6379> INCREX exp-counter EX 3000
1) (integer) 2
2) (integer) 1
127.0.0.1:6379> TTL exp-counter
(integer) 2999 -- 过期时间已被更新
127.0.0.1:6379> INCREX exp-counter EX 5000 ENX
1) (integer) 3
2) (integer) 1
127.0.0.1:6379> TTL exp-counter
(integer) 2985 -- 过期时间未被更新
应用实例:实现时间窗口限速器¶
假设我们正在构建一个网上银行应用,这类应用对安全性有极高的要求,其中一项安全措施就是限制每个用户每天允许尝试登录多少次:当登录由于密码错误等原因而失败的次数超过上限时,该用户在当天将无法再尝试登录,甚至账号还会被直接锁定。
假设应用现在的限制是1天之内最多只能登录失败5次,那么为了实现一个记录用户登录失败次数的时间窗口限速器,可以使用以下命令序列(以UID为511500的用户为例):
MULTI
INCR login_fail_limiter:511500
EXPIRE login_fail_limiter:511500 86400 NX
EXEC
这个命令序列使用INCR命令在用户每次登录失败的时候进行一次自增计数,并使用带NX可选项的EXPIRE命令来确保该计数器在一天(86400秒)的时间窗口内持续存在,并且为了保证上述两条命令在执行时的原子性,还需要使用一个事务来包裹它们。
以上就是在Redis
8.8版本以前构建时间窗口限速器的常见做法,对于一些更复杂的情况,可能还需要用到WATCH命令以实现检查并设置(check-and-set)操作(又或者直接使用服务器端脚本/函数来实现整个限速器)。但是在Redis
8.8版本中,通过使用INCREX命令,只需要一条命令调用即可实现相同的效果:
INCREX login_fail_limiter:511500 EX 86400 ENX
这条命令同时实现了:
为
login_fail_limiter:511500键实现自增一操作仅在该键未设置过期时间的情况下,为其设置86400秒的生存时间
跟旧的实现方法需要用到4条命令相比,新的实现方法只需要1条命令:后者不仅运行更快,而且也更容易让人理解。
结语¶
好的,关于INCREX命令的详细介绍到这里就结束!
欢迎大家在Bilibili上关注我,观看更多Redis相关视频,或者定期浏览本人博客,查看更多Redis相关文章。