欢迎关注微信公众号:互联网全栈架构
在分布式应用系统中,如果需要控制共享资源的访问,就需要用到分布式锁。分布式锁是相对于单机锁而言的,在一个单体系统或者单个进程中,我们可以用synchroinzed这样的关键字对共享资源加锁,然而,系统一旦分布式部署,这样的手段就没有效果了。
记得几年前有一次面试,一个工作五六年的年轻小伙,说他们的系统是集群部署的,可以横向扩展,根据业务要求,系统每天凌晨还会运行一个定时任务(使用Spring Quartz实现),那我就问:这样岂不是就有好几个系统同时去执行这样的定时任务?是不是重复处理了?他一时语塞,然后表示没有仔细考虑过样的问题。对于这样的场景,其中一种实现思路就是分布式锁,同一时刻只有一个任务在真正执行业务处理。
分布式锁有很多的实现方式,今天我们专注于Redis分布式锁。
一
使用命令SETNX
SETNX命令的语法如下:
SETNX key value
它设置key的值为value,如果key不存在,则表示设置成功,在分布式锁的实现中,它用来表示加锁成功;如果key已经存在,则插入失败,也就是加锁失败,它是SET if Not Exist的简写。
当然,还需要设置key的过期时间,也就是EXPIRE命令,这样就带来了一个问题:这两个命令不是原子操作,如果SETNX执行以后程序异常或者退出,导致key没有设置过期时间,那么其它线程加锁就会一直失败。
另外,这个命令在redis的官方文档已经标记为@deprecated(从版本2.6.12开始),可以使用SET NX命令来代替:
SET key value [NX | XX] [GET] [EX seconds | PX milliseconds]
NX:key不存在的时候才设置key的值
XX:key存在的时候才设置key的值
EX:设置key的过期时间单位为秒
PX:设置key的过期时间单位为毫秒
SET命令是原子的,配合del命令,可以用来实现分布式锁,但它可能会存在锁被误删的情况。
二
使用lua脚本
使用SETNX+EXPIRE命令会存在原子性问题,我们可以使用lua脚本来解决,eval命令在执行lua脚本的时候,会它当成一条命令去执行。使用lua加锁的脚本如下:
if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
redis.call('expire',KEYS[1],ARGV[2])
return 1
else
return 0
end;
而释放锁的lua脚本:
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end;
对应的java示例代码如下:
package com.fullstack.commerce.user.util;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class RedisDistributeLock {
// RedisClient是一个操作Redis的客户端,相当于Jedis
private RedisClient jedis;
private String key;
private String value;
private int expireTime; // 锁的过期时间(毫秒)
public RedisDistributeLock(RedisClient jedis, String key, String value, int expireTime) {
this.jedis = jedis;
this.key = key;
this.value = value;
this.expireTime = expireTime;
}
private static final String lock_script =
"if redis.call('setnx',KEYS[1],ARGV[1]) == 1 " +
" then" +
" redis.call('expire',KEYS[1],ARGV[2]) " +
" return 1 " +
" else" +
" return 0 " +
"end";
private static final String unlock_script =
"if redis.call('get',KEYS[1]) == ARGV[1] then " +
" return redis.call('del',KEYS[1]) " +
"else " +
" return 0 " +
"end";
/**
* 加锁
*/
public boolean lock() {
try {
List<String> keys = new ArrayList<>();
keys.add(key);
List<String> args = Arrays.asList(value, String.valueOf(expireTime));
Long result = (Long) jedis.eval(lock_script, keys, args);
return result == 1L;
}catch (Exception e){
e.printStackTrace();
}
return false;
}
/**
* 释放锁
*/
public boolean unlock() {
List<String> keys = new ArrayList<>();
keys.add(key);
List<String> args = Arrays.asList(value, String.valueOf(expireTime));
Long result = (Long) jedis.eval(unlock_script, keys, args);
return result == 1L;
}
}
调用方:
RedisDistributeLock lock = new RedisDistributeLock(jedis, "mylock", "myclient", 2000);
if(lock.lock()) {
try {
System.out.println("加锁成功!");
}catch (Exception e){
e.printStackTrace();;
}finally {
lock.unlock();
}
}else{
System.out.println("加锁失败!");
}
return "操作成功";
三
使用Redisson
Redisson是Redis的Java客户端,它提供了丰富的分布式功能,包括分布式对象、集合、分布式锁、分布式服务等。下面我们用一个最简单的示例来展示如何使用它实现分布式锁的功能,先在pom.xml加入依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.35.0</version>
</dependency>
客户端配置:
package com.fullstack.commerce.user.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
return Redisson.create(config);
}
}
RLock lock = redissonClient.getLock("fullstack_lock");
try {
lock.tryLock(3, 30, TimeUnit.SECONDS);
System.out.println("加锁成功!");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
return "操作成功";
debug进入tryLock方法,会发现它也会执行一些lua脚本。Redisson获取锁的流程如下所示:
四
关于RedLock
在Redis主从架构中,上述的几种方法可能也会存在问题:如果线程A请求分布式锁成功,slave开始同步master信息,但此master宕机,然后slave升级为master,但由于此前的master宕机,锁信息已经丢失,这时候线程B再去申请分布式锁,依然也可以申请成功,这样就不具备独占性。
针对这样的问题,Redis作者antirez提出了一种名为Redlock红锁的算法,它的核心思想是部署多个相互独立的Master节点,然后采用与单实例相同的方法来进行加锁和解锁。它的算法步骤大致如下:
以毫秒为单位获取当前时间
使用相同的key和value,在N实例中按照顺序获取锁,且获取时间要远小于超时时间,比如超时时间是10秒,那么获取时间可能是5到50毫秒之间,这样可以防止:节点down机了,客户端还在尝试与其交互
只有超过半数的Redis实例反馈加锁成功,且获取锁的总时间(用当前时间戳减去第1步的时间)小于锁的超时时间,才认为锁是获取成功
如果获取锁成功,锁的超时时间=初始的超时时间-获取锁的总时间(第三步计算出来的)
如果获取锁失败(不管是加锁的Redis实例没有过半,还是获取锁的总时间超过了超时时间),都会尝试在所有的节点去释放锁。
关于红锁算法,作者与分布式专家Martin Kleppmann还有过争论,具体内容可以参见Redis官网上的相关链接。
五
总结
在分布式系统中,如何对于共享资源进行加锁一直是一个让人头疼的问题,而Redis作为内存数据库中的”王者“,自然也提供了相应的解决方案和办法来实现分布式锁,本文就尝试由浅入深地讲解基于Redis分布式锁的几种方案,总体来讲,使用Redisson库是一个不错的选择。
然而,Redis是一个CA系统,对于一致性要求非常高的分布式系统而言,使用Redis可能不是一个最佳选择,此时可以考虑Zookeeper这样的强一致中间件。
创作不易,烦请点个在看、点个赞。
有任何问题,也欢迎留言讨论。
参考文章:
https://redis.io/docs/latest/develop/use/patterns/distributed-locks/
https://www.codenong.com/cs106559589/
推荐阅读: