1. 首页
  2. >
  3. 编程技术
  4. >
  5. Java

分布式锁设计方案

分布式锁设计方案

一.需求背景

1.背景

系统

背景

解决方案

单机系统

单体单机部署的系统,需要对某一个共享变量进行多线程同步访问的时候,所有的请求都会分配到当前服务器的jvm内部,然后映射为操作系统的线程进行处理。而这个共享变量只是在这个jvm内部的一块内存空间。

使用java并发处理的相关API进行互斥控制(如ReentrantLock或Synchronized)

分布式系统

由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的java API并不能提供分布式锁的能力。这就是分布式锁需要解决的问题。

使用分布式锁

2.分布式锁具备的条件

原子性:同一时刻,只能有一个机器的一个线程得到锁。 可重入性:同一对象(如线程、类)可以重复、递归调用该锁而不发生死锁。 可阻塞:在没有获得锁之前,只能阻塞等待直至获得锁。 高可用:哪怕发生程序故障、机器损坏,锁仍然能够得到被获取、被释放。 高性能:获取、释放锁的操作消耗小。

二.实现方式

1.基于数据库实现

1.1.悲观锁

利用select … where … for update 排他锁 注意: 其他附加功能与实现一基本一致,这里需要注意的是“where name=lock ”,name字段必须要走索引,否则会锁表。有些情况下,比如表不大,mysql优化器会不走这个索引,导致锁表问题。

1.2.乐观锁

所谓乐观锁与前边最大区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到。我们的抢购、秒杀就是用了这种实现以防止超卖。通过增加递增的版本号字段实现乐观锁。

2.基于redis实现

2.1.使用命令

命令1:【SETNX key val】当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。 命令2:【expire key timeout】为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。 命令3:【delete key】删除key。

2.1.实现思想

(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。 (2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。 (3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

3.基于zookeeper实现

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。

基于ZooKeeper实现分布式锁的步骤如下: (1)创建一个目录mylock; (2)线程A想获取锁就在mylock目录下创建临时顺序节点; (3)获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; (4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; (5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

这里推荐一个Apache的开源库Curator,它是一个ZooKeeper客户端,Curator提供的InterProcessMutex是分布式锁的实现,acquire方法用于获取锁,release方法用于释放锁。

优点:具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。 缺点:因为需要频繁的创建和删除节点,性能上不如Redis方式。

三.方案比较

1.数据库分布式锁实现 缺点:1.db操作性能较差,并且有锁表的风险。2.非阻塞操作失败后,需要轮询,占用cpu资源。3.长时间不commit或者长时间轮询,可能会占用较多连接资源。 总结:database分布式锁由于数据库本身的限制:性能不高且不满足高可用(即是存在备份,也会导致数据不一致),因此,工作中很难见到真正使用数据库来作为分布式锁的解决方案,这里使用数据库实现主要是为了理解分布式锁的实现原理。

2.Redis(缓存)分布式锁实现 缺点:1.锁删除失败 过期时间不好控制。2.非阻塞,操作失败后,需要轮询,占用cpu资源。 总结:redis分布式锁(非redlock)由于redis自己的高性能原因,会有很好的性能,但是极端情况下会存在两个客户端获取锁(可以通过监控leader故障和运维措施来缓解和解决该问题),因此适用于高并发的场景。

3.ZK分布式锁实现 缺点:性能不如redis实现,主要原因是写操作(获取锁释放锁)都需要在Leader上执行,然后同步到follower。 总结:zookeeper分布式锁实现简单,集群自己来保证数据一致性,但是会存在建立无用节点且多节点之间需要同步数据的问题,因此一般适合于并发量小的场景使用,例如定时任务的运行等。

从理解的难易程度角度(从低到高) 数据库 >redis 缓存 > Zookeeper 从实现的复杂性角度(从低到高) Zookeeper >= redis缓存 > 数据库 从性能角度(从高到低) redis缓存 > Zookeeper >= 数据库 从可靠性角度(从高到低) Zookeeper > redis缓存 > 数据库