spring boot项目之redis分布式锁的应用
2021-06-22 14:03
起始版本:1.0.0
时间复杂度:O(1)
将key
设置值为value
,如果key
不存在,这种情况下等同SET命令。 当key
存在时,什么也不做。SETNX
是”SET if Not eXists”的简写。
返回值
Integer reply, 特定值:
-
1
如果key被设置了 -
0
如果key没有被设置
##例子
redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis>
Design pattern: Locking with !SETNX
设计模式:使用!SETNX
加锁
Please note that:
请注意:
-
不鼓励以下模式来实现the Redlock algorithm ,该算法实现起来有一些复杂,但是提供了更好的保证并且具有容错性。
-
无论如何,我们保留旧的模式,因为肯定存在一些已实现的方法链接到该页面作为引用。而且,这是一个有趣的例子说明Redis命令能够被用来作为编程原语的。
-
无论如何,即使假设一个单例的加锁原语,但是从 2.6.12 开始,可以创建一个更加简单的加锁原语,相当于使用
SET
命令来获取锁,并且用一个简单的 Lua 脚本来释放锁。该模式被记录在SET
命令的页面中。
也就是说,SETNX
能够被使用并且以前也在被使用去作为一个加锁原语。例如,获取键为foo
的锁,客户端可以尝试一下操作:
SETNX lock.foo
如果客户端获得锁,SETNX
返回1
,那么将lock.foo
键的Unix时间设置为不在被认为有效的时间。客户端随后会使用DEL lock.foo
去释放该锁。
如果SETNX
返回0
,那么该键已经被其他的客户端锁定。如果这是一个非阻塞的锁,才能立刻返回给调用者,或者尝试重新获取该锁,直到成功或者过期超时。
处理死锁
以上加锁算法存在一个问题:如果客户端出现故障,崩溃或者其他情况无法释放该锁会发生什么情况?这是能够检测到这种情况,因为该锁包含一个Unix时间戳,如果这样一个时间戳等于当前的Unix时间,该锁将不再有效。
当以下这种情况发生时,我们不能调用DEL
来删除该锁,并且尝试执行一个SETNX
,因为这里存在一个竞态条件,当多个客户端察觉到一个过期的锁并且都尝试去释放它。
- C1 和 C2 读
lock.foo
检查时间戳,因为他们执行完SETNX
后都被返回了0
,因为锁仍然被 C3 所持有,并且 C3 已经崩溃。 - C1 发送
DEL lock.foo
- C1 发送
SETNX lock.foo
命令并且成功返回 - C2 发送
DEL lock.foo
- C2 发送
SETNX lock.foo
命令并且成功返回 - 错误:由于竞态条件导致 C1 和 C2 都获取到了锁
幸运的是,可以使用以下的算法来避免这种情况,请看 C4 客户端所使用的好的算法:
- C4 发送
SETNX lock.foo
为了获得该锁 - 已经崩溃的客户端 C3 仍然持有该锁,所以Redis将会返回
0
给 C4 - C4 发送
GET lock.foo
检查该锁是否已经过期。如果没有过期,C4 客户端将会睡眠一会,并且从一开始进行重试操作 -
另一种情况,如果因为
lock.foo
键的Unix时间小于当前的Unix时间而导致该锁已经过期,C4 会尝试执行以下的操作:GETSET lock.foo
- 由于
GETSET
的语意,C4会检查已经过期的旧值是否仍然存储在lock.foo
中。如果是的话,C4 会获得锁 - 如果另一个客户端,假如为 C5 ,比 C4 更快的通过
GETSET
操作获取到锁,那么 C4 执行GETSET
操作会被返回一个不过期的时间戳。C4 将会从第一个步骤重新开始。请注意:即使 C4 在将来几秒设置该键,这也不是问题。
为了使这种加锁算法更加的健壮,持有锁的客户端应该总是要检查是否超时,保证使用DEL
释放锁之前不会过期,因为客户端故障的情况可能是复杂的,不止是崩溃,还会阻塞一段时间,阻止一些操作的执行,并且在阻塞恢复后尝试执行DEL
(此时,该LOCK已经被其他客户端所持有)
GETSET key value
起始版本:1.0.0
时间复杂度:O(1)
自动将key对应到value并且返回原来key对应的value。如果key存在但是对应的value不是字符串,就返回错误。
设计模式
GETSET可以和INCR一起使用实现支持重置的计数功能。举个例子:每当有事件发生的时候,一段程序都会调用INCR给key mycounter加1,但是有时我们需要获取计数器的值,并且自动将其重置为0。这可以通过GETSET mycounter “0”来实现:
INCR mycounter
GETSET mycounter "0"
GET mycounter
返回值
bulk-string-reply: 返回之前的旧值,如果之前Key
不存在将返回nil
。
例子
redis> INCR mycounter (integer) 1 redis> GETSET mycounter "0" "1" redis> GET mycounter "0" redis>
那么,我们在处理高并发秒杀下单时,就可以通过redis来实现加锁和解锁的功能来做到
package com.imooc.service;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@Component
@Slf4j
public class RedisLock {@Autowired
private StringRedisTemplate redisTemplate;/**
* 加锁
* @param key
* @param value 当前时间+超时时间
* @return
*/
public boolean lock(String key, String value) {
if(redisTemplate.opsForValue().setIfAbsent(key, value)) {
return true;
}
//currentValue=A 这两个线程的value都是B 其中一个线程拿到锁
String currentValue = redisTemplate.opsForValue().get(key);
//如果锁过期
if (!StringUtils.isEmpty(currentValue)
&& Long.parseLong(currentValue) //获取上一个锁的时间
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
if (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)) {
return true;
}
}return false;
}/**
* 解锁
* @param key
* @param value
*/
public void unlock(String key, String value) {
try {
String currentValue = redisTemplate.opsForValue().get(key);
if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e) {
log.error("【redis分布式锁】解锁异常, {}", e);
}
}}