分布式锁的实现原理
作者:来自 vivo 互联网服务器团队- Xu Yaoming
介绍分布式锁的实现原理。
一、分布式锁概述
分布式锁,顾名思义,就是在分布式环境下使用的锁。众所周知,在并发编程中,我们经常需要借助并发控制工具,如 mutex、synchronized 等,来保障线程安全。但是,这种线程安全仅作用在同一内存环境中。在实际业务中,为了保障服务的可靠性,我们通常会采用多节点进行部署。在这种分布式情况下,各实例间的内存不共享,线程安全并不能保证并发安全,如下例,同一实例中线程A与线程B之间的并发安全并不能保证实例1与实例2之间的并发安全:
因此,当遇到分布式系统的并发安全问题时,我们就可能会需要引入分布式锁来解决。
用于实现分布式锁的组件通常都会具备以下的一些特性:
-
互斥性:提供分布式环境下的互斥原语来加锁/释放锁,当然是分布式锁最基本的特性。
-
自动释放:为了应对分布式系统中各实例因通信故障导致锁不能释放的问题,自动释放的特性通常也是很有必要的。
-
分区容错性:应用在分布式系统的组件,具备分区容错性也是一项重要的特性,否则就会成为整个系统的瓶颈。
目前开源社区中常见的分布式锁解决方案,大多是基于具备集群部署能力的 key-value 存储中间件来实现,最为常用的方案基本上是基于 Redis、zookeeper 来实现,笔者将从上述分布式锁的特性出发,介绍一下这两类的分布式锁解决方案的优缺点。
二、分布式锁的实现原理
2.1 Redis 实现分布式锁
Redis 由于其高性能、使用及部署便利性,在很多场景下是实现分布式锁的首选。首先我们看下 Redis 是如何实现互斥性的。在单机部署的模式下,Redis 由于其单线程处理命令的线程模型,天然的具备互斥能力;而在哨兵/集群模式下,写命令也是单独发送到某个单独节点上进行处理,可以保证互斥性;其核心的命令是 set if not exist:
SET lockKey lockValue NX
成功设置 lockValue 的实例,就相当于抢锁成功。但如果持有锁的实例宕机,因为 Redis 服务端并没有感知客户端状态的能力,因此会出现锁无法释放的问题:
这种情况下,就需要给 key 设置一个过期时间 expireTime:
SET lockKey lockValue EX expireTime NX
如果持有锁的实例宕机无法释放锁,则锁会自动过期,这样可以就避免锁无法释放的问题。在一些简单的场景下,通过该方式实现的分布式锁已经可以满足需求。但这种方式存在一个明显问题:如果业务的实际处理时间比锁过期时间长,锁就会被误释放,导致其他实例也可以加锁:
这种情况下,就需要通过其他机制来保证锁在业务处理结束后再释放,一个常用的方式就是通过后台线程的方式来实现锁的自动续期。
Redssion 是开源社区中比较受欢迎的一个 Java 语言实现的 Redis 客户端,其对 Java 中 Lock 接口定义进行扩展,实现了 Redis 分布式锁,并通过 watchDog 机制(本质上即是后台线程运作)来对锁进行自动续期。以下是一个简单的 Reddison 分布式锁的使用例子:
RLock rLock = RedissonClient.getLock("test-lock");
try {if (rLock.tryLock()) {// do something}
} finally {rLock.unlock();
}
Redssion 的默认实现 RedissonLock 为可重入互斥非公平锁,其 tryLock 方法会基于三个可选参数执行:
-
waitTime(获取锁的最长等待时长):默认为-1,waitTime 参数决定在获取锁的过程中是否需要进行等待,如果 waitTime>0,则在获取锁的过程中线程会等待一定时间并持续尝试获取锁,否则获取锁失败会直接返回。
-
leaseTime(锁持有时长):默认为-1。当 leaseTime<=0 时,会开启 watchDog 机制进行自动续期,而 leaseTime>0 时则不会进行自动续期,到达 leaseTime 锁即过期释放
-
unit(时间单位):标识 waitTime 及 leaseTime 的时间单位
我们不妨通过参数最全的 RedissonLock#tryLock(long waitTime, long leaseTime, TimeUnit unit) 方法源码来一探其完整的加锁过程:
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {...// tryAcquire方法返回锁的剩余有效时长ttl,如果未上锁,则为nullLong ttl = tryAcquire(waitTime, leaseTime, unit, threadId);if (ttl == null) {// 获取锁成功return true;}// 计算剩余等待时长,剩余等待时长小于0,则不再尝试获取锁,获取锁失败,后续有多处同样的判断逻辑,将精简省略time -= System.currentTimeMillis() - current;if (time <= 0) {acquireFailed(waitTime, unit, threadId);return false;}// 等待时长大于0,则会对锁释放的事件进行订阅,持有锁的客户端在锁释放时会发布锁释放事件通知其他客户端抢锁,由此可得知该默认实现为非公平锁。// Redisson对Redis发布订阅机制的实现,底层大量使用了CompletableFuture、CompletionStage等接口来编写异步回调代码,感兴趣的读者可以详细了解,此处不作展开CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);try {subscribeFuture.get(time, TimeUnit.MILLISECONDS);} catch (TimeoutException e) {...} catch (ExecutionException e) {...}try {...// 循环尝试获取锁while (true) {long currentTime = System.currentTimeMillis();ttl = tryAcquire(waitTime, leaseTime, unit, threadId);// lock acquiredif (ttl == null) {return true;}...// 此处通过信号量来将线程阻塞一定时间,避免无效的申请锁浪费资源;在阻塞期间,如果收到了锁释放的事件,则会通过信号量提前唤起阻塞线程,重新尝试获取锁;currentTime = System.currentTimeMillis();if (ttl >= 0 && ttl < time) {// 若ttl(锁过期时长)小于time(剩余等待时长),则将线程阻塞ttlcommandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);} else {// 若等待时长小于ttl,则将线程阻塞timecommandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);}...}} finally {// 取消订阅unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);}
}
上述代码逻辑主要集中在处理 waitTime 参数,在并发竞争不激烈、可以容忍一定的等待时间的情况下,合理设置 waitTime 参数可以提高业务并发运行成功率,避免抢锁失败直接返回错误;但在并发竞争激烈、对性能有较高要求时,建议不设置 waitTime,或者直接使用没有 waitTime 参数的 lock() 方法,通过快速失败来提高系统吞吐量。
一个比较值得注意的点是,如果设置了 waitTime 参数,则 Redisson 通过将 RedissonLockEntry 中信号量(Semaphore)的许可证数初始化为0来达到一定程度的限流,保证锁释放后只有一个等待中的线程会被唤醒去请求 Redis 服务端,把唤醒等待线程的工作分摊到各个客户端实例上,可以很大程度上缓解非公平锁给 Redis 服务端带来的惊群效应压力。
public class RedissonLockEntry implements PubSubEntry<RedissonLockEntry> {...private final Semaphore latch;public RedissonLockEntry(CompletableFuture<RedissonLockEntry> promise) {super();// RedissonLockEntry 中的Semaphore的许可证数初始化为0this.latch = new Semaphore(0);this.promise = promise;}...
}
获取锁的核心逻辑,会通过 RedissonLock#tryAcquire 方法调用到 RedissonLock#tryAcquireAsync 方法。
private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {RFuture<Long> ttlRemainingFuture;if (leaseTime > 0) {// 若leaseTime大于零,会设置锁的租期为leaseTimettlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);} else {// 若leaseTime小于或等于零,会设置锁的租期为internalLockLeaseTime,这是一个通过lockWatchdogTimeout配置的值,默认为30sttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);}// 此处的handleNoSync方法是为了解决Redis发生故障转移,集群拓扑改变后,只有持有锁的客户端能再次获得锁的bug,为3.20.1版本修复,详见Redisson issue#4822CompletionStage<Long> s = handleNoSync(threadId, ttlRemainingFuture);ttlRemainingFuture = new CompletableFutureWrapper<>(s);// 根据加锁情况来进行后续处理CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {// lock acquired// 若ttl为空,说明加锁不成功if (ttlRemaining == null) {if (leaseTime > 0) {// 若leaseTime>0,则将internalLockLeaseTime变量设置为leaseTime,以便后续解锁使用internalLockLeaseTime = unit.toMillis(leaseTime);} else {// 若leaseTime<=0,则开启看门狗机制,通过定时任务进行锁续期scheduleExpirationRenewal(threadId);}}return ttlRemaining;});return new CompletableFutureWrapper<>(f);
}// 加锁的lua脚本
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,"if ((Redis.call('exists', KEYS[1]) == 0) " +"or (Redis.call('hexists', KEYS[1], ARGV[2]) == 1)) then " +"Redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"Redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"return Redis.call('pttl', KEYS[1]);",Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
可以看到,若 leaseTime 大于0,则不会开启看门狗机制,锁在过期后即失效,在使用时请务必留意。上述代码中执行的 scheduleExpirationRenewal 方法即为看门狗机制的实现逻辑:
protected void scheduleExpirationRenewal(long threadId) {// 每个锁都会对应一个ExpirationEntry类,第一次加锁时不存在oldEntryExpirationEntry = new ExpirationEntry();ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);if (oldEntry != null) {// 非首次加锁,重入计数,不作其他操作oldEntry.addThreadId(threadId);} else {// 首次加锁,调用renewExpiration()方法进行自动续期entry.addThreadId(threadId);try {renewExpiration();} finally {// 若当前线程被中断,则取消对锁的自动续期。if (Thread.currentThread().isInterrupted()) {cancelExpirationRenewal(threadId);}}}
}private void renewExpiration() {...// 此处使用的是netty的时间轮来执行定时续期,此处不对时间轮做展开,感兴趣的读者可详细了解Timeout task = getServiceManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {...CompletionStage<Boolean> future = renewExpirationAsync(threadId);future.whenComplete((res, e) -> {if (e != null) {log.error("Can't update lock {} expiration", getRawName(), e);EXPIRATION_RENEWAL_MAP.remove(getEntryName());return;}if (res) {// 若续期成功,则递归调用,等待任务的下一次执行renewExpiration();} else {// 若续期结果为false,说明锁已经过期了,或锁易主了,则清理当前线程关联的信息,等待线程结束cancelExpirationRenewal(null);}});}// 时间轮的执行周期为internalLockLeaseTime / 3,即默认情况下,internalLockLeaseTime为30s时,每10s触发一次自动续期}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);
}protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {// 执行重置过期时间的lua脚本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));
}
上面一段代码即是看门狗调度的核心代码,本质上即是通过定时调度线程执行 lua 脚本来进行锁续期。值得留意的是 scheduleExpirationRenewal
方法中的 ExpirationEntry,该对象与锁一一关联,会存储尝试获取该锁的线程(无论是否获取成功)以及重入锁的次数,在锁失效/锁释放时,会根据该对象中存储的线程逐一进行资源释放操作,以保证资源的正确释放。
最后,对上述 Redisson 可重入非公平锁源码进行一下总结:
-
Redisson 加锁时,根据 waitTime 参数是否大于0来决定加锁失败时采用等待并再次尝试/快速失败的策略;
-
Redisson 加锁时根据 leaseTime 参数是否小于等于0来决定是否开启看门狗机制进行定时续期;
-
Redisson 底层使用了 netty 实现的时间轮来进行定时续期任务的调度,执行周期为 internalLockLeaseTime / 3,默认为10s。
2.2 zookeeper 实现分布式锁
zookeeper(后文均简称 zk )基于 zab 协议实现的分布式协调服务,天生具备实现分布式锁的基础条件。我们可以从zk的一些基本机制入手,了解其是如何实现分布式锁的。
-
zab:为了保证分布式一致性,zk 实现了 zab(Zk Atomic Broadcast,zk 原子广播)协议,在 zab 协议下,zk集群分为 Leader 节点及 Follower 节点,其中,负责处理写请求的 Leader 节点在集群中是唯一的,多个 Follower 则负责同步 Leader 节点的数据,处理客户端的读请求。同时,zk 处理写请求时底层数据存储使用的是 ConcurrentHashMap,以保证并发安全;
public class NodeHashMapImpl implements NodeHashMap {private final ConcurrentHashMap<String, DataNode> nodes;private final boolean digestEnabled;private final DigestCalculator digestCalculator;private final AdHash hash;...}
- 临时顺序节点:zk 的数据呈树状结构,树上的每一个节点为一个基本数据单元,称为 Znode。zk 可以创建一类临时顺序(EPHEMERAL_SEQUENTIAL)节点,在满足一定条件时会可以自动释放;同时,同一层级的节点名称会按节点的创建顺序进行命名,第一个节点为xxx-0000000000,第二个节点则为xxx-0000000001,以此类推;
- session:zk 的服务端与客户端使用 session 机制进行通信,简单来说即是通过长连接来进行交互,zk 服务端会通过心跳来监控客户端是否处于活动状态。若客户端长期无心跳或断开连接,则 zk 服务端会定期关闭这些 session,主动断开与客户端的通信。
了解了上述 zk 特点,我们不难发现 zk 也是具备互斥性、自动释放的特性的。同时,zk 由于 session 机制的存在,服务端可以感知到客户端的状态,因此不需要有由客户端来进行节点续期,zk 服务端可以主动地清理失联客户端创建的节点,避免锁无法释放的问题。zk 实现分布式锁的主要步骤如下:
-
client1 申请加锁,创建 /lock/xxx-lock-0000000000节点(临时顺序节点),并监听其父节点 /lock;
-
client1 查询 /lock 节点下的节点列表,并判断自己创建的 /xxx-lock-0000000000 是否为 /lock 节点下的第一个节点;当前没有其他客户端加锁,所以 client1 获取锁成功;
-
若 client2 此时来加锁,则会创建 /lock/xxx-lock-0000000001 节点;此时 client2 查询 /lock 节点下的节点列表,此时 /xxx-lock-0000000001 并非 /lock 下的第一个节点,因此加锁不成功,此时 client2 则会监听其上一个节点 /xxx-lock-0000000000;
-
client1 释放锁,client1 删除 /xxx-lock-0000000000 节点,zk 服务端通过长连接 session 通知监听了 /xxx-lock-0000000000 节点的 client2 来获取锁
-
收到释放事件的 client2 查询 /lock 节点下的节点列表,此时自己创建的 /xxx-lock-0000000001 为最小节点,因此获取锁成功。
上述是 zk 公平锁的一种常见实现方式。值得注意的是, zk 客户端通常并不会实现非公平锁。事实上,zk 上锁的粒度不局限于上述步骤中的客户端,zk 客户端每次获取锁请求(即每一个尝试获取锁的线程)都会向 zk 服务端请求创建一个临时顺序节点。
以上述步骤为例,如果需要实现非公平锁,则会导致其余的所有节点都需要监听第一个节点 /xxx-lock-0000000000 的释放事件,相当于所有等待锁释放的线程都会监听同一个节点,这种机制无法像 Redisson 一样把唤醒锁的压力分摊到客户端上(或者说实现起来比较困难),会产生比较严重的惊群效应,因此使用 zk 实现的分布式锁一般情况下都是公平锁。
Curator 是一个比较常用的 zk 客户端,我们可以通过 Curator 的加锁过程,来了解 zk 分布式锁的设计原理。Curator 中比较常用的是可重入互斥公平锁 InterProcessMutex:
InterProcessMutex mutex = new InterProcessMutex(zkClient, "/lock");
try {// acquire方法的两个参数:等待时长及时间单位if (mutex.acquire(3, TimeUnit.SECONDS)) {log.info("加锁成功");} else {log.info("加锁失败");}
} finally {mutex.release();
}
InterProcessMutex 同样提供了等待时长参数,用于设置没有立即获取到锁时是快速失败还是阻塞等待,下一步,方法会调用到 InterProcessMutex#internalLock 方法中:
private boolean internalLock(long time, TimeUnit unit) throws Exception
{// 注释的意思:一个LockData对象只会被一个持有锁的线程进行修改,因此不需要对LockData进行并发控制。如此说明的原因是zk的互斥特性保证了下方attemptLock方法的互斥,由此保证了LockData不会被并发修改/*Note on concurrency: a given lockData instancecan be only acted on by a single thread so locking isn't necessary*/Thread currentThread = Thread.currentThread();// LockData用于记录当前持有锁的线程数据LockData lockData = threadData.get(currentThread);if ( lockData != null ){// 线程不为空,则进行重入,重入次数+1// re-enteringlockData.lockCount.incrementAndGet();return true;}// 向zk服务获取分布式锁,getLockNodeBytesString lockPath = internals.attemptLock(time, unit, getLockNodeBytes());if ( lockPath != null ){// 若lockPath不为空,则获取锁成功,记录当前持有锁的线程LockData newLockData = new LockData(currentThread, lockPath);threadData.put(currentThread, newLockData);return true;}return false;
}
InterProcessMutex#internalLock会调用到 LockInternals#attemptLock 方法:
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
{...while ( !isDone ){isDone = true;try{// 创建锁节点ourPath = driver.createsTheLock(client, path, localLockNodeBytes);// 判断是否成功获取锁hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);}catch ( KeeperException.NoNodeException e ){// 捕获由于网络中断、session过期等原因导致的无法获得节点异常,此处根据配置的zk客户端重试策略决定是否重试,默认重试策略为Exponential Backoff...retry or not...}}if ( hasTheLock ){return ourPath;}return null;
}public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception
{String ourPath;if ( lockNodeBytes != null ){ // 在其他类型的锁实现中,lockNodeBytes可能不为空,则根据lockNodeBytes来获取节点路径,此处暂不作展开ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, lockNodeBytes);}else{// 在可重入互斥锁中,客户端向zk服务端请求创建一个 EPHEMERAL_SEQUENTIAL 临时顺序节点ourPath = client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);}return ourPath;
}
上述代码中,创建锁节点并不会产生互斥,而是会直接向 zk 服务端请求创建临时顺序节点。此时,客户端还未真正的获得锁,判断加锁成功的核心逻辑在 LockInternals#internalLockLoop 方法中:
private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception
{boolean haveTheLock = false;boolean doDelete = false;try{if ( revocable.get() != null ){ // curator锁撤销机制,通过实现Curator中的Revocable接口的makeRevocable方法,可以将锁设置为可撤销锁,其他线程可以在符合条件时将锁撤销,此处暂不涉及client.getData().usingWatcher(revocableWatcher).forPath(ourPath);}// 客户端实例就绪,则尝试循环获取锁while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock ) {// 获取当前父节点下的排好序的子节点List<String> children = getSortedChildren();// 得到当前节点名String sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash// 根据 children 列表与当前节点名,计算当前节点是否为第一个节点,若不是第一个节点,则在 PredicateResults中返回需要监听的前一个节点节点,若为最小节点,则获取锁成功PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);if ( predicateResults.getsTheLock() ){// 获取锁成功haveTheLock = true;}else{// 拼接前一个节点的节点路径String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();synchronized(this){try{// 将前一个节点的监听器放到当前客户端中,当前一个节点被释放时,就会唤醒当前客户端client.getData().usingWatcher(watcher).forPath(previousSequencePath);if ( millisToWait != null ){millisToWait -= (System.currentTimeMillis() - startMillis);startMillis = System.currentTimeMillis();// 计算剩余等待时长,若等待时长小于0,则不再尝试获取锁,并标记当前线程创建的节点需要删除if ( millisToWait <= 0 ){doDelete = true; // timed out - delete our nodebreak;}// 若等待时长大于0,则阻塞线程,等待锁释放wait(millisToWait);}else{// 在其他的一些加锁场景中,默认会持久等待到锁释放位置,当前可重入互斥锁暂不涉及wait();}}catch ( KeeperException.NoNodeException e ){// it has been deleted (i.e. lock released). Try to acquire again}}}}}catch ( Exception e ){ThreadUtils.checkInterrupted(e);doDelete = true;throw e;}finally{if ( doDelete ){// 删除当前节点deleteOurPath(ourPath);}}return haveTheLock;
}private synchronized void notifyFromWatcher()
{// 当zk客户端收到锁释放事件时,会遍历当前客户端注册过的所有的监听器,并找到合适的监听器进行回调,最终通过notifyAll唤醒监听被释放节点的线程notifyAll();
}
上述 curator 加锁的核心代码虽然比较长,但整体逻辑与我们前面分析过的加锁逻辑是一致的,主要做了三件事:
-
获取当前父节点的有序子节点序列;
-
判断当前节点是否为第一个节点;
-
若为第一个节点,则获取锁成功,否则为当前 zk 客户端增加一个前一节点的监听器,如果此时还在等待时长内,则使用wait方法挂起线程,否则删除当前节点。
三、总结——如何选择合适的分布式并发安全解决方案?
-
绕不过的 CAP 理论
Redis 与 zk 由于客户端与服务端的交互机制上存在比较大的差异,相应的分布式锁实现原理也有所不同。两者都是优秀的支持分布式部署的系统,自然具备分区容错性,但分布式系统总绕不过去一个经典的问题——CAP理论:在满足了分区容错性的前提下,分布式系统只能满足可用性、数据一致性两者其一。
对比之下,Redis 在可用性上更胜一筹,属于 AP 系统;zk 具备更强的数据一致性,属于 CP 系统,而基于 AP、CP 的特性去实现的分布式锁,自然也会存在不同程度的问题。
-
Redis 分布式锁的一致性问题
Redis 的集群模式并没有严格地实现分布式共识算法,因此 Redis 是不具备一致性的。为了保证高可用性,Redis 集群的主从节点使用的是异步复制,从节点并不保证与主节点数据一致,只能尽量的追赶主节点的最新数据;因此,当主节点发生故障,进行主从切换时,实际上有可能会发生数据丢失问题:
- zk 性能及可用性问题
zk 实现了 zab 算法,在数据一致性上给出了比较可靠的方案,但是由于 zab 协议的两阶段提交要求所有节点的写请求处理就绪后,才算写入成功,这无疑会导致性能的下降。此外,在zk集群发生 leader 重选举的过程中,对外会表现为不可用状态,此时可用性上就会存在问题:
由上可知,分布式并发安全解决方案并不存在完美的“银弹”,因此更多时候我们应当根据自身业务情况,合理地选择合适的解决方案。
显而易见地,如果业务场景有较高的请求量,并发竞争比较激烈,对性能有较高要求,此时通过 Redis 来实现分布式锁会是比较合适的方案。但是如果业务场景对数据一致性要求比较高,或是系统交互链路比较长,一但发生数据不一致时,会导致系统出现难以恢复的问题时,采用zk来实现分布式锁则是更优的解决方案。
-
上述方案都无法满足要求?
总体上看,Redis 由于其本身的高性能可以满足大多数场景下的性能要求,而 zk 则保证了较高数据一致性。但倘若遇到了既要求高性能、又要求数据一致性、还要引入锁机制来保障并发安全的场景,这时候就必须重新审视系统设计是否合理了,毕竟高并发与锁是一对矛盾,可用性与数据一致性是一对矛盾,我们应该通过良好的方案、系统设计,来避免让我们的系统陷入这些矛盾的困境中。
相关文章:
分布式锁的实现原理
作者:来自 vivo 互联网服务器团队- Xu Yaoming 介绍分布式锁的实现原理。 一、分布式锁概述 分布式锁,顾名思义,就是在分布式环境下使用的锁。众所周知,在并发编程中,我们经常需要借助并发控制工具,如 mu…...
怎样提高自己的能量
能量转换的基本原则是让别人需要你,而不是你去求对方。别人需要你,你的能量就高,你去求别人你的能量就低。 怎样提高自己的能量? 第一,留意你的气场和格局。气场不是说你表现的多么霸道,而是你的信念、决心…...
ospf协议(动态路由协议)
ospf基本概念 定义 OSPF 是典型的链路状态路由协议,是目前业内使用非常广泛的 IGP 协议之一。 目前针对 IPv4 协议使用的是 OSPF Version 2 ( RFC2328 );针对 IPv6 协议使用 OSPF Version 3 ( RFC2740 )。…...
【娱乐项目】竖式算术器
Demo介绍 一个加减法随机数生成器,它能够生成随机的加减法题目,并且支持用户输入答案。系统会根据用户输入的答案判断是否正确,统计正确和错误的次数,并显示历史记录和错题记录。该工具适合用于数学练习,尤其适合练习基…...
Qt中模拟鼠标消息并与系统鼠标消息进行区分
功能使用场景: 开发一个教学系统,包含了教师端、学生端,并且教师端支持示教功能。此时,学生端的鼠标、键盘不响应系统事件,但需要响应教师端发过来的鼠标移动、按下消息。 因为共享页面相同,为了提高局域…...
实时数据开发 | 一文理解Flink窗口机制
窗口操作在流处理和批处理之间起到了桥梁的作用。 Flink引擎本质上是流式引擎,认为批处理是流处理的一个特例。因此,通过窗口将流数据划分为有限大小的集合,使得在这些有界的数据集上可以进行批处理风格的计算。 通过配置窗口的参数…...
MFC 自定义树控件:树节点的样式与交互
在本教程中,将介绍如何在 MFC 应用程序中使用树控件 (CTreeCtrl) 进行高级定制,包括设置字体、颜色、徽章、图标、节点的高度等。通过这些自定义设置,可以显著提升用户界面的交互性和视觉效果。 1. 树控件基本设置 首先,我们需要…...
YOLOv8-ultralytics-8.2.103部分代码阅读笔记-loss.py
loss.py ultralytics\utils\loss.py 目录 loss.py 1.所需的库和模块 2.class VarifocalLoss(nn.Module): 3.class FocalLoss(nn.Module): 4.class DFLoss(nn.Module): 5.class BboxLoss(nn.Module): 6.class RotatedBboxLoss(BboxLoss): 7.class KeypointLoss(n…...
像素流送api ue多人访问需要什么显卡服务器
关于像素流送UE推流,在之前的文章里其实小芹和大家聊过很多,不过今天偶然搜索发现还是有很多小伙伴,在搜索像素流送相关的问题,搜索引擎给的提示有这些。当然这些都是比较短的词汇,可能每个人真正遇到的问题和想获取的…...
字符型注入‘)闭合
前言 进行sql注入的时候,不要忘记闭合,先闭合再去获取数据 步骤 判断是字符型注入 用order by获取不了显位,select也一样 是因为它是’)闭合,闭合之后,就可以获取数据了 最后就是一样的步骤...
评分规则的建模,用户全选就是满分10分(分数可自定义), 选2个5分, 选2个以下0分
子夜(603***854) 15:11:40 和各位讨论一下设计问题: 有个有业务场景: 有一组产品共4个产品(数目用户可自定义), 需要一套规则,比如如果用户全选就是满分10分(分数可自定义), 选2个5分, 选2个以下0分 又比如另一组产品 产品有个必选属性,如果选了其中所有的必选则5分, 其他项每1…...
Elasticsearch与NLP的深度融合:文本嵌入与向量搜索实战指南
Elasticsearch与NLP的深度融合:文本嵌入与向量搜索实战指南 引言 在当今信息爆炸的时代,如何从海量文本数据中快速准确地检索出相关信息,成为了一个迫切需要解决的问题。自然语言处理(NLP)技术的发展为这一挑战提供了新的解决方案。Elasticsearch,作为一个强大的搜索引…...
4. STM32_定时器
概述 什么是定时器: 定时器核心就是计数器,是使用精准的时基,通过硬件的方式,实现定时功能的器件。 定时器的工作原理: 主频时钟CLK通过PSC进行分频后产生定时器时钟TIM CLK,计数器CNT根据TIM CLK的频率…...
Mysql 深度分页问题及优化方案
Mysql 深度分页问题及优化方案 一、为什么 MySQL 深度分页慢?二、优化方案三、补充 一、为什么 MySQL 深度分页慢? 在数据量大时,深分页查询速度缓慢,主要原因是多次回表查询。 前言:N个条件为索引,id为主…...
前端性能优化技巧
前端性能优化技巧 1. 介绍 前端性能优化是确保网站或应用程序快速、响应迅速和流畅的关键。本文档将详细探讨提升前端性能的各种策略和最佳实践。 2. 资源加载优化 2.1 资源压缩 代码压缩:使用 Webpack、Terser 等工具压缩 JavaScript、CSS 文件文件大小压缩&a…...
taro使用createAsyncThunk报错ReferenceError: AbortController is not defined
解决办法: 1,安装这俩包:yet-another-abortcontroller-polyfill,event-target-polyfill 2,app.js import: import ‘event-target-polyfill’; import ‘yet-another-abortcontroller-polyfill’; 补充 但…...
Linux:systemd进程管理【1】
整体理解 要快速掌握Linux的systemd并覆盖80%的使用场景,以下是最重要的20%知识点: Systemd简介与核心功能: Systemd是一个系统和服务管理器,作为Linux系统的PID 1进程,负责启动和管理其他系统组件。它提供并行启动服…...
【Maven】继承和聚合
5. Maven的继承和聚合 5.1 什么是继承 Maven 的依赖传递机制可以一定程度上简化 POM 的配置,但这仅限于存在依赖关系的项目或模块中。当一个项目的多个模块都依赖于相同 jar 包的相同版本,且这些模块之间不存在依赖关系,这就导致同一个依赖…...
【线上问题记录 | 排查网络连接问题】
问题描述 现在有我们程序是部署在服务器A的,A链接的是B。程序从B的redis进行存储和取数据的。 我们的业务是: 信息展示,也就是如果发现机器有异常了,实时进行监控。突然发现有一天,信息显示延迟了。 然后我们就开始排查究竟什么原…...
springboot车辆管理系统设计与实现(代码+数据库+LW)
摘要 随着信息技术在管理上越来越深入而广泛的应用,管理信息系统的实施在技术上已逐步成熟。本文介绍了车辆管理系统的开发全过程。通过分析车辆管理系统管理的不足,创建了一个计算机管理车辆管理系统的方案。文章介绍了车辆管理系统的系统分析部分&…...
独家|京东调整职级序列体系
原有的M、P、T、S主序列将正式合并为新的专业主序列P。 作者|文昌龙 编辑|杨舟 据「市象」独家获悉,京东已在近日在内部宣布对职级序列体系进行调整,将原有的M、P、T、S主序列正式合并为新的专业主序列P,合并后的职级体系将沿用原有专业序…...
Arrays.copyOfRange(),System.arraycopy() 数组复制,数组扩容
Arrays.copyOfRange() 当需要将数组中的 长度扩容时, 数组复制 使用 需要用到Arrays 类提供的的 参数解析 * param original the array from which a range is to be copied * param from the initial index of the range to be copied, inclusive * param to the final ind…...
Python学习37天
# 魔术方法 # 创建类Monster,默认为object的子类 class Monster: name None age None gender None def __init__(self, name, age, gender): self.name name self.age age self.gender gender # 重写魔术方法__str__输出实例对象信息…...
flask的第一个应用
本文编写一个简单的实例来记录下flask的使用 文章目录 简单实例flask中的路由 简单实例 flask的依赖包都安装好之后,我们就可以写一个最简单的web应用程序了,我们把这个应用程序命名为first.py: from flask import Flaskapp Flask(__name__)app.route(/…...
【论文格式】同步更新中
1横向和纵向坐标的坐标密度不能太大,显示太多看起来不好看,本课题组采用emf,目前使用页面内紧凑,600dpi 2Force(kN):k小写 3涉及到变量的,变量本身斜体...
Java-GUI(登录界面示例)
简述: 步骤: (1)构造界面(将组件对象加入容器对象,注意:应设定对容器对象的布局策略) (2)为界面加入事件响应处理(如单击按钮) 实现: 两种方式实现,只有用户名为"admin"且密码为…...
看华为,引入IPD的正确路径
目录 前言 引发重视 作者简介 前言 华为将 IPD 的引入过程归结为三步: 先僵化、后优化、再固化。 如果只是单纯模仿,在不清楚底层逻辑的情况下, 就开始走先僵化的流程,去搞削足适履式的引入。 开始执行后,你就…...
计算机毕业设计Spark+大模型知识图谱中药推荐系统 中药数据分析可视化大屏 中药爬虫 机器学习 中药预测系统 中药情感分析 大数据毕业设计
温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:文末有 CSDN 平台官方提供的学长联系方式的名片! 作者简介:Java领…...
pcb线宽与电流
三十年一路高歌猛进的中国经济, 中国经历了几个三十年? 第一个三十年:以计划为导向。 第二个三十年:以经济为导向。 现在,第三个三十年呢? 应该是以可持续发展为导向。 传统企业摇摇欲坠, 新兴企…...
w~视觉~合集26
我自己的原文哦~ https://blog.51cto.com/whaosoft/12663170 #InternVL 本文设计了一个大规模的视觉-语言基础模型(InternVL),将视觉基础模型的参数扩展到60亿,并逐步与LLM对齐,利用来自不同来源的网络规模的图像-文…...
专门做游轮的网站/深圳推广公司排行榜
package study; class Person{void print() {System.out.println("父类的方法");} } class Child extends Person{public void print() {super.print();//利用[super.方法()]可以使用父类被子类覆写的方法System.out.println("子类的方法");} }public cl…...
特价做网站/深圳短视频推广
原来...类模板 与 模板类 1.类模板与模板类的概念 ⑴ 什么是类模板 一个类模板(也称为类属类或类生成类)允许用户为类定义一种模式,使得类中的某些数据成员、默写成员函数的参数、某些成员函数的返回值,能够取任意类型(…...
做app和做网站的区别/seo优化运营专员
http://www.cnblogs.com/archimedes/p/c-library-assert.html...
免费网站开发软件/seo优化教程视频
文章目录前言一.ReentrantLock二.原子类三.信号量 Semaphore四.CountDownLatch五.Callable 接口前言 一.ReentrantLock ReentrantLock 是 Java 中一个提供同步机制的类,用于控制对共享资源的访问。它实现了 Lock 接口,提供了一组方法来获取和释放共享资…...
随州学做网站的学校/网站排名优化化快排优化
pdb是linux的python调试工具,它功能比较齐全,使用起来也很方便, 按一般运维工程师的技术发展来说,最早接触编程语言应该是shell, 所以大多是在shell的基础上开始学习Python的,如果是刚从shell脚本转到写pyt…...
隐藏 感谢使用wordpress进行创作 3.8.1版本/商务软文写作300
(src >> start) & ((1UL << len)-1) // or 1ULL << if you need a 64-bit mask但是,如果len是该类型的整个宽度,则两种生产掩模的方式看起来都很冒险. (提取所有位的角落情况).按类型的整个宽度移位可以产生零或不变. (它实际上调用了未定义的行为,但实际…...