分布式锁解决方案
Demo1
Demo2
说明:暂未实际使用做个大概记录,后面会陆续补充详细的内容和遇到的问题
一、基于数据库实现分布式锁(建一个表存方法锁,方法名做唯一性约束)
缺点:
- 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
- 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
- 这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
- 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
解决方案:
- 数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
- 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
- 非阻塞的?搞一个while循环,直到insert成功再返回成功。
- 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
二、基于缓存(Redis等)实现分布式锁
获取锁使用指令:SET resource_name my_random_value NX PX 30000
方案如下:
try{
lock = redisTemplate.opsForValue().setIfAbsent(lockKey, LOCK);
logger.info("cancelCouponCode是否获取到锁:"+lock);
if (lock) {
// TODO
redisTemplate.expire(lockKey,1, TimeUnit.MINUTES); //成功设置过期时间
return res;
}else {
logger.info("cancelCouponCode没有获取到锁,不执行任务!");
}finally{
if(lock){
redisTemplate.delete(lockKey);
logger.info("cancelCouponCode任务结束,释放锁!");
}else{
logger.info("cancelCouponCode没有获取到锁,无需释放锁!");
}
}
缺点:
- 在这种场景(主从结构)中存在明显的竞态:客户端A从master获取到锁,在master将锁同步到slave之前,master宕掉了。 slave节点被晋级为master节点,客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效!
三、基于Zookeeper实现分布式锁
节点类型:
- 持久节点 (PERSISTENT):默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在
- 持久节点顺序节点(PERSISTENT_SEQUENTIAL):在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号
- 临时节点(EPHEMERAL):当创建节点的客户端与zookeeper断开连接后,临时节点会被删除
- 临时顺序节点(EPHEMERAL_SEQUENTIAL):在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除
过程:
-
首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1
-
之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁
-
这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2;于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态
释放锁:
- 当任务完成时,Client1会显示调用删除节点Lock1的指令
- 获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除
缺点:
- 性能上可能并没有缓存服务那么高。因为每次在创建锁和释放锁的过程中,都要动态创建、销毁瞬时节点来实现锁功能。ZK中创建和删除节点只能通过Leader服务器来执行,然后将数据同不到所有的Follower机器上
- 其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的情况,由于网络抖动,客户端可ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点
四、整体比较:
- zk:有封装好的框架容易实现,有等待锁的队列大大提升抢锁的效率;但添加和删除节点效率低
- redis:set和del性能较高;但实现复杂,要考虑超时、原子性、误删等操作,而且没有等待队列,客户端自旋等锁效率低
三种方案的比较
- 从理解的难易程度角度(从低到高)
数据库 > 缓存 > Zookeeper - 从实现的复杂性角度(从低到高)
Zookeeper >= 缓存 > 数据库 - 从性能角度(从高到低)
缓存 > Zookeeper >= 数据库 - 从可靠性角度(从高到低)
Zookeeper > 缓存 > 数据库
可参考:链接