分布式锁有哪些?

2024-05-11 18:12

1. 分布式锁有哪些?

   单体架构的应用可以直接使用synchronized或者ReentrantLock就可以解决多线程资源竞争的问题。如果公司业务发展较快,可以通过部署多个服务节点来提高系统的并行处理能力。由于本地锁的作用范围只限于当前应用的线程。高并发场景下,集群中某个应用的本地锁并不会对其它应用的资源访问产生互斥,就会产生数据不一致的问题,所以分布锁就派上了用场。  
     利用select … where … for update 排他锁  
     所谓乐观锁与前边最大区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update version失败后才能觉察到。我们的抢购、秒杀就是用了这种实现以防止超卖。通过增加递增的版本号字段实现乐观锁  
      思路:    另启一个服务,利用jdk并发工具来控制唯一资源,如在服务中维护一个concurrentHashMap,其他服务对某个key请求锁时,通过该服务暴露的端口,以网络通信的方式发送消息,服务端解析这个消息,将concurrentHashMap中的key对应值设为true,分布式锁请求成功,可以采用基于netty通信调用,当然你想用java的bio、nio或者整合dubbo、spring cloud feign来实现通信也没问题  
     在高并发场景下,应用程序在执行过程中往往会受到网络、CPU、内存等因素的影响,所以实现一个线程安全的分布式组件,往往需要考虑很多case,这个分布式锁有 3 个重要的考量点:  
     下面是redis分布式锁的各种实现方式和缺点,按照时间的发展排序:  
     直接利用setnx,执行完业务逻辑后调用del释放锁,简单粗暴!  
     为了改正第一个方法的缺陷,我们用setnx获取锁,然后用expire对其设置一个过期时间,如果服务挂了,过期时间一到自动释放  
     redis官方为了解决第二种方式存在的缺点,在2.8版本为set指令添加了扩展参数nx和ex,保证了setnx+expire的原子性,使用方法:set key value ex 5 nx  
     上面所说的第一个缺点,没有特别好的解决方法,只能把过期时间尽量设置的长一点,并且最好不要执行耗时任务 第二个缺点,可以理解为当前线程有可能会释放其他线程的锁,那么问题就转换为保证线程只能释放当前线程持有的锁,即setnx的时候将value设为任务的唯一id,释放的时候先get key比较一下value是否与当前的id相同,是则释放,否则抛异常回滚,其实也是变相地解决了第一个问题  
     我们可以用lua来写一个getkey并比较的脚本,jedis/luttce/redisson对lua脚本都有很好的支持  
     为了解决上面提到的redis集群中的分布式锁问题,redis的作者antirez的提出了red lock的概念,假设集群中所有的n个master节点完全独立,并且没有主从同步,此时对所有的节点都去setnx,并且设置一个请求过期时间re和锁的过期时间le,同时re必须小于le(可以理解,不然请求3秒才拿到锁,而锁的过期时间只有1秒也太蠢了),此时如果有n / 2 + 1个节点成功拿到锁,此次分布式锁就算申请成功  
     ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:  
     (1)redis set px nx + 唯一id + lua脚本  
      综上所得:   
     没有绝对完美的实现方式,具体要选择哪一种分布式锁,需要结合每一种锁的优缺点和业务特点而定。  

分布式锁有哪些?

