基于redis实现分布式锁
缓存锁
开始接触缓存服务,知道很多应用都把缓存作为分布式锁,比如redis。使用缓存作为分布式锁,性能非常强劲,在一些不错的硬件上,redis可以每秒执行10w次,内网延迟不超过1ms,足够满足绝大部分应用的锁定需求。
redis锁的原理是利用setnx命令,即只有在某个key不存在情况才能set成功该key,这样就达到了多个进程并发去set同一个key,只有一个进程能set成功。
redis自带的expire功能可以不需要应用主动去删除锁。而且从 Redis 2.6.12 版本开始,redis的set命令直接直接设置NX和EX属性,NX即附带了setnx数据,key存在就无法插入,EX是过期属性,可以设置过期时间。这样一个命令就能原子的完成加锁和设置过期时间。
缓存锁优势是性能出色,劣势就是由于数据在内存中,一旦缓存服务宕机,锁数据就丢失了。像redis自带复制功能,可以对数据可靠性有一定的保证,但是由于复制也是异步完成的,因此依然可能出现master节点写入锁数据而未同步到slave节点的时候宕机,锁数据丢失问题。
代码实现
public class RedisLock {
private static Random random = new Random();
/**
* Lock key path.
*/
private String lockKey;
/**
* 锁超时时间,防止线程在入锁以后,无限的执行等待
*/
private int expireMsecs = 60 * 1000;
/**
* 锁等待时间,防止线程饥饿
*/
private int timeoutMsecs = 10 * 1000;
private volatile boolean locked = false;
/**
* Detailed constructor with default acquire timeout 10000 msecs and lock
* expiration of 60000 msecs.
*
* @param lockKey
* lock key (ex. account:1, ...)
*/
public RedisLock(String lockKey) {
this.lockKey = lockKey + "_lock";
}
/**
* @return lock key
*/
public String getLockKey() {
return lockKey;
}
/**
* 获得 lock. 实现思路: 主要是使用了redis 的setnx命令,缓存了锁. reids缓存的key是锁的key,所有的共享,
* value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间) 执行过程:
* 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
* 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
*
* @return true if lock is acquired, false acquire timeouted
* @throws InterruptedException
* in case of thread interruption
*/
public synchronized boolean tryLock() throws InterruptedException {
int timeout = timeoutMsecs;
while (timeout >= 0) {
long expires = System.currentTimeMillis() + expireMsecs + 1;
String expiresStr = String.valueOf(expires); // 锁到期时间
if (SingleJedisUtil.getInstance().STRINGS.setnx(lockKey, expiresStr) == 1) {
//如果执行完任务,恰好解锁失败,设置过期时间防止锁不释放
SingleJedisUtil.getInstance().KEYS.expired(lockKey, 60);//过期时间一分钟
locked = true;
return true;
}
String currentValueStr = SingleJedisUtil.getInstance().STRINGS.get(lockKey); // redis里的时间
// 判断是否为空,不为空的情况下,如果被其他线程设置了值,则第二个条件判断是过不去的
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// lock is expired
String oldValueStr = SingleJedisUtil.getInstance().STRINGS.getSet(lockKey, expiresStr);
// 获取上一个锁到期时间,并设置现在的锁到期时间,
// 只有一个线程才能获取上一个线上的设置时间,因为jedis.getSet是同步的
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
// 防止误删(覆盖,因为key是相同的)了他人的锁——这里达不到效果,这里值会被覆盖,但是因为什么相差了很少的时间,所以可以接受
// [分布式的情况下]:如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
// lock acquired
locked = true;
return true;
}
}
int sleepTime = random.nextInt(200);
timeout -= sleepTime;
/*
* 延迟100 毫秒, 这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
* 只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
* 使用随机的等待时间可以一定程度上保证公平性
*/
Thread.sleep(sleepTime);
}
return false;
}
/**
* Acqurired lock release..
*/
public synchronized void unlock() {
if (locked) {
SingleJedisUtil.getInstance().KEYS.del(lockKey);
locked = false;
}
}
这里解释下while (timeout >= 0) ,如果当前线程拿不到锁,进行自旋,不断尝试获取锁,取锁时间大于超时等待时间,直接返回false,获取锁失败。
总结
根据业务的场景、现状以及已经依赖的服务,应用可以使用不同分布式锁实现。不过我个人觉得,如果需要最可靠的分布式锁,还是使用zookeeper会更可靠些。curator-recipes库封装的分布式锁,java应用也可以直接使用。而且如果开始依赖zookeeper,那么zookeeper不仅仅提供了分布式锁功能,选主、服务注册与发现、保存元数据信息等功能都能依赖zookeeper,这让zookeeper不会那么闲置, 当然如果项目没有依赖zookeeper,还是用redis吧。