分布式锁的实现
Posted lujiango
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式锁的实现相关的知识,希望对你有一定的参考价值。
1. 背景
目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论:任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(partition Tolerance),最多只能同时满足两项。
所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
在很多场景中,为了保证数据的最终一致性,需要很多的技术方案来支持,比如分布式事务、分布式锁等。有的时候,需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,Java中其实提供了很多并发处理的相关的API,但是这些API在分布式场景中就无能为力。单纯的Java API并不能提供分布式锁的实现。目前比较常见的有以下几种方案:
- 基于数据库实现分布式锁
- 基于缓存(redis,memcached,tair)实现分布式锁
- 基于zookeeper实现分布式锁
分布式需要具有如下的特性:
- 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行;
- 锁钥匙可重入锁(避免死锁);
- 锁最好是一把阻塞锁(根据业务需求考虑要不要这条);
- 有高可用的获取锁和释放锁功能;
- 获取锁和释放锁的性能要好;
2. 基于数据库实现分布式锁
2.1 基于数据库表
要实现分布式锁,最简单的方式可能就是直接创建一张锁表,然后通过操作该表的数据来实现。
当锁住某个方法或资源时,就在该表中增加一条记录,想要释放锁的时候就删除这条记录。
创建这样一张数据库表:
想要锁住某个方法时,执行以下SQL:
因为method_name是唯一索引,如果有多个请求同时提交到时数据库的话,数据库会保证只有一个操作可以成功,就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。
当方法执行完毕周后,想要释放锁的话,需要执行以下SQL:
上面这种简单的实现有以下几个问题:
- 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用;
- 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁;
- 这把锁只能是非阻塞的,因此数据的insert操作,一旦插入失败就会直接报错,没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作;
- 这把锁是非重入的,同一线程在没有释放锁之前无法再次获得该锁,因为数据中数据已经存在了。
当然可以有其他方式解决上面的问题:
- 数据库时单点:搞两个数据库,数据直接双向同步,一旦挂掉快速切换到备库上;
- 没有失效时间:做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍;
- 非阻塞锁:搞一个while循环,直到insert成功在返回成功;
- 非重入锁:在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
2.2 基于数据库排他锁
除了可以通过增删操作数据库表中的记录之外,其实还可以借助数据库中自带的锁来实现分布式锁。
可以通过数据库的排他锁来实现分布式锁,基于mysql的InnoDB引擎,可以使用一下方法来实现加锁操作:
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁,当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。
(备注:InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。我们希望使用行级锁,就要给method_name添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题,重载方法的话,建议把参数类型也加上)
可以认为获得排他锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,在通过以下方法解锁。
通过connection.commit()操作来释放锁。
这种方法可以有效的解决上面提到定位无法释放锁和阻塞锁的问题。
- 阻塞锁:for update语句会在执行成功后立刻返回,在执行失败时一直处于阻塞状态,直到成功;
- 锁定之后服务宕机,无法释放:使用这种方式,服务宕机之后数据库会自己把锁释放掉。
但是还是无法直接解决数据库单点和可重入问题。
3. 基于缓存实现分布式锁
相比于基于数据库实现分布式锁的方案来说,基于缓存来实现在性能方面会表现的更好一点,而且很多缓存是可以集群部署的,可以解决单点问题。
目前有很多成熟的缓存产品,包括Redis,memcached,tair。
以tair为例来分析使用缓存实现分布式锁的方案。
以上实现同样存在几个问题:
- 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在tair中,其他线程无法再获得锁;
- 这把锁只能是非阻塞的,无论成功还是失败都直接返回;
- 这把锁是非重入的,一个线程获得锁之后,在释放锁之前,无法再次获得该锁,因为使用到的key在tair中已经存在,无法再执行put操作。
4. 基于zookeeper实现分布式锁
以上是关于分布式锁的实现的主要内容,如果未能解决你的问题,请参考以下文章