2. 分布式锁

 与分布式锁对应的是【单机锁】,我们在写多线程程序时,避免同时操作一个共享变量而产生数据问题,通常会使用一把锁来实现【互斥】,其使用范围是在【同一个进程中】。(同一个进程内存是共享的,以争抢同一段内存,来判断是否抢到锁)。
   如果是多个进程,如何互斥呢。就要引入【分布式锁】来解决这个问题。想要实现分布式锁,必须借助一个外部系统,所有进程都去这个系统上去【申请加锁】。
   而这个外部系统,必须要实现【互斥】的能力,即两个请求同时进来,只会给一个进程返回成功,另一个返回失败(或等待)。
   这个外部系统,可以是 MySQL,也可以是 Redis 或 Zookeeper。但为了追求更好的性能,我们通常会选择使用 Redis 或 Zookeeper 来做。
   依赖mysql的行锁 select for update。
   一个特例,唯一索引。唯一索引是找到了就直接停止遍历,非唯一索引还会向后遍历一行。移步第八个case。
   现在的索引:
                                                                                   分析:
   单条命中只会加行锁,不加间隙锁。所以RC/RR是一样的。
   事务1对id=10这条记录加行锁。所以 场景1会锁等待,场景2不会锁等待 。
                                           分析:
   RC隔离级别:
   事务1未命中,不会加任何锁。所以 场景1,场景2,场景3都不会锁等待 。
   RR隔离级别:
   事务1未命中,会加间隙锁。因为主键查询,只会对主键加锁。
   在10和18加间隙锁。
   间隙锁和查询不冲突。 场景1不会锁等待 。
   间隙锁和插入冲突。 场景2和场景3会锁等待 。
                                                                                   分析
   单条命中只会加行锁,不加间隙锁。所以RC/RR是一样的。
   事务1对二级索引和主键索引加行锁。 事务1和事务2都会发生锁等待 。
                                                                                                                           分析
   RC隔离级别:
   事务1未命中,不会加任何锁。所以 场景1,场景2,场景3都不会锁等待 。
   RR隔离级别:
   事务1对二级索引N0007到正无穷上间隙锁,主键索引不上锁。 场景1会锁等待,场景2不会锁等待 。
                                           分析:
   RC隔离级别:
   只会加行锁。 场景1场景2会锁等待 。 场景3不会发生锁等待 。
                                           RR隔离级别:
   会加行锁和间隙锁。 场景1场景2场景3都会锁等待 。
   ps: 如果是唯一索引,只会加行锁。非唯一才会加间隙锁。
                                                                                   RC隔离级别:
   事务1未命中,不会加任何锁。所以 场景1,场景2都不会锁等待 。
   RR隔离级别:
   事务1未命中,会加间隙锁。间隙锁与查询不冲突, 场景1不会发生锁等待 。 场景2会发生锁等待 。
                                                                                   分析
   RC隔离级别:
   事务1加了三个行锁。 场景1会锁等待。场景2,场景3不会发生锁等待 。
                                           RR隔离级别:
   事务1加个三个行锁和间隙锁。 场景1,场景3会发生锁等待 。间隙锁与查询不冲突, 场景2不会锁等待。 
                                                                                   分析:
   RC隔离级别:
   事务1加的都是行锁。 场景1会发生锁等待 , 场景2,场景3不会发生锁等待 。
                                           RR隔离级别:
   事务1会对二级索引加行锁和间隙锁,对主键索引加行锁。
    场景1,场景3会发生锁等待 。间隙锁与查询不冲突, 场景2不会锁等待。 
                                           这么看,二级索引和唯一索引没什么区别。
   那如果是 select * from book where name < 'Jim' for update; 呢
   如果name是唯一索引。因为找到jim就不会向后遍历了,所以jim和rose之间不会有间隙锁。
                                                                                   分析:
   RC隔离级别:
   由于没有走索引,所以只能全表扫描。在命中的主键索引上加行锁。 场景1会锁等待,场景2不会锁等待 。
   RR隔离级别:
   不开启innodb_locks_unsafe_for_binlog。 会发生锁表 。
   开启innodb_locks_unsafe_for_binlog。和RC隔离级别一样。
                                                                                   RC隔离级别:
   未命中不加锁。 场景1,场景2都不会锁等待 。
   RR隔离级别:
   未命中, 锁表 。
   在RR隔离级别下,where条件没有索引,都会锁表。
    加锁命令: 
    释放锁命令: 
   这里存在问题,当释放锁之前异常退出了。这个锁就永远不会被释放了。
   怎么解决呢?加一个超时时间。
   还有问题,不是原子操作。
   redis 2.6.12之后,redis天然支持了
   来看一下还有什么问题:
   试想这样一个场景
   看到了么,这里存在两个严重的问题:
    释放别人的锁 :客户端 1 操作共享资源完成后,却又释放了客户端 2 的锁
    锁过期 :客户端 1 操作共享资源耗时太久,导致锁被自动释放,之后被客户端 2 持有
   解决办法是:客户端在加锁时,设置一个只有自己知道的【唯一标识】进去。
   例如,可以是自己的线程id,也可以是一个uuid
   在释放锁时,可以这么写:
   问题来了,还不是原子的。redis没有原生命令了。这里需要使用lua脚本
   锁的过期时间如果评估不好,这个锁就会有「提前」过期的风险,一般的妥协方案是,尽量「冗余」过期时间,降低锁提前过期的概率。
   其实可以有比较好的方案:
    加锁时,先设置一个过期时间,然后我们开启一个「守护****线程****」,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行「续期」,重新设置过期时间。 
   这个守护线程我们一般把他叫做【看门狗】线程。
                                           我们在使用 Redis 时,一般会采用 主从集群 +   哨兵 的模式部署,这样做的好处在于,当主库异常宕机时,哨兵可以实现「故障自动切换」,把从库提升为主库,继续提供服务,以此保证可用性。
   试想这样的场景:
   为此,Redis 的作者提出一种解决方案,就是我们经常听到的  Redlock(红锁) 。
   现在我们来看,Redis 作者提出的 Redlock 方案,是如何解决主从切换后,锁失效问题的。
   Redlock 的方案基于 2 个前提:
   也就是说,想用使用 Redlock,你至少要部署 5 个 Redis 实例,而且都是主库,它们之间没有任何关系,都是一个个孤立的实例。
                                           Redlock 具体如何使用呢?
   整体的流程是这样的,一共分为 5 步:
   有 4 个重点:
    1) 为什么要在多个实例上加锁? 
   本质上是为了「容错」,部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。
    2) 为什么大多数加锁成功,才算成功? 
   多个 Redis 实例一起来用,其实就组成了一个「分布式系统」。
   在分布式系统中,总会出现「异常节点」,所以,在谈论分布式系统问题时,需要考虑异常节点达到多少个,也依旧不会影响整个系统的「正确性」。
   这是一个分布式系统「容错」问题,这个问题的结论是: 如果只存在「故障」节点,只要大多数节点正常,那么整个系统依旧是可以提供正确服务的。 
    3) 为什么步骤 3 加锁成功后,还要计算加锁的累计耗时? 
   因为操作的是多个节点,所以耗时肯定会比操作单个实例耗时更久,而且,因为是网络请求,网络情况是复杂的,有可能存在 延迟、丢包、超时 等情况发生,网络请求越多,异常发生的概率就越大。
   所以,即使大多数节点加锁成功,但如果加锁的累计耗时已经「超过」了锁的过期时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。
    4) 为什么释放锁,要操作所有节点? 
   在某一个 Redis 节点加锁时,可能因为「网络原因」导致加锁失败。
   例如,客户端在一个 Redis 实例上加锁成功,但在读取响应结果时,网络问题导致 读取失败 ,那这把锁其实已经在 Redis 上加锁成功了。
   所以,释放锁时,不管之前有没有加锁成功,需要释放「所有节点」的锁,以保证清理节点上「残留」的锁。
   好了,明白了 Redlock 的流程和相关问题,看似 Redlock 确实解决了 Redis 节点异常宕机锁失效的问题,保证了锁的「安全性」。
   在martin的文章中,主要阐述了4个论点:
    第一:效率 。
   使用分布式锁的互斥能力,是避免不必要地做同样的工作两次。如果锁失效,并不会带来「恶性」的后果,例如发了 2 次邮件等,无伤大雅。
    第二:正确性 。
   使用锁用来防止并发进程相互干扰。如果锁失败,会造成多个进程同时操作同一条数据,产生的后果是数据严重错误、永久性不一致、数据丢失等恶性问题,后果严重。
   他认为,如果你是为了前者——效率,那么使用单机版redis就可以了,即使偶尔发生锁失效(宕机、主从切换),都不会产生严重的后果。而使用redlock太重了,没必要。
    而如果是为了正确性,他认为redlock根本达不到安全性的要求,也依旧存在锁失效的问题! 
   一个分布式系统,存在着你想不到的各种异常。这些异常场景主要包括三大块,这也是分布式系统会遇到的三座大山: NPC 
   martin用一个进程暂停(GC)的例子,指出了redlock安全性的问题:
   又或者,当多个Redis节点时钟发生了问题时,也会导致redlock锁失效。
    在混乱的分布式系统中,你不能假设系统时钟就是对的。 
   个人理解,相当于在业务层再做一层乐观锁。
    一个好的分布式锁,无论 NPC 怎么发生,可以不在规定时间内给出结果,但并不会给出一个错误的结果。也就是只会影响到锁的「性能」(或称之为活性),而不会影响它的「正确性」。 
    1、Redlock 不伦不类 :它对于效率来讲,Redlock 比较重,没必要这么做,而对于正确性来说,Redlock 是不够安全的。
    2、时钟假设不合理 :该算法对系统时钟做出了危险的假设(假设多个节点机器时钟都是一致的),如果不满足这些假设,锁就会失效。
    3、无法保证正确性 :Redlock 不能提供类似 fencing token 的方案,所以解决不了正确性的问题。为了正确性,请使用有「共识系统」的软件,例如 Zookeeper。
   好了,以上就是 Martin 反对使用 Redlock 的观点,看起来有理有据。
   下面我们来看 Redis 作者 Antirez 是如何反驳的。
   首先,Redis 作者一眼就看穿了对方提出的最为核心的问题: 时钟问题 。
   Redis 作者表示,Redlock 并不需要完全一致的时钟,只需要大体一致就可以了,允许有「误差」。
   例如要计时 5s,但实际可能记了 4.5s,之后又记了 5.5s,有一定误差,但只要不超过「误差范围」锁失效时间即可,这种对于时钟的精度的要求并不是很高,而且这也符合现实环境。
   对于对方提到的「时钟修改」问题,Redis 作者反驳到:
   Redis 作者继续论述,如果对方认为,发生网络延迟、进程 GC 是在客户端确认拿到了锁,去操作共享资源的途中发生了问题,导致锁失效,那这 不止是 Redlock 的问题,任何其它锁服务例如 Zookeeper,都有类似的问题,这不在讨论范畴内 。
   这里我举个例子解释一下这个问题:
   Redis 作者这里的结论就是:
   所以,Redis 作者认为 Redlock 在保证时钟正确的基础上,是可以保证正确性的。
   这个方案必须要求要操作的「共享资源服务器」有拒绝「旧 token」的能力。
   例如,要操作 MySQL,从锁服务拿到一个递增数字的 token,然后客户端要带着这个 token 去改 MySQL 的某一行,这就需要利用 MySQL 的「事物隔离性」来做。
   但如果操作的不是 MySQL 呢?例如向磁盘上写一个文件,或发起一个 HTTP 请求,那这个方案就无能为力了,这对要操作的资源服务器,提出了更高的要求。
   也就是说,大部分要操作的资源服务器,都是没有这种互斥能力的。
    再者,既然资源服务器都有了「互斥」能力,那还要分布式锁干什么? 
   利用 zookeeper 的同级节点的唯一性特性,在需要获取排他锁时,所有的客户端试图通过调用 create() 接口,在  /exclusive_lock  节点下创建临时子节点  /exclusive_lock/lock ,最终只有一个客户端能创建成功,那么此客户端就获得了分布式锁。同时,所有没有获取到锁的客户端可以在  /exclusive_lock  节点上注册一个子节点变更的 watcher 监听事件,以便重新争取获得锁。
   锁释放依赖心跳。集群中占用锁的客户端失联时,锁能够被有效释放。一旦占用Znode锁的客户端与ZooKeeper集群服务器失去联系,这个临时Znode也将自动删除
   zookeeper的高可用依赖zab。简单的说就是写入时,半数follower ack,写入成功。
   zk是100%安全的么:
   分析一个例子:
   所以,得出一个结论: 一个分布式锁,在极端情况下,不一定是安全的。 
   redlock运维成本也比较高。单机有高可用问题。所以还是主从+哨兵这样的部署方式会好一些。
   redis的缺点是:不是100%可靠。
   mysql的缺点是:扛不住高流量请求。
   可以二者结合,先用redis做分布式锁,扛住大部分流量缓解mysql压力。最后一定要用mysql做兜底保证100%的正确性。

