什么是分布式锁
分布式锁是控制不同系统访问共享资源的一种锁机制,保证共享资源的可用、准确。
分布式锁需要具备什么条件
1.互斥(必须):同一时刻,分布式部署的应用中,同一个方法/资源只能被一台机器上的一个线程占用。
2.锁失效保护(必须):出现客户端断电等异常情况,锁仍然能被其他客户端获取,防止死锁。
3.可重入(可选):同一个线程在没有释放锁之前,如果想再次操作,可以直接获得锁。
4.阻塞/非阻塞(可选):若没有获取到锁,返回获取失败
5.高可用、高性能(可选):获取释放锁最好是原子操作,获取释放锁的性能要好
分布式锁的实现有哪些
1.基于数据库实现
2.基于缓存(redis,memcached)实现
3.基于zookeeper实现
redis实现方案
本篇文章我们先来讲讲redis的实现方案。
version1
lock:SETNX key value
unlock:DEL key [key ...]
指令含义参考:http://doc.redisfans.com/string/setnx.html
这是第一版最简单的方案,保证在没有出现任何异常的时候多个客户端可以使用分布式锁。
但是问题来了,如下图中所示,client2在获取锁之后突然挂了,这时候锁k将无法释放,其他client就永远拿不到这把锁了。这就是需要解决的锁失效保护问题。
version2
我们可以给锁引入一个过期时间,这样即使client2挂了,锁过期之后其他client仍然能用。
EXPIRE key seconds
但此时同样会存在一些问题:
1)误删
解决方法是每个client塞给锁的value设定为唯一的随机字符串,在删除的时候先get一把,如果还是这个字符串的话才去删。
2)过期时间需大于业务执行时间,不然任务还没搞完就被别人抢了
这个时候需要开启另外一个线程专门去刷新锁的过期时间。
version3
我们需要尽量保证获取、释放锁的操作是原子性的,才能避免极端的异常情况。
原子性地加锁
SET key uniquevalue NX EX 20
原子性地解锁
我们可以使用原生的lua脚本
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
// java
public void unlock() {
// 使用lua脚本进行原子删除操作
String checkAndDelScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else " +
"return 0 " +
"end";
jedis.eval(checkAndDelScript, 1, lockKey, lockValue);
}
version4
对于阻塞/非阻塞的要求,我们可以根据自己的业务特性,如果要阻塞,使用while循环调用;如果要非阻塞,这次调用失败,就需要增加事后的补偿机制。
对于可重入的特性,在一个线程获取到锁之后,可以把当前主机信息和线程信息保存起来,下次再获取之前先检查自己是不是当前锁的拥有者。
总结
通过redis实现分布式锁的必要可选条件之后,方案基本成型了,这个方案可以提供很好的性能。但是对于超时时间的设置,以及集群部署redis避免单点问题等还需要进一步优化。