1. 实现原理
1.1. 使用setnx命令
- 加锁:setnx(lock_key,val),根据返回结果若值设置成功,则key不存在,加锁成功,反之key已经存在,加锁失败。
- 解锁:del(lock_key)
- 死锁问题:线程1获取锁成功,在未执行完任务时挂掉,没有显示的释放锁,那么其它线程就永远无法获取改锁造成死锁。所以需要设置过期时间,可以利用
expire命令,但是setnx和expire命令是两个动作无法保证加锁操作原子性。还有个问题,假设线程1设置锁成功,但是任务没有执行完时锁已经超时,此时线程2抢占了锁,然后线程1执行完了进行del解锁,此时将会错误的对线程2进行锁释放。
1.2. 使用set(locl_key,val ,expire_time,NX)命令
针对setnx的问题,可以利用set(locl_key,val ,expire_time,NX)命令,该命令类似setnx并且可以设置过期时间,将val值设置成服务器节点地址加上当前线程id。如果业务代码执行时间大于过期时间,针对这个问题,我们可以让获得锁的线程开启一个守护线程,使用expire命令用来给快要过期的锁“续航”。比如,设置过期时间为60s,每当50s该key还存在时就进行续命50s。
2. 实现代码
2.1. 锁实现 RedisLock.java
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.net.InetAddress;
import java.util.Objects;
/**
* @author 陈玉林
* @desc TODO
* @date 2020/6/28 9:42
*/
public class RedisLock {
private String lockKey;
private static final int EXPIRE_TIME = 30;
private static final String SUCCESS = "OK";
private static JedisPool jedisPool = new JedisPool();
public RedisLock(String lockKey) {
this.lockKey = lockKey;
}
public boolean lock() {
SetParams setParams = new SetParams().nx().ex(EXPIRE_TIME);
final String lockVal = getLockValue();
final Jedis jedis = jedisPool.getResource();
final String lockResult = jedis.set(lockKey, lockVal, setParams);
jedis.close();
boolean result = Objects.nonNull(lockResult) && SUCCESS.equals(lockResult);
if (result) {
GuardThread guardThread = new GuardThread(lockKey, lockVal);
guardThread.setDaemon(true);
guardThread.start();
}
return result;
}
/**
* 使用主机ip+线程id作为锁的内容,在锁续命时判断
* @return String
*/
private String getLockValue() {
try {
InetAddress addr = InetAddress.getLocalHost();
return addr.getHostAddress() + Thread.currentThread().getId();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private class GuardThread extends Thread {
private String key;
private String lockVal;
public GuardThread(String key, String lockVal) {
super("GuardThread");
this.key = key;
this.lockVal = lockVal;
}
@Override
public void run() {
while (true) {
try {
//间隔1s检测一次,节约cpu资源
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
final Jedis jedis = jedisPool.getResource();
String lockVal = jedis.get(key);
if (this.lockVal.equals(lockVal)) {
final Long ttl = jedis.ttl(key);
System.out.println(String.format("线程【%s】持有锁剩余时间:%s", lockVal, ttl));
//锁寿命小于10s进行续命
if (ttl < 10) {
jedis.expire(key, EXPIRE_TIME);
System.out.println(String.format("线程【%s】续期后剩余时间:%s", lockVal, jedis.ttl(key)));
}
}
jedis.close();
}
}
}
public void unLock() {
final Jedis jedis = jedisPool.getResource();
jedis.del(lockKey);
jedis.close();
}
}
2.2. 测试
package com.maxch.test;
import java.util.concurrent.*;
/**
* @author 陈玉林
* @desc TODO
* @date 2020/6/29 14:38
*/
public class RedisLockTest {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(3);
service.execute(new MyTask());
service.execute(new MyTask());
service.execute(new MyTask());
service.shutdown();
}
private static class MyTask implements Runnable {
@Override
public void run() {
RedisLock redisLock = new RedisLock("1");
boolean lock = redisLock.lock();
if (lock) {
System.out.println(String.format("线程【%s】获取锁成功!", Thread.currentThread().getId()));
try {
//模拟业务处理时间超过锁超时时间,使用守护线程为锁续命
System.out.println(String.format("线程【%s】模拟阻塞40秒!", Thread.currentThread().getId()));
Thread.sleep(40000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
boolean unLock = redisLock.unLock();
if (!unLock) {
System.out.println(String.format("线程【%s】解锁失败!", Thread.currentThread().getId()));
}
}
} else {
System.out.println(String.format("线程【%s】获取锁失败!", Thread.currentThread().getId()));
}
}
}
}