基于Redis的分布式锁实现

锁:解决多个线程争抢资源的情况,保证任何时候有且只有一个线程能持有资源,并且避免死锁。
关注问题:分布式、过期、宕机、代码原子性、GC、重入(lock次数)

分布式锁必须保证可靠性,需满足以下四个条件:

  • 1、互斥性。在任意时刻,只有一个客户端能持有锁。
  • 2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  • 3、具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  • 4、解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

一、基于setnx和lua脚本实现分布式锁

下面以非阻塞锁代码讲解其原理

public class RedisTool {
    private static final String LOCK_SUCCESS = "OK";
    private static final Long RELEASE_SUCCESS = 1L;
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;

    }
}

加锁过程

加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

  • 第一个为key,我们使用key来当锁,因为key是唯一的。
  • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。
  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

解锁过程

首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。其中eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

参考:
http://www.cnblogs.com/linjiqin/p/8003838.html

一个封装的基于Redis的分布式锁工具类(包含阻塞锁和非阻塞锁):
由于公司的JimDB不支持执行lua脚本,因此释放锁没做过多的操作。而加锁过程采用的如下原理

SETNX lock_key value 
expire lock_key seconds 
ttl lock_key

ttl:当 key 不存在时,返回 -2 。当 key 存在但没有设置剩余生存时间时,返回 -1 。否则,以秒为单位,返回 key 的剩余生存时间。
P1执行setnx成功但是在expire之前程序挂掉
P2执行setnx返回0,然后执行ttl命令返回-1,则执行expire lock_key timeout设置失效时间

package com.xstore.pms.auto.order.common.lock;
import com.jd.jim.cli.Cluster;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
 * 分布式锁
 */
public class RedisLock {
    private Cluster redisClient;
    /**
     * 超时时间(毫秒为单位)
     */
    private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 10 * 60 * 1000;
    public void setRedisClient(Cluster redisClient) {
        this.redisClient = redisClient;
    }
    public Cluster getRedisClient() {
        return redisClient;
    }
    /**
     * 阻塞直到获得分布式锁
     * @param lockName
     * @param acquireTimeout
     * @param lockTimeout
     * @return
     */
    public String acquireLockWithTimeout(String lockName, long acquireTimeout, long lockTimeout){
        try{
            String identifier = UUID.randomUUID().toString(); //锁的值
            String lockKey = "acquireLock:" + lockName; //锁的键
            int lockExpire = (int)(lockTimeout / 1000); //锁的过期时间
            long end = System.currentTimeMillis() + acquireTimeout; //尝试获取锁的时限
            while (System.currentTimeMillis() < end) { //判断是否超过获取锁的时限
                if (redisClient.setNX(lockKey, identifier)){ //判断设置锁的值是否成功
                    redisClient.expire(lockKey, lockExpire, TimeUnit.SECONDS); //设置锁的过期时间
                    return identifier; //返回锁的值
                }
                if(redisClient.ttl(lockKey)==-1){//判断如果没有设置过期时间,则重新设置过期时间
                    redisClient.expire(lockKey, lockExpire, TimeUnit.SECONDS); //设置锁的过期时间
                }
                try {
                    Thread.sleep(100); //等待0.1秒后重新尝试设置锁的值
                }catch(InterruptedException ie){
                    Thread.currentThread().interrupt();
                }
            }
        }catch (Exception e){
        }
        return null;
    }
    /**
     * 阻塞获得锁(acquireLock和releaseLock搭配使用)
     * 用法举例:
     * String lockName = "key";
     * String locaVal = acquireLock("key");
     * try{
     * // todo thing
     *
     * }catch (Exception e){
     * }finally {
     * releaseLock(lockName, locaVal);
     * }
     * @param lockName
     * @return
     */
    public String acquireLock(String lockName){
        int expireMsecs = (int)(0.75 * DEFAULT_ACQUIRY_RESOLUTION_MILLIS);
        return acquireLockWithTimeout(lockName,DEFAULT_ACQUIRY_RESOLUTION_MILLIS,expireMsecs);
    }
    /**
     * 释放锁(acquireLock和releaseLock搭配使用)
     * @param lockName
     * @param lockVal
     * @return
     */
    public boolean releaseLock(String lockName, String lockVal) {
        try{
            String lockKey = "acquireLock:" + lockName; //锁的键
            if (lockVal.equals(redisClient.get(lockKey))){ //判断锁的值是否和加锁时设置的一致,即检查进程是否仍然持有锁
                redisClient.del(lockKey);
                return true;
            }
        }catch (Exception e){
        }
        return false;
    }
    /**
     * 阻塞锁,成功则true,否则为false(tryLock和unLock搭配使用)
     * 用法举例:
     * String localName = "key";
     * if(tryLock(localName)){
     * try{
     * // todo thing
     *
     * }catch (Exception e){
     * }finally {
     * unLock(lockName);
     * }
     * }
     * @param lockName
     * @return
     * @throws Exception
     */
    public boolean tryLock(String lockName) {
        if(StringUtils.isEmpty(lockName)){
            return false;
        }
        try{
            int lockExpire = (int)(DEFAULT_ACQUIRY_RESOLUTION_MILLIS / 1000); //锁的过期时间
            if (redisClient.setNX(lockName, "1")){ //判断设置锁的值是否成功
                redisClient.expire(lockName, lockExpire, TimeUnit.SECONDS); //设置锁的过期时间
                return true;
            }
            if(redisClient.ttl(lockName)==-1){//判断如果没有设置过期时间,则重新设置过期时间
                redisClient.expire(lockName, lockExpire, TimeUnit.SECONDS); //设置锁的过期时间
            }
        }catch (Exception e){
        }
        return false;
    }
    /**
     * 删除锁(tryLock和unLock搭配使用)
     * @param lockName
     * @return
     * @throws Exception
     */
    public void unLock(String lockName) {
        if(StringUtils.isEmpty(lockName)){
            return;
        }
        try{
            if (StringUtils.isNotEmpty(redisClient.get(lockName))){
                redisClient.del(lockName);
            }
        }catch (Exception e){
        }
    }
}

二、使用Redisson实现分布式锁

https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值