3. 分布式锁的介绍

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

分布式锁的介绍

4. 分布式锁原理

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。原理就是,当我们要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。

就是要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。

在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。

当在分布式模型下,数据可能只有一份,此时需要利用锁的技术控制某一时刻修改数据的进程数。与单机模式下的锁不同,分布式锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。

分布式情况下之所以问题变得复杂,主要就是需要考虑到网络的延时和不可靠。分布式锁还是可以将标记存在内存,只是该内存不是某个进程分配的内存而是公共内存如Redis、Memcache。至于利用数据库、文件等做锁与单机的实现是一样的,只要保证标记能互斥就行。

5. 分布式锁解决什么问题

分布式锁为解决分布式系统中多个应用同时访问同一个资源的问题。一般是在两个场景下会防止对同一个资源的重复访问:
提高效率。比如多个节点计算同一批任务,如果某个任务已经有节点在计算了,那其他节点就不用重复计算了,以免浪费计算资源。不过重复计算也没事,不会造成其他更大的损失。也就是允许偶尔的失败。
保证正确性。这种情况对锁的要求就很高了,如果重复计算,会对正确性造成影响。这种不允许失败。

分布式锁解决什么问题

6. 什么是分布式锁?实现方式有哪些?

什么是分布式锁?实现分布式锁的三种方式                在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。那具体什么是分布式锁,分布式锁应用在哪些业务场景、如何来实现分布式锁呢?
一 为什么要使用分布式锁
我们在开发应用的时候,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的锁进行处理,并且可以完美的运行,毫无Bug!注意这是单机应用,后来业务发展,需要做集群,一个应用需要部署到几台机器上然后做负载均衡,大致如下图:

