Redis分布式锁(二)基于Redis的分布式锁

一、redis锁

1、思路:利用set nx ex获取锁,并设置过期时间,保存线程标识;释放锁时先判断线程标识是否与自己一致,一致则删除

2、特性:利用set nx满足互斥性;利用set ex保证故障时锁依然能释放,避免死锁,提高安全性;利用Redis集群保证高可用和高并发特性

3、redis实现加锁的几种命令:redis能用的的加锁命令分表是INCR、SETNX、SET

(1)INCR:这种加锁的思路是, key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一。其它用户在执行 INCR 操作进行加一时,如果返回的数大于 1 ,说明这个锁正在被使用当中。

1、 客户端A请求服务器获取key的值为1表示获取了锁 
2、 客户端B也去请求服务器获取key的值为2表示获取锁失败
3、 客户端A执行代码完成,删除锁
4、 客户端B在等待一段时间后在去请求的时候获取key的值为1表示获取锁成功
5、 客户端B执行代码完成,删除锁

$redis->incr($key);
$redis->expire($key, $ttl); //设置生成时间为1秒

 (2)SETNX:这种加锁的思路是,如果 key 不存在,将 key 设置为 value;如果 key 已存在,则 SETNX 不做任何动作

 1、 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
 2、 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
 3、 客户端A执行代码完成,删除锁
 4、 客户端B在等待一段时间后在去请求设置key的值,设置成功
 5、 客户端B执行代码完成,删除锁   


$redis->setNX($key, $value);
$redis->expire($key, $ttl);

(3)SET:上面两种方法都需要设置 key 过期,这是防止意外情况锁无法释放。但是借助 Expire 来设置就不是原子性操作了,所以官方就引用了另外一个,使用 SET 命令本身已经从版本 2.6.12 开始包含了设置过期时间的功能。

1、 客户端A请求服务器设置key的值,如果设置成功就表示加锁成功
2、 客户端B也去请求服务器设置key的值,如果返回失败,那么就代表加锁失败
3、 客户端A执行代码完成,删除锁
4、 客户端B在等待一段时间后在去请求设置key的值,设置成功
5、 客户端B执行代码完成,删除锁


$redis->set($key, $value, array('nx', 'ex' => $ttl)); //ex表示秒

redis自身也存在单节点和redis分布式,下面看下redis单节点和分布式模式下锁的应用:

二、redis单节点锁:

1、过程:

(1)使用SETNX实现排他性锁

(2)使用超时限制特性来避免死锁

(3)释放锁的时候需要进行检查来避免误释放别的进程的锁

三、redis分布式锁:

1、介绍:RedLock是Redis之父Salvatore Sanfilippo提出来的基于多个Redis实例的分布式锁的实现方案。其核心思想就在于使用多个Redis冗余实例来避免单Redis实例的不可靠性。RedLock采用的就是依据法团准则的方案:

Redis分布式锁(二)基于Redis的分布式锁_第1张图片

2、redis分布式锁的过程分析:redis分布式锁就几个方法:

① setnx(key,value) 返回boolean 1为获取锁 0为没获取锁

② expire() 设置锁的有效时间

③ getSet(key,value) 获取锁当前key对应的锁的有效时间

④ deleteKey() 删除锁

(1)setnx(lockkey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转向 2;

(2)get(lockkey) 获取值 oldExpireTime ,并将这个 value 值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 3;

(3)计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime。判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前 getset 设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
(4)在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。

3、可靠性:为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

(1)互斥性。在任意时刻,只有一个客户端能持有锁。

(2)不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

(3)具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。

(4)加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

4、实现分布式锁:主要就两个方法:

(1)getlock() 获取锁方法

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    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;
    }
}

 可以看到加锁就一行代码 jedis.set(String key, String value, String nxxx, String expx, int time) ,这里requestId可以使用UUID.randomUUID().toString()方法生成。这样就知道这把锁是哪个请求加的了,在解锁的时候可以有依据。当然requestId也可以替换成其他标志性的业务字段。

错误写法:

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {

    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。
        jedis.expire(lockKey, expireTime);
    }
}

(2)releaselock()释放锁方法:

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @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;
    }
}

第一行代码是一个简单的Lua脚本代码,第二行代码将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。使用eval()执行Lua语言是确保上述操作是原子性的,官网对eval命令的部分解释:简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

错误写法

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
        
    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此时,这把锁突然不是这个客户端的,则会误解锁。比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。
        jedis.del(lockKey);
    }
}

6、demo:

/**
*异步导出
*/
public void runTask(ExportRecord record) {
  logger.info("--------导出XXX任务开始 -------------");
  boolean lock = fileExportService.lockTask(record);
        if (!lock) {
            return;
        }
  //...todo...导出业务逻辑
  fileExportService.finishTask(record);
  logger.info("--------导出XXX任务结束 -------------");
}


FileExportCommonServiceImpl{
    /**
     * redis锁  最长时间  /s
     */
    public static final Integer REDIS_LOCK_DOWNLOAD_MAX_TIME = 60 * 2;

    @Override
    public boolean lockTask(ExportRecord record) {
        boolean lock = redisClient.getLock(record.getId(), record.getId(), REDIS_LOCK_DOWNLOAD_MAX_TIME);
        if (!lock) {
            logger.error("获取锁失败,taskId:{}", record.getId());
            return lock;
        }
        //更新record任务状态为进行中
        //todo
        return lock;
    }

    @Override
    public boolean finishTask(ExportRecord record) {
        record.setTaskEndTime(new Date());
        //更新task任务结束时间
        //todo
        return redisClient.releaseLock(record.getId(), record.getId());
    }
}



RedisClient{


 /**
     * 获取分布式锁
     *
     * @param lockKey
     *            key为锁
     * @param requestId
     *            加锁请求
     * @param expireTime
     *            key的过期时间
     * @return
     */
  public  boolean getLock(String lockKey, String requestId, int expireTime) {
        boolean ret = false;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            if (jedis == null) {
                return ret;
            }
            String status = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
            if (SUCCESS_OK.equalsIgnoreCase(status)) {
                ret = true;
            }
        } catch (Exception e) {
            logger.error("redis 获取分布式锁 出错", e);
            jedisPool.returnBrokenResource(jedis);
        } finally {
            if (null != jedis) {
                jedisPool.returnResource(jedis);
            }
        }
        return ret;
    }



/**
     * 释放分布式锁
     *
     * @param lockKey
     * @param requestId
     */
    public  boolean releaseLock(String lockKey, String requestId) {
        boolean ret = false;
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            if (jedis == null) {
                return ret;
            }
			/*
			 * 其他请求误解锁问题 if(requestId.equals(jedis.get(lockKey))) { jedis.del(lockKey); }
			 */

            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object status = jedis.eval(script, Collections.singletonList(lockKey),
                    Collections.singletonList(requestId));
            if (SUCCESS_STATUS_LONG.equals(status)) {
                ret = true;
            }
        } catch (Exception e) {
            logger.error("redis 释放分布式锁 出错", e);
            jedisPool.returnBrokenResource(jedis);
        } finally {
            if (null != jedis) {
                jedisPool.returnResource(jedis);
            }
        }
        return ret;
    }
}

你可能感兴趣的:(redis,redis,分布式,数据库)