1.概述

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

1.1 加锁

概念:在读取数据前,客户端需要先获取锁,否则无法进行操作;当一个客户端获取锁后,就会一直持有这把锁,直到客户端完成数据更新,才释放这把锁。

问题:

  • 加锁操作多,降低系统的并发访问性能;
  • Redis客户端加锁时,需要用到分布式锁,而分布式锁实现复杂,需要用额外的存储系统来提供加解锁操作。

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

指对多个客户端访问操作同一份数据的过程进行控制,以保证任何一个客户端发送的操作在Redis实例上执行时具有互斥性。并发访问控制主要是数据修改操作。修改数据时,基本流程为两步:

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

我们把这个流程叫做 “读取-修改-写回” 操作(Read-Modify-Write,简称为 RMW 操作)。访问同一份数据的RMW操作代码,叫做临界区代码。

例子:

加锁前:

img

出现这个现象的原因是,临界区代码中的客户端 读取数据、更新数据、再写回数据 涉及了三个操作,而这三个操作在执行时并不具有互斥性,多个客户端基于相同的初始值进行修改,而不是基于前一个客户端修改后的值再修改。

加锁后:

img

1.2 Redis原子操作方法

  1. 把多个操作放到Redis中实现成一个操作,即单命令操作;
  2. 把多个操作写到一个Lua脚本中,以原子性方式执行单个Lua脚本。

1.2.1 单命令操作

Redis是使用单线程来串行处理客户端的请求操作命令的。当执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的。在实际应用中,数据修改需要包含多个操作,至少包含读数据、数据增减、写数据。Redis提供了incr/decr命令,将三个操作转变成一个原子操作了。所以,若我们执行的RMW操作是对数据增减的话,incr/decr可以帮助我们进行并发控制。

Lua脚本

Redis会把整个Lua脚本作为一个整体执行,执行的过程中,不会被其他命令打断,从而保证了Lua脚本中操作的原子性。如果我们有多个操作要执行,但是又无法用incr/decr这种命令来实现,就可以把要执行的操作写到Lua脚本中,使用Rediseval命令来执行脚本,这些操作就具有了互斥性。

总结

在并发访问时,并发的RMW操作会导致数据错误,所以需要进行并发控制。所谓并发控制,就是要保证临界区代码的互斥执行。Redis提供了两种原子操作的方法来实现并发控制,分别是单命令操作和Lua脚本,因为原子操作本身不会对太多的资源限制访问,可以维持较高的系统并发性能。但是,单命令原子操作的使用范围较小,并不是所有的RMW操作都能转变成单命令的原子操作(例如incr/decr命令只能在读取数据后做原子增减),当我们需要对读取的数据做更多的判断,或者是我们对数据的修改不是简单的增减时,单命令操作就不适用了。而RedisLua脚本可以包含多个操作,这些操作都会以原子性的方式执行,绕开了单命令的限制。不过,如果把很多操作都放在Lua脚本中原子执行,会导致Redis执行脚本的时间增加,同样也会降低Redis的并发性能。所以,建议:在编写Lua脚本时,避免把不需要做并发控制的操作写入脚本中。当然,加锁也能实现临界区代码的互斥执行,只是如果有多个客户端加锁时,就需要分布式锁的支持了。