当事务遇上分布式锁

2023-11-02

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 transaction start transaction
lock wait
select rest=5 wait
update rest=4 wait
unlock wait
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 事物2 n个事务 事务3
lock
start transaction lock lock lock
wait wait wait
do update wait wait
commit
unlock
do update commit unlock wait wait
lock fail
start transaction start transaction
select rest 4
update rest 3 select rest 4
commit update rest 3
unlock commit
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 命令

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

当事务遇上分布式锁 的相关文章

  • Mysql 按特定字符串排序

    我似乎找不到这个问题的答案 假设我有一个像这样的表 ID Name 1 AAAAAAAAA 2 ABAAAAAAA 3 BBAAAAAAA 4 CDAAAAAAA 5 BBAAAAAAA 有什么办法可以通过以下方式订购name 但是 从说开
  • 将数组写入文件的最佳方法? [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 我想避免写入数据库并使用常量 数组作为 lang 文件等 i e lang array hello gt hello world 并能够从后台编
  • 如何在node-mysql查询后获取警告

    如何获取查询执行后识别的相应警告 如下所示 connection query squery function err rows search for OkPacket in 2 dimension array var warningCoun
  • PHP-MySQL 或 MySQLi 中哪个最快?

    我想知道是否有人对这种二分法有任何第一手经验 一些博客说 mysql 扩展比 mysqli 更快 这是真的 我只是问速度 我知道 mysqli 具有旧扩展中不存在的功能 在我看到的大多数基准测试中 MySQL 扩展比 MySQLi 稍快一些
  • mysqli_query() 需要至少 2 个参数,其中 1 个参数在? [复制]

    这个问题在这里已经有答案了 每次运行这个 php ini 时 我都会遇到同样的 3 个错误 我不知道我做错了什么 有人可以帮忙吗 以下是错误 2014 年 5 月 5 日 19 20 50 美洲 芝加哥 PHP 警告 mysqli quer
  • 自动生成序列号

    我编写了下面的计数代码 目的是让它为我的数据生成自动序列号 以代替删除行后的 MySQL 序列号 但是当我运行它时 我发现 MySQL 表中没有任何条目 后来我将代码更改为 Dreamweaver 插入记录 并观察到 SN 序列号 字段不需
  • MYSQL插入GB大小的巨大SQL文件

    我正在尝试创建 Wikipedia DB 副本 大约 50GB 但在处理最大的 SQL 文件时遇到问题 我使用 linux split 实用程序将 GB 大小的文件拆分为 300 MB 的块 例如 split d l 50 enwiki 2
  • 如何防止mysql隐式提交

    mysql文档 http dev mysql com doc refman 5 5 en implicit commit html指出某些语句将在事务期间导致隐式提交 例如 CREATE TABLE foo bar INT START TR
  • 从 MySql 返回最后插入的 id

    我在执行以下查询时遇到了一些问题 START TRANSACTION SET LASTID 0 INSERT INTO Accounts Col1 col2 col3 col4 VALUES param1 param2 param3 par
  • MySQL 中两个 Select 查询的结果相减

    我编写了两个 mysql 查询 一个获取一年中特定月份的总用户 注册 另一个获取一年中特定月份的活跃用户 我需要找到数量inactive当年的用户 为此 我正在考虑减去通过两个单独的查询获得的总用户数和活动用户列 以下是查询 1 Fetch
  • Mysql Workbench 无法选择外键

    首先 我检查了很多问题并用谷歌搜索了很多 但没有一个解决我的问题 我正在使用 Mysql Workbench 6 3 创建表 我仅使用 gui 而不是单个查询来创建它们 之后我尝试创建一些外键int 11 列 但 GUI 不允许我这样做 这
  • mysql 中 int(11) 列的大小是多少(以字节为单位)?

    柱子的尺寸是多少int 11 在mysql中以字节为单位 该列中可以存储的最大值 An INT无论指定什么长度 都将始终为 4 个字节 TINYINT 1 字节 8 位 SMALLINT 2 字节 16 位 MEDIUMINT 3 字节 2
  • 拥有两张单独的用户表还是一张更好?

    我的网络应用程序将拥有两种 100 不同的用户 领域 功能和网站目的 一类用户的目的是发布博客 另一类用户的目的是阅读博客并 关注 发布者 他们唯一的共同点是需要 ID 电子邮件 密码和其他一些元数据 例如加入日期等 我应该尝试将它们从同一
  • 字符集和排序规则到底是什么意思?

    我可以阅读MySQL文档而且非常清楚 但是 如何决定使用哪种字符集呢 校对对什么数据有影响 我要求解释这两者以及如何选择它们 来自 MySQLdocs http dev mysql com doc refman 5 0 en charset
  • 具有 TINYTEXT 列的 CREATE TABLE 语句中出现语法错误 1064?

    这是我到目前为止的 MySQL 代码 CREATE DATABASE bankbase USE bankbase CREATE TABLE clienttable ClientID SMALLINT 15 NOT NULL DEFAULT
  • 如何实现复杂的sql命令

    我在 MySQL 中有一个 sql 表 其中包含以下记录 user dob john 1 10 96 jane 3 4 97 jill 1 8 96 jack 2 9 00 jane 12 14 07 john 1 11 98 这是我要执行
  • mysql_upgrade 失败 - innodb 表不存在?

    我正在将 mysql 5 5 docker 容器数据库升级到 mysql 5 6 docker 容器 我能够解决所有其他问题 最后我的服务器运行的是 5 6 但是当我运行 mysql upgrade 时出现以下错误 ERROR root 1
  • 如何使用 PHP mysqli 增加 MySQL 中的值

    我在 MySQL 表中有一个整数列 名为col1 现在 我需要的是将其值增加某个数字 例如 1 可能是 2 3 或任何数字 也就是说 如果它的值已经是 10 现在我希望它变成 11 我知道 我可以通过首先选择原始值 用 PHP 增加它 然后
  • 从命令提示符启动mysql服务器时出错

    我是 sql 新手 我安装了 mysql 并且正在阅读这本书 Java 如何编程连接到 mysql 我收到以下错误 关于时间戳 我明白原因 但我正在尝试修复 无法创建测试文件 C Program Files MySQL MySQL Serv
  • WAMP/MySQL 错误语言不正确

    我已经多次重新安装WAMP 搜索了数百页 但仍未解决此问题 我查看了 phpmyadmin 配置文件 设置 cfg Lang en utf 8 卸载了多次 如上所述 似乎没有运气 任何帮助 将不胜感激 要更改 MySQL 在报告时使用的语言

随机推荐

  • Max函数比较两数大小 int main调用函数举例 C语言入门

    目录 问题描述 思路 代码实现 其他 问题描述 定义一个Max函数 用来比较两整数大小 并在main函数中调用它 思路 定义Max函数 int Max int x int y return 最大值 在main函数中调用Max函数 int m
  • 小米 红米 Redmi note11 4G 5G 手机解锁BL 秒BL解锁 教程 跳过168小时 selenes evergo线刷机包下载

    红米 Redmi Note 11 5G 手机BL解锁 红米note11 4G 5G 秒解锁BL锁 方法 教程 跳过168小时 新版本 selenes evergo 红米Note11系列版本非常多 从4G开始一直到后面出现的11R 我们常见的
  • 陶瓷电容的ESR-谐振频率去哪儿查?

    B站视频演示 陶瓷电容的ESR 谐振频率去哪儿查 https www bilibili com video BV1wT4y1J7oL from search seid 12386875788285776321 建议使用低ESR的电容 如陶瓷
  • Mybatis分页查询的实现(Rowbounds和PageHelper)

    我们实现查询除了 org junit Test public void test02 SqlSession session MybatisUtil getSession UserDao mapper session getMapper Us
  • Python + Selenium(二十六)模拟手机浏览器

    现在的 IT 以移动互联网为主 所以一般网站系统都会支持移动模式 移动设备与 PC 网页浏览区别最大的就是分辨率 大型网站为了体验使用移动设备访问时都会专门提供移动版本 而其他很多站点也是采用响应式设计 也可以支持移动设备以较好的访问 我们
  • Python项目:外星人入侵(一)

    1 安装Pygame 使用pip模块下载并安装Python包 要安装Pygame 在终端提示符下执行如下命令 下载完成 2 创建Pygame窗口及响应用户输入 新建文件夹 将其保存为alien invasion py 在其中输入以下代码 i
  • EC200U open方案环境搭建

    EC200U open方案环境搭建 1 资源 平台 UIS8910DM 500MHz Cortex A5 频段 LTE Cat 1 上行最大5M 下行最大10M LTE FDD B1 B3 B5 B8 LTE TDD B34 B38 B39
  • centOS 7下无法启动网络(service network start)错误解决办法

    问题描述 打开虚拟机 启动CentOS7 检查网络不通 重启service network restart 不行 报错 报错信息 Restarting network via systemctl Job for network servic
  • R极简教程:R语言和RStudio的安装

    R极简教程 R语言和RStudio的安装 R语言是一种广泛使用的统计分析和数据可视化编程语言 RStudio是一个强大的集成开发环境 IDE 提供了便捷的代码编辑 调试和数据可视化工具 本教程将详细介绍如何安装R语言和RStudio 并提供
  • ng: Can't bind to 'ngModel' since it isn't a known property of 'input'. - Angular 6

    出现的问题 ng Can t bind to ngModel since it isn t a known property of input 解决方法 导入 FormsModule 到 app module ts 如下代码粉色标记 imp
  • C++"char"类型的一些解释

    C char 类型的一些解释 char 类型 char 类型的cin和cout 说明 本文适合初学者阅读 为方便叙述和理解 本文牺牲部分严谨性 本文中的概念 数字 代表整数等日常意义上的数而并非书写符号 char 类型 C 中用char类型
  • DRAM 内存介绍(一)

    参考资料 http www anandtech com show 3851 everything you always wanted to know about sdram memory but were afraid to ask SDR
  • (称重问题)假设你有8个球,其中一个略微重一些,但是找出这个球的惟一方法是将两个球放在天平上对比

    问题描述 假设你有8个球 其中一个略微重一些 但是找出这个球的惟一方法是将两个球放在天平上对比 最少要称多少次才能找出这个较重的球 解答思路 至少要称2次 将8个球分成3份 其中2份每份有3个球 设为A B 剩下一份有2个球 设为C 第一次
  • CSS鼠标特效【动画跟随】

    JS CSS body background 111
  • 从 Twitter 运维技术经验可以学到什么

    没有一个网站的性能像 Twitter 这样这么令人牵肠挂肚 看见那条大鲸鱼总是让人感觉很无奈 Twitter 的运维专家 John Adams 在 Velocity 2009 上做了一篇题为 Fixing Twitter 的技术分享 PDF
  • QML Canvas 保存画布内容

    作者 一去 二三里 个人微信号 iwaleon 微信公众号 高效程序员 不知大家想过没有 我们好不容易在 Canvas 上绘制了各种图形 该如何把它保存起来呢 比如 我们实现了一个画板 当用户制作完成自己的作品之后 是不是要将其保存起来 以
  • 关于keil编译STM32例程出现错误的解决方法

    文章目录 错误示例 我的实际操作一 实际没有解决 我的实际操作二 真相大白 用户名没有修改 还是中文 用户名已经修改 乱码原因 学习经验 错误示例 错误如下所示 OBJ LED axf error L6002U Could not open
  • 基于python的股票客户流失数据分析模型

    目录 1 案例背景 2 2 读取数据 2 3 划分特征变量和目标变量 3 4 模型的搭建和使用 3 5 模型的使用 4 6 ROC曲线对模型的评估 7 7 总结 10 8 参考文献 10 9 致谢 10 1 案例背景 在进行一笔股票交易时候
  • 机器学习库--dlib

    dlib是什么呢 见面了 总要认识一下吧 dlib其实就是一个跨平台的用C 编写的代码库 这个库的机器学习算法和工具可以用来解决现实世界的很多工程问题 它在工业界和学术界有着广泛的应用 主要在机器人 嵌入式设备 手机以及高性能计算设备上有着
  • 当事务遇上分布式锁

    文章目录 1 分布式锁的几种实现方式 2 MySQL使用自带锁进行分布式同步控制 2 1 环境准备 2 2 可重复读下的for update的验证 3 Redis实现分布式锁进行同步控制 3 1 Redisson 3 2 Redisson实