java 使用 Redis 实现分布式锁

Posted 业余草

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java 使用 Redis 实现分布式锁相关的知识,希望对你有一定的参考价值。

既然有人想看,那我就写!为你服务!

前面我写到了使用,其实使用乐观锁也可以实现。但是乐观锁比悲观锁更麻烦!基于 Redis 实现的悲观锁,在实际应用中可能用的更多。

分布式锁其实最终解决的是不同 JVM 进程竞争同一资源问题。使用 Redis 实现分布式锁,主要用到的是 Redis 的 set 函数。

我们先来看代码:

  1. package com.xttblog.lock;

  2. import java.util.Collections;

  3. import redis.clients.jedis.Jedis;

  4. import redis.clients.jedis.JedisPool;

  5. public class XttblogLock {

  6.    private static final String LOCK_SUCCESS = "OK";

  7.    private static final String SET_IF_NOT_EXIST = "NX";

  8.    private static final String SET_WITH_EXPIRE_TIME = "PX";

  9.    private static final Long RELEASE_SUCCESS = 1L;

  10.    private static void validParam(JedisPool jedisPool, String lockKey, String requestId, int expireTime) {

  11.        if (null == jedisPool) {

  12.            throw new IllegalArgumentException("jedisPool obj is null");

  13.        }

  14.        if (null == lockKey || "".equals(lockKey)) {

  15.            throw new IllegalArgumentException("lock key  is blank");

  16.        }

  17.        if (null == requestId || "".equals(requestId)) {

  18.            throw new IllegalArgumentException("requestId is blank");

  19.        }

  20.        if (expireTime < 0) {

  21.            throw new IllegalArgumentException("expireTime is not allowed less zero");

  22.        }

  23.    }

  24.    /**

  25.     * @param jedis

  26.     * @param lockKey

  27.     * @param requestId

  28.     * @param expireTime

  29.     * @return

  30.     */

  31.    public static boolean tryLock(JedisPool jedisPool, String lockKey, String requestId, int expireTime) {

  32.        validParam(jedisPool, lockKey, requestId, expireTime);

  33.        Jedis jedis = null;

  34.        try {

  35.            jedis = jedisPool.getResource();

  36.            String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

  37.            if (LOCK_SUCCESS.equals(result)) {

  38.                return true;

  39.            }

  40.        } catch (Exception e) {

  41.            throw e;

  42.        } finally {

  43.            if (null != jedis) {

  44.                jedis.close();

  45.            }

  46.        }

  47.        return false;

  48.    }

  49.    /**

  50.     *

  51.     * @param jedis

  52.     * @param lockKey

  53.     * @param requestId

  54.     * @param expireTime

  55.     */

  56.    public static void lock(JedisPool jedisPool, String lockKey, String requestId, int expireTime) {

  57.        validParam(jedisPool, lockKey, requestId, expireTime);

  58.        while (true) {

  59.            if (tryLock(jedisPool, lockKey, requestId, expireTime)) {

  60.                return;

  61.            }

  62.        }

  63.    }

  64.    /**

  65.     *

  66.     * @param jedis

  67.     * @param lockKey

  68.     * @param requestId

  69.     * @return

  70.     */

  71.    public static boolean unLock(JedisPool jedisPool, String lockKey, String requestId) {

  72.        validParam(jedisPool, lockKey, requestId, 1);

  73.        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

  74.        Jedis jedis = null;

  75.        try {

  76.            jedis = jedisPool.getResource();

  77.            Object result = jedis.eval(script, Collections.singletonList(lockKey),Collections.singletonList(requestId));

  78.            if (RELEASE_SUCCESS.equals(result)) {

  79.                return true;

  80.            }

  81.        } catch (Exception e) {

  82.            throw e;

  83.        } finally {

  84.            if (null != jedis) {

  85.                jedis.close();

  86.            }

  87.        }

  88.        return false;

  89.    }

  90. }

看不懂上面的代码?看我来给你解读一下!

首先 Jedis 的 public String set(final String key, final String value, final String nxxx, final String expx,final int time)方法。

前两个值是 key 和 value 我就不多说。nxxx为模式,这里我们设置为NX,意思是说如果key不存在则插入该key对应的value并返回OK,否者什么都不做返回null。expx这里我们设置为PX,意思是设置key的过期时间为time 毫秒。

先通过tryLock方法尝试获取锁,内部是具体调用Redis的set方法,多个线程同时调用tryLock时候会同时调用set方法,但是set方法本身是保证原子性的,对应同一个key来说,多个线程调用set方法时候只有一个线程返回OK,其它线程因为key已经存在会返回null,所以返回OK的线程就相当与获取到了锁,其它返回null的线程则相当于获取锁失败。

注意,我们需要保证value(requestId)值是唯一的。因为在后面释放锁时会用到。

再通过lock 方法让使用tryLock获取锁失败的线程本地自旋转重试获取锁,这类似JUC里面的CAS。

现在来看解锁。Redis有一个叫做eval的函数,支持Lua脚本执行,并且能够保证脚本执行的原子性,也就是在执行脚本期间,其它执行redis命令的线程都会被阻塞。

  1. if redis.call('get', KEYS[1]) == ARGV[1]

  2. then return redis.call('del', KEYS[1])

  3. else

  4. return 0 end

对上面的 Lua 脚本解释如下:

其中keys[1]为unLock方法传递的key,argv[1]为unLock方法传递的requestId;脚本redis.call(‘get’, KEYS[1])的作用是获取key对应的value值,这里会返回通过Lock方法传递的requetId, 然后看当前传递的RequestId是否等于key对应的值,等于则说明当前要释放锁的线程就是获取锁的线程,则继续执行redis.call(‘del’, KEYS[1])脚本,删除key对应的值。

基于 Redis 的分布式锁,其实是利用了redis单实例的特性,并结合redis的set方法和eval函数实现了一个简单的分布式锁,但是这个实现还是明显有问题的。虽然使用set方法设置了超时时间,以避免线程获取到锁后redis挂了后锁没有被释放的情况,但是超时时间设置为多少合适那?如果设置太小,可能会存在线程获取锁后执行业务逻辑时间大于锁超时时间,那么就会存在逻辑还没执行完,锁已经因为超时自动释放了,而其他线程可能获取到锁,那么之前获取锁的线程的业务逻辑的执行就没有保证原子性。

另外还有一个问题是Lock方法里面是自旋调用tryLock进行重试,这就会导致像JUC中的AtomicLong一样,在高并发下多个线程竞争同一个资源时候造成大量线程占用cpu进行重试操作。这时候其实可以随机生成一个等待时间,等时间到后在进行重试,以减少潜在的同时对一个资源进行竞争的并发量。

没有完美的方案,只有更好的方案!你赞不赞同?请留言!

以上是关于java 使用 Redis 实现分布式锁的主要内容,如果未能解决你的问题,请参考以下文章

java 使用 Redis 实现分布式锁

80% 人不知道的 Redis 分布式锁的正确实现方式(Java 版)

编程实践分布式锁的实现代码

Springboot使用redis的setnx和getset实现并发锁、分布式锁

java实现Redis分布式锁

Java分布式:分布式锁之Redis实现