什么是分布式锁?
与分布式锁相对应的是线程锁、进程锁。
线程锁:它主要是给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized和Lock。synchronized是java中的一个关键字,也就是说是Java语言内置的特性。Lock不是Java语言内置的,Lock是一个类。synchronized不需要用户去手动释放锁。而Lock则必须要用户去手动释放锁。
进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。可以用lock做线程锁。
分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。

实现要点
1.互斥性,同一时刻,只能有一个客户端持有锁。
2.防止死锁发生,如果持有锁的客户端崩溃没有主动释放锁,也要保证锁可以正常释放及其他客户端可以正常加锁。
3.加锁和释放锁必须是同一个客户端。
4.容错性,只有redis还有节点存活,就可以进行正常的加锁解锁操作。

我们先看一些redis锁的方式。

代码实现

@Resource
    private JedisPool jedisPool;

    public static final int SLEEP_TIME = 500;

    public boolean addLock(String lockkey, int maxWait) throws Exception {
        long start = System.currentTimeMillis();
        boolean success = true;
        Jedis jedis = null;
        try{
            jedis = jedisPool.getResource();
            System.out.println("获取锁:{}开始"+lockkey);
            do {
            //lockkey做锁
                Long lock = jedis.incrBy(lockkey, 1L);
                success = lock == 1;
                if (!success) {
                    jedis.expire(lockkey,  maxWait / 1000);
                    Thread.sleep(SLEEP_TIME);
                }
            } while (!success && System.currentTimeMillis() < start + maxWait);
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (jedis != null) {
                jedis.close();
            }
        }

        System.out.println("获取锁{}{},耗时{}ms"+lockkey+ (success ? "成功" : "失败")+
                (System.currentTimeMillis() - start));
        return success;
    }
    //解锁
    public void unLock(String lockkey) {
        long start = System.currentTimeMillis();
        Jedis jedis = null;
        try{
            jedis = jedisPool.getResource();
            jedis.del(lockkey);
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (jedis != null) {
                jedis.close();
            }
        }

        System.out.println("释放锁{}成功,耗时{}ms"+lockkey+(System.currentTimeMillis() - start));
    }

测试;

@Test
    public void getRedisLock() {
        try {
            System.out.println("#################开始加锁");
            redisController.addLock(redislockKey,30 * 1000);
            System.out.println("执行代码。。。。");
        }catch (Exception e){
            System.out.println("#################加锁失败");
            e.printStackTrace();
        }finally {

            try {

                redisController.unLock(redislockKey);
                System.out.println("#################释放锁成功");

            } catch (Exception e) {

                System.out.println("#################释放锁失败"+e);

            }
        }
    }

redisson连锁如果顺序乱了会不会死锁 redis锁和synchronized_客户端


上面的这种式是肯定有问题,它并没有给自己一个时间限定,一旦死锁只能是别的哭护短解锁。加锁和释放锁必须是同一个客户端。锁不具备拥有者标识,即任何客户端都可以解锁。而且这个加锁代码逻辑也是很复杂。

还有就是上面的解锁方式最常见的就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

还有一种和上面代码差不多的加锁方式。

public boolean addLock2(Jedis jedis,String lockkey, int maxWait) throws Exception {
        long expires = System.currentTimeMillis() + maxWait;
        String expiresStr = String.valueOf(expires);
        // 如果当前锁不存在,返回加锁成功
        if (jedis.setnx(lockkey, expiresStr) == 1) {
            return true;
        }

        // 如果锁存在,获取锁的过期时间
        String currentValueStr = jedis.get(lockkey);
        if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
            // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
            String oldValueStr = jedis.getSet(lockkey, expiresStr);
            if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
                return true;
            }
        }

        // 其他情况,一律返回加锁失败
        return false;
    }

上面的代码问题和第一套代码问题差不多。而且由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
正确的解锁

/**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    private static final Long RELEASE_SUCCESS = 1L;
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "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));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }

首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。要确保上述操作是原子性。eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
什么是原子性操作
在多进程(线程)访问共享资源时,能够确保所有其他的进程(线程)都不在同一时间内访问相同的资源,(要么完全执行,要么完全不执行)

而redis普通方式加锁太过于繁琐和复杂,要考虑的地方有很多。所以现在的大部分公司采用Redis做分布式锁,一般就是用Redisson框架,非常的简便易用。