到底啥是分布式锁?
大白话来说就是,以前的锁,是一台机器上的锁,服务拆分成分布式了,就是对多个机器共同添加的一个锁.
对分布式的部署的多台机器操作同一份共享数据的时候进行的加锁操作,就是分布式锁的核心.
那么我们为什么需要分布式锁呢?不使用会怎么样?
我们假设一个场景,角色A,角色B和角色C三个个人都需要在不同的银行都需要对同一个账号进行打钱和拿钱.
场景如下:
此时呢,ABC,三人有一下操作.
A: 往账户里面打5000块.
B: 往账户里面打5000块,并且查询余额.
C: 查询账里面的余额.
此时A B同时进行操作,就可能会有如下的问题.
数据库里面记录的可能只有5000块,因为A,B同时操作的数据库,导致只有一条数据是成功的.
假设ABC同时进行操作,那么还可能出现C查询不到已经插入的金额.此时C的心里岂不是崩溃了么??? why?我的钱呢?
所以针对这种场景下我们就需要假如分布式锁进行操作,例如常见的读写锁去规避这个问题.
下面我们就来揭秘一下分布式锁基于读写锁的运行原理.
分布式锁原理揭秘.
实现分布式锁的三剑客
1.读写锁
2.排队
3.监听与通知
一般我们使用的分布式锁的都是采取synchronized锁或者基于读写锁进行加锁操作的,但是基于28法则(一般读操作占80%,写操作占20%)的场景来考虑的话,那么读写锁的明显更满足使用场景.
所以我们就基于curator(zookeeper操作框架)框架下的源码,基于zk的临时顺序节点,以及节点监听通知的特性进行说明分布式锁的运行原理.
- 首先创建一个Curator客户端对zk进行连接.
//创建Curator客户端
CuratorFramework client = null;
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
client = CuratorFrameworkFactory.newClient(zookeeperConnectionString, retryPolicy);
//启动客户端
client.start();
- 创建读写锁,并尝试获取锁
InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock(client,"/lock/locks");
//创建读锁
InterProcessReadWriteLock.ReadLock readLock = interProcessReadWriteLock.readLock();
try {
//尝试获取锁
readLock.acquire();
} catch (Exception e) {
throw new RuntimeException(e);
}
//创建写锁
InterProcessReadWriteLock.WriteLock writeLock = interProcessReadWriteLock.writeLock();
try {
//尝试获取锁
writeLock.acquire();
} catch (Exception e) {
throw new RuntimeException(e);
}
上面代码我们基于Cutrtor创建了两个分布式的读写锁,我们一起来深入源码揭开一下它的神奇面纱.
InterProcessReadWriteLock interProcessReadWriteLock = new InterProcessReadWriteLock(client,"/lock/locks");
public InterProcessReadWriteLock(CuratorFramework client, String basePath, byte[] lockData) {
this.writeMutex = new WriteLock(client, basePath, lockData);
this.readMutex = new ReadLock(client, basePath, lockData, this.writeMutex);
}
上面这段代码的内部呢其实就是针对zk创建了1个临时顺序的znode,只是名称不一样.说白了就是根据给于的路径进行临时顺序创建节点具体类似如下
/lock/locks/__WRIT__01
/lock/locks/__READ__02
那么具体里面是怎么玩的呢,我们继续往下看.
首先我们把上面的临时节点可以抽象成下面这种顺序列表,读锁,代表一个读请求,写锁代表一个写请求.形成一个列表.
- 当读锁进行获取分布式锁,做了什么操作呢?
以下代码仅仅截取精华的代码进行讲解
if (writeLock.isOwnedByCurrentThread()) {
return new PredicateResults((String)null, true);
} else {
int index = 0;
int firstWriteIndex = Integer.MAX_VALUE;
int ourIndex = -1;
String node;
//遍历所有节点
for(Iterator var8 = children.iterator(); var8.hasNext(); ++index) {
node = (String)var8.next();
//检查是否有写节点
if (node.contains("__WRIT__")) {
//对比找到第一个写节点
firstWriteIndex = Math.min(index, firstWriteIndex);
} else if (node.startsWith(sequenceNodeName)) {
ourIndex = index;
break;
}
}
validateOurIndex(sequenceNodeName, ourIndex);
//如果这个写节点在自己前面,则获取锁失败.
boolean getsTheLock = ourIndex < firstWriteIndex;
node = getsTheLock ? null : (String)children.get(firstWriteIndex);
return new PredicateResults(node, getsTheLock);
}
以上代码操作流程就是遍历所有节点,检查第一个写节点,然后确认当前自己位置前面是否有写节点.没有则获取锁,有则获取锁失败.主要通过firstWriteIndex = Math.min(index, firstWriteIndex),循环节点找出第一个发现的写节点进行位置判断.
- 当写锁进行获取分布式锁,做了什么操作呢?
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception {
//判断自己当前位置
int ourIndex = children.indexOf(sequenceNodeName);
validateOurIndex(sequenceNodeName, ourIndex);
//maxLeases默认就是1.那么证明只有自己的位置是第一个,也就是0才可以进行获取写锁
boolean getsTheLock = ourIndex < maxLeases;
String pathToWatch = getsTheLock ? null : (String)children.get(ourIndex - maxLeases);
return new PredicateResults(pathToWatch, getsTheLock);
}
这段代码的解读是判断自己当前的位置,是不是第一位,是第一位则获取写锁,不是则进行等待.
- 总体来说,原理如下图进行说明.
读锁只需要判断自己前面是否还有写节点,有则获取失败,无则获取成功
写锁只需要判断自己是否在第一节点位置,在第一个节点位置,则获取写锁.不在则获取失败.
根据上图已经很明显了, 整体的运作流程就是利用zk的临时顺序节点 以及 节点监听反向通知.
读节点只需要监听第一个节点后判断自己前面是否还有写锁, 有则加锁失败.
写节点只需要监听自己前面一个节点, 当通知自己进行判断的逻辑就是自己是不是第一个临时节点,是的话,则加锁成功,否则加锁失败.
以上就是这次的分布式锁的运行原理揭秘啦,希望大家都能理解和掌握.