java中的锁,Synchronized是基于对象头部的锁标志位,Lock是基于volatile的一个变量,但是在微服务多个不同的进程之间这些标志位是不共享的,因此需要一个为分布式服务,存储共享锁标志。常见的分布式锁:redis分布式锁,zookeeper分布式锁,数据库的分布式锁等。
基于分布式锁现在已经有很多开源的实现,我们可以直接引用就行,基于redis的redission,基于zookeeper的 Curator框架,Spring框架也为此为我们提供了统一的分布式锁的定义接口。
参考学习链接: https://blog.csdn.net/qq_35529801/article/details/103878784
我们也可以手动实现下redis分布式锁,体验一下快感(参考下spring 框架的定义):
1. 先创建一个spring boot项目
引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
配置redis连接信息
server:
port: 8888
spring:
redis:
host: 192.168.10.103
port: 6379
2.实现
2.1 最简单的实现version1
在redis中判断key,如果存在表示已经有人持有锁了,没有则我们放入这个key去获取锁,执行完业务逻辑将这个key删除。
key我们自定义: lock:consume
value随便设置: hjj
@Component
public class LockDemo {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean lockVersion1() {
String lockValue = redisTemplate.opsForValue().get("lock:consume");
if (lockValue == null) {
redisTemplate.opsForValue().set("lock:consume", "hjj");
return true;
}
return false;
}
public void unlockVersion1() {
redisTemplate.delete("lock:consume");
}
}
测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class LockDemoTest {
@Autowired
private LockDemo lockDemo;
@Test
public void lockVersion1() {
System.out.println("第1个人获取锁" + lockDemo.lockVersion1());
System.out.println("第2个人获取锁" + lockDemo.lockVersion1());
}
@Test
public void unlockVersion1() {
lockDemo.unlockVersion1();
}
}
redis中的key已经存在
然后我们运行释放锁的测试方法,redis的键已经不存在了。
我们来模仿下实际应用
@Test
public void lockTest() {
try {
if (lockDemo.lockVersion1()) {
System.out.println("执行业务逻辑,睡100秒钟");
Thread.sleep(100000);
} else {
System.out.println("获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("释放锁");
lockDemo.unlockVersion1();
}
}
@Test
public void lockTest2() {
try {
if (lockDemo.lockVersion1()) {
System.out.println("执行业务逻辑,睡100秒钟");
Thread.sleep(100000);
} else {
System.out.println("获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("释放锁");
lockDemo.unlockVersion1();
}
}
测试1方法
测试2方法
你会发现他们使用同一个Key在没获取到锁的时候也会去释放锁,删除key,这样会使test1在执行业务逻辑期间,它的锁被test2获取失败后,释放掉了,这样再来test3用户又能去获取了,很明显是有问题的,我们需要一个标识来标记这个锁属于此人,如果不是它的,执行释放锁操作就不能进行操作。
2.1 实现version2(预防非法释放锁)
怎样去标识呢,UUID了解下,百度介绍,简而言之就是uuid全宇宙不会重复
@Component
public class LockDemo2 {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private String lockValue;
public boolean lockVersion2() {
String lockValue = redisTemplate.opsForValue().get("lock:consume");
if (lockValue == null) {
String uuid = UUID.randomUUID().toString();
this.lockValue = uuid;
redisTemplate.opsForValue().set("lock:consume", uuid);
return true;
}
return false;
}
public void unlockVersion2() {
if (lockValue != null && lockValue.equals(redisTemplate.opsForValue().get("lock:consume"))) {
System.out.println("我的锁我自己释放了");
redisTemplate.delete("lock:consume");
} else {
System.out.println("不是我的锁我不释放");
}
}
}
测试方法
@RunWith(SpringRunner.class)
@SpringBootTest
public class LockDemoTest {
@Autowired
private LockDemo2 lockDemo2;
@Test
public void lockTest() {
try {
if (lockDemo2.lockVersion2()) {
System.out.println("用户1执行业务逻辑,睡100秒钟");
Thread.sleep(100000);
} else {
System.out.println("用户1获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("用户1释放锁");
lockDemo2.unlockVersion2();
}
}
@Test
public void lockTest2() {
try {
if (lockDemo2.lockVersion2()) {
System.out.println("用户2执行业务逻辑,睡100秒钟");
Thread.sleep(100000);
} else {
System.out.println("用户2获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("用户2释放锁");
lockDemo2.unlockVersion2();
}
}
@Test
public void lockTest3() {
try {
if (lockDemo2.lockVersion2()) {
System.out.println("用户3执行业务逻辑,睡100秒钟");
Thread.sleep(100000);
} else {
System.out.println("用户3获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("用户3释放锁");
lockDemo2.unlockVersion2();
}
}
}
redis的key还存在,并且其他用户获取锁失败了
这次解决了非法释放的问题,我们再来看加锁的代码
public boolean lockVersion2() {
String lockValue = redisTemplate.opsForValue().get("lock:consume");
if (lockValue == null) {
String uuid = UUID.randomUUID().toString();
this.lockValue = uuid;
redisTemplate.opsForValue().set("lock:consume", uuid);
return true;
}
return false;
}
虽然redis是单线程的,但是如果两个人同时读到key为lock:consumer的没有设置值的情况
因此我们需要将查看redis的值是否存在和设置值弄成一个不可分割的操作,类似于事务,而redis也为我们提供了这个命令 setnx key value,只有在不存在的时候才会去设置值,存在就不设置值了。
2.3 version3(操作原子性)
将判断锁和加锁一步完成。
java代码
@Component
public class LockDemo3 {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private String lockValue;
public boolean lockVersion2() {
String uuid = UUID.randomUUID().toString();
if (redisTemplate.opsForValue().setIfAbsent("lock:consume", uuid)) {
this.lockValue = uuid;
return true;
}
return false;
}
public void unlockVersion2() {
if (lockValue != null && lockValue.equals(redisTemplate.opsForValue().get("lock:consume"))) {
System.out.println("我的锁我自己释放了");
redisTemplate.delete("lock:consume");
} else {
System.out.println("不是我的锁我不释放");
}
}
}
这次看似肯定没问题了,分布式服务有个最大的特点就是防止单点灾难,如果你在加锁期间你的服务挂了咋办,你的key一直不会被释放,这样大家一块服务不能使用了,肯定不行,redis也有设置键的过期命令set key value ex number nx 其中number就是时间,nx表示不存在才会执行。
但是超时时间设置多长呢,这是一个问题哦,设置的长了,没影响,运行完业务直接删除就行了,但 是设置的短了,你还在执行业务,锁没了,其他人直接用了,我们改咋整呢。
聪明的大家肯定能想到定时任务其周期刷新,如果我们设置一个定时任务去周期性的帮我们续费key的时间。如果这个线程一直在,就一直续费,感觉不错。
2.4 version4(续费)
大体思路是:
获取锁成功,启动一个定时任务去周期设置key的失效时间,当然在key不存在或者此线程已经没了,也就是执行完业务之后,我们应该停止此定时任务
@Component
public class LockDemo4 {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private String lockValue;
public boolean lockVersion4() {
String uuid = UUID.randomUUID().toString();
if (redisTemplate.opsForValue().setIfAbsent("lock:consume", uuid)) {
this.lockValue = uuid;
renewKey(Thread.currentThread(), "lock:consume");
return true;
}
return false;
}
public void unlockVersion4() {
if (lockValue != null && lockValue.equals(redisTemplate.opsForValue().get("lock:consume"))) {
System.out.println("我的锁我自己释放了");
redisTemplate.delete("lock:consume");
} else {
System.out.println("不是我的锁我不释放");
}
}
/**
* 定时续费
* @param thread
* @param key
*/
public void renewKey(Thread thread, String key) {
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (thread.isAlive() && redisTemplate.hasKey(key)) {
System.out.println("线程还在,给key续30秒");
redisTemplate.expire(key, 30, TimeUnit.SECONDS);
} else {
System.out.println("线程已经不存在,终止定时任务");
throw new RuntimeException("终止定时任务");
}
}
}, 10, 10, TimeUnit.SECONDS);
}
}
测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class LockDemoTest {
@Autowired
private LockDemo4 lockDemo4;
@Test
public void lockTest() {
try {
if (lockDemo4.lockVersion4()) {
System.out.println("用户1执行业务逻辑,睡50秒钟");
Thread.sleep(50000);
} else {
System.out.println("用户1获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("用户1释放锁");
lockDemo4.unlockVersion4();
}
}
@Test
public void lockTest2() {
try {
if (lockDemo4.lockVersion4()) {
System.out.println("用户2执行业务逻辑,睡100秒钟");
Thread.sleep(100000);
} else {
System.out.println("用户2获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("用户2释放锁");
lockDemo4.unlockVersion4();
}
}
@Test
public void lockTest3() {
try {
if (lockDemo4.lockVersion4()) {
System.out.println("用户3执行业务逻辑,睡100秒钟");
Thread.sleep(100000);
} else {
System.out.println("用户3获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("用户3释放锁");
lockDemo4.unlockVersion4();
}
}
}
启动测试方法1和测试方法2
redis的key已经删除
测试一下异常情况,直接终止方法
实现到现在只能感觉自己太牛了,只需要改动下代码让其更符合使用的逻辑即可,比如说key让用户传进来,让用户自己设置过期时间,阻塞获取锁,或者定时一段时间内去获取锁。
2.5 version5(优化下接口)
实现逻辑
@Component
public class LockDemo5 {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private String lockValue;
private ThreadLocal<String> keyMap = new ThreadLocal<>();
@Autowired
private ScheduledExecutorService scheduledExecutorService;
public boolean tryLock(String key) {
keyMap.set(key);
String uuid = UUID.randomUUID().toString();
if (redisTemplate.opsForValue().setIfAbsent(key, uuid)) {
this.lockValue = uuid;
renewKey(Thread.currentThread(), key);
return true;
}
return false;
}
public boolean tryLock(String key, long time) {
keyMap.set(key);
String uuid = UUID.randomUUID().toString();
Instant endTime = Instant.now().plusMillis(time);
while(Instant.now().getEpochSecond() < endTime.getEpochSecond()) {
if (redisTemplate.opsForValue().setIfAbsent(key, uuid)) {
this.lockValue = uuid;
renewKey(Thread.currentThread(), key);
return true;
}
}
keyMap.remove();
return false;
}
public void lock(String key) {
keyMap.set(key);
String uuid = UUID.randomUUID().toString();
while (true) {
if (redisTemplate.opsForValue().setIfAbsent(key, uuid)) {
this.lockValue = uuid;
renewKey(Thread.currentThread(), key);
break;
}
}
}
public void unlock() {
String key = keyMap.get();
System.out.println(lockValue);
System.out.println(redisTemplate.opsForValue().get(key));
if (lockValue != null && lockValue.equals(redisTemplate.opsForValue().get(key))) {
System.out.println(LocalDateTime.now() + " 我的锁我自己释放了");
redisTemplate.delete(key);
keyMap.remove();
} else {
System.out.println(LocalDateTime.now() + " 不是我的锁我不释放");
}
}
/**
* 定时续费
* @param thread
* @param key
*/
public void renewKey(Thread thread, String key) {
scheduledExecutorService.scheduleAtFixedRate(() -> {
if (thread.isAlive() && redisTemplate.hasKey(key)) {
System.out.println(LocalDateTime.now() + " 线程还在,给key续30秒");
redisTemplate.expire(key, 30, TimeUnit.SECONDS);
} else {
System.out.println("线程已经不存在,终止定时任务");
throw new RuntimeException("终止定时任务");
}
}, 10, 10, TimeUnit.SECONDS);
}
}
配置类
@Configuration
public class LockDemo5Config {
@Bean
public ConcurrentHashMap<Thread, String> map() {
return new ConcurrentHashMap<>();
}
/**
* 使用线程池优化新性能
* @return
*/
@Bean
public ScheduledThreadPoolExecutor scheduledThreadPoolExecutor() {
return new ScheduledThreadPoolExecutor(10);
}
}
测试阻塞获取
@RunWith(SpringRunner.class)
@SpringBootTest
public class LockDemoTest {
@Autowired
private LockDemo5 lockDemo5;
private String key = "lock";
@Test
public void lockTest() {
try {
System.out.println(LocalDateTime.now() + " 用户1开始获取锁");
lockDemo5.lock(key);
System.out.println(LocalDateTime.now() + " 用户1获取锁成功");
System.out.println(LocalDateTime.now() + " 用户1执行业务逻辑,睡50秒钟");
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(LocalDateTime.now() + " 用户1释放锁");
lockDemo5.unlock();
}
}
@Test
public void lockTest2() {
try {
System.out.println(LocalDateTime.now() + " 用户2开始获取锁");
lockDemo5.lock(key);
System.out.println(LocalDateTime.now() + " 用户2获取锁成功");
System.out.println(LocalDateTime.now() + " 用户2执行业务逻辑,睡50秒钟");
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(LocalDateTime.now() + " 用户2释放锁");
lockDemo5.unlock();
}
}
}
测试一直阻塞获取方法
两个用户的执行业务时间完美隔开,nice!
测试尝试获取,如果获取不到直接返回
@Test
public void lockTest3() {
try {
System.out.println(LocalDateTime.now() + " 用户1尝试开始获取锁");
if (lockDemo5.tryLock(key)) {
System.out.println(LocalDateTime.now() + " 用户1获取锁成功");
System.out.println(LocalDateTime.now() + " 用户1执行业务逻辑,睡50秒钟");
Thread.sleep(50000);
} else {
System.out.println(LocalDateTime.now() + " 用户1尝试获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(LocalDateTime.now() + " 用户1释放锁");
lockDemo5.unlock();
}
}
@Test
public void lockTest4() {
try {
System.out.println(LocalDateTime.now() + " 用户2尝试开始获取锁");
if (lockDemo5.tryLock(key)) {
System.out.println(LocalDateTime.now() + " 用户2获取锁成功");
System.out.println(LocalDateTime.now() + " 用户2执行业务逻辑,睡50秒钟");
Thread.sleep(50000);
} else {
System.out.println(LocalDateTime.now() + " 用户2尝试获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(LocalDateTime.now() + " 用户2释放锁");
lockDemo5.unlock();
}
}
测试等待多久获取,获取不到再返回
测试业务执行50秒,等待60秒能获取到的情况
测试业务执行50秒,等待40秒获取不到的情况
这个时候感觉自己已经做得很棒了,我们来百度下分布式锁的特性:
分布式锁实现有多种方式,其原理都基本类似,只要满足下列要求即可:
多进程可见:多进程可见,否则就无法实现分布式效果
互斥():同一时刻,只能有一个进程获得锁,执行任务后释放锁
可重入(可选):同一个任务再次获取改锁不会被死锁
阻塞锁(可选):获取失败时,具备重试机制,尝试再次获取锁
性能好(可选):效率高,应对高并发场景
高可用:避免锁服务宕机或处理好宕机的补救措施
高可用,我们好像不满足,这个需要redis集群来满足,可重入,感觉没用到多次获取这种情况,但是想想synchronized可以调用synchronized的方法并且不会出现问题,我们先来搭建个redis集群吧,我们现在的锁功能好像满足了,但是我们应该满足设计模式的面向接口编程,我们可以抄袭下spring 分布式锁的接口定义。
docker搭建redis集群: https://mp.csdn.net/postedit/103963576
2.6 最终版(提供可重入,优化接口设计)
synchronized是以一个标志位monitor如果是0表示,没有用,这个时候进行+1,当再次获取时(synchronized方法调用另一个synchronized方法)会判断这个线程是够持有这个锁,如果持有进行+1,运行完这个方法,进行-1,相对的我们可以使用redis中的hash来存储这个。
key 分布式锁 field uuid value count
获取锁的步骤:
1.先判断key是否存在
2.如果存在,判断是否是自己的锁,使用唯一的uuid表示,如果是,给count +1,如果不是表示锁已经被别人占有,加锁失败
3.如果不存在,表示锁还没有被持有,则添加hash,key为分布式锁的标识,field为uuid,唯一的锁身份标识,标识是谁的锁,value设置为1表示进入了一次
释放锁的步骤:
1.先判断key是否存在
2.如果存在,则判断是不是自己的锁,通过唯一的身份标识uuid,如果是,count进行-1操作,-1之后如果值为0,则删除这个hash。如果不是自己的锁,则不做任何操作
3.如果不存在,不做任何操作
这个时候你的注意大家如果都读取到那个能获取锁的时间,同时加锁咋整,虽然redis是单线程的,但是如果两个人读取Key是否存在刚好同时操作,就会出问题,为此我们需要将获取锁和释放锁以数据库的事务一样要么全部完成,要么都失败,但是很不幸redis的事务并不是数据库的事务,不过也相应的提供了lua脚本功能,你可以在脚本中,将执行的redis命令一次性执行完,对于redis而言他就是一条命令。需要专门去学这东西吗,我个人感觉用处不大,用的时候直接复制过来就行,而且看起来也不是很难懂。
LockRegistry接口
public interface LockRegistry {
/**
* 创建锁
* @param key
* @return
*/
Lock obtain(String key);
}
Lock接口
public interface Lock {
/**
* 尝试加锁,如果获取不到,就阻塞
*/
void lock();
/**
* 尝试加锁,如果获取到返回true,如获取不到返回false
* @return
*/
boolean tryLock();
/**
* 尝试加锁,如果获取指定时间内没有获取到返回true,如获取不到返回false
* @param time
* @return
*/
boolean tryLock(long time);
/**
* 释放锁
*/
void unlock();
}
RedisLockConfiguration
@Configuration
public class RedisLockConfiguration {
@Bean
public LockRegistry lockRegistry(StringRedisTemplate redisTemplate) {
return new RedisLockRegistry(redisTemplate,"hjj");
}
}
RedisLockRegistry
public class RedisLockRegistry implements LockRegistry {
protected String lockPrefix;
protected StringRedisTemplate redisTemplate;
public RedisLockRegistry(StringRedisTemplate redisTemplate,String lockPrefix) {
this.redisTemplate = redisTemplate;
this.lockPrefix = lockPrefix;
}
@Override
public Lock obtain(String key) {
return new RedisLockImpl(redisTemplate, lockPrefix + ":" + key);
}
}
RedisLockImpl接口
public class RedisLockImpl implements Lock {
private StringRedisTemplate redisTemplate;
private String lockKey;
private String lockKeyValue;
private long DEFAULT_RELEASE_TIME = 30;
private static final DefaultRedisScript<Long> LOCK_SCRIPT;
private static final DefaultRedisScript<Object> UNLOCK_SCRIPT;
private ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
static {
// 加载释放锁的脚本
LOCK_SCRIPT = new DefaultRedisScript<>();
LOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new
ClassPathResource("lock.lua")));
LOCK_SCRIPT.setResultType(Long.class);
// 加载释放锁的脚本
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setScriptSource(new ResourceScriptSource(new
ClassPathResource("unlock.lua")));
}
public RedisLockImpl(StringRedisTemplate redisTemplate, String lockKey) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey;
this.lockKeyValue = UUID.randomUUID().toString();
}
@Override
public boolean tryLock() {
// 执行脚本
Long result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockKey),
lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));
// 判断结果
return result != null && result.intValue() == 1;
}
@Override
public boolean tryLock(long time) {
Instant endTime = Instant.now().plusMillis(time);
while(Instant.now().getEpochSecond() < endTime.getEpochSecond()) {
Long result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockKey),
lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));
if (result != null && result.intValue() == 1) {
renewKey(Thread.currentThread());
return true;
}
}
return false;
}
@Override
public void lock() {
while (true) {
Long result = redisTemplate.execute(LOCK_SCRIPT, Collections.singletonList(lockKey),
lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));
if (result != null && result.intValue() == 1) {
renewKey(Thread.currentThread());
break;
}
}
}
@Override
public void unlock() {
// 执行脚本
redisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(lockKey),
lockKeyValue, String.valueOf(DEFAULT_RELEASE_TIME));
}
/**
* 定时续费
* @param thread
*/
public void renewKey(Thread thread) {
scheduledExecutorService.scheduleAtFixedRate(() -> {
if (thread.isAlive() && redisTemplate.hasKey(lockKey)) {
redisTemplate.expire(lockKey, DEFAULT_RELEASE_TIME, TimeUnit.SECONDS);
} else {
throw new RuntimeException("终止定时任务");
}
}, 10, 10, TimeUnit.SECONDS);
}
}
lua脚本放在resource文件夹下
lock.lua
local key = KEYS[1]
local threadId = ARGV[1]
local releaseTime = ARGV[2]
if(redis.call('exists', key) == 0)
then
redis.call('hset', key, threadId, '1')
redis.call('expire', key, releaseTime)
return 1
end
if(redis.call('hexists', key, threadId) == 1)
then
redis.call('hincrby', key, threadId, '1')
redis.call('expire', key, releaseTime)
return 1
end
return 0
unlock.lua
local key = KEYS[1]
local threadId = ARGV[1]
local releaseTime = ARGV[2]
if (redis.call('HEXISTS', key, threadId) == 0) then
return nil
end
local count = redis.call('HINCRBY', key, threadId, -1)
if (count > 0) then
redis.call('EXPIRE', key, releaseTime)
return nil
else
redis.call('DEL', key)
return nil
end
lua脚本有中文注释报这种奇怪的错误,有毒
测试阻塞获取 void lock()
测试尝试获取boolean tryLock()
测试一段时间内不停尝试获取 boolean tryLock(long time)
2.7 关于redis高可用的疑问
主从模式下的集群,都存在数据的复制延迟,可能在主节点用户1加锁成功,这个时候数据还没有复制到从节点,然后主节点挂掉了,然后从节点升级为主节点,这个时候就存在两人同时拥有锁的场景。
在Redis的分布式环境中,我们假设有5个Redis master。这些节点完全互相独立,不存在主从复制或
者其他集群协调机制。我们确保将在每个实例上使用之前介绍过的方法获取和释放锁,这样就能保
证他们不会同时都宕掉。实现高可用。
为了取到锁,客户端应该执行以下操作:
1. 获取当前Unix时间,以毫秒为单位。
2. 依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端
应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自
动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉
的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应
该尽快尝试另外一个Redis实例。
3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当
且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结
果)。
5. 如果因为某些原因,获取锁失败(没有 在至少N/2+1个Redis实例取到锁或者取锁时间已经超
过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有
加锁成功)。
不过,这种方式并不能完全保证锁的安全性,因为我们给锁设置了自动释放时间,因此某些极端特例
下,依然会导致锁的失败,例如下面的情况:
- 如果 Client 1 在持有锁的时候,发生了一次很长时间的 FGC 超过了锁的过期时间。锁就被释放了。
- 这个时候 Client 2 又获得了一把锁,提交数据。
- 这个时候 Client 1 从 FGC 中苏醒过来了,又一次提交数据。冲突发生了
还有一种情况也是因为锁的超时释放问题,例如:
- Client 1 从 A、B、D、E五个节点中,获取了 A、B、C三个节点获取到锁,我们认为他持有了锁
- 这个时候,由于 B 的系统时间比别的系统走得快,B就会先于其他两个节点优先释放锁。
- Clinet 2 可以从 B、D、E三个节点获取到锁。在整个分布式系统就造成 两个 Client 同时持有锁了。
不过,这种因为时钟偏移造成的问题,我们可以通过延续超时时间、调整系统时间减少时间偏移(配置ntp时间同步)等方式
来解决。Redis作者也对超时问题给出了自己的意见:
在工作进行的过程中,当发现锁剩下的有效时间很短时,可以再次向redis的所有实例发送一个Lua脚
本,让key的有效时间延长一点(前提还是key存在并且value是之前设置的value)。客户端扩展TTL时必须像首次取得锁一样在大多数实例上扩展成功才算再次取到锁,并且是在有效时
间内再次取到锁(算法和获取锁是非常相似的)。
简单来说就是在获取锁成功后,监视锁的失效时间,如果即将到期,可以再次去申请续约,延长锁的有
效期。
我们可以采用看门狗(watch dog)解决锁超时问题,开启一个任务,这个任务在 获取锁之后10秒后,重
新向redis发起请求,重置有效期,重新执行expire(实际我们已经实现了)。
而Redission已经帮我们实现了这些功能,什么各种奇怪的安全问题,实现了各种锁,后面我们也会发文章介绍使用: