INCRBYFLOAT:对数字值执行浮点数加法操作

除了用于执行整数加法操作的 INCR 命令以及 INCRBY 命令之外,Redis 还提供了用于执行浮点数加法操作的 INCRBYFLOAT 命令:

INCRBYFLOAT key increment

INCRBYFLOAT 命令可以把一个浮点数增量加到字符串键存储的数字值上面,并返回键在执行加法操作之后的数字值作为命令的返回值。

以下代码展示了如何使用 INCRBYFLOAT 命令去增加一个浮点数的值:

redis> SET decimal 3.14 -- 一个存储着浮点数值的键
OK
redis> GET decimal
"3.14"
redis> INCRBYFLOAT decimal 2.55 -- 将键decimal的值加上2.55
"5.69"
redis> GET decimal
"5.69"

处理不存在的键

INCRBYFLOAT 命令在遇到不存在的键时,会先将键的值初始化为 0,然后再执行相应的加法操作。

在以下代码中,INCRBYFLOAT 命令就是先把 x-point 键的值初始化为 0,然后再执行加法操作的:

redis> GET x-point -- 不存在的键
(nil)
redis> INCRBYFLOAT x-point 12.7829
"12.7829"
redis> GET x-point
"12.7829"

使用 INCRBYFLOAT 执行浮点数减法操作

Redis 为 INCR 命令提供了相应的减法版本 DECR 命令,也为 INCRBY 命令提供了相应的减法版本 DECRBY 命令,但是并没有为 INCRBYFLOAT 命令提供相应的减法版本,因此用户只能通过给 INCRBYFLOAT 命令传入负数增量来执行浮点数减法操作。

以下代码展示了如何使用 INCRBYFLOAT 命令执行浮点数减法计算:

redis> SET pi 3.14
OK
redis> GET pi
"3.14"
redis> INCRBYFLOAT pi -1.1 -- 值减去1.1
"2.04"
redis> INCRBYFLOAT pi -0.7 -- 值减去0.7
"1.34"
redis> INCRBYFLOAT pi -1.3 -- 值减去1.3
"0.04"

INCRBYFLOAT与整数值

INCRBYFLOAT 命令对于类型限制的要求比 INCRBY 命令和 INCR 命令要宽松得多:

  • INCRBYFLOAT 命令既可用于浮点数值,也可以用于整数值。

  • INCRBYFLOAT 命令的增量既可以是浮点数,也可以是整数。

  • 当 INCRBYFLOAT 命令的执行结果可以表示为整数时,命令的执行结果将以整数形式存储。

以下代码展示了如何使用 INCRBYFLOAT 去处理一个存储着整数值的键:

redis> SET pi 1 -- 创建一个整数值
OK
redis> GET pi
"1"
redis> INCRBYFLOAT pi 2.14
"3.14"

以下代码展示了如何使用整数值作为 INCRBYFLOAT 命令的增量:

redis> SET pi 3.14
OK
redis> GET pi
"3.14"
redis> INCRBYFLOAT pi 20 -- 增量为整数值
"23.14"

以下代码展示了 INCRBYFLOAT 命令是如何把计算结果存储为整数的:

redis> SET pi 3.14
OK
redis> GET pi
"3.14"
redis> INCRBYFLOAT pi 0.86 -- 计算结果被存储为整数
"4"

小数位长度限制

虽然 Redis 并不限制字符串键存储的浮点数的小数位长度,但是在使用 INCRBYFLOAT 命令处理浮点数的时候,命令最多只会保留计算结果小数点后的 17 位数字,超过这个范围的小数将被截断:

redis> GET i
"0.01234567890123456789" -- 这个数字的小数部分有20位长
redis> INCRBYFLOAT i 0
"0.01234567890123457" -- 执行加法操作之后,小数部分只保留了17位

其它信息

  • 复杂度:O(1)。

  • 版本要求:INCRBYFLOAT 命令从 Redis 2.6.0 开始可用。

示例:ID生成器

在构建应用程序的时候,我们经常会用到各式各样的 ID(identifier,标识符)。比如,存储用户信息的程序在每次出现一个新用户的时候就需要创建一个新的用户ID,而博客程序在作者每次发表一篇新文章的时候也需要创建一个新的文章 ID。

ID 通常会以数字形式出现,并且通过递增的方式来创建出新的 ID。比如,如果当前最新的 ID 值为 10086,那么下一个 ID 就应该是 10087,再下一个 ID 则是 10088,以此类推。

代码清单2-6展示了一个使用字符串键实现的 ID 生成器,这个生成器通过执行 INCR 命令来产生新的 ID,并且可以通过执行 SET 命令来保留指定数字之前的 ID,从而避免用户为了得到某个指定的 ID 而生成大量无效 ID。

代码清单2-6 使用字符串键实现的 ID 生成器:/string/id_generator.py

class IdGenerator:

    def __init__(self, client, key):
        self.client = client
        self.key = key

    def produce(self):
        """
        生成并返回下一个 ID 。
        """
        return self.client.incr(self.key)

    def reserve(self, n):
        """
        保留前 n 个 ID ,使得之后执行的 produce() 方法产生的 ID 都大于 n 。
        为了避免 produce() 方法产生重复 ID ,
        这个方法只能在 produce() 方法和 reserve() 方法都没有执行过的情况下使用。
        这个方法在 ID 被成功保留时返回 True ,
        在 produce() 方法或 reserve() 方法已经执行过而导致保留失败时返回 False 。
        """
        result = self.client.set(self.key, n, nx=True)
        return result is True

在这个 ID 生成器程序中,produce() 方法要做的就是调用 INCR 命令,对字符串键存储的整数值执行加 1 操作,并将执行加法操作之后得到的新值用作 ID。

用于保留指定 ID 的 reserve() 方法是通过执行 SET 命令为键设置值来实现的:当用户把一个字符串键的值设置为 N 之后,对这个键执行 INCR 命令总是会返回比 N 更大的值,因此在效果上相当于把所有小于等于 N 的 ID 都保留下来了。

需要注意的是,这种保留 ID 的方法只能在字符串键还没有值的情况下使用,如果用户已经使用过 produce() 方法来生成 ID,或者已经执行过 reserve() 方法来保留 ID,那么再使用 SET 命令去设置 ID 值可能会导致 produce() 方法产生出一些已经用过的 ID,并因此引发 ID 冲突。

为此,reserve() 方法在设置字符串键时使用了带有 NX 选项的 SET 命令,从而确保了对键的设置操作只会在键不存在的情况下执行:

self.client.set(self.key, n, nx=True)

以下代码展示了这个 ID 生成器的使用方法:

>>> from redis import Redis
>>> from id_generator import IdGenerator
>>> client = Redis(decode_responses=True)
>>> id_generator = IdGenerator(client, "user::id")
>>> id_generator.reserve(1000000) # 保留前100万个ID
True
>>> id_generator.produce() # 生成ID,这些ID的值都大于100万
1000001
>>> id_generator.produce()
1000002
>>> id_generator.produce()
1000003
>>> id_generator.reserve(1000) # 键已经有值,无法再次执行reserve()方法
False

示例:计数器

除了 ID 生成器之外,计数器也是构建应用程序时必不可少的组件之一,如对于网站的访客数量、用户执行某个操作的次数、某首歌或者某个视频的播放量、论坛帖子的回复数量等,记录这些信息都需要用到计数器。实际上,计数器在互联网中几乎无处不在,因此如何简单、高效地实现计数器一直都是构建应用程序时经常会遇到的一个问题。

代码清单2-7展示了一个计数器实现,这个程序把计数器的值存储在一个字符串键里面,并通过 INCRBY 命令和 DECRBY 命令对计数器的值执行加法操作和减法操作,在需要时,用户还可以通过调用 GETSET 方法来清零计数器并取得清零之前的旧值。

代码清单2-7 使用字符串键实现的计数器:/string/counter.py

class Counter:

    def __init__(self, client, key):
        self.client = client
        self.key = key

    def increase(self, n=1):
        """
        将计数器的值加上 n ,然后返回计数器当前的值。
        如果用户没有显式地指定 n ,那么将计数器的值加上一。
        """
        return self.client.incr(self.key, n)

    def decrease(self, n=1):
        """
        将计数器的值减去 n ,然后返回计数器当前的值。
        如果用户没有显式地指定 n ,那么将计数器的值减去一。
        """
        return self.client.decr(self.key, n)

    def get(self):
        """
        返回计数器当前的值。
        """
        # 尝试获取计数器当前的值
        value = self.client.get(self.key)
        # 如果计数器并不存在,那么返回 0 作为计数器的默认值
        if value is None:
            return 0
        else:
            # 因为 redis-py 的 get() 方法返回的是字符串值
            # 所以这里需要使用 int() 函数,将字符串格式的数字转换为真正的数字类型
            # 比如将 "10" 转换为 10
            return int(value)

    def reset(self):
        """
        清零计数器,并返回计数器在被清零之前的值。
        """
        old_value = self.client.getset(self.key, 0)
        # 如果计数器之前并不存在,那么返回 0 作为它的旧值
        if old_value is None:
            return 0
        else:
            # 跟 redis-py 的 get() 方法一样, getset() 方法返回的也是字符串值
            # 所以程序在将计数器的旧值返回给调用者之前,需要先将它转换成真正的数字
            return int(old_value)

在这个程序中,increase() 方法和 decrease() 方法在定义时都使用了 Python 的参数默认值特性:

def increase(self, n=1):
def decrease(self, n=1):

以上定义表明,如果用户直接以无参数的方式调用 increase() 或者 decrease(),那么参数 n 的值将会被设置为 1。

在设置了参数 n 之后,increase() 方法和 decrease() 方法会分别调用 INCRBY 命令和 DECRBY 命令,根据参数 n 的值,对给定的键执行加法或减法操作:

# increase()方法
return self.client.incr(self.key, n)
# decrease()方法
return self.client.decr(self.key, n)

注意,increase()方法在内部调用的是incr()方法而不是 incrby()方法,并且 decrease()方法在内部调用的也是 decr()方法而不是 decrby()方法,这是因为在 redis-py 客户端中,INCR 命令和 INCRBY 命令都是由 incr()方法负责执行的:

  • 如果用户在调用 incr()方法时没有给定增量,那么 incr()方法就默认用户指定的增量为 1,并执行 INCR 命令。

  • 如果用户在调用 incr()方法时给定了增量,那么 incr()方法就会执行 INCRBY 命令,并根据给定的增量执行加法操作。

decr()方法的情况也与此类似,只是被调用的命令变成了 DECR 命令 和 DECRBY 命令。

以下代码展示了这个计数器的使用方法:

>>> from redis import Redis
>>> from counter import Counter
>>> client = Redis(decode_responses=True)
>>> counter = Counter(client, "counter::page_view")
>>> counter.increase() # 将计数器的值加上1
1
>>> counter.increase() # 将计数器的值加上1
2
>>> counter.increase(10) # 将计数器的值加上10
12
>>> counter.decrease() # 将计数器的值减去1
11
>>> counter.decrease(5) # 将计数器的值减去5
6
>>> counter.reset() # 重置计数器,并返回旧值
6
>>> counter.get() # 返回计数器当前的值
0

示例:限速器

为了保障系统的安全性和性能,并保证系统的重要资源不被滥用,应用程序常常会对用户的某些行为进行限制,比如:

  • 为了防止网站内容被网络爬虫抓取,网站管理者通常会限制每个 IP 地址在固定时间段内能够访问的页面数量,比如 1min 之内最多只能访问 30 个页面,超过这一限制的用户将被要求进行身份验证,确认本人并非网络爬虫,或者等到限制解除之后再进行访问。

  • 为了防止用户的账号遭到暴力破解,网上银行通常会对访客的密码试错次数进行限制,如果一个访客在尝试登录某个账号的过程中,连续好几次输入了错误的密码,那么这个账号将被冻结,只能等到第二天再尝试登录,有的银行还会向账号持有者的手机发送通知来汇报这一情况。

实现这些限制机制的其中一种方法是使用限速器,它可以限制用户在指定时间段之内能够执行某项操作的次数。

代码清单2-8 展示了一个使用字符串键实现的限速器,这个限速器程序会把操作的最大可执行次数存储在一个字符串键里面,然后在用户每次尝试执行被限制的操作之前,使用 DECR 命令将操作的可执行次数减 1,最后通过检查可执行次数的值来判断是否执行该操作。

代码清单2-8 倒计时式的限速器:/string/limiter.py
class Limiter:

    def __init__(self, client, key):
        self.client = client
        self.key = key

    def set_max_execute_times(self, max_execute_times):
        """
        设置操作的最大可执行次数。
        """
        self.client.set(self.key, max_execute_times)

    def still_valid_to_execute(self):
        """
        检查是否可以继续执行被限制的操作。
        是的话返回 True ,否则返回 False 。
        """
        num = self.client.decr(self.key)
        return (num >= 0)

    def remaining_execute_times(self):
        """
        返回操作的剩余可执行次数。
        """
        num = int(self.client.get(self.key))
        if num < 0:
            return 0
        else:
            return num

这个限速器的关键在于 set_max_execute_times() 方法和 still_valid_to_execute() 方法:前者用于将最大可执行次数存储在一个字符串键里面,后者则会在每次被调用时对可执行次数执行减 1 操作,并检查目前剩余的可执行次数是否已经变为负数,如果为负数,则表示可执行次数已经耗尽,不为负数则表示操作可以继续执行。

以下代码展示了这个限制器的使用方法:

>>> from redis import Redis
>>> from limiter import Limiter
>>> client = Redis(decode_responses=True)
>>> limiter = Limiter(client, 'wrong_password_limiter') # 密码错误限制器
>>> limiter.set_max_execute_times(3) # 最多只能输入3次错误密码
>>> limiter.still_valid_to_execute() # 前3次操作能够顺利执行
True
>>> limiter.still_valid_to_execute()
True
>>> limiter.still_valid_to_execute()
True
>>> limiter.still_valid_to_execute() # 从第4次开始,操作将被拒绝执行
False
>>> limiter.still_valid_to_execute()
False

以下伪代码则展示了如何使用这个限速器去限制密码的错误次数:

# 试错次数未超过限制
while limiter.still_valid_to_execute():
    # 获取访客输入的账号和密码
    account, password = get_user_input_account_and_password()
    # 验证账号和密码是否匹配
    if password_match(account, password):
        ui_print("密码验证成功")
    else:
        ui_print("密码验证失败,请重新输入")
    # 试错次数已超过限制
else:
    # 锁定账号
    lock_account(account)
    ui_print("连续尝试登录失败,账号已被锁定,请明天再来尝试登录。")