Redis学习笔记 无锁的原子操作:Redis如何应对并发访问

2023年1月2日09:29:43

为了保证并发访问的正确性,Redis 提供了两种方法,分别是加锁和原子操作。

但是加锁会遇到两个问题:

  • 首先是加锁操作过多会降低系统的并发访问性能
  • 其次,Redis客户端要加锁时,需要使用分布式锁,而分布式锁实现复杂

原子操作是另一种提供并发访问控制的方法,实现了无锁操作。既可以保证并发控制,还能减少系统对并发性能的影响。

并发访问中需要对什么进行控制?

当客户端需要修改数据时,基本流程分成两步:

  1. 客户端先把数据读取到本地,在本地进行修改;
  2. 客户端修改完数据后,再写回 Redis。

即为“读取 - 修改 - 写回”操作,也称为RMW操作。当有多个客户端对同一份数据执行 RMW 操作的话,我们就需要让 RMW 操作涉及的代码以原子性方式执行。访问同一份数据的 RMW 操作代码,就叫做临界区代码。

Redis的两种原子操作方法

为了实现并发控制要求的临界区代码互斥执行,Redis 的原子操作采用了两种方法:

  • 把多个操作在 Redis 中实现成一个操作,也就是单命令操作
  • 把多个操作写到一个 Lua 脚本中,以原子性方式执行单个 Lua 脚本

单命令操作

Redis 提供了INCR/DECR命令,把RMW这三个操作转变为一个原子操作了。

INCR/DECR 命令可以对数据进行增值 / 减值操作,而且它们本身就是单个命令操作,Redis 在执行它们时,本身就具有互斥性。

比如,下面的伪代码显示了使用锁来控制临界区代码,对库存值做了一次扣减

LOCK()
current = GET(id)
current--
SET(id, current)
UNLOCK()

而使用单命令操作则如下:

DECR id

所以,如果我们执行的 RMW 操作是对数据进行增减值的话,Redis 提供的原子操作 INCR 和 DECR 可以直接帮助我们进行并发控制。

Lua脚本

如果我们要执行的操作不是简单地增减数据,而是有更加复杂的判断逻辑或者是其他操作,那么,Redis 的单命令操作已经无法保证多个操作的互斥执行了。所以,这个时候,我们需要使用第二个方法,也就是 Lua 脚本。

Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性。

比如,我们有时需要限制某个客户端在一定时间范围内的访问次数,在一分钟内只能访问一次。所以,这个例子中的操作无法用 Redis 单个命令来实现,此时,我们就可以使用 Lua 脚本来保证并发控制。我们可以把访问次数加 1、判断访问次数是否为 1,以及设置过期时间这三个操作写入一个 Lua 脚本,如下所示:

local current
current = redis.call("incr",KEYS[1])
if tonumber(current) == 1 then
    redis.call("expire",KEYS[1],60)
end

假设我们编写的脚本名称为 lua.script,我们接着就可以使用 Redis 客户端,带上 eval 选项,来执行该脚本。脚本所需的参数将通过以下命令中的 keys 和 args 进行传递。

redis-cli  --eval lua.script  keys , args

这样一来,访问次数加 1、判断访问次数是否为 1,以及设置过期时间这三个操作就可以原子性地执行了。即使客户端有多个线程同时执行这个脚本,Redis 也会依次串行执行脚本代码,避免了并发操作带来的数据错误。

Redis 的 Lua 脚本可以包含多个操作,这些操作都会以原子性的方式执行,绕开了单命令操作的限制。不过,如果把很多操作都放在 Lua 脚本中原子执行,会导致 Redis 执行脚本的时间增加,同样也会降低 Redis 的并发性能。所以,在编写 Lua 脚本时,你要避免把不需要做并发控制的操作写入脚本中

  • 作者:qq_34132502
  • 原文链接:https://blog.csdn.net/qq_34132502/article/details/118702868
    更新时间:2023年1月2日09:29:43 ,共 1493 字。