redis从零开始学习系列(四)—— redis加锁

    含义:程序首先需要通过获取锁来对数据进行排他性访问的能力,然后才能对数据执行一系列操作,最后还要将锁释放给其他程序。根据获取锁的方式:可分为乐观锁和悲观锁。

    乐观锁:每次拿数据的时候都认为别人不会修改数据,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。一句话概括“直接对数据进行操作,执行完后,判断是否能执行成功”。redis的watch命令就是使用了乐观锁。

    悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿这个数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。一句话概括就是“首先获取锁,然后执行操作,最后释放锁”

    锁出现不正确的行为原因,以及锁不正确运行时的症状:

        1.持有锁的进程因为操作时间过长而导致锁被自动释放,但进程本身并不知晓这一点,甚至还可能错误地释放掉了其他进程持有的锁。

        2.一个持有锁并打算执行长时间操作的进程已经崩溃,但其他想要获取锁的进程不知道哪个进程持有锁,也无法检测出持有锁的进程已经崩溃,只能白白地浪费时间等待锁被释放。

        3.在一个进程持有的锁过期之后,其他多个进程同时尝试去获取锁,并且都获得了锁。

        4.上面提到的第1点和第3点情况同时出现,导致多个进程获得了锁,而每个进程都以为自己是唯一一个获得锁的进程。

    redis加锁的实例:考虑程序在尝试获取锁失败时,它会不断进行重试,所以需要设定超时时间。

public String acquireLock(Jedis conn, String lockName, long acquireTimeout){
        String identifier = UUID.randomUUID().toString();  //生成随机标识符 

        long end = System.currentTimeMillis() + acquireTimeout;
        while (System.currentTimeMillis() < end){
            if (conn.setnx("lock:" + lockName, identifier) == 1){ //尝试获取锁
                return identifier;
            }

            try {
                Thread.sleep(1);
            }catch(InterruptedException ie){
                Thread.currentThread().interrupt();
            }
        }

        return null;
    }

    redis释放锁的实例:因为程序持有锁期间,其他程序可能会擅自对锁进行修改;所以函数首先使用watch命令监视代表锁的键,接着检查键目前是否和加锁时设置的值相同,并在确认值没有变化之后删除该键。

public boolean releaseLock(Jedis conn, String lockName, String identifier) {
        String lockKey = "lock:" + lockName;

        while (true){
            conn.watch(lockKey);
            if (identifier.equals(conn.get(lockKey))){  //检查进程是否仍然持有锁
                Transaction trans = conn.multi();
                trans.del(lockKey);  //释放锁
                List<Object> results = trans.exec();
                if (results == null){
                    continue;
                }
                return true;
            }

            conn.unwatch();
            break;
        }

        return false;
    }

    上诉操作的加锁,持有者在崩溃的时候不会自动释放锁,这将导致锁一直处于已被获得状态。所以需要为锁添加超时功能。

    给锁设置超时的好处:为了确保锁在客户端已经崩溃的情况下仍然能够自动被释放,客户端会在尝试获取锁失败之后,检查锁的超时时间,并为未设置超时时间的设置超时时间。因此锁总会带有超时时间,并最终因为超时而自动被释放,使得其他客户端可以继续尝试获取已经被释放的锁。

    代码实例:

public String acquireLockWithTimeout(Jedis conn, String lockName, long acquireTimeout, long lockTimeout)
    {
        String identifier = UUID.randomUUID().toString();
        String lockKey = "lock:" + lockName;
        int lockExpire = (int)(lockTimeout / 1000);  //确保传给expire的都是整数

        long end = System.currentTimeMillis() + acquireTimeout;
        while (System.currentTimeMillis() < end) {
            if (conn.setnx(lockKey, identifier) == 1){  //获取锁并设置过期时间
                conn.expire(lockKey, lockExpire );
                return identifier;
            }
            if (conn.ttl(lockKey) == -1) {
                conn.expire(lockKey, lockExpire);  //检查过期时间,并在有需要时对其进行更新
            }

            try {
                Thread.sleep(1);
            }catch(InterruptedException ie){
                Thread.currentThread().interrupt();
            }
        }

        return null;
    }

猜你喜欢

转载自blog.csdn.net/huxiaodong1994/article/details/80950833