一、什么是分布式锁
在Java的多线程编程中,锁可以看成是多线程情况下访问共享资源的一种线程同步机制。在单个进程中,所有的线程都在同一个JVM进程里,Java语言提供的锁机制可以同步对共享资源的访问。但是在分布式环境下,Java语言提供的锁就无法同步多个不同线程对共享资源的访问了,这个时候就必须借助分布式锁来解决分布式环境下共享资源的同步问题。分布式锁一般有三种实现方式:
- 基于数据库的乐观锁
- 基于Redis的分布式锁
- 基于zookeeper的分布式锁
分布式锁在设计过程中存在如下原则:
- 独享:即互斥属性,在同一时刻,一个资源只能有一把锁被一个客户端持有
- 无死锁:当持有锁的客户端崩溃后,锁仍然可以被其它客户端获取
- 容错性:当部分节点失活之后,其余节点客户端依然可以获取和释放锁
- 统一性:即释放锁的客户端只能由获取锁的客户端释放
二、Redis分布式锁
2.1 Redis分布式锁的原则和设计原理
Redis可以实现分布式锁的思路比较简单,主要用到的是Redis函数setnx(),该函数set命令中的NX选项和lus脚本的执行都是原子的,因此当多个客户端去争抢执行上锁或解锁代码时,最终只会有一个客户端执行成功。同时set命令还可以指定key的有效期,这样即使当前客户端崩溃,过一段时间锁也会被Redis自动释放,这就给其它客户端获取锁的机会。setnx()的执行逻辑可以简约为,将某一任务标识名作为键存到Redis里,并设置一个过期时间,后续如果有其它任务标识请求过来,先查看setnx()是否能够将标识名插入到Redis中,可以的情况下就返回true,否则返回false。
根据Redis的部署情况,Redis分布式锁的实现方案可以分为两类,单例Redis分布式锁,和集群Redis分布式锁。为了方便进行介绍,先从单例分布式锁开始介绍起。
2.2 单例分布式锁
在pom文件中添加如下依赖,spring-boot的自动注册功能会准备好后续的一切。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis分布式锁代码实现
/**
* @author koma <komazhang@foxmail.com>
* @date 2018-09-19 11:24
*/
@Slf4j
@Service
public class CacheService {
private static final Long RELEASE_SUCCESS = 1L;
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "EX";
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 该加锁方法仅针对单实例 Redis 可实现分布式加锁
* 对于 Redis 集群则无法使用
*
* 支持重复,线程安全
*
* @param lockKey 加锁键
* @param clientId 加锁客户端唯一标识(采用UUID)
* @param seconds 锁过期时间
* @return
*/
public Boolean tryLock(String lockKey, String clientId, long seconds) {
redisTemplate.opsForValue().set();
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
if (LOCK_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
/**
* 与 tryLock 相对应,用作释放锁
*
* @param lockKey
* @param clientId
* @return
*/
public Boolean releaseLock(String lockKey, String clientId) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(lockKey),
Collections.singletonList(clientId));
if (RELEASE_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
}
上述代码仅对Redis单例架构有效,在面对Redis集群时就无效了。但是一般情况下,Redis架构存在主备模式,然后再通过Redis哨兵实现主从切换,这种模式下应用服务器直接面向的是主机,也可以看成是一个单实例,因此上述代码实现也有效,但是若在主机宕机,从机被升级为主机的一瞬间的那一刻,由于Redis主从复制的异步性,导致从机数据没有即时同步,那么上述代码依然会失效,导致同一资源有可能产生两把锁,违背了分布式锁的原则。
2.3 Redis分布式锁Redlock方案
在Redis的分布式环境中,假设有N个Redis master。这些节点完全相互独立,不存在主从复制或者其他集群协调机制。之前已经描述了单实例下如何安全的获取和释放锁。我们将在每(N)个实例上使用此方法获取和释放锁。在这个样例中,我们假设有5个Redis master节点,这是一个比较合理的设置,所以我们需要在5台机器上面或者5台虚拟机上面运行这些实例,这样保证他们不会同时宕掉。为了获取到锁,客户端应该执行以下操作:
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实例根本就没有加锁成功)。
Redlock方案的异步性
Redlock算法基于这样一个假设:多个进程之间没有时钟同步,但每个进程都以相同的时钟频率前进,时间差相对于失效时间来说几乎可以忽略不计。这种假设和我们的真实世界非常接近:每个计算机都有一个本地时钟,我们可以容忍多个计算机之间有较小的时钟漂移。
从这点来说,必须再次强调互相排斥的规则:在锁的有效时间(步骤3计算的结果)范围内客户端能够做完它的工作,锁的安全性才能得到保证(锁的实际有效时间通常要比设置的短,考虑到计算机之间存在时钟漂移的情况)。
失败时重试
当客户端无法取到锁时,应该在一个随机延迟后重试,防止多个客户端在同时抢夺同一资源的锁。同样,如果客户端取得大部分Redis实例锁所花费的时间越短,多个客户端同时抢夺锁的情况就会越低。所以,理想情况下,客户端应该同时(并发地)向所有Redis发送SET命令。
另外需要强调的是,当客户端从大多数Redis实例获取锁失败时,应该尽快地释放(部分)已经成功取得到的锁,这样其他的客户端就不必非得等到锁过完“有效时间”才能取到。
释放锁
释放锁比较简单,向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁。
2.4 Redis分布式锁redisson方案
redisson是Redis官网推荐的Java语言实现的分布式锁的项目。redission的GitHub项目请参考redission Github项目。redisson分布式锁支持如下四种Redis部署方式:
Cluster(集群)
Sentinel servers(哨兵)
Master/Slave servers(主从)
Single server(单机)
在spring boot项目中使用redisson分布式锁中可参考如下:
1.在pom.xml中添加依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.8.2</version>
</dependency>
2.添加配置,管理redission的初始化操作
public class RedissonManager {
private static final String RAtomicName = "genId_";
private static Config config = new Config();
private static Redisson redisson = null;
public static void init(){
try {
config.useClusterServers() //这是用的集群server
.setScanInterval(2000) //设置集群状态扫描时间
.setMasterConnectionPoolSize(10000) //设置连接数
.setSlaveConnectionPoolSize(10000)
.addNodeAddress("127.0.0.1:6379","127.0.0.1:6380");
redisson = Redisson.create(config);
//清空自增的ID数字
RAtomicLong atomicLong = redisson.getAtomicLong(RAtomicName);
atomicLong.set(1);
}catch (Exception e){
e.printStackTrace();
}
}
public static Redisson getRedisson(){
return redisson;
}
/** 获取redis中的原子ID */
public static Long nextID(){
RAtomicLong atomicLong = getRedisson().getAtomicLong(RAtomicName);
atomicLong.incrementAndGet();
return atomicLong.get();
}
}
3.编写DistributedRedisLock,提供锁和解锁方法
public class DistributedRedisLock {
private static Redisson redisson = RedissonManager.getRedisson();
private static final String LOCK_TITLE = "redisLock_";
public static void acquire(String lockName){
String key = LOCK_TITLE + lockName;
RLock mylock = redisson.getLock(key);
mylock.lock(2, TimeUnit.MINUTES); //lock提供带timeout参数,timeout结束强制解锁,防止死锁
System.err.println("======lock======"+Thread.currentThread().getName());
}
public static void release(String lockName){
String key = LOCK_TITLE + lockName;
RLock mylock = redisson.getLock(key);
mylock.unlock();
System.err.println("======unlock======"+Thread.currentThread().getName());
}
}
4.在测试代码中进行调用
private static void redisLock(){
RedissonManager.init(); //初始化
for (int i = 0; i < 100; i++) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
try {
String key = "test123";
DistributedRedisLock.acquire(key);
Thread.sleep(1000); //获得锁之后可以进行相应的处理
System.err.println("======获得锁后进行相应的操作======");
DistributedRedisLock.release(key);
System.err.println("=============================");
} catch (Exception e) {
e.printStackTrace();
}
}
});
t.start();
}
}
参考博客
1.如何优雅地用Redis实现分布式锁?
2.spring-boot 中实现标准 redis 分布式锁
3.用Redis实现分布式锁 与 实现任务队列
4.Redis分布式锁
5.分布式锁之redisson
6.Redission实现分布式锁