当前位置:首页 > 问答 > 正文

Redis里怎么快速搞定数值的加减,递增递减那些事儿分享下

说到在Redis里处理数字的加减、递增递减这些操作,这可以说是Redis最顺手、最高效的功能之一了,它不像关系型数据库,改个数字还得先查出来,在程序里计算,再写回去,搞不好还有并发问题,Redis直接用几个简单的命令就能原子性地搞定,又快又安全,下面我就把这些事儿给你捋一捋。

核心命令:INCR、DECR、INCRBY、DECRBY

最基本的操作就是给一个键的值加1或者减1,这用的就是INCRDECR命令。(来源:Redis官方文档对INCR命令的描述)

你有一个键叫 page_views,用来记录网站浏览量,每次有人访问,你都不用先去读这个值是多少,直接发一条命令:

INCR page_views

Redis收到这个命令后,会做三件事:

  1. 看一下 page_views 这个键存不存在,如果不存在,就当它的值是0。
  2. 在这个值的基础上加1。
  3. 把加1后的新值返回给你。

整个过程是一个原子操作,也就是说,就算有成千上万个客户端同时发送 INCR page_views 命令,Redis也会确保每个加1操作都一步步完成,绝对不会出现两个客户端读到同一个值然后加1,导致最终计数变少的情况,这就是它厉害的地方。

减1也一样,用 DECR 命令:

DECR items_in_stock

比如库存数量,卖出一件商品就减1。

那如果我想加或减的不是1,而是一个指定的数呢?比如给用户积分一次加10分,或者商品库存一次入库100件,这时候就用 INCRBYDECRBY 命令。(来源:Redis官方文档对INCRBY命令的描述)

INCRBY 是增加一个整数:

INCRBY user:1000:score 10

这条命令的意思是把键 user:1000:score 的值增加10。

DECRBY 是减少一个整数:

Redis里怎么快速搞定数值的加减,递增递减那些事儿分享下

DECRBY inventory:item001 5

这条命令是把库存减少5。

处理浮点数:INCRBYFLOAT

上面那几个命令都是针对整数的,但现实世界里,我们经常要处理小数,比如账户余额、温度值、权重等等,Redis也为这种情况准备了命令,叫 INCRBYFLOAT。(来源:Redis官方文档对INCRBYFLOAT命令的描述)

它的用法和 INCRBY 很像,但是后面跟的数可以是小数:

INCRBYFLOAT account:alice:balance 50.5  // 充值50.5元
INCRBYFLOAT account:alice:balance -20.1 // 消费20.1元

这个命令非常灵活,既可以加正数,也可以加负数(相当于减)。

这些操作为什么这么快?

你可能会想,这些操作这么快,是不是有什么魔法?其实道理很简单。(来源:基于对Redis内部实现机制的普遍理解)

Redis里怎么快速搞定数值的加减,递增递减那些事儿分享下

  1. 单线程模型:Redis是单线程处理命令的,这意味着在任何一瞬间,都只有一个命令在被执行,所以INCR这类命令根本不用考虑复杂的锁机制,因为不会被打断,天然就是线程安全的。
  2. 纯内存操作:数据都在内存里,读写速度极快,不像硬盘数据库那样有沉重的I/O开销。
  3. 简单高效的数据结构:对于整数,Redis在内部会尝试把它存成一个叫“整数集合”的紧凑结构,进行算术运算的效率非常高。

实战中用起来是什么样子?

光说命令可能有点干,我们想象几个实际场景:

  • 限流器 你想限制一个API接口每分钟只能被一个用户调用60次,你可以这样做:

    • 键名设计为 rate_limit:user_id:202405201330(用户ID + 年月日时分)。
    • 每次用户调用接口时,执行 INCR rate_limit:user_id:202405201330
    • 如果是第一次调用,这个键的值会变成1,同时你可以用 EXPIRE 命令给这个键设置60秒的过期时间。
    • 之后每次调用都INCR一下,并检查返回值,如果超过60,就拒绝请求。
    • 等到下一分钟,键名变了,又是新的开始,这个方法非常简洁高效。
  • 商品库存扣减 秒杀场景下,库存扣减是典型的“判断+扣减”的原子性问题,用Redis可以这么搞:

    • 商品库存初始化:SET inventory:seckill_item 100
    • 用户下单时,不用繁琐的事务,直接一条命令:DECR inventory:seckill_item
    • 然后判断这个命令的返回值,如果返回值大于等于0,说明扣减成功,库存充足;如果返回-1,说明库存已经从0开始减了,扣减失败,库存不足。 这样就完美避免了超卖问题。
  • 实时排行榜的分数更新 虽然更复杂的排行榜用ZSET(有序集合)更强大,但如果是简单的积分榜,用INCRBY也能做。

    • 每个用户一个积分键:INCRBY user:1000:score 5
    • 要查排名的时候,可以用GET命令取出所有用户的分数,然后在应用层做个排序,对于用户量不是特别巨大的情况,完全够用。

需要注意的几个小地方

虽然这些命令很好用,但也有几点要留意:

  • 类型要对:你只能对数字类型的值进行这些操作,如果你对一个存着字符串"hello"的键执行INCR,Redis会报错。
  • 精度问题INCRBYFLOAT处理的是浮点数,在计算机中浮点数计算可能存在微小的精度损失,这在所有编程语言和系统中都是普遍现象,所以如果是要求精确计算的金融场景,比如一分钱都不能差的账户,可能需要用整数来存(比如用分做单位),或者使用数据库的事务功能。
  • 非唯一性INCR生成的序列是唯一的、递增的,但它不是严格连续的,如果你有多个Redis实例(集群),每个实例自己生成ID,或者你删掉了一个键,再重新INCR,中间就会有间隔,如果需要绝对连续,得想别的办法。

Redis提供的这一套递增递减命令,就像是给你了一套顺手的小工具,对于计数器、限流、实时统计这类需要高频且原子性修改数值的场景,简直是量身定做,关键是理解它们的原子特性和适用场景,然后就能在项目里灵活运用,大大提升性能和开发效率。