Redis应对高并发
应对高并发
我们在使用 Redis 时,不可避免地会遇到并发访问的问题。
为了保证并发访问的正确性,Redis 提供了两种方法,分别是加锁和原子操作。
加锁
加锁是一种常用的方法,在读取数据前,客户端需要先获得锁,否则就无法进行操作。当一个客户端获得锁后,就会一直持有这把锁,直到客户端完成数据更新,才释放这把锁。
缺点:
- 如果加锁操作多,会降低系统的并发访问性能。
- Redis 客户端要加锁时,需要用到分布式锁,而分布式锁实现复杂,需要用额外的存储系统来提供加解锁操作。
分布式锁
基于单个 Redis 节点实现分布式锁
用Redis存锁变量,key为变量名,value为锁的值;例如:
lock_key:0
为了保证锁操作的原子性,需要使用原子操作的语句来实现加锁与释放锁。
加锁:
SET key value [EX seconds | PX milliseconds] [NX]
例如:SET lock_key unique_value NX PX 10000
其中,unique_value 是客户端的唯一标识,可以用一个随机生成的字符串来表示,PX 10000 则表示 lock_key 会在 10s 后过期,以免客户端在这期间发生异常而无法释放锁。释放锁:使用lua脚本保证原子性
1
2
3
4
5
6//释放锁 比较unique_value是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end其中,KEYS[1]表示 lock_key,ARGV[1]是当前客户端的唯一标识
多节点的高可靠分布式锁
基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁失败。
实现步骤:
- 客户端获取当前时间
- 客户端按顺序依次向 N 个 Redis 实例执行加锁操作。(这里的加锁操作和在单实例上执行的加锁操作一样)
- 一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时。
客户端只有在满足下面的这两个条件时,才能认为是加锁成功。
- 条件一:客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁;
- 条件二:客户端获取锁的总耗时没有超过锁的有效时间,并且剩余有效时间够完成数据操作。
如果客户端在和所有实例执行完加锁操作后,没能同时满足这两个条件,那么,客户端向所有 Redis 节点发起释放锁的操作。
原子操作
原子操作是另一种提供并发访问控制的方法。原子操作是指执行过程保持原子性的操作,而且原子操作执行时并不需要再加锁,实现了无锁操作。这样一来,既能保证并发控制,还能减少对系统并发性能的影响。
实现方式
把多个操作在 Redis 中实现成一个操作,也就是单命令操作。
虽然 Redis 的单个命令操作可以原子性地执行,但是在实际应用中,数据修改时可能包含多个操作,至少包括读数据、数据增减、写回数据三个操作,这显然就不是单个命令操作了,那该怎么办呢?
Redis 提供了 INCR/DECR 命令,把这三个操作转变为一个原子操作了。INCR/DECR 命令可以对数据进行增值 / 减值操作,而且它们本身就是单个命令操作,Redis 在执行它们时,本身就具有互斥性。
如果操作语句比较复杂,Redis 的单命令操作已经无法保证多个操作的互斥执行了,我们只有使用Lua脚本。
把多个操作写到一个 Lua 脚本中,以原子性方式执行单个 Lua 脚本。
Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性。
在编写 Lua 脚本时,你要避免把不需要做并发控制的操作写入脚本中