当事务遇上分布式锁

1.分布式锁的几种实现方式

  • 直接使用MySQL进行同步异常处理
  • redis Lua脚本或者redison
  • zookeeper原子树结构

2. MySQL使用自带锁进行分布式同步控制

X锁:排他锁,也称之为写锁,即 Write Lock。一个写锁会阻塞其他的 X 锁和 S 锁。当事务需要修改一条记录时,需要先获取该记录的 X 锁。显式加锁的方式:for update;

S锁:共享锁,读锁,S 锁之间是共享的,互不阻塞的。当事务读取一条记录时,需要先获取该记录的 S 锁。显式加锁的方式:for share mode;

2.1 环境准备

show databases;
creat database xxx;
use xxx;
DROP TABLE IF EXISTS `metering`;
CREATE TABLE `metering`  (
  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
  `rest` double(20, 2) NOT NULL DEFAULT 0.00,
  `version` bigint(20) UNSIGNED NOT NULL DEFAULT 0,
  `remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of metering
-- ----------------------------
INSERT INTO `metering` VALUES (1, '小碗熊干脆面', 100.00, 0, '');
INSERT INTO `metering` VALUES (2, '魔法师干脆面', 100.00, 0, '');
INSERT INTO `metering` VALUES (3, '飞旺辣条', 100.00, 0, '');
INSERT INTO `metering` VALUES (4, '绿箭辣条', 100.00, 0, '');

输入以下命令查看隔离级别

show variables like 'tx_isolation';
 // MySQL5.7之前
select @@tx_isolation;

// MySQL8.0之后
select @@transaction_isolation;

设置隔离级别

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
 
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED
 
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
 
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE
 
 
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
 
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED
 
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ
 
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE

输入以下命令查看是否自动提交事务

show variables like 'autocommit';

ON 自动提交 OFF不是自动提交

修改自动提交状态:

set autocommit=on;

手动提交命令

start transaction;
SQL;
commit;

查看锁记录等待时间

SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'; 

2.2 可重复读下的for update的验证

在可重复读隔离级别下读不到别的事务还未提交的内容,并且在事务未提交之前都是对事务开启时刻的数据的快照读,即读不到别的事务已经修改的内容,这样就导致了使用version 字段的时候普通select 无法读最新数据。

事务1事务2
start transaction;
start transaction;
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述事务1进行解锁
在这里插入图片描述事务2获取到锁就读到当前最新数据,但是也同时对数据进行了加锁

有一种强制读的办法就是加for update,但如果别的事务没有提交修改内容就会处于等待锁的过程中。

同时需注意的是,如果没有开启事务,如果事务1获取到X锁,在未开启事务场景下的事务2使用for update 会阻塞,而且也是属于快照读。读不到事务1未提交的内容,但是不加for update时可以读到已经提交的内容,验证结果如下:

事务1未开事务
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

验证2,事务外可读事务已提交内容:

事务1未开事务
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述读到已经提交的内容

如果在for update 加X锁读的时候不是按照索引来读的,则会锁表。

事务1未开事务
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

所以使用X锁在innodb引擎下注意要和索引进行使用。

但依赖于数据库,主要有两个缺点:单点故障问题。一旦数据库不可用,会导致整个系统崩溃。死锁问题。数据库锁没有失效时间,未获得锁的进程只能一直等待已获得锁的进程主动释放锁。倘若已获得共享资源访问权限的进程突然挂掉、或者解锁操作失败,使得锁记录一直存在数据库中,无法被删除,而其他进程也无法获得锁,从而产生死锁现象。

3.Redis实现分布式锁进行同步控制

redis实现分布式锁是否真的可靠?

Set Key Value EX|PX NX|XX
EX second :设置键的过期时间为 second 秒。
PX millisecond :设置键的过期时间为 millisecond 毫秒。
NX :只在键不存在时,才对键进行设置操作。
XX:只在键已经存在时,才对键进行设置操作。
SET 操作成功完成时,返回 OK ,否则返回 nil。

  • Lua脚本解锁
@Override
public void unlock(String key, String value) {
    try {
        Object result = jcodis.eval(script, Collections.singletonList(key),
                Collections.singletonList(value));
        if (result == null || !"1".equals(result.toString())) {
            log.warn("Redis unlock failed kv[{}:{}]: {}", key, value);
        }
    } catch (Exception e) {
        log.warn("Redis unlock exception kv[{}:{}]: {}", key, value, e.toString());
    }
}
//只删除自己创建的锁 避免释放了其他的锁 单一脚本语句原子操作
String scirpt ="if redis.call('get',KEYS[1]) == ARGV[1] then
      return redis.call('del',KEYS[1])
   else
     return 0
   end ";

Java代码加锁

/**
 * 加锁
 *
 * @param key
 * @param value
 * @param px      锁过期时间 毫秒
 * @param timeout 锁超时时间 毫秒
 */
@Override
public boolean lock(String key, String value, long px, long timeout) {
    long start = System.currentTimeMillis();
    try {
        do {
            //SET命令返回OK ,则证明获取锁成功
            //nxxx的值只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
            //expx的值只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒。
            String result = jcodis.set(key, value, "NX", "PX", px);
            if ("OK".equalsIgnoreCase(result)) {
                return true;
            }

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } while ((System.currentTimeMillis() - start) < timeout);
        log.warn("Redis add lock failed! kv[{}:{}]", key, value);
    } catch (Exception e) {
        log.warn("Redis add lock exception kv[{}:{}]: {}", key, value, e.toString());
    }
    return false;
}

存在两个超时时间:
锁过期时间,redis服务端控制的Key过期时间(死锁处理)
锁超时时间,等待获取锁的时间,客户端控制


实现库存扣减V1(锁在事务内):

start transaction;
lock(key,UUID);
select rest from metering where id =1;
update metering set rest = rest -1 where id =1;
unlock(key,UUID);
commit;

这样会存在一个问题,读到其他事务未提交的内容,其可验证如下:

事务1事务2
start transactionstart transaction
lockwait
select rest=5wait
update rest=4wait
unlockwait
lock
select rest = 5
commit
update rest =4
unlcok
commit

实现库存扣减V2(锁在事务外),保证事务的串行:

lock(Key,UUID);
start transaction;
select rest from metering where id = 1;
update metering set rest = rest -1 where id = 1;
commit;
unlock(Key,UUID);

但是在处理这个问题时,在压测下仍然出现了并发修改问题。通过压测现象现象分析,在争抢下,同步异常基本不出现,在高争抢(并发)下,修改异常现象比较明显,进一步通过日志分析线程等锁时间过长,导致锁争抢超时,直接进行了代码的执行,验证如下:

事务1事物2n个事务事务3
lock
start transactionlocklocklock
waitwaitwait
do updatewaitwait
commit
unlock
do update commit unlockwaitwait
lock fail
start transactionstart transaction
select rest 4
update rest 3select rest 4
commitupdate rest 3
unlockcommit
unlock

这样就抛出了问题,当等待争抢锁,是一直等下去还是超时中断?
如果一直等下去则需要进行锁的续期以及client侧超时时间的延长,并得思考这样的长时间争抢锁是否合适?
其次超时中断,当线程在一定时间内获取不到锁,可以直接认为本次操作失败,服务器繁忙等异常给调用方。
结合配额校验的使用场景,配额校验大多只在资源创建和扩缩容场景下进行,并不是一个高频次需求,因此选择了超时中断的策略进行锁超时的处理。伪代码如下:

实现库存扣减V3(锁在事务外),保证事务的串行,并超时退出。

if(trylock(accountCode,UUID)){
start transaction;
select rest from metering where id = 1;
update metering set rest =rest -1 where id = 1;
commit;
}else{
  "服务器繁忙!"
}finally{
  unlock(accountCode,UUID);
}

所以在使用分布式锁的时候如果用到事务需要注意,锁与事务的包含关系。应当是锁包含事务才能保证本次操作的原子性。

3.1 Redisson

在续期场景下,还有很多锁的优异特性可以通过Redis来实现,比如锁的可重入特性。
Redisson - 是一个高级的分布式协调Redis客服端,能帮助用户在分布式环境中轻松实现一些Java的对象,Redisson、Jedis、Lettuce 是三个不同的操作 Redis 的客户端,Jedis、Lettuce 的 API 更侧重对 Reids 数据库的 CRUD(增删改查),而 Redisson API 侧重于分布式开发。

3.2 Redisson实现

导入Redisson SpringBoot依赖:

   <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.17.5</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

RedissonConfig 配置类:

@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    //@Value("${spring.redis.password}")
    //private String password;

    /**
     * RedissonClient,单机模式
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException {
        Config config = new Config();
        //config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
        config.useSingleServer().setAddress("redis://" + host + ":" + port);
        return Redisson.create(config);
    }
}
spring:
  redis:
    host: localhost
    port: 6379

为了方便演示示范,以下只使用org.redisson进行示范,注意得使用高版本的org.redisson,修复了很多bug。

<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.5</version>
</dependency>
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.config.Config;

import java.util.concurrent.TimeUnit;

public class WatchDogDemo
{
    public static final String LOCKKEY = "AAA";

    private static Config config;
    private static Redisson redisson;

    static {
        config = new Config();
        config.useSingleServer().setAddress("redis://"+"192.168.111.147"+":6379").setDatabase(0);
        redisson = (Redisson)Redisson.create(config);
    }

    public static void main(String[] args)
    {
        RLock redissonLock = redisson.getLock(LOCKKEY);

        redissonLock.lock();
        try
        {
            System.out.println("1111");
            //暂停几秒钟线程
            try { TimeUnit.SECONDS.sleep(25); } catch (InterruptedException e) { e.printStackTrace(); }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
           redissonLock.unlock();
        }

        System.out.println(Thread.currentThread().getName() + " main ------ ends.");

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        redisson.shutdown();
    }
}

续期主要对应Redis的EXPIRE、PEXPIRE:设置生存时间。
EXPIRE命令用于设置秒级精度的生存时间
PEXPIRE命令则用于设置毫秒级精度的生存时间
那么Redisson是如何进行加解锁的呢?

加锁:org.redisson.RedissonLock#tryAcquireAsync
通过exists判断,如果锁不存在,则设置值和过期时间,加锁成功。
通过hexists判断,如果锁已存在,并且锁的是当前线程,则证明是重入锁,加锁成功。
如果锁已存在,但锁的不是当前线程,则证明有其他线程持有锁。返回当前锁的过期时间(代表了lockzzyy这个锁key的剩余生存时间),加锁失败。
在这里插入图片描述

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Long> ttlRemainingFuture;
    if (leaseTime > 0) {
        ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }
    CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
        // lock acquired
        if (ttlRemaining == null) {
            if (leaseTime > 0) {
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
            //如果没有设置自定义过期时间,则启动看门狗自动续期
                scheduleExpirationRenewal(threadId);
            }
        }
        return ttlRemaining;
    });
    return new CompletableFutureWrapper<>(f);
 }

自动续期:org.redisson.RedissonLock#scheduleExpirationRenewal
如果加锁的时候指定了过期时间,那么 Redission 不会给你开启看门狗的机制。

protected void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
    //重入加锁
        oldEntry.addThreadId(threadId);
    } else {
    //第一次加锁
        entry.addThreadId(threadId);
        try {
        // 开启续期,看门狗启动
            renewExpiration();
        } finally {
        //如果当前线程被打断,取消续期
            if (Thread.currentThread().isInterrupted()) {
                cancelExpirationRenewal(threadId);
            }
        }
    }
}

protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return 0;",
            Collections.singletonList(getRawName()),
            internalLockLeaseTime, getLockName(threadId));
}

解锁:org.redisson.RedissonLock#unlockInnerAsync
需将可重入计数减1,当为0即可删除Key进行解锁。

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                    "end; " +
                    "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                    "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                    "else " +
                    "redis.call('del', KEYS[1]); " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return nil;",
            Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

Redisson分布锁锁的特性(其实也是分布式锁应当具备的特性):

  • 互斥性
    任意时刻,只能有一个客户端获取锁,不能同时有两个客户端获取到锁。
  • 同一性
    锁只能被持有该锁的客户端删除,不能由其它客户端删除。
  • 可重入性
    持有某个锁的客户端可继续对该锁加锁,实现锁的续租
  • 容错性
    锁失效后(超过生命周期)自动释放锁(key失效),其他客户端可以继续获得该锁,防止死锁

Redis分布式锁的缺点:
在这里插入图片描述

如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。
但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。
接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。
此时就会导致多个客户端对一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。
所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。

但是在集群模式下,Redisson 使用了 Redlock 算法,避免在 Master 节点崩溃切换到另外一个 Master 时,多个应用同时获得锁。

在不同的节点上使用单个实例获取锁的方式去获得锁,且每次获取锁都有超时时间,如果请求超时,则认为该节点不可用。当应用服务成功获取锁的 Redis 节点超过半数(N/2+1,N 为节点数) 时,并且获取锁消耗的实际时间不超过锁的过期时间,则获取锁成功。
一旦获取锁成功,就会重新计算释放锁的时间,该时间是由原来释放锁的时间减去获取锁所消耗的时间;而如果获取锁失败,客户端依然会释放获取锁成功的节点。

4.Zookeeper实现分布式锁进行同步控制

4.1 了解ZooKeeper

因为Redis在宕机时,会丢失锁,所以可以认为它不是一种高可用的分布式锁方案,那么就可以对分布式锁进行补充一个特性,分布式锁得满足高可用的特性。

Zookeeper是一个保证了弱一致性即最终一致性的分布式组件。

Zookeeper采用称为Quorum Based Protocol的数据同步协议。假如Zookeeper集群有N台Zookeeper服务器(N通常取奇数,3台能够满足数据可靠性同时有很高读写性能,5台在数据可靠性和读写性能方面平衡最好),那么用户的一个写操作,首先同步到N/2 + 1台服务器上,然后返回给用户,提示用户写成功。基于Quorum Based Protocol的数据同步协议决定了Zookeeper能够支持什么强度的一致性。
因为Zookeeper是同步写N/2+1个节点,还有N/2个节点没有同步更新,所以Zookeeper不是强一致性的。
用户的数据更新操作,不保证后续的读操作能够读到更新后的值,但是最终会呈现一致性。

ZooKeeper 基于树形数据存储结构实现分布式锁,来解决多个进程同时访问同一临界资源时,数据的一致性问题。ZooKeeper 的树形数据存储结构主要由 4 种节点构成:

  • 持久节点(PERSISTENT)。这是默认的节点类型,一直存在于 ZooKeeper 中。
  • 持久顺序节点(PERSISTENT_SEQUENTIAL)。在创建节点时,ZooKeeper 根据节点创建的时间顺序对节点进行编号命名。
  • 临时节点(EPHEMERAL)。当客户端与 Zookeeper 连接时临时创建的节点。与持久节点不同,当客户端与 ZooKeeper 断开连接后,该进程创建的临时节点就会被删除。
  • 临时顺序节点(EPHEMERAL_SEQUENTIAL)。就是按时间顺序编号的临时节点。

它的分布式锁实现可以基于 ZooKeeper 的临时节点和顺序特性。临时节点具备数据自动删除的功能。当 client 与 ZooKeeper 连接和 session 断掉时,相应的临时节点就会被删除。其次 ZooKeeper 也提供了 Watch 特性可监听 key 的数据变化。

在分布式锁问题中,会经常遇到羊群效应。所谓羊群效应,就是在整个 ZooKeeper 分布式锁的竞争过程中,大量的进程都想要获得锁去使用共享资源。每个进程都有自己的“Watcher”来通知节点消息,都会获取整个子节点列表,使得信息冗余,资源浪费。

当共享资源被解锁后,Zookeeper 会通知所有监听的进程,这些进程都会尝试争取锁,但最终只有一个进程获得锁,使得其他进程产生了大量的不必要的请求,造成了巨大的通信开销,很有可能导致网络阻塞、系统性能下降。那如何解决这个问题呢?具体方法可以分为以下三步。在与该方法对应的持久节点的目录下,为每个进程创建一个临时顺序节点。每个进程获取所有临时节点列表,对比自己的编号是否最小,若最小,则获得锁。若本进程对应的临时节点编号不是最小的,则注册 Watcher,监听自己的上一个临时顺序节点,当监听到该节点释放锁后,获取锁。

4.2 Curator实现Zookeeper分布式锁

引入pom依赖

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>5.2.0</version>
</dependency>

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.2.0</version>
</dependency>

配置类

@Configuration
public class CuratorConfig {

    @Bean
    public CuratorFramework curatorFramework() {
        CuratorFramework curatorFramework = CuratorFrameworkFactory
                .builder()
                .connectString("localhost:2181")
                .sessionTimeoutMs(15000)
                .connectionTimeoutMs(20000)
                .retryPolicy(new ExponentialBackoffRetry(1000, 10))
                .build();
        curatorFramework.start();
        return curatorFramework;
    }
}

demo

    public String getLock1() throws Exception {
        InterProcessMutex lock = new InterProcessMutex(zkConfiguration, PATH);
        for (int i = 0; i < 3; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        log.info(Thread.currentThread().getName() + "尝试获取锁....");
                        lock.acquire();
                        log.info(Thread.currentThread().getName() + "获取锁成功....");
                        log.info(Thread.currentThread().getName() + "开始执行业务逻辑....");
                        Thread.sleep(10000);
                        lock.release();
                        log.info(Thread.currentThread().getName() + "释放锁成功....");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();
        }
        return "";
    }

加锁

在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端(Client1)想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。

[-s] : 创建有序节点。
[-e] : 创建临时节点。
create -e /app1/test1  # 创建临时节点
create -s /app1/test2 # 创建持久有序节点
create -e -s /node3 "123456879" # 创建临时有序节点

之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。
这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。
Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。

于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。

get /path watch
stat /path watch
//为子节点创建watcher,该命令为/path下的子节点的监听事件
ls /path watch

在这里插入图片描述
解锁

当任务完成时,Client1会显示调用删除节点Lock1的指令。
但获得锁的Client1在任务执行过程中,如果崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。
由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。


推荐阅读:

临界区(CriticalSection),互斥量(Mutex),信号量(Semphore)与事件(Handle)

间隙锁相关

MySQL 如何解决幻读(MVCC 原理分析)

踩到一个关于Redisson分布式锁的非比寻常的BUG!

Zookeeper实现分布式锁

Zookeeper Watch 命令

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值