分布式锁小结
在我们实现redis分布式锁之前应该需要注意几个问题
1独占排他锁
一个线程在使用当前锁时其他线程无法对这个锁进行修改,如redis的setnx指令实现了独占排他锁,但问题在于redis是c/s的锁和java线程锁和jvm锁相比起来,如果没有手动释放可能会发生死锁,setnx的程序之间也不可重入
2防止死锁
如何防止死锁,比如redis客户端程序获取到锁后立马宕机,没有给锁添加获取时间,这极有可能发生死锁问题,所以添加过期时间是有必要的,如set k v ex long nx,由于其不可重入也可能导致死锁
3防止误删
A客户端添加了锁以后,B客户端立刻删除了锁,这种错误是无法忍受的,所以要保证是自己的锁才能删
4原子性
通过应用程序操作redis在每一步都有可能会因宕机引发redis的问题,比如加锁和设置过期时间之间,判断和释放之间,所以在这两步的处理我们要考虑的它的原子性,否则会出现加了一半的锁,突然程序挂掉这个锁一直存在
5如何实现可重入和原子性
Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断->所以我们通过lua脚本进行实现,哦对了什么叫做可以重入,在了解可重入之前我们先来看看java的ReentrantLock:
// ReentrantLock 实现了Lock接口
ReentrantLock implements Lock, java.io.Serializable
void lock();
boolean tryLock();
void unlock();
Lock接口是java锁的标准实现类 我们new 一个ReentrantLock 调用他的lock()方法
public void lock() {
sync.lock();
}
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
sync继承了aqs并写了lock()方法
他有两个实现方法 公平锁和非公平锁 那她使用的那一个呢我们看看构造方法
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
看来是默认的非公平锁了,我们解读一下代码
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
先是进行一波内存操作CAS 比较并交换看看是否成功
如果成功则记录当前线程,此时是加锁的情况,
否则调用acquire(1);(!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
很明显前半部分为false,后半部分才会执行我们看看前半部分的实现
//tryAcquire(1)
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
1获得当前锁操作对象的状态 为0 说明没有锁,被当前线程抢到了,开始CAS操作,如果成功记录当前线程为获得锁线程true
2第二步非常关键 acquires是上个方法传过来的1 如果状态不是0还看看是不是当前有锁线程如果是,在状态值上加一个1 返回true
3如果否则为false
原来是这样 为什么会加1呢 这就是我们说的重入次数看看下面这个代码
public void A(){
reentrantLock.lock();
B();
reentrantLock.unlock();
}
public void B(){
reentrantLock.lock();
reentrantLock.unlock();
}
A方法调用了B方法两个都是同一把锁当前又是同一个线程,所以一把锁在2个方法上同时加入,当B在进行加锁时重入次数加一,重复使用了这把锁,可以得出如果线程有锁,再次获取锁,那么就是记录锁的使用状态+1(重入)
我们接着看如果加锁失败,说明当前线程没有抢到锁执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),我全部写在代码注释上面
private Node addWaiter(Node mode) {
//给当前线程生成一个node节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//获取尾节点
Node pred = tail;
//尾节点不为空
if (pred != null) {
//新的节点的上一个节点指向尾节点
node.prev = pred;
//交换尾节点和新节点的顺序加入到节点中
if (compareAndSetTail(pred, node)) {
//上一个节点的的尾节点(原尾节点)的下一个指向新的节点
pred.next = node;
return node;
}
}
//如果尾节点为空那么说明没有在等待队列
enq(node);
return node;
}
//初始化等待队列
private Node enq(final Node node) {
//自旋2次
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
//尾节点为空生成一个节点设置为头结点
if (compareAndSetHead(new Node()))
//头尾指向一个节点 等待队列生成好了
tail = head;
} else {
//尾节点不为空
node.prev = t;
//交换尾节点和新节点的顺序加入到节点中
if (compareAndSetTail(t, node)) {
//上一个节点的的尾节点(原尾节点)的下一个指向新的节点
t.next = node;
return t;
}
}
}
}
//现在就有等待队列了
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//获取一个节点
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果不是头结点,继续获取,直到抢到锁
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
我们了解了获取锁再看看解锁,releases的参数为1
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
和加锁有点类似
1判断是不是当前线程如果是去解锁,不是抛出异常
2如果为0说明每一层锁都解完了
3如果不为0重入次数-1直到为0
基于java的锁和redis的加锁细节我们先注意这么多
具体实现可以看我的第二篇