Redis核心技术22-无锁原子操作
# Redis核心技术22-无锁原子操作
Redis是不可能避免会涉及到并发访问的问题,比如说如果多个用户同时下单,就会对缓存在 Redis 中的商品库存并发更新。一旦有了并发写操作,数据就会被修改,如果我们没有对并发写请求做好控制,就可能导致数据被改错。
为了保证并发访问的正确性,Redis提供了两种方法:
- 加锁
- 原子操作
本文主要对原子操作进行深入,原子操作是指执行过程保持原子性的操作,而且原子操作执行时并不需要再加锁,实现了无锁操作。这样一来,既能保证并发控制,还能减少对系统并发性能的影响。
# 并发访问中需要对什么进行控制
并发访问控制:是指对多个客户端访问操作同一份数据的过程进行控制,以保证任何一个客户端发送的操作在 Redis 实例上执行时具有互斥性。
并发访问控制对应的操作主要是数据修改操作。当客户端需要修改数据时,基本流程分成两步:
- 客户端先把数据读取到本地,在本地进行修改;
- 客户端修改完数据后,再写回 Redis。
我们把这个流程叫做“读取 - 修改 - 写回”操作(Read-Modify-Write,简称为 RMW 操作)。当有多个客户端对同一份数据执行 RMW 操作的话,我们就需要让 RMW 操作涉及的代码以原子性方式执行。访问同一份数据的 RMW 操作代码,就叫做临界区代码。
当多个客户端并发执行临界区代码的时候,就会存在一些潜在问题,下面是多个客户端对库存进行修改的操作:
可以看到如果按照逻辑流程下来,最后库存应该为8才对,但是因为临界区代码中的客户端读取数据、更新数据、再写回数据涉及了三个操作,而这三个操作在执行时并不具有互斥性,多个客户端基于相同的初始值进行修改,而不是基于前一个客户端修改后的值再修改。
为了保证数据并发修改的正确性,我们可以用锁把并行操作变成串行操作,串行操作就具有互斥性。一个客户端持有锁后,其他客户端只能等到锁释放,才能拿锁再进行修改。
LOCK()
current = GET(id)
current--
SET(id, current)
UNLOCK()
虽然加锁保证了互斥性,但是加锁也会导致系统并发性能降低。
可以看到在客户端A操作的时候,客户端B和C都需要等待的,当客户端B操作的时候,客户端C还需要进行等待。
由于原子操作也能实现并发控制,并且原子操作对系统并发性能的影响较小,下面我们来深入原子操作。
# Redis中的两种原子操作
为了实现并发控制要求的临界区代码互斥执行,Redis 的原子操作采用了两种方法:
- 把多个操作在 Redis 中实现成一个操作,也就是单命令操作;
- 把多个操作写到一个 Lua 脚本中,以原子性方式执行单个 Lua 脚本。
Redis 是使用单线程来串行处理客户端的请求操作命令的,所以,当 Redis 执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的。
由于我们的操作可能涉及到数据读取、数据增删、数据写回三个操作,并非单独一个命令操作。而Redis提供了 INCR/DECR 命令,把这三个操作转变为一个原子操作了。INCR/DECR 命令可以对数据进行增值 / 减值操作,而且它们本身就是单个命令操作,Redis 在执行它们时,本身就具有互斥性。
DECR id
所以,如果我们执行的 RMW 操作是对数据进行增减值的话,Redis 提供的原子操作 INCR 和 DECR 可以直接帮助我们进行并发控制。
但是如果我们的操作涉及到复杂的逻辑判断的时候,就需要使用第二种方法:Lua脚本。这种方法,即使客户端有多个线程同时执行这个脚本,Redis 也会依次串行执行脚本代码,避免了并发操作带来的数据错误。