redis-分布式锁
redis实现分布式锁四大条件:
- 互斥:key
- 不能死锁:过期时间
- 解铃还须系铃人:value存储 uuid+threadId
- 容错: 关于容错,redis集群环境下是没办法保证分布式锁的容错性的,具体原因如下:
如图所示:一个三主三从的redis集群,当客户端发送写命令时,master会直接返回给用户写成功,并不会等master把命令复制到slave上再返回给用户,也就是说redis的复制是异步复制的,这会导致一个问题:
试想当master把锁信息写入成功,返回给用户了,此时master挂掉了,slvae变成master,但是由于slave没有锁信息,导致第二个线程进来加锁成功。
解决方案: RedLock, 但其实我个人觉得这种方案很鸡肋,为了一个锁我得单独开好几个独立的redis服务,其实很得不偿失,说白了内部只不过维护了一个过半机制而已,做分布式锁最好还是用Zk,为什么?原因如下:
- zk提供了临时节点,不用像redis还要设置过期时间,设置完后极端情况业务可能还要看门狗续期,麻不麻烦啊
- zk是一个强一致性的注册中心,他的同步机制是必须有半数以上的机器同步成功才会响应给用户,那如果master挂掉了,重新选主采用过半机制选主也一定会选出一个拥有最新数据的master,另外过半机制还防脑裂,一举多得。
说了这么多redis的不足,那为啥还要学redis的分布式锁呢?我也不知道。。。
talk is cheap,show me the code
方案一:
set k v ex 30 nx
k就是业务标识,v是uuid+threadId(唯一就好) ,过期时间30s,nx:不存在才设置
pom<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.10.0-m1</version>
</dependency> 常量private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";//NX 不存在才会set
private static final String SET_WITH_EXPIRE_TIME = "PX";//EX:s PX:ms 加锁public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
//setnx指令没办法续期,需要使用lua脚本实现分布式锁进行续期操作
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
//启动看门狗
Thread t = new Thread(new RedisLock.WatchDog((long)expireTime+System.currentTimeMillis()-2000,lockKey));
t.setDaemon(true);
t.start();
return true;
}
return false;
} 解锁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;
} 看门狗static class WatchDog implements Runnable{
private String name;
private Long future;
public WatchDog(Long future, String name) {
this.name = name;
this.future = future;
}
@Override
public void run() {
System.out.println("启动看门狗,lock="+name);
while(true){
long cur = System.currentTimeMillis();
//如果到过期时间节点锁还没有被释放,给锁续期10s
if(cur>=future){
//判断锁存不存在,如果存在就续期10s
StringBuilder script = new StringBuilder();
script.append("if redis.call('exists',KEYS[1]) ==1 then ")
.append("redis.call('expire',KEYS[1],10000);")
.append("return 1;")
.append("end;")
.append("return 0;");
long ret = (long) JedisTemplate.operate().eval(script.toString(),1,name);
if(1==ret){
future = System.currentTimeMillis()+10000-2000;
System.out.println("续期10s,lock="+name);
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} 可重入锁public class ReentrantRedisLock {
//<key,重入次数>
private final ThreadLocal<Map<String,Integer>> locks = new MapThreadLocal();
private static class MapThreadLocal extends ThreadLocal<Map<String, Integer>> {
@Override
protected Map<String, Integer> initialValue() {
return new HashMap();
}
}
private Jedis jedis;
private int expireTime;//过期时间ms
private String reqId;
public ReentrantRedisLock(Jedis jedis,int expireTime,String reqId){
this.jedis = jedis;
this.expireTime = expireTime;
this.reqId = reqId;
}
public boolean lock(String key){
Map<String,Integer> refs = locks.get();
Integer refCount = refs.get(key);
if(null!=refCount){
refs.put(key,refCount+1);
return true;
}
boolean ok = RedisLock.tryGetDistributedLock(jedis,key,reqId,expireTime);
if(!ok)
return false;
refs.put(key,1);
return true;
}
public boolean unlock(String key){
Map<String,Integer> refs = locks.get();
Integer refCount = refs.get(key);
if(null==refCount){
return false;
}
refCount-=1;
if(refCount>0){
refs.put(key,refCount);
}else{
refs.remove(key);
RedisLock.releaseDistributedLock(jedis,key,reqId);
}
return true;
}
} 方案二:
全部使用lua脚本,锁使用hash
hset key field value
hset key uuid 重入次数
过期时间采用pexpire指令
pexpire key 时间ms@Slf4j
public class DistributeLock {
public static final String READ_LOCK_PREFIX = "distribute_lock_";
public static String getLockKey(String name){
return READ_LOCK_PREFIX + name;
}
public static String getUUID(){
return JedisConnectPoll.JEDIS_CONNECT_POLL_UUID.toString() + ":" + Thread.currentThread().getId();
}
public void lock(String name){
tryLock(name, Long.MAX_VALUE, 30, TimeUnit.SECONDS);
}
public void lock(String name, long leaseTime, TimeUnit unit){
tryLock(name, Long.MAX_VALUE, leaseTime, unit);
}
/**
*
* @param name 业务名称,加锁唯一key
* @param waitTime 加锁过程最多消耗的时间,超过这个时间失败
* @param leaseTime 过期时间
* @param unit 时间单位
* @return
*/
public boolean tryLock(String name, long waitTime, long leaseTime, TimeUnit unit){
Long waitUntilTime = unit.toMillis(waitTime) + System.currentTimeMillis();
if(waitUntilTime < 0){
waitUntilTime = Long.MAX_VALUE;
}
Long leastTimeLong = unit.toMillis(leaseTime);
StringBuilder script = new StringBuilder();
//如果锁不存在,加锁,设置过期时间,设置重入次数1
//如果锁存在,判断uuid是不是自己,是的话设置过期时间,设置重入次数+1
//否则,加锁失败
script.append("if redis.call('exists',KEYS[1]) ==0 then ")
.append("redis.call('hset',KEYS[1],ARGV[2],1);")
.append("redis.call('pexpire',KEYS[1],ARGV[1]);")
.append("return -1;")//代表加锁成功
.append("end;")
.append("if redis.call('hexists',KEYS[1],ARGV[2])==1 then ")
.append("redis.call('hincrby',KEYS[1],ARGV[2],1);")
.append("redis.call('pexpire',KEYS[1],ARGV[1]);")
.append("return -2;")//代表重入成功
.append("end;")
//没有获取锁,返回过期时间
.append("return redis.call('pttl',KEYS[1]);");
for(;;){
if(System.currentTimeMillis() > waitUntilTime){
log.info("线程"+Thread.currentThread()+"在指定时间获锁失败,lock="+getLockKey(name));
return false;
}
Long res = (Long) JedisTemplate.operate().eval(script.toString(),1,getLockKey(name),leastTimeLong.toString(),getUUID());
if(res.equals(-1L)){
log.info("线程"+Thread.currentThread()+"获锁成功,lock="+getLockKey(name));
//启动看门狗
Thread t = new Thread(new WatchDog(leastTimeLong+System.currentTimeMillis()-2000,name,getUUID()));
t.setDaemon(true);
t.start();
break;
}else if(res.equals(-2L)){
log.info("线程"+Thread.currentThread()+"获锁成功-重入获锁,lock="+getLockKey(name));
break;
}else if(res>0){
// log.info("线程"+Thread.currentThread()+"休眠一会等待别人释放锁,lock="+getLockKey(name));
try {
Thread.sleep(1);
}catch (InterruptedException e) {
log.info("线程"+Thread.currentThread()+"休眠一会等待别人释放锁-出现异常,lock="+getLockKey(name));
e.printStackTrace();
}
}
}
return true;
}
/**
* 释放锁逻辑
* 根据名字+uuid找count,如果找到了,-1,如果<=0 就释放锁
* @param name
*/
public void unlock(String name){
StringBuilder script = new StringBuilder();
script.append("local count = redis.call('hget',KEYS[1],KEYS[2]);")
.append("if count then ")
.append("local delCount = redis.call('hincrby', KEYS[1], KEYS[2], -1); ")
.append("if tonumber(delCount)<=0 then ")
.append("redis.call('HDEL',KEYS[1],KEYS[2]);")
.append("end;")
.append("else ")
.append("redis.call('HDEL',KEYS[1],KEYS[2]);")
.append("end;")
.append("return;");
JedisTemplate.operate().eval(script.toString(),2,getLockKey(name),getUUID());
log.info("线程"+Thread.currentThread()+"释放锁成功,lock="+getLockKey(name));
}
class WatchDog implements Runnable{
private String name;
private Long future;
private String uuid;
public WatchDog(Long future, String name,String uuid) {
this.name = name;
this.future = future;
this.uuid = uuid;
}
@Override
public void run() {
System.out.println("启动看门狗,lock="+getLockKey(name));
while(true){
long cur = System.currentTimeMillis();
//如果到过期时间节点锁还没有被释放,给锁续期10s
if(cur>=future){
//判断锁存不存在,如果存在就续期10s
StringBuilder script = new StringBuilder();
script.append("if redis.call('exists',KEYS[1]) ==1 then ")
.append("if redis.call('hexists',KEYS[1],ARGV[1]) == 1 then ")
.append("redis.call('pexpire',KEYS[1],10000);")
.append("return 1;")
.append("end;")
.append("end;")
.append("return 0;");
long ret = (long) JedisTemplate.operate().eval(script.toString(),1,getLockKey(name),uuid);
if(1==ret){
future = System.currentTimeMillis()+10000-2000;
log.info("给线程"+Thread.currentThread()+"续期10s,lock="+getLockKey(name));
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
} 看门狗测试:
|