在 Java 中利用 redis 实现一个分布式锁服务

0.139字数 2377阅读 141

在 Java 中利用 redis 实现一个分布式锁服务

在现代的编程语言中,接触过多线程编程的程序员多多少少对锁有一定的了解。简单的说,多线程中的锁就是在多线程环境下,多个线程对共享资源进行修改的时候,保证共享资源一致性的机制。这里不展开说。在分布式环境下,原来的多线程的锁就不管用了,也就出现了分布式锁的需求。所谓分布式锁服务也就是在分布式环境下,保证多个分布式的服务共享的资源一致性的服务。

在分布式环境下实现一个分布式锁服务并不太容易,需要考虑很多在单进程下的锁服务不需要考虑的问题。分布式锁锁的实现也有很多。这里我们讨论在 Java 中通过 redis 来实现。在 GitHub 中的 redisson 项目中已经有开源的实现。但是那个太复杂了。现在我们来基于单机的 redis 实现一个简单的分布式锁服务。这个服务必须满足下面的要求

支持立即获取锁方式,如果获取到返回true,获取不到则返回false;

支持等待获取锁方式,如果获取到,直接返回true,获取不到在等待一小段时间,在这一小段时间内反复尝试,如果尝试成功,则返回true,等待时间过后还获取不到则返回false;

不能产生死锁的情况;

不能释放非自己加的锁;

下面我们用实例来演示在 Java 中利用 redis 实现分布式锁服务

##加锁

通过 redis 来实现分布式锁的加锁逻辑如下所示:

根据这个逻辑,实现上锁的核心代码如下所示:

jedis.select(dbIndex);

String key = KEY_PRE + key;

String value = fetchLockValue();

if(jedis.exists(key)){

  jedis.set(key,value);

  jedis.expire(key,lockExpirseTime);

  return value;

}

表面上看这段代码好像没有什么问题,实际上并不能在分布式环境中正确的实现加锁的操作。要能够正确的实现加锁操作,“判断 key 是否存在”“保存 key-value”、**“设置 key 的过期时间”**这三步操作必须是原子操作。如果不是原子操作,那么可能会出现下面两种情况:

在**“判断 key 是否存在”得出 key 不存在的结果步骤后,“保存 key-value”步骤前,另一个客户端执行同样的逻辑,并且执行到了“判断 key 是否存在”**步骤,同样得出了 key 不存在的结果。这样回导致多个客户端获得了同一把锁;

在客户端执行完**“保存 key-value”** 步骤后,需要设置一个 key 的过期时间,防止客户端因为代码质量未解锁,在或者进程崩溃未解锁导致的死锁情况。在**“保存 key-value”步骤之后,“设置 key 的过期时间”步骤之前,可能进程崩溃,导致“设置 key 的过期时间”**步骤失败;

redis 在2.6.12版本之后,对 set 命令进行了扩充,能够规避上面的两个问题。新版的 redis set 命令的参数如下

SET key value [EX seconds] [PX milliseconds] [NX|XX]

新版的 set 命令增加了 EX 、 PX 、 NX|XX 参数选项。他们的含义如下

EX seconds – 设置键 key 的过期时间,单位时秒

PX milliseconds – 设置键 key 的过期时间,单位时毫秒

NX – 只有键 key 不存在的时候才会设置 key 的值

XX – 只有键 key 存在的时候才会设置 key 的值

这样,原来的三步操作就可以在一个 set 的原子操作里面来完成,规避了上面我们提到的两个问题。新版的 redis 加锁核心代码修改如下所示:

jedis = redisConnection.getJedis();

jedis.select(dbIndex);

String key = KEY_PRE + key;

String value = fetchLockValue();