上图可以看到,变量A存在三个服务器内存中(这个变量A主要体现是在一个类中的一个成员变量,是一个有状态的对象),如果不加任何控制的话,变量A同时都会在分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的!即使不是同时发过来,三个请求分别操作三个不同内存区域的数据,变量A之间不存在共享,也不具有可见性,处理的结果也是不对的!如果我们业务中确实存在这个场景的话,我们就需要一种方法解决这个问题!为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用并发处理相关的功能进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的应用并不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
二、分布式锁应该具备哪些条件
在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;2、高可用的获取锁与释放锁;3、高性能的获取锁与释放锁;4、具备可重入特性;5、具备锁失效机制,防止死锁;6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
三、分布式锁的三种实现方式
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。在很多场景中,我们为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,我们需要保证一个方法在同一时间内只能被同一个线程执行。

7. 什么是分布式锁服务Chubby

含义: Chubby是Google设计的提供粗粒度锁服务的一个文件系统,它基于松耦合分布式系统,解决了分布的一致性问题。
作用:  通过使用Chubby的锁服务,用户可以确保数据操作过程中的一致性。不过值得注意的是,这种锁只是一种建议性的锁(Advisory Lock)而不是强制性的锁(Mandatory Lock),如此选择的目的是使系统具有更大的灵活性。
希望可以帮到您

