一、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采用的就是依据法团准则的方案:
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;
}
}