暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

基于Redis的分布式锁

疯狂造轮子 2020-02-24
263

什么时候用到分布式锁呢?

在分布式系统中,同一行代码可能同时被不同机器的请求运行,已经不是简单的单台机器串行,你们如果想要让处理某个资源即使不同机器也是串行的那就得需要一把锁,这把锁得不受不同节点的限制,而且需要高可用,高性能的,这样的诉求下分布式锁应运而生。

Redis分布式锁

锁是需要一个共享资源的或者一个共享状态来判断这把锁是否已经被占用和被释放,redis作为一个高性能的nosql数据库,在高并发环境下作为共享资源修改和释放性能上来说是非常优秀的;

最初分布式锁借助于setnx和expire命令,但是这两个命令不是原子操作,如果执行setnx之后获取锁但是此时客户端挂掉,这样无法执行expire设置过期时间就导致锁一直无法被释放,因此在2.8版本中Antirez为setnx增加了参数扩展,使得setnx和expire具备原子操作性。

在单Matster-Slave的Redis系统中,正常情况下Client向Master获取锁之后同步给Slave,如果Client获取锁成功之后Master节点挂掉,并且未将该锁同步到Slave,之后在Sentinel的帮助下Slave升级为Master但是并没有之前未同步的锁的信息,此时如果有新的Client要在新Master获取锁,那么将可能出现两个Client持有同一把锁的问题,这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生。来看个图来想下这个过程:

不过这种不安全也仅仅是在主从发生 failover 的情况下才会产生,而且持续时间极短,业务系统多数情况下可以容忍。

为了解决这个问题,Antirez 发明了 Redlock 算法,它的流程比较复杂,不过已经有了很多开源的 library 做了良好的封装,用户可以拿来即用;

简单过程如下

// 获取锁 unique_value作为唯一性的校验
SET resource_name unique_value NX PX 30000

// 释放锁 比较unique_value是否相等 避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

基于java jedis的实现
package com.data.tructure.array.redis.分布式锁;

import com.data.tructure.array.redis.jedis.JedisClient;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;

import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class RedisLockUtil {

private static final int MAX_TIME_OUT = 30;
private static final Long RELEASE_SUCCESS = 1L;
private static volatile String uniqueValue;


/*
* String set(String key, String value, String nxxx, String expx, long time);
* 该方法是: 存储数据到缓存中,并制定过期时间和当Key存在时是否覆盖。
* nxxx 只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
* expx 只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒。
* time 过期时间,单位是expx所代表的单位。
*/


/**
* 上锁
*
* @param lockName
*/

public static void lock(String lockName) {
if (!tryLock(lockName, MAX_TIME_OUT)) {
while (!tryLock(lockName, MAX_TIME_OUT)) {
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public static boolean tryLock(String key, int seconds) {
Jedis jedis = JedisClient.getJedis();
try {
SetParams setParams = new SetParams();
setParams.nx();
setParams.ex(seconds);
String uuid = UUID.randomUUID().toString();
String result = jedis.set("LOCK_KEY_PREFIX" + key, uuid, setParams);
if ("OK".equals(result)) {
uniqueValue = uuid;
return true;
}
} catch (Exception e) {
throw new RuntimeException("set redis lock error", e);
} finally {
JedisClient.closeJedis(jedis);
}
return false;
}

/**
* if redis.call("get",KEYS[1]) == ARGV[1] then
* return redis.call("del",KEYS[1])
* else
* return 0
* end
* <p>
* 释放锁
*
* @param lockName
*/

public static boolean unlock(String lockName) {
Jedis jedis = JedisClient.getJedis();
try {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockName), Collections.singletonList(uniqueValue));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JedisClient.closeJedis(jedis);
}
return false;
}

}


Redlock算法基本过程是怎样的?

Redlock算法是Antirez在单Redis节点基础上引入的高可用模式。在Redis的分布式环境中,我们假设有N个完全互相独立的Redis节点,在N个Redis实例上使用与在Redis单实例下相同方法获取锁和释放锁。

现在假设有5个Redis节点(大于3的奇数个),这样基本保证他们不会同时都宕掉,获取锁和释放锁的过程中,客户端会执行以下操作:

  • 获取当前Unix时间,以毫秒为单位

  • 依次尝试从5个实例,使用相同的key和具有唯一性的value获取锁
    当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间,这样可以避免客户端死等

  • 客户端使用当前时间减去开始获取锁时间就得到获取锁使用的时间。当且仅当从半数以上的Redis节点取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功

  • 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间,这个很重要

  • 如果因为某些原因,获取锁失败(没有在半数以上实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁,无论Redis实例是否加锁成功,因为可能服务端响应消息丢失了但是实际成功了,毕竟多释放一次也不会有问题


Redlock算法是否安全的争论

2016年2月8号分布式系统的专家马丁·克莱普曼博士(Martin Kleppmann)在一篇文章How to do distributed locking 指出分布式锁设计的一些原则并且对Antirez的Redlock算法提出了一些质疑。笔者找到了马丁·克莱普曼博士的个人网站以及一些简介,一起看下:

1.我是剑桥大学计算机科学与技术系的高级研究助理和附属讲师,由勒弗乌尔姆信托早期职业奖学金和艾萨克牛顿信托基金资助。我致力于本地优先的协作软件和分布式系统安全
2.我也是剑桥科珀斯克里斯蒂学院计算机科学研究的研究员和主任,我在那里从事本科教学。
3.2017年,我为奥雷利出版了一本名为《设计数据密集型应用》的书。它涵盖了广泛的数据库和分布式数据处理系统的体系结构,是该出版社最畅销书之一。
4.我经常在会议上发言,我的演讲录音已经被观看了超过15万次。
5.我参与过各种开源项目,包括自动合并、Apache Avro和Apache Samza。
6.2007年至2014年间,我是一名工业软件工程师和企业家。我共同创立了Rapportive(2012年被领英收购)和Go Test(2009年被红门软件收购)。
7.我创作了几部音乐作品,包括《二月之死》(德语),这是唐克·德拉克特对该书的音乐戏剧改编,于2007年首映,共有150人参与。

马丁博士文章的主要观点

马丁·克莱普曼在文章中谈及了分布式系统的很多基础问题,特别是分布式计算的异步模型,文章分为两大部分前半部分讲述分布式锁的一些原则,后半部分针对Redlock提出一些看法:

  • Martin指出即使我们拥有一个完美实现的分布式锁,在没有共享资源参与进来提供某种fencing栅栏机制的前提下,我们仍然不可能获得足够的安全性

  • Martin指出,由于Redlock本质上是建立在一个同步模型之上,对系统的时间有很强的要求,本身的安全性是不够的

针对fencing机制马丁给出了一个时序图:

  • 带有自动过期功能的分布式锁,必须提供某种fencing栅栏机制来保证对共享资源的真正互斥保护,Redlock算法提供不了这样一种机制

  • Redlock算法构建在一个不够安全的系统模型之上,它对于系统的记时假设有比较强的要求,而这些要求在现实的系统中是无法保证的


文章转载自疯狂造轮子,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论