什么是分布式锁服务Chubby

8. 什么是分布式锁

成员变量 A 存在 JVM1、JVM2、JVM3 三个 JVM 内存中
成员变量 A 同时都会在 JVM 分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的
不是同时发过来,三个请求分别操作三个不同 JVM 内存区域的数据,变量 A 之间不存在共享,也不具有可见性,处理的结果也是不对的
注:该成员变量 A 是一个有状态的对象
如果我们业务中确实存在这个场景的话,我们就需要一种方法解决这个问题,这就是分布式锁要解决的问题

分布式锁应该具备哪些条件
在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
高可用的获取锁与释放锁
高性能的获取锁与释放锁
具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
具备锁失效机制,防止死锁
具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
分布式锁的实现有哪些
Memcached:利用 Memcached 的 add 命令。此命令是原子性操作,只有在 key 不存在的情况下,才能 add 成功,也就意味着线程得到了锁。
Redis:和 Memcached 的方式类似,利用 Redis 的 setnx 命令。此命令同样是原子性操作,只有在 key 不存在的情况下,才能 set 成功。
Zookeeper:利用 Zookeeper 的顺序临时节点,来实现分布式锁和等待队列。Zookeeper 设计的初衷,就是为了实现分布式锁服务的。
Chubby:Google 公司实现的粗粒度分布式锁服务,底层利用了 Paxos 一致性算法。
通过 Redis 分布式锁的实现理解基本概念
分布式锁实现的三个核心要素:

加锁
最简单的方法是使用 setnx 命令。key 是锁的唯一标识,按业务来决定命名。比如想要给一种商品的秒杀活动加锁,可以给 key 命名为 “lock_sale_商品ID” 。而 value 设置成什么呢?我们可以姑且设置成 1。加锁的伪代码如下:

setnx(lock_sale_商品ID,1)
当一个线程执行 setnx 返回 1,说明 key 原本不存在,该线程成功得到了锁;当一个线程执行 setnx 返回 0,说明 key 已经存在,该线程抢锁失败。

解锁
有加锁就得有解锁。当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行 del 指令,伪代码如下:

del(lock_sale_商品ID)
释放锁之后,其他线程就可以继续执行 setnx 命令来获得锁。

锁超时
锁超时是什么意思呢?如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住(死锁),别的线程再也别想进来。所以,setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。