分布式锁的3种实现方式

发布时间:2022-07-14T00:39:14 Java

说起分布式的概念,首当其冲就是CAP理论,即满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)。但是CAP理论告诉我们,任何系统只能满足其中两个,所以都要求去做取舍。那么人们常说的一般都是,需要牺牲一致性来保证系统的高可用性,只要保证系统的最终一致性,并且允许的时间差值能够被接受就行。

对于这个,本人的体会就是订单系统,对于订单系统来说,用户端的一致性需要保证强一致性,但是对于后台或者商家来说的话,这个订单的状态只要保证最终一致性就行,时间差值在可接受范围内就OK。

1.单机的情况下:

单机情况要解决共享资源的访问很容易,Java的API提供了很丰富的解决方案,常见的诸如synchronize,lock,volatile,c.u.t包等等,很多,但是这是在单机情况下,因为只有一个JVM在运行我们的代码。

2.多机的情况下:

这个时候就会出现一套代码出现在多个JVM中,请求落在哪一个上面是随机的。这个时候上面提到的基于Java的API提供的一些解决机制就没法满足要求,它只能解决当前机器中能保证顺序访问共享资源,但是不能保证其他机器。

分布式锁的3种实现方式第1张

那么对于多机的情况怎么去解决这个问题呢,其实很简单,只要保证互斥就行了,原理和单机是一样的,找到一个互斥点就行。那么这个互斥点就必须在大家共有的一个环境中。

那么我所了解到现在分布式锁有三种实现方案。

1.基于数据库。

2.基于缓存环境,redis,memcache等。

3.基于zookeeper。

方案的实现注意点

1.首先保证在分布式的环境中,同一个方法只能被同一个服务器上的一个线程执行。

2.锁要可重入,严重一点的场景不能获取锁之后如果需要再次获取时发现不能获取了,造成死锁。

3.锁要可阻塞。这一般只要保证有个超时时间就行。

4.高可用的加锁和释放锁功能。

5.加锁和释放锁的性能要好。

一、基于数据库的实现方式

1.1 基于数据库表获取

此时这张表类似一个公共资源池,每个线程都要来这边获取条件,看能不能获取到当前方法的锁。

分布式锁的3种实现方式第2张

1.1.1 获取锁时,只要执行insert语句insert into lock_table("method_name","time");

1.1.2 释放锁时,执行对应的delete语句就行。

一个简单的分布式锁就实现了,但是里面会存在很多问题,因为这只是一个初步方案,需要不断改进。

可能出现的问题

1.2.1 这个表中没有设计失效时间,一旦出现加锁成功但是解锁失败的情况,会出现其他线程无法获取到锁。

1.2.2 这把锁不是可重入的,同一个线程在没有释放之前无法再insert。

1.2.3 这把锁不是阻塞的,这边阻塞的意思就是有加锁时间限制,在这个时间内不断去尝试,类似Java里面的自旋。超过时间就失败。出现这个问题的原因和1.2.2一致。

1.2.4 最后一点也是要考虑的,它的可用性怎么样?并不好,一旦数据库挂了,就不能使用了。

针对的解决方案

1.2.1 -->

1.2.1.1可以存在一个定时任务,但是要注意判定失效的时间点的把握,既不能太短也不能太长。

1.2.1.2 代码中在加锁时可以先判断当前记录是不是已经超过最大允许时间,超过了说明已经失效了,先手动释放锁,再加锁

1.2.2 -->

重入的需求可以加入一个字段记录当前JVM的机器标识和线程标识,再次获取时判断一下就行。

1.2.3 -->

阻塞的问题很简单了,代码里执行while循环,设置一个允许最大时间,超过了,直接失败就是了。

1.2.4 -->

单机的问题更好解决了,上两台,互为准备,随时备份,搞定。

1.2 基于排他锁的实现

排它锁测试

一般就可以理解为加上写锁,导致其他事务不能加写锁,只能读而已。就是人们常说的select * for update。

 

public boolean lock(){
    connection.setAutoCommit(false);
    while(true){
    try{
        result = select * from methodLock where method_name=xxx for update;
    if(result==null){
        return true;
    }

    }catch(Exception e){

    }
        sleep(1000);
    }
    return false;
}



public void unlock(){
    connection.commit();
}

 

可解决问题:

1.2.1 它是阻塞的吗?是的,因为通过排它锁测试这一篇张,可以看见,当你使用了select * for update时,其他想要获取锁的事务读不出数据,一直阻塞在那儿。

1.2.2 宕机?宕机之后就自动释放了。

没法解决的问题:

1.2.3 单点问题

1.2.4 可重入问题。

二、基于缓存实现(redis,memchache等常见缓存框架)

redis锁的实现

基于redis的实现可以参考我之前的一篇文章

主要存在的问题:

1. 重入的问题没有解决。其实在笔者项目中,不存在需要现场重入的场景,基本都是在方法外面用redis的加锁包住,finally后释放。

2.redis中如何保证锁的容错性。需要注意加锁成功,但是设置失效时间时宕机的场景,保证不出现死锁。文章里有解决方案。

1.3 基于zk的实现

zk锁分析

zk锁的问题和实现

zk锁的实现

相比较稳定性而言,zk锁无疑是最好的实现方式,但是zk锁的实现依靠一个zk平台,它的理解程度也比较复杂,包括它是怎么保证多节点数据的一致性,怎么对外提供稳定的服务等等。复杂程度也最高,但是最稳定。

想比较而言,采用redis实现分布式锁还是比较好的。个人意见。

使用threadLocal存储当前线程持有redis锁,可实现重入。