Redis分布式锁的个人实现(一)

分布式锁小结

在我们实现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的加锁细节我们先注意这么多
具体实现可以看我的第二篇

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值