在我们并发访问Redis时,为了保证并发访问的正确性,Redis提供了两种方法,分别是加锁和原子操作。
加锁是一种常用的方法,在读取数据前,客户端需要先获得锁,否则就无法进行操作。当一个客户端获得锁后,就会一直持有这把锁,直到客户端完成数据更新,才释放这把锁。这种方式存在两个问题,分别是加锁操作多会降低系统的并发访问性能,和Redis客户端要加锁时,需要用到分布式锁分布式锁实现复杂,需要额外的存储系统来提供加锁操作。
原子操作是指执行过程保持原子性操作,而原子操作执行时并不需要加锁,实现了无锁操作,既能保证并发控制,还能减少对系统并发性能的影响。
加锁操作比较简单,我们也常用,重点看一下原子操作。
Redis的两种原子操作方法
为了实现并发控制要求的临界区代码互斥执行,Redis的原子操作采用了两种方法:
- 把多个操作在Redis中实现成一个操作,也就是单命令操作
- 把多个操作写到一个Lua脚本中,以原子性方式执行单个Lua脚本。
Redis是单线程来串行处理客户端的请求操作命令的,所以当Redis执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的。Redis的快照生成、AOF重新这些操作,可以使用后台线程或者子线程执行,也就是和主线程的操作并行执行。不过,这些操作只是读取数据,不会修改数据,所以,我们并不需要对它们做并发控制。
实际应用中,数据修改时可能包含多个操作,至少包括读数据、数据增减、写会数据三个操作,这就涉及三个操作,所以Redis提供了INCR/DECR 命令,把这三个操作转变为一个原子操作了。INCR/DECR 命令可以对数据进行增值 / 减值操作,而且它们本身就是单个命令操作,Redis 在执行它们时,本身就具有互斥性。
如果执行的操作不是简单的增减数据,而是复杂的判断逻辑或者其他操作,Redis的单命令操作已经无法保证多个操作的互斥操作执行,这个时候我们就需要使用Lua脚本。Redis会把整个Lua脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性。
如果把多个操作都放在Lua脚本中原子执行,会导致Redis执行脚本的时间增加,会降低Redis的并发性能,所以,在写Lua脚本的时候,尽量避免把不需要做并发控制的操作写入脚本中。
学习来源:极客时间 《Redis核心技术与实战》 学习笔记 Day19