1.分布式锁
- 单应用中使用锁:(单进程多线程)
synchronize、ReentrantLock
- 分布式应用中使用锁:(多进程多线程)
分布式锁是控制分布式系统之间同步访问共享资源的一种方式
2.分布式锁的实现方式
- 基于数据库的乐观锁实现分布式锁
- 基于
zookeeper
临时节点的分布式锁 - 基于
Redis
的分布式锁
3.分布式锁的注意事项
- 互斥性:在任意时刻,只有一个客户端能持有锁
- 同一性:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
- 可重入性:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3.redis分布式锁解决的问题
保证setnx()和expire的 原子性,从而保证redis的原子性。 wiki上解释说:所谓原子性,就是执行过程中不会被其他的线程所影响。也就是说任务一旦开始执行,执行过程中不会出现cpu的切换,直到任务执行结束。
setnx()可以保证一个锁只被一个客户端持有,使用完成,再用del释放。但是如果setnx()执行完后,
出现异常没有调用del,就会导致锁无法释放的问题。所以可以添加一个expire超时时间,
setnx()后即使出现异常,超时后,也可以自动的释放锁。但是setnx和expire不是原子操作,
如果这两个一起执行就不会出现问题了,因为expire依赖setnx的执行结果,如果setnx没有抢到锁,
不能执行expire操作。
4.解决方案
方案一: redis2.8中加入了set的扩展参数,使setnx和expire可以一起执行,解决了这个问题:
/**
* 使用redis的set命令实现获取分布式锁
* @param lockKey 可以就是锁
* @param requestId 请求ID,保证同一性
* @param expireTime 过期时间,避免死锁
* @return
*/
public static boolean getLock(String lockKey,String requestId,int expireTime) {
//NX:保证互斥性
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
if("OK".equals(result)) {
return true;
}
return false;
}
方案二:redis+lua脚本实现(推荐)
public static boolean releaseLock(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 (result.equals(1L)) {
return true;
}
return false;
}
5.不可用场景
超时问题:如果redis加了锁后,在加锁和释放锁之间的逻辑处理太长,以至于超出了锁的超时时间,是得第一次操作还没有完成,第二次操作就获取到了锁,不能保证第一次操作的原子性。