1. 基于setnx命令实现
- 加锁:setnx(lock_key,val),根据返回结果若值设置成功,则key不存在,加锁成功,反之key已经存在,加锁失败。
- 解锁:del(lock_key)
- 死锁问题:线程1获取锁成功,在未执行完任务时挂掉,没有显示的释放锁,那么其它线程就永远无法获取改锁造成死锁。所以需要设置过期时间,可以利用
expire命令,但是setnx和expire命令是两个动作无法保证加锁操作原子性。还有个问题,假设线程1设置锁成功,但是任务没有执行完时锁已经超时,此时线程2抢占了锁,然后线程1执行完了进行del解锁,此时将会错误的对线程2进行锁释放。
2. set(locl_key,val ,expire_time,NX)
针对setnx的问题,可以利用set(locl_key,val ,expire_time,NX)命令,该命令类似setnx并且可以设置过期时间,同时让获得锁的线程开启一个守护线程,使用expire命令用来给快要过期的锁“续航”。比如,设置过期时间为60s,每当50s该key还存在时就进行续命50s。
实现代码
import redis.clients.jedis.*;
import redis.clients.jedis.params.SetParams;
import java.net.InetAddress;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @author 陈玉林
* @desc TODO
* @date 2020/6/28 9:42
*/
public class RedisLock {
private final String lockKey;
private static final int EXPIRE_TIME = 20;
private static final String SUCCESS = "OK";
private final ShardedJedisPool jedisPool;
{
// 配置Redis信息
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(100);
config.setMaxIdle(50);
config.setMaxWaitMillis(3000);
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
JedisShardInfo jedisShardInfo1 = new JedisShardInfo("localhost");
jedisShardInfo1.setPassword("123456");
List<JedisShardInfo> list = new LinkedList<>();
list.add(jedisShardInfo1);
jedisPool = new ShardedJedisPool(config, list);
}
public RedisLock(String lockKey) {
this.lockKey = lockKey;
}
public boolean lock() {
SetParams setParams = new SetParams().nx().ex(EXPIRE_TIME);
final String lockVal = getLockValue();
final ShardedJedis 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 final String key;
private final 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资源
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
final ShardedJedis jedis = jedisPool.getResource();
String lockVal = jedis.get(key);
if (this.lockVal.equals(lockVal)) {
final Long ttl = jedis.ttl(key);
System.out.printf("节点【%s】持有锁剩余时间:%s%n", lockVal, ttl);
//锁寿命小于10s进行续命
if (ttl < 10) {
jedis.expire(key, EXPIRE_TIME);
System.out.printf("节点【%s】续期后剩余时间:%s%n", lockVal, jedis.ttl(key));
}
}
jedis.close();
}
}
}
public void unLock() {
final ShardedJedis jedis = jedisPool.getResource();
jedis.del(lockKey);
}
}
测试代码
import java.util.concurrent.TimeUnit;
public class Test {
public static void main(String[] args) {
RedisLock lock = new RedisLock("myLock");
new Thread(new MyTask(lock), "张三").start();
new Thread(new MyTask(lock), "李四").start();
}
public static void readStore() {
RedisLock lock = new RedisLock("myLock");
new Thread(new MyTask(lock), "张三").start();
new Thread(new MyTask(lock), "李四").start();
}
private static class MyTask implements Runnable {
final RedisLock lock;
public MyTask(RedisLock lock) {
this.lock = lock;
}
@Override
public void run() {
boolean locked = this.lock.lock();
if (locked) {
//模拟读取数据库
System.out.println(Thread.currentThread().getName() + ",正在读取中...");
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.lock.unLock();
System.out.println(Thread.currentThread().getName() + ",完成数据读取,释放锁");
}else{
// System.out.println(Thread.currentThread().getName() + ",需要排队");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.run();
}
}
}
}