Junhc

岂止于博客

Redis分布式锁

Maven引入Jedis组件
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
代码实现
public class JedisClient {

    private static final String isOK = "OK";

    /**
     * 正确的获取分布式锁
     *
     * @param jedis     jedis对象
     * @param lockKey   使用key当锁
     * @param requestId 客户端请求唯一标识
     * @param expire    过期时间
     */
    public static boolean tryDistributedLock(Jedis jedis, String lockKey, String requestId, int expire) {
        // NX, 可以保证只有一个客户端持有锁, 满足互斥性
        // 1. 第三个参数为nxxx, NX 表示 SET IF NOT EXIST 即当key不存在时, 进行set操作, 否则不做任何操作

        // 过期时间, 可以保证不会发生死锁
        // 2. 第四个参数pxxx, 给这个key加一个过期时间
        // 3. 第五个参数expx, 配合第四个参数使用, 代表key的过期时间
        String result = jedis.set(lockKey, requestId, "NX", "PX", expire);
        if (isOK.equals(result)) {
            return true;
        }
        return false;
    }


    /**
     * 错误的获取分布式锁一
     *
     * @param jedis     jedis对象
     * @param lockKey   使用key当锁
     * @param requestId 客户端请求唯一标识
     * @param expire    过期时间
     */
    public static boolean wrongDistributedLock(Jedis jedis, String lockKey, String requestId, int expire) {
        if (jedis.setnx(lockKey, requestId) == 1) {
            // 由于这里执行了两条redis的命令, 不具备原子性
            // 如果程序崩溃, 导致没有执行设置过期时间, 那么有可能会发生死锁
            jedis.expire(lockKey, expire);
            return true;
        }
        return false;
    }


    /**
     * 错误的获取分布式锁二
     *
     * @param jedis   jedis对象
     * @param lockKey 使用key当锁
     * @param expire  过期时间
     */
    public static boolean wrongDistributedLock2(Jedis jedis, String lockKey, int expire) {
        long expireTime = System.currentTimeMillis() + expire;
        // 如果当前锁不存在, 返回加锁成功
        if (jedis.setnx(lockKey, String.valueOf(expireTime)) == 1) {
            return true;
        }
        // 如果锁存在, 获取锁的过期时间
        String currentExpireTime = jedis.get(lockKey);
        if (null != currentExpireTime && Long.parseLong(currentExpireTime) < System.currentTimeMillis()) {
            // 锁已过期, 获取上一个锁的过期时间, 并设置现在的过期时间
            String oldExpireTime = jedis.getSet(lockKey, String.valueOf(expireTime));
            if (null != oldExpireTime && oldExpireTime.equals(currentExpireTime)) {
                // 考虑多线程并发时, 只有一个线程的设置值和当前值相同, 它才有权加锁
                // 有点cas的感觉..只有期望值相等时, 才允许赋值..但是可能会出现类似ABA的问题..
                return true;
            }
        }
        return false;
        // 问题一 由于客户端生成过期时间, 所以需要强制要求分布式下每个客户端的时间必须同步
        // 问题二 如果多个客户端同时执行getSet方法, 虽然最终只有一个客户端可以加锁成功, 但是这个客户端锁的过期时间可能被其他客户端覆盖
        // 问题三 锁不具备客户端标识, 任何客户端都可以解锁
    }

    /**
     * 正确的释放分布式锁
     *
     * @param jedis     jedis对象
     * @param lockKey   使用key当锁
     * @param requestId 客户端请求唯一标识
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        // Lua脚本代码
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // KEYS[1]赋值为lockKey
        // ARGV[1]赋值为requestId

        // redis在eval命令执行Lua代码时, 将被当作一个命令去执行, 确保了原子性
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (isOK.equals(result)) {
            return true;
        }
        return false;

    }

    /**
     * 错误的释放分布式锁一
     */
    public static boolean wrongReleaseDistributedLock1(Jedis jedis, String lockKey) {
        // 任意客户端都可以解锁
        return jedis.del(lockKey) == 1;
    }


    /**
     * 错误的释放分布式锁二
     */
    public static boolean wrongReleaseDistributedLock2(Jedis jedis, String lockKey, String requestId) {
        // 判断加锁与解锁是不是同一个客户端
        if (requestId.equals(jedis.get(lockKey))) {
            // 由于不具备原子性,如果此时这把锁突然不是这个客户端的,则会误解锁
            return jedis.del(lockKey) == 1;
        }
        return false;
    }

}
开源项目