分布式锁
- 什么是锁
- 背景介绍
- 分布式锁的基本原则
- 常见的分布式锁实现方式
什么是锁
简单的来说就是对资源操作时的一种控制策略
何为资源:能被程序访问的所有信息或是媒介;比如常见的文件,数据库,内存中的数据等
何为操作:像读,写,修改等
背景介绍
以Java开发为例子说明
在单体应用或是单实例部署的情况下,所有的请求处理都是在同一个JVM中。可借助java 中的锁机制来控制对资源的访问比如:synchronized关键字,java.util.concurrent.* 中的api
但是如果应用程序是分布式部署或是集群部署,请求会别分发到不同的实例上来处理。这样就会带来一个问题:
如果资源是被一个JVM给锁定了(比如说通过synchronized),那么其它的JVM是如何知道资源的锁定状态呢?
实际上java在处理请求时没有办法直接跨越JVM的,这个时候只能是借用一个"第三方"来饰演资源的公共角色?
这个“第三方”,就可以是我们常见的文件,数据库等外部的一切可访问资源
分布式锁的基本原则
那么在设计分布式锁的时候,需要考虑哪些因素呢:
1.互斥性:在同一时刻只能由一个客户端持久
2.独立性:在正常情况下,只有锁的拥有者才可以释放锁即可识别锁拥有者的身份
3.可用性:即在使用端出现异常的情况下,锁还是可以正常被使用的,即不会存在死锁或是长时间无资源可用的状态
常见的分布式锁实现方式
常见实现有三种
1.基于数据库
2.基于Redis
3.基于Zookeeper
4.其它第三实现
1.基本数据库
不做详细的说明,大体的思路就是一张表来记录资源的各种状态
2.基于Redis
查看代码实例:
public class CustomRedisLock implements java.util.concurrent.locks.Lock {
private StringRedisTemplate stringRedisTemplate;
//资源的唯一key
private String lockKey=null;
//默认锁时间
private int defaultLockTimeout;
//锁ID标识
private AtomicReference<String> lockedID = new AtomicReference<>();
//本机JVM锁
private Lock lock=null;
//
ThreadLocal<String> threadLocal=new ThreadLocal();
/**
stringRedisTemplate:
lockKey:锁资源标识
defaultLockTimeout:锁超时时间
*/
public CustomRedisLock(StringRedisTemplate stringRedisTemplate,String lockKey,int defaultLockTimeout){
this.stringRedisTemplate=stringRedisTemplate;
this.lockKey="Lock-"+lockKey;
this.defaultLockTimeout=defaultLockTimeout;
this.lock=new ReentrantLock();
}
@Override
public void lock() {
}
@Override
public void unlock() {
String findRequestId=this.stringRedisTemplate.opsForValue().get(this.lockKey);
//是当前线程获取到的锁
if(!StringUtils.isEmpty(findRequestId) && findRequestId.equals(threadLocal.get())){
//删除redis锁标识,只能释放锁定状态是当前线程操作的Key.即要满足原则的第三点
String scriptText = "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(lockKey), Collections.singletonList(requestId));
List<String> keys=Collections.singletonList(lockKey);
DefaultRedisScript<Integer> script =new DefaultRedisScript<>();
script.setScriptText(scriptText);
script.setResultType(Integer.class);
Object result=stringRedisTemplate.execute(script,keys,findRequestId);
log.info("Delete Result ="+result.getClass().getName()+",value="+((Long)result).longValue());
}
/*
log.info("release lock:{key:{},uuid:{}}", key, uuid);
return redisTemplate.execute(
(RedisConnection connection) -> connection.eval(
RELEASE_LOCK_LUA_SCRIPT.getBytes(),
ReturnType.INTEGER,
1,
key.getBytes(),
uuid.getBytes())
).equals(RELEASE_LOCK_SUCCESS_RESULT);
*/
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try{
lock.tryLock();//JVM的锁
String requestId=UUID.randomUUID().toString().replace("-","")+Thread.currentThread().getId();
Boolean execute = this.stringRedisTemplate.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
//??蛋疼: 1.5.12.RELEASE 版本的这个Set操作没有返回r操作结果
//其它版本的,如果此RedisConnection接口可以返回操作结果,则无需要后的比较确认步骤
//设置key 与 设置过滤时间必须是原子性
connection.set(lockKey.getBytes(Charset.forName("UTF-8")),requestId.getBytes(Charset.forName("UTF-8")),
Expiration.seconds(defaultLockTimeout),RedisStringCommands.SetOption.SET_IF_ABSENT);
log.info("------------------------------");
threadLocal.set(requestId);
return true;
}
});
//鉴于上述的蛋疼点,比较redis中锁身份标识是不是当前线程的,如果是则表明当前线程获取到了锁
String findRequestId=this.stringRedisTemplate.opsForValue().get(this.lockKey);
log.info("execute result="+execute.booleanValue()+", value="+findRequestId);
if(requestId.equals(findRequestId)){
return true;
}else {
threadLocal.remove();
return false;
}
}finally {
lock.unlock();
}
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
缺点:为什么一定要设置Key的超时时间呢?
原因:是防止客户端在获取到了锁之后没有释放锁或是客户端在获致到锁之后宕机了,锁不能被释放。
加入超时时间设置,也是一种被动的检测机制
虽然引入了超时时间的检测机制,但会引发另外一个问题就是:这个超时时间该设置多长时间的问题?
有方案说可以自动延续key的超时时间,但是这种方法也不能全面覆盖拟所有的场景(比如:java在触发了GC动作后)
也就是说这个问题最终还是由业务使用方来确定一个合理的时间值
3.基于Zookeeper
实现方案:
第三方实现->curator-recipes
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-client</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.11.1</version>
</dependency>
其中有几种锁的实现方式:
InterProcessMutex
InterProcessReadWriteLock
InterProcessSemaphoreMutex
InterProcessMultiLock
实现原理为:
在Lock-Key节点下创建临时有序列节点
获致Lock-key下所有的子节点,按照有序规则排序,如果最小的节点是当前连接创建的则代表获取到了
监听Lock-Key下所有比自己大的节点的删除事件
备注:因为是临时节点,连接使用完/断开之后节点就会被自动清除

4.其它第三方实现
redisson / https://github.com/redisson/redisson