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

Redis里那些防止并发冲突的小妙招,保证数据不乱的实用技巧

主要整合自Redis官方文档关于事务和锁的说明、以及《Redis实战》等常见技术书籍中的实践案例,并结合常见的开发场景)

首先得明白,当很多人同时往Redis里存东西、改东西的时候,就像很多人同时要进一个门,如果没人管,肯定会挤成一团,数据就乱套了,比如一个热门商品就剩最后一件,好几个人同时点击购买,如果不做处理,很可能就超卖了,下面这些就是防止这种混乱的实用办法。

第一招,也是最基本的:用Redis自带的事务(MULTI/EXEC)。

这个事务和数据库里的事务不太一样,它更像一个“打包”的过程,你可以把它理解成去超市买东西,没有事务的时候,你每拿一个商品就去收银台结一次账,效率低而且别人可能在你中间插队,而Redis的事务是,你先推个购物车(用MULTI命令开始),然后把所有要买的商品(也就是各种操作命令,比如DECR减少库存,HSET修改信息)一个个放进车里,等你全部选好了,推到收银台,一次性结账(用EXEC命令执行)。

关键点在于,在“往购物车里放东西”这个过程中,Redis并不会真正执行这些命令,只是把它们记下来,等到EXEC那一刻,它才一口气按顺序执行所有命令,在这个过程中,不会有其他客户端的命令插进来,这样就保证了这一批命令的执行是不会被中途打断的,具有原子性,这适合在你需要连续执行多个命令,并且希望它们作为一个整体完成时使用,但要注意,它不提供回滚功能,一旦某个命令出错,后面的会继续执行。

第二招,配合WATCH命令,实现一种“乐观锁”。

这是Redis里非常实用和巧妙的一招,还拿抢购商品举例,乐观锁的心态是:“我认为在我修改数据之前,大概率没人会来动它,但我还是得防着一手。”

具体做法是:

  1. 你先用WATCH命令盯住那个关键的数据,比如商品的库存键 item:1001:stock
  2. 你去读取这个库存的当前值,比如是1。
  3. 你开启事务(MULTI)。
  4. 在事务里,你发出命令,如果库存大于0,就将其减1(DECR)。
  5. 你尝试提交事务(EXEC)。

魔法就在这里:在执行EXEC的那一刻,Redis会检查一下你之前WATCH的那个键(item:1001:stock),从你WATCH它之后到现在,它的值有没有被其他客户端改变过,如果没人动过,那么好,你提交的事务成功执行,库存减1,抢购成功,但如果在你WATCH之后、EXEC之前的这个极短间隙里,已经有另一个人把库存从1改成0了,那么Redis会发现这个键被改动了,于是它会毫不犹豫地让你的整个事务失败,返回一个nil。

这时候你的程序逻辑就需要处理这个失败,比如告诉用户“手慢了,没抢到”,或者最常见的,自动重试整个流程(重新WATCH,重新读值,再走一遍),直到成功或者重试次数用完,这种方式非常高效,因为在大部分情况下数据冲突很少,它避免了真正加锁的开销,只在发生冲突时进行重试。

第三招,使用SET命令本身的高级参数来实现简单的原子操作。

对于一些非常简单的场景,其实不需要动用事务或WATCH那么复杂,Redis的很多命令自己就是原子性的,并且提供了一些“附加条件”的参数,可以直接一步到位。

最典型的就是 SET key value NX 命令,NX的意思是“只有当这个键不存在的时候,我才能设置成功”,这个特性天生就是用来做分布式锁的雏形,多个实例要抢着一个执行某个定时任务,你可以让它们都尝试用 SET task_lock "1" NX 来抢锁,只有一个会成功,成功的那个去执行任务,执行完了再DEL掉这个锁,其他的因为NX条件不满足,设置失败,就知道自己没抢到锁。

还有像 INCR(原子性增加)、DECR(原子性减少)、HINCRBY(哈希结构原子增减)等命令,它们本身执行就是原子的,不用担心并发问题,直接用在计数场景下就很好。

第四招,对于更复杂的场景,使用Lua脚本。

如果上面的方法还觉得不够用,比如你的业务逻辑非常复杂,需要先判断很多条件,然后执行一系列操作,那终极武器就是Lua脚本,你可以把一整段Lua代码作为一个字符串发给Redis服务器。

Redis保证的是:Lua脚本在执行时,是原子性的,在执行过程中不会被任何其他命令打断,它就像是把事务(MULTI/EXEC)和WATCH的检查能力合二为一,并且拥有了强大的逻辑处理能力,你把所有复杂的判断和操作都写在脚本里,一次性发送,服务器一次性执行,这样你根本不需要担心在判断和执行之间的空隙里数据被篡改,因为脚本执行是排他的。

你可以写一个Lua脚本,内容大概是:检查用户积分是否足够,检查商品库存是否大于零,如果都满足,则扣除用户积分,减少商品库存,然后生成订单记录,这一大坨操作在Redis看来就是一个命令,完美地解决了并发问题,写Lua脚本要注意别写太耗时的逻辑,否则会阻塞Redis服务器,影响其他请求。

  • 简单增减:直接用原子命令,如INCR/DECR。
  • 简单抢锁/设置:用SET NX。
  • 一组命令需要原子性,但逻辑简单:用MULTI/EXEC事务。
  • 有读后写的检查需求(如抢购):用WATCH+事务,也就是乐观锁。
  • 复杂逻辑,需要绝对的数据一致性:用Lua脚本。

这些技巧从简单到复杂,覆盖了大部分日常开发中会遇到Redis并发问题的场景,核心思想就是利用Redis提供的原子操作特性,来避免多个客户端同时修改数据时产生的冲突。

Redis里那些防止并发冲突的小妙招,保证数据不乱的实用技巧