if ("OK".equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {

    return value;

}

##解锁 解锁的基本流程如下:

根据这个逻辑,在 Java 中解锁的核心代码如下所示:

jedis.select(dbIndex);

String key = KEY_PRE + key;

if(jedis.exists(key) && value.equals(jedis.get(key))){

    jedis.del(key);

    return true;

}

return false;

和加锁的时候一样,key 是否存在判断是否自己持有锁、**删除 key-value **这三步操作需要是原子操作,否则当一个客户端执行完“判断是否自己持有锁”步骤后,得出自己持有锁的结论,此时锁的过期时间到了,自动被 redis 释放了,同时另一个客户端又基于这个 key 加锁成功,如果第一个客户端还继续执行删除 key-value的操作,就将不属于自己的锁给释放了。这显然是不运行的。在这里我们利用 redis 执行 Lua 脚本的能力来解决原子操作的问题。修改后的解锁核心代码如下所示:

jedis.select(dbIndex);

String key = KEY_PRE + key;

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

if (1L.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) {

    return true;

}

另外,判断是否自己持有锁的机制是用加锁的时候的 key-value 来判断当前的 key 的值是否等于自己持有锁时获得的值。所以加锁的时候的 value 必须是一个全局唯一的字符串。

##完整的代码如下所示

package com.x9710.common.redis.impl;

import com.x9710.common.redis.LockService;

import com.x9710.common.redis.RedisConnection;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import redis.clients.jedis.Jedis;

import java.text.DateFormat;

import java.text.SimpleDateFormat;

import java.util.Collections;

import java.util.Date;

import java.util.UUID;

/**

* 分布式锁 redis 实现

*

* @author 杨高超

* @since 2017-12-14

*/

public class LockServiceRedisImpl implements LockService {

private static Log log = LogFactory.getLog(LockServiceRedisImpl.class);

private static String SET_SUCCESS = "OK";

private static String KEY_PRE = "REDIS_LOCK_";

private DateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");

private RedisConnection redisConnection;

private Integer dbIndex;

private Integer lockExpirseTime;

private Integer tryExpirseTime;

public void setRedisConnection(RedisConnection redisConnection) {

    this.redisConnection = redisConnection;

}

public void setDbIndex(Integer dbIndex) {

    this.dbIndex = dbIndex;

}

public void setLockExpirseTime(Integer lockExpirseTime) {

    this.lockExpirseTime = lockExpirseTime;

}

public void setTryExpirseTime(Integer tryExpirseTime) {

    this.tryExpirseTime = tryExpirseTime;

}

public String lock(String key) {

    Jedis jedis = null;

    try {

        jedis = redisConnection.getJedis();

        jedis.select(dbIndex);

        key = KEY_PRE + key;

        String value = fetchLockValue();

        if (SET_SUCCESS.equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {

            log.debug("Reids Lock key : " + key + ",value : " + value);

            return value;

        }

    } catch (Exception e) {

        e.printStackTrace();

    } finally {

        if (jedis != null) {

            jedis.close();

        }

    }

    return null;

}

public String tryLock(String key) {

    Jedis jedis = null;

    try {

        jedis = redisConnection.getJedis();

        jedis.select(dbIndex);

        key = KEY_PRE + key;

        String value = fetchLockValue();

        Long firstTryTime = new Date().getTime();

        do {

            if (SET_SUCCESS.equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {

                log.debug("Reids Lock key : " + key + ",value : " + value);

                return value;

            }

            log.info("Redis lock failure,waiting try next");

            try {

                Thread.sleep(100);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        } while ((new Date().getTime() - tryExpirseTime * 1000) < firstTryTime);

    } catch (Exception e) {

        e.printStackTrace();

    } finally {

        if (jedis != null) {

            jedis.close();

        }

    }

    return null;

}

public boolean unLock(String key, String value) {

    Long RELEASE_SUCCESS = 1L;

    Jedis jedis = null;

    try {

        jedis = redisConnection.getJedis();

        jedis.select(dbIndex);

        key = KEY_PRE + key;

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

        if (RELEASE_SUCCESS.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) {

            return true;

        }

    } catch (Exception e) {

        e.printStackTrace();

    } finally {

        if (jedis != null) {

            jedis.close();

        }

    }

    return false;

}

/**

* 生成加锁的唯一字符串

*

* @return 唯一字符串

*/

private String fetchLockValue() {

    return UUID.randomUUID().toString() + "_" + df.format(new Date());

}

}

##测试代码

package com.x9710.common.redis.test;

import com.x9710.common.redis.RedisConnection;

import com.x9710.common.redis.impl.LockServiceRedisImpl;

public class RedisLockTest {

public static void main(String[] args) {

    for (int i = 0; i < 9; i++) {

        new Thread(new Runnable() {

            public void run() {

                RedisConnection redisConnection = RedisConnectionUtil.create();

                LockServiceRedisImpl lockServiceRedis = new LockServiceRedisImpl();

                lockServiceRedis.setRedisConnection(redisConnection);

                lockServiceRedis.setDbIndex(15);

                lockServiceRedis.setLockExpirseTime(20);

                String key = "20171228";

                String value = lockServiceRedis.lock(key);

                try {

                    if (value != null) {

                        System.out.println(Thread.currentThread().getName() + " lock key = " + key + " success! ");

                        Thread.sleep(25 * 1000);

                    }else{

                        System.out.println(Thread.currentThread().getName() + " lock key = " + key + " failure! ");

                    }

                } catch (Exception e) {

                    e.printStackTrace();

                } finally {

                    if (value == null) {

                        value = "";

                    }

                    System.out.println(Thread.currentThread().getName() + " unlock key = " + key + " " + lockServiceRedis.unLock(key, value));

                }

            }

        }).start();

    }

}

}

测试结果

Thread-1 lock key = 20171228 failure!

Thread-2 lock key = 20171228 failure!

Thread-4 lock key = 20171228 failure!

Thread-8 lock key = 20171228 failure!

Thread-7 lock key = 20171228 failure!

Thread-3 lock key = 20171228 failure!

Thread-5 lock key = 20171228 failure!

Thread-0 lock key = 20171228 failure!

Thread-6 lock key = 20171228 success!

Thread-1 unlock key = 20171228 false

Thread-2 unlock key = 20171228 false

Thread-4 unlock key = 20171228 false

Thread-8 unlock key = 20171228 false

Thread-3 unlock key = 20171228 false

Thread-5 unlock key = 20171228 false

Thread-0 unlock key = 20171228 false

Thread-7 unlock key = 20171228 false

Thread-6 unlock key = 20171228 true

从测试结果来看可以看到,9个线程同时给一个 key 加锁,只有一个能够成功获取到锁,其余的客户端都无法获取到锁。

我这里有一个大牛交流群,里面每天更新新的视频新的技术,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家。如果想学习Java工程化、高性能及分布式、深入浅出。性能调优、Spring,MyBatis,Netty源码分析的朋友可以加Java进阶群:171662117,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给大家

推荐阅读更多精彩内容