一.目的
解决多个并发请求时,相应资源的并安全性。
分布式锁根据业务场景诉求需要保证以下几点:
1.互斥性
2.高可用
3.不能无限占有锁:当锁没设置超时时间时会存在
相对应的设置了超时时间,业务操作还没执行完成,此时也可能导致锁超时释放其它并发线程进入临界点。所以这里也是一个需要考虑的点。
4.可重入性(对于锁来讲都有这个诉求)
5.安全性
锁只能被自已这个客户端删除
二.分类
redis,zk,db做为存储进行实现。
三.各类实现对比
redis | zk | db | |
性能 | 高 | 中 | 低 |
可靠性 | 低 | 中 | 高 |
实现复杂度 | 高 | 中 | 低 |
四.各类实现细节
1.redis
方案一:
主要通过setnx,来设置要加的锁及超时时间。
但这种有个问题,超时时间不够长,其它线程会进入,从而出现并发。
这种只能设置较长的超时时间。
所以可靠性redis分布式锁不是很高。
另外如果是主从的redis集群,master挂了后,对应的锁就没掉了,相应从结点升级为主结点,这时业务都能拿到锁。
解决了死锁问题,阻塞排队只能通过当前获取锁的线程重试来解决。
这个方案如果要解决安全性(锁只能由加锁的客户端删除)则可以在value中设置当前客户端标识,然后在这个客户端做释放时判断下。
伪代码:
try{
if(redis.setNx(key_1,value_客户端标识,expires)){
//业务操作
}
}finally{
if(redis.get(key_1).equals(value_客户端标识)){
//释放锁,此处极端情况存在当前客户端的锁已超时,然后这个锁由另外一个客户端持有了,然后这里由于无原子性操作所以可能会误删除其它客户端的锁
}
}
上述释放的操作不是原子性的,要保证原子需要借助lua脚本。
方案二:
方案一存在业务操作没执行完时,锁就释放的问题。
所以可以在线程加锁成功时启动一个锁来看锁是否将要到期,如果将要到期就续下锁。
这个就要redisson分布式锁框架有的功能。
方案三:
方案一方案二都是单机版锁,如果redis集群是分布式的,存在主子结点,那就有可能出现锁还没从主同步到从。此时主挂了,从升级成主,这个锁在新的主是没有的,所以导致后续进程要再获取锁。
redis redLock方案解决主挂掉后锁失效的问题,但需要n/2+1个获取到。性能会有影响。
解决了死锁问题,阻塞排队只能通过当前获取锁的线程重试来解决。
redis这种应用场景就是CP架构,即如果发生网络分区,然后网络分区无法形成集群总数n/2+1的结点时,此时整体集群是不可用的。
非公平锁。
2.zk
父结点是持久化结点,创建zk的临时结点,如果当前创建的临时结点编号是当前目录下最小的结点,则获取到锁。eg: 线程1和线程2并发获取锁,分别创建目录a(并发锁对应的key)下的临时顺序节点,线程1创建好的结点id为100,线程2创建的是101,此时线程1获取到锁。
是公平锁。
优点:
解决了死锁问题:
zk检测和客户端心跳以及客户端处理完成后自动删除临时结点从而达到锁的释放。
可重入:
可在存储临时节点时带上线程信息,获取锁的时候如果发现是当前线程则可重入。
排队:
利用zk的顺序临时节点达到阻塞排队效果。
缺点:
性能上不如redis锁,因为需要频繁创建和销毁临时结点。
极端情况下会产生2个客户端同时拿到锁:
线程1拿到锁a,此时线程1对应客户端与zk断开了,zk在心跳一段时间后认为这个客户端挂了,此时会删除临时结点。
这种情况比如客户端gc停顿了。
线程2再获取锁时就会拿到。
3.db
方案一:
使用select for update,或隐式加锁(使用update,insert,delete)
优点:
db层相对可靠性会高即保证一个锁不会被2个客户端拿到。
互斥性,无可重入,有阻塞。
实现简单,不依赖中间件。
缺点:
可能产生死锁(比较长的死锁检测时间,无超时机制)。
死锁在以下2种情况会发生:
a.2个线程互相持有对方需要的锁就会发生。
b.当前线程获取到锁,但客户端挂了,此时这个锁还是在数据库中的。
并发性能不高。
方案二:
使用一个锁记录表。每次获取锁时往锁记录表中插入唯一的记录。
优点:
可实现互斥,可重入,不能实现阻塞。
实现简单,不依赖中间件。
缺点:
如果发生持有锁的客户端挂了,此时就有问题了,这个锁永远在这个客户端这里,所以这种方案还需要扫表或其它形式做定时清理。
方案三:
乐观锁。
优:实现简单。可实现互斥。
缺:并发性能不高。