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

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

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

  1. 支持当即获取锁方式,若是获取到返回true,获取不到则返回false;
  2. 支持等待获取锁方式,若是获取到,直接返回true,获取不到在等待一小段时间,在这一小段时间内反复尝试,若是尝试成功,则返回true,等待时间事后还获取不到则返回false;
  3. 不能产生死锁的状况;
  4. 不能释放非本身加的锁;

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

##加锁github

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

redis 分布式锁上锁实现逻辑

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

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 的过时时间”**这三步操做必须是原子操做。若是不是原子操做,那么可能会出现下面两种状况:编程

  1. 在**“判断 key 是否存在”得出 key 不存在的结果步骤后,“保存 key-value”步骤前,另外一个客户端执行一样的逻辑,而且执行到了“判断 key 是否存在”**步骤,一样得出了 key 不存在的结果。这样回致使多个客户端得到了同一把锁;
  2. 在客户端执行完**“保存 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 参数选项。他们的含义以下dom

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;
}
复制代码

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

redis 分布式锁解锁实现逻辑

根据这个逻辑,在 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 加锁,只有一个可以成功获取到锁,其他的客户端都没法获取到锁。

##后记 这个代码里面还实现了一个 tryLock 的接口。这个主要是客户端没法获取到锁的时候会在一小段时间内反复尝试是否可以获取到锁。

这个程序实在前面的文章《在 Java 中使用 redis》的基础上添加新的实现类的方式完成的。代码同步发布在 GitHub 仓库

原文发表在简书中,原始连接

相关文章
相关标签/搜索