redis分布式锁,面试官请随便问,我都会

前言

如今的业务场景愈来愈复杂,使用的架构也就愈来愈复杂,分布式、高并发已是业务要求的常态。像腾讯系的很多服务,还有CDN优化、异地多备份等处理。 说到分布式,就必然涉及到分布式锁的概念,如何保证不一样机器不一样线程的分布式锁同步呢?程序员

实现要点

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

正确的redis分布式锁实现

错误加锁方式

错误方式一

保证互斥和防止死锁,首先想到的使用redis的setnx命令保证互斥,为了防止死锁,锁须要设置一个超时时间。redis

public static void wrongLock(Jedis jedis, String key, String uniqueId, int expireTime) {
        Long result = jedis.setnx(key, uniqueId);
        if (1 == result) {
            //若是该redis实例崩溃,那就没法设置过时时间了
            jedis.expire(key, expireTime);
        }
    }
复制代码

在多线程并发环境下,任何非原子性的操做,均可能致使问题。这段代码中,若是设置过时时间时,redis实例崩溃,就没法设置过时时间。若是客户端没有正确的释放锁,那么该锁(永远不会过时),就永远不会被释放。bash

错误方式二

比较容易想到的就是设置值和超时时间为原子原子操做就能够解决问题。那使用setnx命令,将value设置为过时时间不就ok了吗?服务器

public static boolean wrongLock(Jedis jedis, String key, int expireTime) {
        long expireTs = System.currentTimeMillis() + expireTime;
        // 锁不存在,当前线程加锁成果
        if (jedis.setnx(key, String.valueOf(expireTs)) == 1) {
            return true;
        }

        String value = jedis.get(key);
        //若是当前锁存在,且锁已过时
        if (value != null && NumberUtils.toLong(value) < System.currentTimeMillis()) {
            //锁过时,设置新的过时时间
            String oldValue = jedis.getSet(key, String.valueOf(expireTs));
            if (oldValue != null && oldValue.equals(value)) {
                // 多线程并发下,只有一个线程会设置成功
                // 设置成功的这个线程,key的旧值必定和设置以前的key的值一致
                return true;
            }
        }
        // 其余状况,加锁失败
        return true;
    }
复制代码

乍看之下,没有什么问题。但仔细分析,有以下问题:多线程

  1. value设置为过时时间,就要求各个客户端严格的时钟同步,这就须要使用到同步时钟。即便有同步时钟,分布式的服务器通常来讲时间确定是存在少量偏差的。
  2. 锁过时时,使用 jedis.getSet虽然能够保证只有一个线程设置成功,可是不能保证加锁和解锁为同一个客户端,由于没有标志锁是哪一个客户端设置的嘛。

错误解锁方式

解锁错误方式一

直接删除key架构

public static void wrongReleaseLock(Jedis jedis, String key) {
        //不是本身加锁的key,也会被释放
        jedis.del(key);
    }
复制代码

简单粗暴,直接解锁,可是不是本身加锁的,也会被删除,这好像有点太随意了吧!并发

解锁错误方式二

判断本身是否是锁的持有者,若是是,则只有持有者才能够释放锁。dom

public static void wrongReleaseLock(Jedis jedis, String key, String uniqueId) {
        if (uniqueId.equals(jedis.get(key))) {
            // 若是这时锁过时自动释放,又被其余线程加锁,该线程就会释放不属于本身的锁
            jedis.del(key);
        }
    }
复制代码

看起来很完美啊,可是若是你判断的时候锁是本身持有的,这时锁超时自动释放了。而后又被其余客户端从新上锁,而后当前线程执行到jedis.del(key),这样这个线程不就删除了其余线程上的锁嘛,好像有点乱套了哦!分布式

正确加锁释放锁方式

基本上避免了以上几种错误方式以外,就是正确的方式了。要知足如下几个条件:高并发

  1. 命令必须保证互斥
  2. 设置的key必需要有过时时间,防止崩溃时锁没法释放
  3. value使用惟一id标志每一个客户端,保证只有锁的持有者才能够释放锁

加锁直接使用set命令同时设置惟一id和过时时间;其中解锁稍微复杂些,加锁以后能够返回惟一id,标志此锁是该客户端锁拥有;释放锁时要先判断拥有者是不是本身,而后删除,这个须要redis的lua脚本保证两个命令的原子性执行。 下面是具体的加锁和释放锁的代码:

@Slf4j
public class RedisDistributedLock {
    private static final String LOCK_SUCCESS = "OK";
    private static final Long RELEASE_SUCCESS = 1L;
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    // 锁的超时时间
    private static int EXPIRE_TIME = 5 * 1000;
    // 锁等待时间
    private static int WAIT_TIME = 1 * 1000;

    private Jedis jedis;
    private String key;

    public RedisDistributedLock(Jedis jedis, String key) {
        this.jedis = jedis;
        this.key = key;
    }

    // 不断尝试加锁
    public String lock() {
        try {
            // 超过等待时间,加锁失败
            long waitEnd = System.currentTimeMillis() + WAIT_TIME;
            String value = UUID.randomUUID().toString();
            while (System.currentTimeMillis() < waitEnd) {
                String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME);
                if (LOCK_SUCCESS.equals(result)) {
                    return value;
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (Exception ex) {
            log.error("lock error", ex);
        }
        return null;
    }

    public boolean release(String value) {
        if (value == null) {
            return false;
        }
        // 判断key存在而且删除key必须是一个原子操做
        // 且谁拥有锁,谁释放
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = new Object();
        try {
            result = jedis.eval(script, Collections.singletonList(key),
                    Collections.singletonList(value));
            if (RELEASE_SUCCESS.equals(result)) {
                log.info("release lock success, value:{}", value);
                return true;
            }
        } catch (Exception e) {
            log.error("release lock error", e);
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        log.info("release lock failed, value:{}, result:{}", value, result);
        return false;
    }
}
复制代码

单是一个redis的分布式锁就有这么多道道,不知道你是否看明白了?留言讨论下吧!

程序员的小伙伴们,以为本身孤单么,那就加入公众号[程序员之道],一块儿交流沟通,走出咱们的程序员之道!

扫码加入吧!
相关文章
相关标签/搜索