分布式锁

基于数据库实现分布式锁:

基于数据库表:

要实现分布式锁,最简单的方法可能就是直接创建一张锁表,然后通过操作该表中的数据来实现了。
   当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。

问题:

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

解决:

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

基于数据库排他锁:

可以借助数据中自带的锁来实现分布式锁。
通过数据库的排他锁,基于 InnoDB 引擎。

总结:

使用数据库来实现分布式锁,这两种方式都是依赖数据库的一张表,一种是通过表中记录的存在情况确定当前是否有锁存在,另一种是通过数据库的排他锁来实现分布式锁。

优点:

直接借助数据库,容易理解。

缺点:

会有各种各样的问题,在解决问题的过程中,会使整个方案变得越来越复杂。操作数据库会有一定的开销,性能问题需要考虑。使用数据库的行级锁并不一定靠谱,尤其是当我们的锁表并不大的时候。


基于缓存实现分布式锁:

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

问题:

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

解决:

  1. 没有失效时间:设置固定时间,到期后自动删除。失效时间比较难以确定,时间太短,方法没执行完释放锁,就会产生并发问题;时间太长,其他线程就要浪费很多时间。
  2. 非阻塞:while 重复执行。
  3. 非可重入:在一个线程获取到锁之后,把当前主机信息和线程信息保存起来,下次再获取前先检查自己是不是当前锁的拥有者。

总结:

优点:

性能好,实现起来较为方便。

缺点:

通过超时来控制锁的失效时间并不是十分的靠谱。


基于 Zookeeper 实现分布式锁:

基于 zookeeper 临时有序节点可实现的分布式锁。
   大致思想为:每个客户端对每个方法加锁时,在 zookeeper 上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生死锁的问题。

如何解决前面的问题:

锁无法释放:

使用 Zookeeper 可以有效的解决锁无法释放的问题,因为在创建的时候,客户端会在 zk 中创建一个临时节点,一旦客户端获取到锁之后突然挂掉,那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。

非阻塞锁:

使用 zookeeper 可以实现阻塞的锁,客户端可通过在 zk 中创建顺序节点,并在节点上绑定监听器,一旦节点有变化,zookeeper 会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑。

不可重入:

使用 zookeeper 可以有效解决不可重入的问题,客户端在创建节点时,把当前客户端的主机信息和线程信息直接写入节点中,下次想要获取锁的时候和当前最小节点中的数据对比一下就可以。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就在创建一个临时的顺序节点,参与排队。

单点问题:

使用 zookeeper 可以有效解决单点问题,zk 是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。

使用 zk 实现的分布式锁其实存在一个缺点,那就是性能上可能并没有缓存服务器那么高。因为在每次创建锁和释放锁的过程中,都要动态创建,销毁瞬时节点来实现锁功能。zk 中创建和删除节点只能通过 leader 服务器来执行,然后将数据同步到所有 follower 机器上。
   使用了 zk 也有可能带来并发问题,只是不常见。由于网络抖动,客户端集群的 session 连接断了,那么 zk 以为客户端挂了,就会删除临时节点,这是其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为 zk 有重试机制,一旦 zk 集群检测不到客户端的心跳,就会重试,多次重试还不行的话就会删除临时节点。

总结:

优点:

有效的解决单点问题,不可重入问题,非阻塞问题,以及锁无法释放的问题。实现起来较为简单。

缺点:

性能上不如缓存实现分布式锁。需要最 zk 的原理有所了解。


比较:

理解程度:

数据库>缓存>Zookeeper

实现的复杂性角度:

Zookeeper>=缓存>数据库

性能角度:

缓存>Zookeeper>=数据库

可靠性角度:

Zookeeper>缓存>数据库