分布式锁的实现

Posted lujiango

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式锁的实现相关的知识,希望对你有一定的参考价值。

1. 背景

目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论:任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(partition Tolerance),最多只能同时满足两项。

所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

在很多场景中,为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,Java中其实提供了很多并发处理的相关的API,但是这些API在分布式场景中就无能为力。单纯的Java API并不能提供分布式锁的实现。目前比较常见的有以下几种方案:

  • 基于数据库实现分布式锁
  • 基于缓存(redis,memcached,tair)实现分布式锁
  • 基于zookeeper实现分布式锁

分布式需要具有如下的特性:

  1. 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行;
  2. 锁钥匙可重入锁(避免死锁);
  3. 锁最好是一把阻塞锁(根据业务需求考虑要不要这条);
  4. 有高可用的获取锁和释放锁功能;
  5. 获取锁和释放锁的性能要好;

2. 基于数据库实现分布式锁

2.1 基于数据库表

要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表的数据来实现。

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

创建这样一张数据库表:

技术分享图片

想要锁住某个方法时,执行以下SQL:

技术分享图片

因为method_name是唯一索引,如果有多个请求同时提交到时数据库的话,数据库会保证只有一个操作可以成功,就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。

当方法执行完毕周后,想要释放锁的话,需要执行以下SQL:

技术分享图片

上面这种简单的实现有以下几个问题:

  1. 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用;
  2. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁;
  3. 这把锁只能是非阻塞的,因此数据的insert操作,一旦插入失败就会直接报错,没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作;
  4. 这把锁是非重入的,同一线程在没有释放锁之前无法再次获得该锁,因为数据中数据已经存在了。

当然可以有其他方式解决上面的问题:

  1. 数据库时单点:搞两个数据库,数据直接双向同步,一旦挂掉快速切换到备库上;
  2. 没有失效时间:做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍;
  3. 非阻塞锁:搞一个while循环,直到insert成功在返回成功;
  4. 非重入锁:在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

2.2 基于数据库排他锁

除了可以通过增删操作数据库表中的记录之外,其实还可以借助数据库中自带的锁来实现分布式锁。

可以通过数据库的排他锁来实现分布式锁,基于mysql的InnoDB引擎,可以使用一下方法来实现加锁操作:

技术分享图片

在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁,当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。

(备注:InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。我们希望使用行级锁,就要给method_name添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题,重载方法的话,建议把参数类型也加上)

可以认为获得排他锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,在通过以下方法解锁。

技术分享图片

通过connection.commit()操作来释放锁。

这种方法可以有效的解决上面提到定位无法释放锁和阻塞锁的问题。

  • 阻塞锁:for update语句会在执行成功后立刻返回,在执行失败时一直处于阻塞状态,直到成功;
  • 锁定之后服务宕机,无法释放:使用这种方式,服务宕机之后数据库会自己把锁释放掉。

但是还是无法直接解决数据库单点和可重入问题。

3. 基于缓存实现分布式锁

相比于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点,而且很多缓存是可以集群部署的,可以解决单点问题。

目前有很多成熟的缓存产品,包括Redis,memcached,tair。

以tair为例来分析使用缓存实现分布式锁的方案。

技术分享图片

以上实现同样存在几个问题:

  1. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在tair中,其他线程无法再获得锁;
  2. 这把锁只能是非阻塞的,无论成功还是失败都直接返回;
  3. 这把锁是非重入的,一个线程获得锁之后,在释放锁之前,无法再次获得该锁,因为使用到的key在tair中已经存在,无法再执行put操作。

4. 基于zookeeper实现分布式锁

 

以上是关于分布式锁的实现的主要内容,如果未能解决你的问题,请参考以下文章

分布式锁的实现方式

分布式锁的解决方案

常见分布式锁的基本实现

分布式锁的设计与实现

Redis分布式锁的实现

分布式锁的3种实现