Redis并发操作
为了保证并发访问的正确性,Redis 提供了两种方法,分别是加锁和原子操作。
原子操作
Redis 的原子操作采用了两种方法:
- 把多个操作在 Redis 中实现成一个操作,也就是单命令操作;
- 把多个操作写到一个 Lua 脚本中,以原子性方式执行单个 Lua 脚本。
Redis 本身的单命令操作
Redis 是使用单线程来串行处理客户端的请求操作命令的,所以,当 Redis 执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的。
Redis 的单命令操作已经无法保证多个操作的互斥执行。
Lua 脚本
Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性。使用 Redis 的 EVAL 命令来执行脚本。
脚本所需的参数将通过以下命令中的 keys 和 args 进行传递:
1 | redis-cli --eval lua.script keys , args |
而 Redis 的 Lua 脚本可以包含多个操作,这些操作都会以原子性的方式执行,绕开了单命令操作的限制。不过,如果把很多操作都放在 Lua 脚本中原子执行,会导致 Redis 执行脚本的时间增加,同样也会降低 Redis 的并发性能。所以,我给你一个小建议:在编写 Lua 脚本时,你要避免把不需要做并发控制的操作写入脚本中。
问题
是否需要把读取客户端 ip 的访问次数 GET(ip),以及判断访问次数是否超过 20 的判断逻辑,也加到 Lua 脚本中?
我觉得不需要,理由主要有2个。
1、这2个逻辑都是读操作,不会对资源临界区产生修改,所以不需要做并发控制。
2、减少 lua 脚本中的命令,可以降低Redis执行脚本的时间,避免阻塞 Redis。
另外使用lua脚本时,还有一些注意点:
1、lua 脚本尽量只编写通用的逻辑代码,避免直接写死变量。变量通过外部调用方传递进来,这样 lua 脚本的可复用度更高。
2、建议先使用SCRIPT LOAD命令把 lua 脚本加载到 Redis 中,然后得到一个脚本唯一摘要值,再通过EVALSHA命令 + 脚本摘要值来执行脚本,这样可以避免每次发送脚本内容到 Redis,减少网络开销。
分布式锁
SET key value [EX seconds] [PX milliseconds] [NX|XX]
将字符串值 value
关联到 key
。
如果 key
已经持有其他值, SET 就覆写旧值,无视类型。
对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。
从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
EX second
:设置键的过期时间为second
秒。SET key value EX second
效果等同于SETEX key second value
。PX millisecond
:设置键的过期时间为millisecond
毫秒。SET key value PX millisecond
效果等同于PSETEX keymillisecond value
。NX
:只在键不存在时,才对键进行设置操作。SET key value NX
效果等同于SETNX key value
。XX
:只在键已经存在时,才对键进行设置操作
返回值:
在 Redis 2.6.12 版本以前, SET 命令总是返回 OK
。
从 Redis 2.6.12 版本开始, SET 在设置操作成功完成时,才返回 OK
。
如果设置了 NX
或者 XX
,但因为条件没达到而造成设置操作未执行,那么命令返回空批量回复(NULL Bulk Reply)。
http://doc.redisfans.com/string/set.html
1 | SET key value [EX seconds | PX milliseconds] [NX] |
其中,unique_value 是客户端的唯一标识,可以用一个随机生成的字符串来表示,PX 10000 则表示 lock_key 会在 10s 后过期,以免客户端在这期间发生异常而无法释放锁。
因为在加锁操作中,每个客户端都使用了一个唯一标识,所以在释放锁操作时,我们需要判断锁变量的值,是否等于执行释放锁操作的客户端的唯一标识,如下所示:
1 | //释放锁 比较unique_value是否相等,避免误释放 |
这是使用 Lua 脚本(unlock.script)实现的释放锁操作的伪代码,其中,KEYS[1]表示 lock_key,ARGV[1]是当前客户端的唯一标识,这两个值都是我们在执行 Lua 脚本时作为参数传入的。