分布式锁实现方案(Redis 版本)

前言

  在日常的代码编写中我们常常会遇到多个线程/进程同时对同一临界资源(一次仅允许一个进程/线程使用的资源称为临界资源)进行访问操作的情况。这个时候我们就需要使用锁,来保证在任一时间只有一个线程/进程在操作临界资源。

一、临界区

  一段加锁的程序段称之为临界区,用来执行对临界资源的访问和控制。临界区需要满足以下几点要求:

  • 互斥性:当有线程进入后其他线程必须等待直到该线程退出后,其他线程才可以进入
  • 原子性:进入临界区和退出临界区必须是原子性的操作,一次只能一个线程操作成功,并且进入和退出操作要么全部成功要么全部失败,不可以进入到一半或退出到一半

二、锁的实现

  根据上述的临界区描述,可以定义一个锁的基本功能包括两个显式操作:加锁、解锁和一个隐式操作:线程阻塞。它们分别对应:进入临界区、离开临界区、进入临界区后阻塞其他线程,强制后续线程等待当前线程执行结束,以达到互斥的目的。
  在单机或单进程中,锁的实现通常是通过一个公共的信号量S和两个原子性的对信号量执行加减操作的指令(如操作系统的PV操作)组成。在程序初始化时将信号量S置为1,有线程进入临界区时,通过减操作将S减1。如果减1后,S的值大于等于0,则表示该线程可以进入临界区。如果小于0则线程进入等待状态。当进入临界区的线程执行结束,退出临界区时,将S加1。如果加1后S值小于等于0(表示有线程在等待),则还需要唤醒一个等待的线程。

三、Redis分布式锁的实现

1:准备工作

  先在项目代码中引入Redis操作工具,并配置好相关的Redis配置。这里不详细的对如何引入操作工具和配置进行介绍,未掌握的可以自行搜索学习。作者这里使用的的是Java语言和Java语言的Jedis,后续的代码示例也会使用Java代码。为方便学习,后续的代码示例会分为实现思路和代码示例两部分,并且代码示例中会对所有的操作进行注释解释,方便使用其他语言和操作工具的同学。

2:代码实现
2.1 实现思路

  在Redis中存在一个原子操作SETNX,该操作会判断Redis中指定 key是否存在值,如果存在值,返回0,如果不存在值,则将指定value写入到指定的key下。可以通过此操作判断一个临界区是否已经存在线程在使用,如果返回结果为0则表示临界区正在使用,当前线程进行等待,如果返回结果为1,则表示自己进入了临界区。
  不过该操作虽然满足加锁的原子性,但存在锁释放的问题,即锁标记(信号量)在Redis服务上,当程序在获取到锁之后,出现程序内部执行错误和外部引起的程序问题(进程被kill,计算机被关闭,网络异常等问题)时,锁标记会一直得不到释放,从而产生死锁。
  为了解决此问题,我们可以在Redis中使用lua脚本(脚本的执行也是原子性的操作),自定义逻辑在Redis中一次性执行多个命令。并在线程获取锁之后,给锁设置一个最大持有时间,当线程持有锁时间大于当前时间之后,自动释放锁。
  同时为了防止出现当前线程执行未结束,锁标记就自动释放的问题,当一个线程获取到锁时,被动的创建一个线程,定时检查临界区内线程的存活情况,并根据存活情况选择是否延长锁标记自动释放的时间。这样当临界区的内的线程无论执行时间长短,是否出现异常,都可以保证始终只有一个线程可以获取进入临界区的权限,并且不论临界区内执行结果如何,只要线程退出了临界区,锁最终都会释放。

2.2 代码示例

  在Java中存在一个Lock接口,该接口定义了几个好用的方法,丰富了锁的使用场景。分别为:

  • lock:尝试获取锁,直到获取成功
  • tryLock:该方法有两个一个是尝试获取锁,不论获取成功还是失败都直接返回获取结果,另一个是尝试获取锁失败之后会等待一段时间,如果一直获取不到会在一段时间之后返回获取结果。
  • lockInterruptibly:尝试获取锁,直到获取成功或者被其他线程通知中断时抛出异常并退出。
  • unlock:解锁
      在代码中我们定义一个父类RedisLock实现Lock接口,对Lock中的方法分别进行实现。因为在日常的使用中,锁也存在很多的种类,为了方便不同类型的锁的实现,需要对RedisLock类新定义几个抽象方法交给子类来进行实现,各个子类可以根据自己想要实现的锁的特性进行自定义这些方法。如:自定义加锁的lua脚本、自定义如何使用加锁的lua脚本等。
    RedisLock类则用于定义公共属性、配置和方法,如:Jedis连接、锁的最大持有时间、锁的最小刷新周期、刷新锁的业务逻辑等。实现代码如下:
