锁:解决多个线程争抢资源的情况,保证任何时候有且只有一个线程能持有资源,并且避免死锁。
关注问题:分布式、过期、宕机、代码原子性、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