Redis学习笔记29——无锁的原子操作:Redis如何应对并发访问
Posted qq_34132502
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis学习笔记29——无锁的原子操作:Redis如何应对并发访问相关的知识,希望对你有一定的参考价值。
为了保证并发访问的正确性,Redis 提供了两种方法,分别是加锁和原子操作。
但是加锁会遇到两个问题:
- 首先是加锁操作过多会降低系统的并发访问性能
- 其次,Redis客户端要加锁时,需要使用分布式锁,而分布式锁实现复杂
原子操作是另一种提供并发访问控制的方法,实现了无锁操作。既可以保证并发控制,还能减少系统对并发性能的影响。
并发访问中需要对什么进行控制?
当客户端需要修改数据时,基本流程分成两步:
- 客户端先把数据读取到本地,在本地进行修改;
- 客户端修改完数据后,再写回 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 脚本时,你要避免把不需要做并发控制的操作写入脚本中。
以上是关于Redis学习笔记29——无锁的原子操作:Redis如何应对并发访问的主要内容,如果未能解决你的问题,请参考以下文章