import redis.clients.jedis.Jedis;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * Redis分布式锁的定义
 */
public abstract class RedisLock implements Lock {

    /**
     * jedis 连接
     */
    private Jedis jedis = null;

    {
        //一个redis的连接,在项目中建议引入三方的redis连接池工具,服用用一个Redis在多线程的情况下会出现传输异常的情况。如A,B两个方法同时传输命令到redis
        jedis = new Jedis("127.0.0.1", 6379);
    }

    /**
     * 存储 lua 脚本在Redis中的SHA1校验和
     * lua脚本在redis中使用时会缓存到redis中,并且返回一个SHA1校验和,下次调用lua脚本就可以使用该SHA1校验和来进行
     */
    private String lockScriptKey = null;
    private String unlockScriptKey = null;
    private String refreshScriptKey = null;

    /**
     * 锁标记
     */
    private final String key;
    /**
     * 一个锁标记最大的存在时间(时间单位毫秒)
     */
    private long expiration = 50000;
    /**
     * 最小刷新周期
     */
    private long minRefreshCycle = 1000;

    RedisLock(String key) {
        this.key = key.intern();
    }

    Jedis getJedis() {
        return jedis;
    }

    String getKey() {
        return key;
    }

    long getExpiration() {
        return expiration;
    }

    /**
     * 执行加锁需要的lua脚本,由子类负责定义
     * @return 加锁的lua脚本
     */
    protected abstract String getLockScript();

    /**
     * 执行解锁需要的lua脚本,由子类负责定义
     * @return 解锁的lua脚本
     */
    protected abstract String getUnlockScript();

    /**
     * 执行锁标记释放时间刷新的lua脚本,由子类负责定义
     * @return 刷新的lua脚本
     */
    protected abstract String getRefreshScript();

    /**
     * 尝试获取一次锁(由子类来实现,因为每个脚本有不同的变量这个需要具体的子类来进行定义)
     * @param threadKey 线程名称
     * @return true/false   如果最终获取到锁则返回true,反之返回false
     */
    protected abstract boolean tryLock(String threadKey);

    /**
     * 释放锁(由子类来实现,因为每个脚本有不同的变量这个需要具体的子类来进行定义)
     * @param threadKey 线程名称
     */
    protected abstract void unlock(String threadKey);

    /**
     * 刷新锁(由子类来实现,因为每个脚本有不同的变量这个需要具体的子类来进行定义)
     * @param threadKey 线程名称
     * @return true/false true表示刷新成功  false表示刷新失败后续不需要再刷新了(通常为线程已经释放了锁)
     */
    protected abstract boolean refreshLock(String threadKey);

    /**
     * 获取锁
     * 如果获取不到锁,将会一直等待,直到成功获取到锁
     */
    public void lock() {
        //获取当前线程的唯一标识
        String threadKey = ThrowKeyUtil.getKey();

        //使用空循环来不停的尝试获取锁,直到获取成功,才可以退出此方法进入下一步
        while (!tryLockAndListen(threadKey));
    }

    /**
     * 尝试获取锁,与其他方法相比,该方法在等待获取锁期间可被强制中断
     * @throws InterruptedException 被中断时抛出此异常
     */
    public void lockInterruptibly() throws InterruptedException {
        //获取当前线程的唯一标识
        String threadKey = ThrowKeyUtil.getKey();

        //使用空循环来不停的尝试获取锁,直到获取成功,或者遇到中断请求
        while (!tryLockAndListen(threadKey)) {
            if (Thread.interrupted()) {
                throw new InterruptedException();
            }
        }
    }

    /**
     * 尝试获取锁(只尝试一次)
     * @return true/false  如果获取到锁则返回true,反之返回false
     */
    public boolean tryLock() {
        return tryLockAndListen(ThrowKeyUtil.getKey());
    }

    /**
     * 尝试获取锁
     * 如果获取锁失败则会等待一定时间
     * @param time 等待的时间长度
     * @param unit 等待的时间长度的单位
     * @return true/false   如果最终获取到锁则返回true,反之返回false
     * @throws InterruptedException
     */
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        //获取当前线程的唯一标识
        String threadKey = ThrowKeyUtil.getKey();
        //获取尝试获取锁的结束时间
        long stopTime = System.currentTimeMillis() + (unit.toMillis(time));

