Redis里那些读写原子操作,怎么帮咱们数据不出错又稳当
- 问答
- 2026-01-02 16:31:55
- 2
要搞清楚Redis的原子操作怎么保证数据不出错又稳当,咱们得先想象一个没有这些操作的场景,问题就一目了然了,一个很常见的场景是“秒杀”:一件商品库存就剩最后1件了,这时候,成百上千个请求同时涌过来,都要买这个商品,如果Redis处理不好,很可能发生“超卖”,就是实际卖出去的商品数量超过了库存,比如卖出去10件,但库存明明只有1件,这可就出大乱子了。
为什么会出现这种错误呢?问题就出在“读取”和“写入”这两个动作,如果不是一个不可分割的整体(即原子操作),就很容易被打断,一个简单的扣减库存的逻辑,在程序里可能是三步:

- 从Redis里读取当前库存,
stock = GET product_100_stock,读出来是1。 - 在程序里判断一下,
stock > 0,那就准备扣减。 - 向Redis发送命令,将库存减1,
SET product_100_stock 0。
想象一下,如果两个请求A和B,在几乎同一时刻都执行到了第一步,它们读到的库存都是1,都认为可以购买,然后A执行了第三步,把库存扣成了0,但此时B并不知道库存已经变了,它接着执行第三步,也把库存扣减1,结果库存被设置成了0(或者更糟,如果代码是DECR,会变成-1),这样,一件商品就被卖了两回,这就是典型的“非原子操作”导致的数据错误。
Redis是怎么用原子操作来解决这个问题的呢?它的核心思路就是:把一个可能被拆散的“读-判断-写”组合操作,变成一条直接在Redis服务器端瞬间完成的指令。 因为Redis是单线程处理命令的,任何一条命令在执行时都不会被其他命令打断,这就保证了原子性。

Redis提供了很多这样的“复合”原子命令,它们能帮我们应对各种场景:
应对数值计算的 INCR 和 DECR 家族命令
(根据Redis官方文档对字符串命令的描述)这是最直接的原子操作,比如上面的库存问题,我们根本不需要先读再写,直接使用 DECR product_100_stock 命令,这个命令的作用是:将键 product_100_stock 对应的值原子性地减少1,Redis服务器收到这个命令后,会在自己内部完成读取当前值、判断是否数字、减1、写回新值这一系列动作,这个过程对于其他客户端来说是完全不可见的、不会被中断的。DECR 命令还会返回执行后的新值,我们可以根据这个返回值来判断是否扣减成功,如果返回的值是大于等于0,说明扣减成功;如果返回-1,说明库存从0开始减,扣减失败(即卖完了),这样,无论多少请求同时发来 DECR 命令,Redis都会让它们排队一个一个执行,最终库存只会被准确地减少相应的次数,绝不会超卖。

应对复杂判断的 SETNX、GETSET 以及更强大的 SET 命令带选项
(根据Redis官方文档对分布式锁使用模式的说明)有些场景比简单的加减法复杂,我们要实现一个“分布式锁”:多个客户端要竞争一个资源,但同一时间只允许一个客户端使用,这时候就可以用 SETNX 命令。SETNX key value 的意思是:只有当这个 key 不存在的时候,才进行设置,这个“判断不存在”和“设置”的动作是原子性的,第一个客户端执行 SETNX lock_key 1 成功了,表示它拿到了锁,在它释放锁(删除这个key)之前,其他所有客户端执行 SETNX lock_key 1 都会失败,因为key已经存在,这就保证了互斥性。
后来,Redis又增强了 SET 命令本身(根据Redis官方文档对SET命令参数的描述),可以通过附加参数实现更丰富的原子操作。SET key value NX 的效果和 SETNX 一样,但功能更强,还可以用 XX 参数表示“仅当key存在时才设置”,这为我们实现各种需要先判断后更新的逻辑提供了原子性保障。
应对数据结构内部操作的原子命令
(根据Redis官方文档对哈希、集合等数据结构的描述)Redis的复杂数据类型,其内部操作也大多是原子的。HSET 命令向哈希表里设置一个字段,SADD 向集合里添加一个成员,LPUSH 向列表头部插入一个元素,这些操作都是原子的,这意味着,即使多个客户端同时往同一个哈希表里塞不同的字段,或者往同一个集合里加人,Redis也会确保最终数据是正确一致的,不会出现字段丢失或者成员重复(集合本身会去重)但数据错乱的情况。
应对多命令组合的利器:Lua脚本
(根据Redis官方文档对EVAL命令的说明)业务逻辑非常复杂,一两条原生命令无法完成,只有当用户积分大于100且库存大于0时,才扣减库存并增加用户积分”,这种“读取多个值-进行复杂判断-修改多个值”的逻辑,如果分成多条命令发送,中间还是可能被其他操作打断,Redis的解决方案是支持Lua脚本,我们可以把这一整套逻辑写成一个Lua脚本,然后用 EVAL 命令一次性发给Redis服务器,Redis会保证这个脚本在执行时是原子性的:脚本在执行期间,服务器不会处理任何其他命令,相当于给一系列操作加了一个“超级锁”,这样,无论逻辑多复杂,都能像一条命令一样被原子执行,确保了数据的一致性。
Redis正是通过提供这些丰富的原生原子命令(如 DECR、SETNX)和可编排复杂原子操作的Lua脚本,把那些容易出错的、“读-改-写”分离的非原子流程,全部收拢到Redis服务器内部,以单线程、不间断的方式完成,这样做,相当于把数据竞争和冲突的风险从应用程序层面,转移到了Redis这个更擅长处理内部数据一致性的组件层面,对于我们开发者来说,只要在需要保证数据准确性的关键地方,有意识地选择使用这些原子操作,而不是自己分步骤读写,就能很大程度上避免数据出错,让系统在并发环境下依然表现得稳当、可靠。
本文由钊智敏于2026-01-02发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:http://waw.haoid.cn/wenda/73184.html
