# Distributed-Lock-Service **Repository Path**: westudyclub/Distributed-Lock-Service ## Basic Information - **Project Name**: Distributed-Lock-Service - **Description**: 分布式锁服务 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-08-18 - **Last Updated**: 2021-08-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 分布式锁 ## 理论 ### 锁的本质 - 锁是一种可以保证在竟态情况下,可以让一个以上线程访问排他性资源时能够保证原子性,有序性的一种机制。简单点说,锁就是一种通过互斥来保证原子性的一种机制。当一个线程持有一个锁时,其他线程申请获取锁将发生阻塞等待,直到前一个线程释放了持有的锁时,才能在等待线程中只有一个线程能成功获取锁。 - 什么是临界区? 临界区就是锁作用的范围,也就是锁开始的代码行到锁结束的代码行,在此中间户的所有代码行称为临界区。在临界区中只能有一个线程能在此区间中执行命令。 所以可以简单理解为锁将所线程并发,甚至并行对共享资源的访问,转换成了穿行的排队访问。从更大范围来看多线程中带锁的业务的场景实际变成了部分逻辑鱼贯而行的情况。 ### 语言级的锁 在Java开发语言中存在两大类锁,一种被称为Lock,另一种被称为Compare And Swap(简称CAS)。前者通过编程语言实现的锁;后者通过计算机硬件实现的一种机制,也就是在不满足变更条件时CPU进行空转。不管哪一种锁都是将多线程的操作从并发执行转变为串行执行。不考虑多线程并发执行的额外性能损耗的情况下,锁就是将高性能执行(本应该并发执行)的指令在临界区里转变为低效率执行(实际变成串行执行)。从某种意义上就是以时间换取数据的一致性、顺序性、可见性。这两者的性能不能放在一起草率的说谁的性能损耗大,其情况实际是比较复杂的。 ### 数据库的锁 在数据库领域也存在两大类锁,一种是悲观锁,另一种是乐观锁。悲观锁是数据库系统本身提供的一种具有阻塞、排他的锁机制,例如X锁(排他锁)、S锁(共享锁)等。乐观锁是一种使用层面上的锁,它的本质就是提供一种比较,当比较条件满足时就更新,也就是说满足check-then-act的模式,他的原子性由数据库系统来保证,例如: ```sql update tb_usr_inf set name='张三',version=2 --2是act部分 where uid=1 and version=1 --1是check部分 ``` 操作是否成功可利用数据库提供的变更条数来判断,如果为0则表示操作失败,如果大于0则表示成功。 ## 分布式锁的实现 语言级的锁只能对单机的CPU来说,不具有支持分布式系统的能力。数据库锁虽然能在使用层面上,具有支持分布式系统的能力,但是其自身锁实现机制的复杂导致的低效率,从而导致业务系统低吞吐,其次乐观锁虽然能在部分场景下的适用,但依然存在适用的局限性。所以必须寻找一种既高效率,又能满足分布式系统需求的锁。 分布式锁应具备以下特点 - 互斥性:在任意时刻,只有一个客户端(进程)能持有锁 - 安全性:避免死锁情况,当一个客户端在持有锁期间内,由于意外崩溃而导致锁未能主动解锁,其持有的锁也能够被正确释放,并保证后续其它客户端也能加锁 - 可用性:分布式锁需要有一定的高可用能力,当提供锁的服务节点故障(宕机)时不影响服务运行,避免单点风险,如Redis的集群模式、哨兵模式,ETCD/zookeeper的集群选主能力等保证HA,保证自身持有的数据与故障节点一致。 - 对称性:对同一个锁,加锁和解锁必须是同一个进程,即不能把其他进程持有的锁给释放了,这又称为锁的可重入性。 ### 不可重入锁 不可重入是最简单的锁,从字面上可理解到,持有锁的当前线程是不能再次进入临界区的,第二次进入时只能阻塞等待。如果仅仅实现这样的锁其实很简单,只要实现一种令牌机制就可以,简单来说就是某个系统能够将有限个令牌颁发给1个以上的线程,只有持有令牌的线程才能进入临界区,在临界区里可以进行任何不破坏令牌机制的操作。当出临界区时,将令牌归还给系统,系统将回收的令牌重新颁发给等待的其他线程。 ```java package club.westudy.distributed.lock; public interface DistributedLockService{ /** * 获取锁 */ void obtain(); /** * 释放锁 */ void release(); } public class NonReerentDistributedLockService implements DistributedLockService{ //令牌池 List tokenPool; //...省略方法实现 } ``` 这样的定义看起来,如果定义的令牌池支持无限个,就能支持高并发了。然而,这样的想法其实是草率的,没有意义。因为令牌是由锁的服务确定的,具有随机性,而大量的线程进入时并未定义竞争关系,这样就导致N个线程进入,进入的操作对某个或者某些数据共享造成意外的变更,从而破坏了原子性。如果竞争的令牌特征是外部传入的,凡是想获取符合这个令牌特征的线程都要排队。 ### 简单的分布式不可重入锁 在version1的基础上进行改进,支持传入竞争特征。 ```java package club.westudy.distributed.lock; public interface DistributedLockService{ /** * 获取锁 * @param feature 特征 */ void obtain(String feature); /** * 释放锁 * @param feature 特征 */ void release(String feature); } public class NonReerentDistributedLockService implements DistributedLockService{ //令牌池 List tokenPool; //...省略方法实现 } ``` 在分布式所使用上,分为编程式和声明式。编程式是原生Java代码方法或者Spring Bean注入方式调用使用。声明式使用通过切面方式实现调用。 ### 编程式分布式不可重入锁 ```java package club.westudy.distributed.lock; public interface DistributedLockService{ /** * 初始化分布式锁 */ void init(); /** * 立即强制关闭分布式锁服务 */ void shutdownNow(); /** * 关闭分布式锁服务 * 1.当实现支持非强制关闭时,该方法执行后将发生阻塞 * 2.当实现支持强制关闭时,该方法执行后将释放资源,并强制杀掉持有锁的线程 * @param force 是否强制关闭锁服务 */ void shutdown(boolean force); /** * 获取锁 * @param feature 锁特征 * @param maxLeaseTimeMs 持有锁最长时间(单位毫秒) * @param tryLockWaitTimeMs 等待锁超时时间(单位毫秒) */ void obtain(String feature, long maxLeaseTimeMs, long tryLockWaitTimeMs); /** * 释放某个特征的锁,此方法用于编程式方式使用分布式锁服务 * @param feature 特征 */ void release(String feature); } ``` ## 声明式分布式不可重入锁 在编程式服务的基础上增加具有能够执行临界区上下文的功能,更容易实现保证临界区一致性。 ```java package club.westudy.distributed.lock; import java.util.Date; public interface DistributedLockService { /** * 初始化分布式锁 */ void init(); /** * 立即强制关闭分布式锁服务 */ void shutdownNow(); /** * 关闭分布式锁服务 * 1.当实现支持非强制关闭时,该方法执行后将发生阻塞 * 2.当实现支持强制关闭时,该方法执行后将释放资源,并强制杀掉持有锁的线程 * * @param force 是否强制关闭锁服务 */ void shutdown(boolean force); /** * 获取锁 * * @param feature 锁特征 * @param maxLeaseTimeMs 持有锁最长时间(单位毫秒) * @param tryLockWaitTimeMs 等待锁超时时间(单位毫秒) */ void obtain(String feature, long maxLeaseTimeMs, long tryLockWaitTimeMs); /** * 释放某个特征的锁,此方法用于编程式方式使用分布式锁服务 * * @param feature 特征 */ void release(String feature); /** * 临界区执行体,用于封装需要执行的业务代码 * 1.正常执行不发生异常的情况下,自动解锁 * 2.执行发生异常时,也会自动解锁 */ interface CriticalRegionRunnable { /** * 执行临界区代码之前的前置处理 * * @param ctx 临界区上下文 */ void before(CriticalRegionContext ctx); /** * 执行业务代码 */ void run(); /** * 执行临界区代码之前的前置处理 * * @param ctx 临界区上下文 */ void after(CriticalRegionContext ctx); } /** * 临界区上下文,承载分布式锁的相关信息 */ interface CriticalRegionContext { /** * 是否执行成功 * * @return 执行成功返回真 */ boolean isSuccess(); /** * 获取分布式锁的特征,与竞争特征相同,代表了唯一的一把锁 * * @return 锁特征数组 */ String[] getLockFeatures(); /** * 获取分布式锁获取租约开始的时间 * * @return 开始持有时间 */ long getBeginLeaseTimeMs(); /** * 获取分布式锁具体实现类类对象 * * @return 分布式锁实现类 */ Class getDistributedLockServiceImpl(); } } ``` 同时定义AOP切面,AOP切面分为前置通知,后置通知,返回通知,异常通知,环绕通知。在这里面最适合的应该是环绕通知,可以将临界区执行接口放在环绕通知这种执行,如果发生异常,可以拦截异常,并进行释放锁;如果执行正常,也能正常释放锁。 ```java ``` # 实现 ## Redis分布式锁 Redis分布式锁由于在主从时造成的锁死锁问题,一般采用多个单节点的Redis实例构成红锁 # 使用