        //如果没有获取到锁,则不停的尝试获取锁,直到获取成功,或者等待时间超时
        while (!tryLockAndListen(threadKey)) {
            //如果当前时间大于线程等待结束时间则直接返回false
            if (System.currentTimeMillis() > stopTime) {
                return false;
            }
        }
        return true;
    }

    /**
     * 解锁
     */
    public void unlock() {
        unlock(ThrowKeyUtil.getKey());
    }

    public Condition newCondition() {
        //暂不实现
        return null;
    }

    /**
     * 获取加锁脚本在Redis中的SHA1校验和(key)
     * @return scriptKey
     */
    String getLockScriptKey() {
        //如果lua脚本存在于redis缓存中,则返回该脚本的scriptKey
        if (null != lockScriptKey) {
            return lockScriptKey;
        }
        //如果脚本不存在与redis缓存中,则将脚本写入到redis缓存中
        return loadLockScript();
    }

    /**
     * 获取解锁脚本在Redis中的SHA1校验和(key)
     * @return scriptKey
     */
    String getUnlockScriptKey() {
        //如果lua脚本存在于redis缓存中,则返回该脚本的scriptKey
        if (null != unlockScriptKey) {
            return unlockScriptKey;
        }
        //如果脚本不存在与redis缓存中,则将脚本写入到redis缓存中
        return loadUnlockScript();
    }

    /**
     * 获取刷新锁标记脚本在Redis中的SHA1校验和
     * @return scriptKey
     */
    String getRefreshScriptKey() {
        //如果lua脚本存在于redis缓存中,则返回该脚本的scriptKey
        if (null != refreshScriptKey) {
            return refreshScriptKey;
        }
        //如果脚本不存在与redis缓存中,则将脚本写入到redis缓存中
        return loadRefreshScript();
    }

    /**
     * 加载加锁脚本到redis中
     * @return 加载到redis中之后,redis返回的SHA1校验和
     */
    private synchronized String loadLockScript() {
        //这里再判断一次脚本在Redis中的校验和是否存在是因为可能存在同时有多个线程同时请求该方法
        // 为了防止redis资源被浪费,对该方法加了锁,第一个进入的线程会去将脚本加载到redis缓存中,后续并发请求的线程只需要取第一个线程 获取到的结果就可以了
        if (null != lockScriptKey) {
            return lockScriptKey;
        }
        //加载lua脚本到redis中,并获取redis返回的SHA1校验和,后续的脚本执行可以通过此值来调用该脚本
        String key = jedis.scriptLoad(getLockScript());
        lockScriptKey = key;
        return key;
    }

    /**
     * 加载解锁脚本到redis中
     * @return 加载到redis中之后,redis返回的SHA1校验和
     */
    private synchronized String loadUnlockScript() {
        if (null != unlockScriptKey) {
            return unlockScriptKey;
        }
        //加载lua脚本到redis中,并获取redis返回的SHA1校验和,后续的脚本执行可以通过此值来调用该脚本
        String key = jedis.scriptLoad(getUnlockScript());
        unlockScriptKey = key;
        return key;
    }

    /**
     * 加载刷新锁标记时长的lua脚本到redis中
     * @return 加载到redis中之后,redis返回SHA1校验和
     */
    private synchronized String loadRefreshScript() {
        if (null != refreshScriptKey) {
            return refreshScriptKey;
        }
        //加载lua脚本到redis中,并获取redis返回的SHA1校验和,后续的脚本执行可以通过此值来调用该脚本
        String key = jedis.scriptLoad(getRefreshScript());
        refreshScriptKey = key;
        return key;
    }

    /**
     * 尝试获取一次锁,如果获取成功,则添加添加一个守护线程监听锁的持有状态,并定期刷新锁的持有时间
     * @param threadKey 线程名称
     * @return true/false   如果最终获取到锁则返回true,反之返回false
     */
    private boolean tryLockAndListen(final String threadKey) {
        synchronized (key) {
            //尝试获取锁
            if (tryLock(threadKey)) {
                //如果获取锁成功则创建一个新的线程
                Thread thread = new Thread(new Runnable() {
                    //线程具体执行的内容
                    public void run() {
                        //计算刷新周期,取最大持有锁时间的9/10为刷新周期,并且刷新周期最小为:minRefreshCycle变量定义的值
                        long refreshCycle = Math.min((long) (expiration * 0.9), minRefreshCycle);
                        try {
                            do {
                                //线程休眠,刷新周期的时间
                                Thread.sleep(refreshCycle);
                                //线程休眠结束之后调用刷新方法,如果刷新成功,则进入循环,继续休眠、刷新,直到刷新失败(锁释放)
                            } while (refreshLock(threadKey));
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                });
                thread.setDaemon(true);
                thread.start();
                return true;
            }
            return false;
        }
    }
}

四、自定义Redis分布式锁

  RedisLock代码已经实现了Redis分布式锁的基本逻辑,但对具体的lua脚本以及lua脚本的执行没有定义。我们可以通过实现这些定义,来制定不同的Redis分布式锁。

1:基础锁
1.1 实现思路

  基础锁的实现定义比较简单,当获取锁时,就通过Redis的SETNX命令,将线程的名称记录在锁标记下。如果命令返回结果为0,则表示当先锁正在被线程持有,无法获取。反之则表示获取锁成功,接着对锁标记设置最大存活时间。
  当需要刷新锁持有时间时,就通过GET命令获取锁对应的线程名,比较是否和当前线程名一致,防止执行刷新操作时锁已经释放,而刷新了其他线程持有的锁。如果线程名称一致,则使用PEXPIRE命令给锁标志重新设置存活时间。
  当需要解锁时,同样先获取对应的线程名,如果线程名称一致,则将锁标志清空。

1.2 代码示例

  在代码中定义一个基础锁的实现类:RedisSimpleLock 并继承自RedisLock。实现其定义的lua脚本定义方法和lua脚本执行方法:代码如下:

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * 基础锁实现
 */
public class RedisSimpleLock extends RedisLock {

    RedisSimpleLock(String key) {
        super(key);
    }

    protected String getLockScript() {
        //加锁的lua脚本,其中KEYS[1]表示锁标志,ARGV[1]表示想要获取锁权限的线程名,ARGV[2]表示持有锁的最大时间(单位是毫秒)
        //使用setnx命令给KEYS[1]赋值,如果赋值结果为0表示失败,则表示锁已经有人占用,返回0
        return "if (redis.call('SETNX', KEYS[1],ARGV[1]) == 0) " +
                "then " +
                "return 0 " +
                "else " +
                //反之表示获取锁成功,通过PEXPIRE设置最大持有锁时间并返回1
                "redis.call('PEXPIRE', KEYS[1], ARGV[2]) " +
                "return 1 " +
                "end ";
    }

    protected String getUnlockScript() {
        //解锁的lua脚本,其中KEYS[1]表示锁标志,ARGV[1]表示想要获取锁权限的线程名
        //使用 GET命令获取当前持有锁的线程名称,并与当前线程名称进行比较,如果一致则使用DEL删除锁标记释放锁
        return " if (redis.call('GET', KEYS[1]) == ARGV[1]) " +
                "then " +
                "redis.call('DEL', KEYS[1]) " +
                "end ";
    }

    protected String getRefreshScript() {
        //解锁的lua脚本,其中KEYS[1]表示锁标志,ARGV[1]表示想要获取锁权限的线程名,ARGV[2]表示刷新后持有锁的最大时间(单位是毫秒)
        //通过GET获取当前持有锁的线程名,如果为自身,则通过PEXPIRE刷新存活时间,并返回1,反之不刷新,并返回0
        return "if (redis.call('GET', KEYS[1]) == ARGV[1]) " +
                "then " +
                "redis.call('PEXPIRE', KEYS[1], ARGV[2]) " +
                "return 1 " +
                "else " +
                "return 0 " +
                "end ";
    }

    protected boolean tryLock(String threadKey) {

        //组装加锁需要的数据
        List<String> keys = Collections.singletonList(getKey());
        List<String> values = Arrays.asList(threadKey,String.valueOf(getExpiration()));
        //获取redis连接,通过eval执行加锁脚本。  getLockScriptKey:获取加锁的脚本  keys:redis定义的执行脚本需要的key参数  values:redis定义的执行脚本需要的value参数
        Object r = getJedis().evalsha(getLockScriptKey(), keys, values);
        if (null == r) {
            return false;
        }
        return ((Number) r).intValue() != 0;
    }

    protected void unlock(String threadKey) {

        //组装解锁需要的数据
        List<String> keys = Collections.singletonList(getKey());
        List<String> values = Collections.singletonList(threadKey);

        //获取解锁的脚本并执行
        getJedis().evalsha(getUnlockScriptKey(), keys, values);
    }

    protected boolean refreshLock(String threadKey) {
        //组装刷新需要的数据
        List<String> keys = Collections.singletonList(getKey());
        List<String> values = Arrays.asList(threadKey,String.valueOf(getExpiration()));

        Object r = getJedis().evalsha(getRefreshScriptKey(), keys, values);
        if (null == r) {
            return false;
        }
        return ((Number) r).intValue() != 0;
    }
}
2:可重入锁

  可重入锁的定义是在基础锁的定义上加入同一线程可以重复获取同一个锁标志的定义。用于满足类似于递归调用的场景。而这样的场景使用基础锁会使线程自己与自己产生互斥从而死锁。

2.1 实现思路

  可重入锁的实现方式不再使用Redis的SETNX指令,因为该指令操作的是一个字符串类型,无法满足线程重入后,对重入次数的计数功能。我们使用Redis的Hash类型对应的指令,借用HASH的KEY/VALUE结构来保存持锁的线程名和重入计数。
  在线程尝试获取锁时,先通过EXISTS命令检查锁标志是否存在,如果不存在则使用 HINCRBY 命令,设置锁标志和锁的持有线程并给该线程的重入次数加1,之后再给锁标志设置超时时间。如果存在锁标志,则通过 HEXISTS 命令判断持有锁的线程是否为自身,不为自身则获取锁失败,反之如果为自身则通过 HINCRBY 命令将重入次数加1,获取锁成功。
  当需要刷新锁持有时间时,和基础锁的方式一致,只是将判断持有线程是否为自己的命令换做了 HEXISTS 。
  当需要解锁时,同样是先判断锁的持有者是否是自身,如果是自身则重入次数通过 HINCRBY 命令减1。并且如果减1后的结果为0,则表示锁使用完毕,通过 DEL 命令释放锁标志。

2.2 代码示例

  在代码中定义一个可重入锁的实现类:RedisReentrantLock 并继承自RedisLock。实现其定义的lua脚本定义方法和lua脚本执行方法:代码如下:

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * 可重入锁实现
 */
public class RedisReentrantLock extends RedisLock {

    RedisReentrantLock(String key) {
        super(key);
    }

    protected String getLockScript() {
        //加锁的lua脚本,其中KEYS[1]表示锁标志,ARGV[1]表示想要获取锁权限的线程名,ARGV[2]表示持有锁的最大时间(单位是毫秒)
        //通过 EXISTS 判断是否已经有人持有锁,如果 EXISTS 结果为0表示,锁标志不存在,获取锁成功
        return "if (redis.call('EXISTS', KEYS[1]) ==0) " +
                "then " +
                //使用HINCRBY命令设置锁标志并设置持锁线程名和重入次数加1(默认为0)
                "redis.call('HINCRBY', KEYS[1],ARGV[1], 1) " +
                //设置超时时间
                "redis.call('PEXPIRE', KEYS[1], ARGV[2]) " +
                "return 1 " +
                //如果存在锁,则通过HEXISTS 命令判断是否为自己持有锁
                "elseif (redis.call('HEXISTS', KEYS[1], ARGV[1]) == 0) " +
                "then " +
                //如果不是则直接返回
                "return 0 " +
                "else " +
                //反之则表示锁重入了,使用HINCRBY 和 PEXPIRE 设置锁的重入次数和超时时间
                "redis.call('HINCRBY', KEYS[1],ARGV[1], 1) " +
                "redis.call('PEXPIRE', KEYS[1], ARGV[2]) " +
                "return 1 " +
                "end";
    }

    protected String getUnlockScript() {
        //解锁的lua脚本,其中KEYS[1]表示锁标志,ARGV[1]表示想要获取锁权限的线程名
        //通过HEXISTS判断持有锁的人是否为自己,如果持有锁的人是自己,则使用HINCRBY对锁重入次数-1,如果重入次数-1后为0则通过DEL释放锁的使用权。
        return  "if (redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1) " +
                "then " +
                "if (redis.call('HINCRBY', KEYS[1], ARGV[1], -1) == 0) " +
                "then " +
                "redis.call('DEL', KEYS[1]) " +
                "end " +
                "end";
    }

    protected String getRefreshScript() {
        //刷新锁持有时间的lua脚本,其中KEYS[1]表示锁标志,ARGV[1]表示想要刷新锁权限的线程名,ARGV[2]表示刷新后持有锁的最大时间(单位是毫秒)
        //通过HEXISTS判断持有锁的人是否为自己,如果持有锁的人是自己,则通过PEXPIRE刷新持有锁的时间并返回1,反之不刷新返回0
        return  "if (redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1) " +
                "then " +
                "redis.call('PEXPIRE', KEYS[1], ARGV[2]) " +
                "return 1 " +
                "else " +
                "return 0 " +
                "end";
    }

    protected boolean tryLock(String threadKey) {

        //组装加锁需要的数据
        List<String> keys = Collections.singletonList(getKey());
        List<String> values = Arrays.asList(threadKey,String.valueOf(getExpiration()));
        //获取redis连接,通过eval执行加锁脚本。  getLockScriptKey:获取加锁的脚本  keys:redis定义的执行脚本需要的key参数  values:redis定义的执行脚本需要的value参数
        Object r = getJedis().evalsha(getLockScriptKey(), keys, values);
        if (null == r) {
            return false;
        }
        return ((Number) r).intValue() != 0;
    }

    protected void unlock(String threadKey) {

        //组装解锁需要的数据
        List<String> keys = Collections.singletonList(getKey());
        List<String> values = Collections.singletonList(threadKey);

        //获取解锁的脚本并执行
        getJedis().evalsha(getUnlockScriptKey(), keys, values);
    }

    protected boolean refreshLock(String threadKey) {
        //组装刷新需要的数据
        List<String> keys = Collections.singletonList(getKey());
        List<String> values = Arrays.asList(threadKey,String.valueOf(getExpiration()));

        Object r = getJedis().evalsha(getRefreshScriptKey(), keys, values);
        if (null == r) {
            return false;
        }
        return ((Number) r).intValue() != 0;
    }
}
3:读写锁

  在上述的可重入锁和基础锁中都存在这样一个问题,就是当获取到临界资源的操作权限之后。就完全独占了该临界资源,而不区分对临界资源的操作。这种实现对读多写少的情形是很不友好的。针对读多写少的情况我们可以引入读写锁。

3.1 实现思路

  读写锁分为两部分:读锁和写锁。读锁是共享锁,它允许其他线程也使用读锁,共享临界资源。写锁是独占锁,在使用写锁独占临界资源时,其他线程不可以获取该资源。当读锁和写锁共同操作同一共享资源时,它们互相排斥。为了防止读锁不停的存在新的线程进入,不停的排斥使用写锁的线程。则还需要给读写锁设置优先级,使写锁优先。
  当读线程持有锁时,进来了一个写线程,则写线程定义一个写等待标记,用于通知后续读线程不可以再获取该锁。并且使写线程阻塞,等待持有读锁的读线程全部结束,释放读锁后,写线程获取写锁,并清理写等待标记。为了防止写等待中的线程因意外原因停止获取写锁的操作,并且不能清理写等待标记,则还需要给写等待标记设置较短的等待时间。并在写等待期间不停的刷新等待时间。
  当读线程尝试获取锁时,需要先检查是否存在写等待或者锁是否已经被写线程持有。如果都没有则读线程可以获取读锁,反之读线程需要等待写等待和写锁释放之后才可以获取读锁。
  同样的当写线程需要获取锁时,需要先检查是否存在读锁,如果不存在,则还需要检查写锁是否被其他线程持有。只有当读写锁都未被占用或者写锁占用为自己的时候才可以成功获取到锁。

3.2 代码示例

  在代码中定义读写锁类RedisReadWriteLock,用于实现ReadWriteLock接口的readLock和writeLock方法,这两个方法分别返回读锁和写锁的对象用于不同场景的的使用。
  在定义好RedisReadWriteLock类后,在该类的内部分别定义读锁类ReadLock和写锁类WriteLock,并且这两个类都继承自RedisLock。
  在读锁尝试加锁时,先通过 EXISTS 命令判断是否存在写等待标记,如果存在写等待标记则加锁失败。如果不存在写等待标记,则通过使用 EXISTS 命令判断是否存在写锁,如果存在写锁,则读锁加锁失败。反之加锁成功使用 INCR 命令对读锁的占用计数加1。
  当需要刷新读锁的持有时间时,只需要通过EXISTS判断当前是否存在读锁即可,如果存在读锁,则使用PEXPIRE进行刷新。
  当读锁执行结束,需要释放时使用 DECR 命令,将读锁的占用计数减1,当减1之后的结果为0时,表示读锁没有线程持有,则使用 DEL 命令删除读标记。
  在写锁尝试加锁时,先通过 EXISTS 命令判断是否存在读锁,如果存在读锁则获取写锁失败。反之使用EXISTS判断是否存在写锁或者使用HEXISTS判断存在的写锁是否为自己所有。如果是则表示获取写锁成功,通过 HINCRBY 命令设置写锁标记设置重入次数为1,并删除写等待标记。如果不是则表示获取写锁失败。
  写锁的刷新和解锁则和可重入锁脚本保持一致(实质上写锁就是可重入锁的一种优化,增加了在加锁时对读锁的判断)。
  具体的代码实现方式如下:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;

/**
 * 可重入读写锁
 */
public class RedisReentrantReadWriteLock implements ReadWriteLock {

    private String key;

    public RedisReentrantReadWriteLock(String key) {
        this.key = key;
    }

    public Lock readLock() {
        return new ReadLock(key);
    }

    public Lock writeLock() {
        return new WriteLock(key);
    }

    public static class ReadLock extends RedisLock {

        ReadLock(String key) {
            super(key);
        }

        protected String getLockScript() {
            //加读锁的lua脚本,其中KEYS[1]表示等待写的锁标志,KEYS[2]表示正在写的锁标志,KEYS[3]表示正在读的锁标志,ARGV[1]表示持有锁的最大时间(单位是毫秒)
            //通过EXISTS 判断是否存在写等待,如果存在写等待则获取读锁失败
            return "if (redis.call('EXISTS', KEYS[1]) == 1) " + "then " + "return 0 " +
                    //通过 EXISTS 判断是否存在写锁,如果存在写锁则获取读锁失败
                    "elseif (redis.call('EXISTS', KEYS[2]) == 1) " + "then " + "return 0 " + "else " +
                    //获取读锁成功 读锁数量加1并更新最大持有锁时间
                    "redis.call('INCR', KEYS[3])" +
                    "redis.call('PEXPIRE', KEYS[3], ARGV[1]) " +
                    "return 1 " +
                    "end ";
        }

        protected String getUnlockScript() {
            //对读锁的持有数量减1,如果读锁的持有数量为0,则删除读锁
            return "if (redis.call('DECR', KEYS[1]) == 0) " + "then "+
                    "redis.call('DEL', KEYS[1]); " + "end; ";
        }

        protected String getRefreshScript() {

            //判断当前读锁是否存在
            return "if (redis.call('EXISTS', KEYS[1]) == 1) " +
                    "then " +
                    //如果读锁存在对读锁设置最大超时时间
                    "redis.call('PEXPIRE', KEYS[1], ARGV[1]) " +
                    "return 1 " +
                    "else " +
                    "return 0 " +
                    "end ";
        }

        protected boolean tryLock(String threadKey) {
            //组装加锁需要的数据
            List<String> keys = Arrays.asList(getKey()+ ":write_wait",getKey()+":write",getKey()+":read");
            List<String> values = Collections.singletonList(String.valueOf(getExpiration()));
            //获取redis连接,通过eval执行加锁脚本。  getLockScriptKey:获取加锁的脚本  keys:redis定义的执行脚本需要的key参数  values:redis定义的执行脚本需要的value参数
            Object r = getJedis().evalsha(getLockScriptKey(), keys, values);
            if (null == r) {
                return false;
            }
            return ((Number) r).intValue() != 0;
        }

        protected void unlock(String threadKey) {
            //组装解锁需要的数据
            List<String> keys = Collections.singletonList(getKey()+":read");
            getJedis().evalsha(getUnlockScriptKey(), keys, new ArrayList<String>());
        }

        protected boolean refreshLock(String threadKey) {

            List<String> keys = Collections.singletonList(getKey()+":read");
            List<String> values = Collections.singletonList(String.valueOf(getExpiration()));

            Object r = getJedis().evalsha(getRefreshScriptKey(), keys, values);
            if (null == r) {
                return false;
            }
            return ((Number) r).intValue() != 0;
        }
    }

    public static class WriteLock extends RedisLock {

        WriteLock(String key) {
            super(key);
        }

        protected String getLockScript() {

            //keys[1]写等待标记key keys[2]写锁 keys[3]读锁  values[1]线程名  values[2]:写等待时长
            //EXISTS 判断当前是否存在读锁
            return "if(redis.call('EXISTS', KEYS[3]) == 1) " +
                    "then " +
                    //如果存在读锁,则加锁失败
                    "return 0 " +
                    //反之通过EXISTS判断是否存在写锁或通过HEXISTS判断持有写锁的是否为自己
                    "elseif(redis.call('EXISTS', KEYS[2]) == 0 or redis.call('HEXISTS', KEYS[2], ARGV[1]) == 1) " +
                    "then " +
                    //如果写锁不存在或者持有写锁的为自己,则获取锁成功,通过HINCRBY 记录持有锁的线程和重入次数加1
                    "redis.call('HINCRBY', KEYS[2], ARGV[1], 1) " +
                    //设置写锁的最大持有时间
                    "redis.call('PEXPIRE', KEYS[1], ARGV[2])  " +
                    "return 1 " +
                    "else " +
                    "return 0 " +
                    "end";
        }

        protected String getUnlockScript() {
            //通过HEXISTS判断持有锁的人是否为自己
            return  "if (redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1) " +
                    "then " +
                    //如果持有锁的人是自己,则锁重入次数-1,并且如果减1后的值为0则表示全部线程已经释放,使用DEL删除锁标记
                    "if (redis.call('HINCRBY', KEYS[1], ARGV[1], -1) == 0) " +
                    "then " +
                    "redis.call('DEL', KEYS[1]) " +
                    "end " +
                    "end";
        }

        protected String getRefreshScript() {
            //刷新锁持有时间的lua脚本,其中KEYS[1]表示锁标志,ARGV[1]表示想要刷新锁权限的线程名,ARGV[2]表示刷新后持有锁的最大时间(单位是毫秒)
            //通过HEXISTS判断持有锁的人是否为自己,如果持有锁的人是自己,则刷新持有锁的时间并返回1,反之不刷新返回0
            return  "if (redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1) " +
                    "then " +
                    "redis.call('PEXPIRE', KEYS[1], ARGV[2]) " +
                    "return 1 " +
                    "else " +
                    "return 0 " +
                    "end ";
        }

        protected boolean tryLock(String threadKey) {
            //组装加锁需要的数据
            List<String> keys = Arrays.asList(getKey() + ":write_wait",getKey()+":write",getKey()+":read");
            //100表示写等待标记只存在100毫秒
            List<String> values = Arrays.asList(threadKey,String.valueOf(getExpiration()),"100");
            //获取redis连接,通过eval执行加锁脚本。  getLockScriptKey:获取加锁的脚本  keys:redis定义的执行脚本需要的key参数  values:redis定义的执行脚本需要的value参数
            Object r = getJedis().evalsha(getLockScriptKey(), keys, values);
            if (null == r) {
                return false;
            }
            return ((Number) r).intValue() != 0;
        }

        protected void unlock(String threadKey) {

            //组装解锁需要的数据
            List<String> keys = Collections.singletonList(getKey()+":write");
            List<String> values = Collections.singletonList(threadKey);
            getJedis().evalsha(getUnlockScriptKey(), keys, values);
        }

        protected boolean refreshLock(String threadKey) {
            //组装加锁需要的数据
            List<String> keys = Collections.singletonList(getKey() + ":write");
            List<String> values = Arrays.asList(threadKey,String.valueOf(getExpiration()));
            Object r = getJedis().evalsha(getRefreshScriptKey(), keys, values);
            if (null == r) {
                return false;
            }
            return ((Number) r).intValue() != 0;
        }
    }
}

五、使用示例

  上述代码的使用方式为,在通过锁类new对象时,为该对象指定锁标识,在使用的过程中,无论是否为同一个对象,只要锁标识一致,即为同一个锁。

    public static void main(String[] args) {
        //下方的两个锁,虽然是不同的对象,但因为锁标记一致,所以是同一个锁
        RedisSimpleLock a = new RedisSimpleLock("指定的锁标记");
        RedisSimpleLock b = new RedisSimpleLock("指定的锁标记");

        //这里会产生死锁,因为a和b是同一个锁,所以在a解锁之前,使用b加锁会产生互斥
        //lock加锁 unlock解锁
        a.lock();
        b.lock();
        a.unlock();
        b.unlock();
    }

注意事项

  上述的内容中介绍了一个基本的Redis实现分布式锁的思想以及如何通过Java代码实现一个简单的Redis分布式锁。不过代码实现方式过于简陋,对性能和安全方面未完全处理,谨慎copy。
性能及安全问题,以及解决思路,有需要的可以自行编写,后续作者也会完善后发布到码云或github:

  • 代码实现中只创建了一个连接,多线程下会出现连接异常和数据传输异常,解决方案:使用三方的redis连接池,使每个线程都有一个独立的连接可以使用
  • 线程阻塞方式采用死循环会产生机器性能的浪费和网络带宽占用问题,解决方案:通过创建一个线程安全的线性表,对所有想要获取锁的线程采用统一管理,一次只有一个线程可以尝试去获取锁,其他线程则进入休眠状态等待唤醒(解锁时或锁等待超时时)
  • 刷新锁的线程存活时间没有设置,将会一直执行,解决方案:每个锁设置一个公共的变量,用来记录当前的加锁情况,只有存在线程持有锁时,才使刷新线程执行刷新操作,或者使线程结束
  • 锁重入或多次进入时,会创建多个刷新线程的问题,解决方案:每个锁统一使用同一个刷新线程,不再获取锁时再创建,这个线程的创建和回收与锁对象保持一致
  • 读写锁的写等待时间没有实现刷新,解决方案:参照锁的持有时间的刷新

其他

Redis命令参考: redis中文网

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值