分布式锁的方法论

分布式锁是解决多进程并发控制的关键技术,常见实现包括基于数据库和缓存。数据库实现中,通过插入/删除记录来加锁和解锁,但存在单点、无法设置过期时间及不重入等问题。而基于Redis等缓存的实现,利用SETNX和超时时间避免永久锁,但释放锁时的原子性操作需借助脚本解决。缓存实现性能较好,但过期时间管理复杂。
摘要由CSDN通过智能技术生成

 

目录

什么是分布式锁

基于数据库实现分布式锁

 基于缓存实现分布式锁


什么是分布式锁

根据分布式系统的CAP理论,高可用性(Availability)、强一致性(Consistency)和分区容错性(Partition tolerance)三者无法同时满足只能满足其中两项[23]。系统往往需要牺牲强一致性保证高可用,为了达到数据最终一致性,我们往往需要分布式事务,分布式锁等技术。Java本身对于多线程并发编程有着很好的支持,在Java的Concurren并发包里面有各种锁的实现,最常见的是ReentrantLock和Sychronized。Java并发包里面的这两个锁生效的前提是在一个Java虚拟机里面,如果你同时启动两个JVM跑两个Java程序,那么不同进程间这两个锁是不会生效的。通过阅读过这两个锁实现的源码,我们可以知道Sychronized会用JVM进程中对某个对象头做个标记位,而ReentrantLock本质上是基于AQS的,也是内部自定义了一个Status标记位。这二个的实现其实本质上是一样的,都是使用JVM进程中某个共享变量做一个标识。

在实际的业务中,我们一般会有多台机器,如果要控制这些机器(可以理解成不同进程),那么我们就无法在某个进程或者某个机器中定义共享变量了,这时候我们必须依赖一个所有机器都能读取到的外部存储,来实现进程间的并发控制,这就是分布式锁。

       目前有两种常见实现方法:基于数据库实现分布式锁和基于缓存(Redis等缓存)实现分布式锁。我们将从以下几个个方面考察两种实现方式的优劣:

基本功能:在分布式系统中能够保证不会有两个线程同时执行同一方法。

可重入锁:在线程本身拥有锁的情况下,再次执行本方法不需要重新获取锁,避免发生死锁。

高可用获取和释放锁:不能释放别人的锁。

最好是阻塞锁:方法的执行时间都很短,往往只需要阻塞等待一段时间就能获取到锁。

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

我们需要在数据库创建一张锁表,通过操作表中的数据完成锁的释放和获取。基本思路是在表中插入一行数据表示锁的获取,删除一行数据表示锁的释放。首先创建一张锁表,要存储锁获取时间、锁定的方法名和锁的持有人等信息,其中方法名要加唯一索引限制。

当我们想要获取方法的锁时只需要插入一条关于锁定方法的数据,包括要锁定的方法、获取时间和申请锁的程序。因为我们对方法名进行了唯一索引限制,所以当锁存在的时候会插入失败,插入成功时可以视为已经获取到方法的锁。当我们需要释放方法锁时只需要删除包含此方法名的这行数据。

但是这种简单的基于数据库实现存在很多问题:首先,由于是单点数据库可用性无法保证,如果改成双机热备数据库,方案复杂。其次,受限于数据库的功能无法设置过期时间,如果加锁后系统挂掉,那么这把锁永远无法销毁。最后,这个锁不重入,虽然可以在插入之前先检查是否获得了这把锁(查询数据库看数据库是否有本线程信息),但是会引入分布式事务问题。还有一种常见的用法是利用MySQL的InnoDB引擎特性在查询语句后面加for update实现加锁操作。首先给查条件方法名设置成唯一索引,然后数据库在查询过程中会给数据表增加行级排他锁,其他线程就无法再在该行记录上增加排他锁。这种模式可以有效解决上面的无法释放锁的问题:服务器宕机之后会自动释放排他锁。但是由于InnoDB引擎自身优化特性,当表比较小时可能会使用表级排他锁,严重影响性能。最后这种方式需要长时间占用数据库连接,一旦数据库连接多了就会导致连接资源消耗殆尽。

  •  基于缓存实现分布式锁

常见缓存包括Redis、Memcache等都是集群部署,不存在单点问题。接下来以Redis为例来分析使用Redis实现分布式锁的方案。基本思路是向Redis缓存中写入要占用的资源名等数据,表示占锁。大多数应用都使用SET NX接口来设置一个分布式锁,一般会设置一个超时时间,防止业务崩溃或者错误导致资源无法释放。SET resource_name random_value NX PX 300,如果命令执行成功则代表获得分布式锁,执行失败则代表锁被别的线程占用。

Redis方案的问题在于释放锁的过程即删除数据的过程,我们分析以下案例:

  1. App1设置了分布式锁resource_1,随机value是value_1,超时时间是300ms。
  2. App1由于等待或者程序慢或者各种原因,超过了300ms,则引擎中resource_1已经被释放。
  3. 这个时候App2就可以获得这个分布式锁了。
  4. 然后App1突然又运行起来了,运行了DEL resource_1,恰好把App2刚获得的分布式锁给删除了。

此时删除了别的线程占用的锁,分布式锁的功能失效,程序可能就错误了。所以在删除之前,程序需要做一次判断,当前缓存内的数据是否是本线程的,如果是再删除。这又引入了另一个问题,这是两步操作,Java没法保证这两步的原子性,我们需要使用三方脚本来进行锁的释放问题,这个方法也是Redis官方推荐的。

具体脚本代码可以参考我的另一篇文章:

[Redis 实现分布式锁](https://blog.csdn.net/h2453532874/article/details/98082127)

总结来说,使用缓存实现分布式锁性能好,因为Redis作为内存数据库比MySQL更适合做分布式锁,但是数据的过期时间不容易把控。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值