当前位置: 首页 > news >正文

浅谈Redis分布式锁(下)

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

自定义Redis分布式锁的弊端

在上一篇我们自定义了一个Redis分布式锁,用来解决多节点定时任务的拉取问题(避免任务重复执行):

但仍然存在很多问题:

  • 加锁操作不是原子性的(setnx和expire两步操作不是原子性的,中间宕机会导致死锁)
public boolean tryLock(String lockKey, String value, long expireTime, TimeUnit timeUnit) {// 1.先setnxBoolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, value);if (lock != null && lock) {// 2.再expireredisTemplate.expire(lockKey, expireTime, timeUnit);return true;} else {return false;}
}

当然啦,高版本的SpringBoot Redis依赖其实提供了加锁的原子性操作:

/*** 尝试上锁:setNX + expire** @param lockKey    锁* @param value      对应的值* @param expireTime 过期时间* @param timeUnit   时间单位* @return*/
@Override
public boolean tryLock(String lockKey, String value, long expireTime, TimeUnit timeUnit) {try {// 高版本SpringBoot的setIfAbsent可以设置4个参数,一步到位redisTemplate.opsForValue().setIfAbsent(lockKey, value, expireTime, timeUnit);return true;} catch (Exception e) {e.printStackTrace();}return false;
}

从 Redis 2.6.12 版本开始(现在6.x了...), SET 命令的行为可以通过一系列参数来修改,也因为 SET 命令可以通过参数来实现和 SETNX 、 SETEX 和 PSETEX 三个命令的效果,所以将来的 Redis 版本可能会废弃并最终移除 SETNX 、 SETEX 和 PSETEX 这三个命令。

  • 解锁操作不是原子性的(可能造成不同节点之间互相删锁)

虽然上一篇设计的unLock()不是原子操作,但可以避免不同节点之间互相删锁

public boolean unLock(String lockKey, String value) {// 1.获取锁的value,存的是MACHINE_IDString machineId = (String) redisTemplate.opsForValue().get(lockKey);if (StringUtils.isNotEmpty(machineId) && machineId.equals(value)) {// 2.只能删除当前节点设置的锁redisTemplate.delete(lockKey);return true;}return false;
}
  • 畏难情绪作祟,不想考虑锁续期的问题,企图采用队列的方式缩减定时任务执行时间,直接把任务丢到队列中。但实际上可能存在任务堆积,个别情况下会出现:上次已经拉取某个任务并丢到Redis队列中,但由于队列比较繁忙,该任务还未被执行,数据库状态也尚未更改为status=1(已执行),结果下次又拉取一遍,重复执行(简单的解决策略是:虽然无法阻止入队,但是出队消费时可以判断where status=0后执行)

引入Redis Message Queue会让系统变得更加复杂,我之前就因为使用了上面的模型导致各种偶发性的BUG,非常不好排查。一般来说,定时任务应该设计得简单点:

也就是说,绕来绕去,想要设计一个较完备的Redis分布式锁,必须至少解决3个问题:

  • 加锁原子性(setnx和expire要保证原子性,否则会容易发生死锁)
  • 解锁原子性(不能误删别人的锁)
  • 需要考虑业务/定时任务执行的时间,并为锁续期

如果不考虑性能啥的,加解锁原子性都可以通过lua脚本实现(利用Redis单线程的特性):

一次执行一个脚本,要么成功要么失败,不会和其他指令交错执行。

最难的是如何根据实际业务的执行时间给锁续期!虽然我们已经通过判断MACHINE_ID避免了不同节点互相删除锁:

但本质上我们需要的是:

本文我们的主要目标就是实现锁续期!

好在Redisson已经实现了,所以目标又变成:了解Redisson的锁续期机制。

Redisson案例

Redisson环境搭建

server:port: 8080spring:redis:host:  password:  database: 1# 调整控制台日志格式,稍微精简一些(非必要操作)
logging:pattern:console: "%d{yyyy-MM-dd HH:mm:ss} - %thread - %msg%n"
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--大家也可以单独引入Redisson依赖,然后通过@Configuration自己配置RedissonClient--><dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.13.6</version></dependency>
</dependencies>

然后就可以在test包下测试了~

lock()方法初探

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class RLockTest {@Autowiredprivate RedissonClient redissonClient;@Testpublic void testRLock() throws InterruptedException {new Thread(this::testLockOne).start();new Thread(this::testLockTwo).start();TimeUnit.SECONDS.sleep(200);}public void testLockOne(){try {RLock lock = redissonClient.getLock("bravo1988_distributed_lock");log.info("testLockOne尝试加锁...");lock.lock();log.info("testLockOne加锁成功...");log.info("testLockOne业务开始...");TimeUnit.SECONDS.sleep(50);log.info("testLockOne业务结束...");lock.unlock();log.info("testLockOne解锁成功...");} catch (InterruptedException e) {e.printStackTrace();}}public void testLockTwo()  {try {RLock lock = redissonClient.getLock("bravo1988_distributed_lock");log.info("testLockTwo尝试加锁...");lock.lock();log.info("testLockTwo加锁成功...");log.info("testLockTwo业务开始...");TimeUnit.SECONDS.sleep(50);log.info("testLockTwo业务结束...");lock.unlock();log.info("testLockTwo解锁成功...");} catch (InterruptedException e) {e.printStackTrace();}}}

结果

2023-12-21 14:24:33 - Thread-3 - testLockTwo尝试加锁...

2023-12-21 14:24:33 - Thread-2 - testLockOne尝试加锁...

=====> testLockOne()执行过程中,testLockTwo()一直阻塞 <=====

2023-12-21 14:24:33 - Thread-2 - testLockOne加锁成功...

2023-12-21 14:24:33 - Thread-2 - testLockOne业务开始...

2023-12-21 14:25:23 - Thread-2 - testLockOne业务结束...

2023-12-21 14:25:23 - Thread-2 - testLockOne解锁成功...

=====> testLockOne()执行结束释放锁,testLockTwo()抢到锁 <=====

2023-12-21 14:25:23 - Thread-3 - testLockTwo加锁成功...

2023-12-21 14:25:23 - Thread-3 - testLockTwo业务开始...

2023-12-21 14:26:13 - Thread-3 - testLockTwo业务结束...

2023-12-21 14:26:13 - Thread-3 - testLockTwo解锁成功...

通过上面的代码,我们有以下疑问:

  • lock()方法是原子性的吗?
  • lock()有设置过期时间吗?是多少?
  • lock()实现锁续期了吗?
  • lock()方法怎么实现阻塞的?又怎么被唤醒?

先忘了这些,跟着我们走一遍lock()源码就明白了。

lock()源码解析

lock()加锁,去除异常的情况,无非加锁成功、加锁失败两种情况,我们先看加锁成功的情况。

流程概览

我们从这段最简单的代码入手:

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class RLockTest {@Autowiredprivate RedissonClient redissonClient;@Testpublic void testLockSuccess() throws InterruptedException {RLock lock = redissonClient.getLock("bravo1988_distributed_lock");log.info("准备加锁...");lock.lock();log.info("加锁成功...");TimeUnit.SECONDS.sleep(300);}
}

大家跟着我们先打几个断点(SpringBoot2.3.4):

注意啊,把截图中能看到的断点都打上。

OK,接着大家自己启动DEBUG,感受一下大致流程,然后看下面的注释:

// redisson.lock()
Override
public void lock() {try {lock(-1, null, false);} catch (InterruptedException e) {throw new IllegalStateException();}
}// 为了方便辨认,我直接把传进来的参数写在参数列表上
private void lock(long leaseTime=-1, TimeUnit unit=null, boolean interruptibly=false) throws InterruptedException {// 获取当前线程idlong threadId = Thread.currentThread().getId();// 尝试上锁。上锁成功返回null,上锁失败返回ttlLong ttl = tryAcquire(-1, leaseTime=-1, unit=null, threadId=666);// 上锁成功,方法结束,回到主线程执行业务啦(后台有个定时任务在给当前锁续期)if (ttl == null) {return;}// 上锁成功就不走下面的流程了,所以这里直接省略// 略:加锁失败后续流程...
}// 尝试上锁。上锁成功返回null,上锁失败返回【当前已经存在的锁】的ttl,方便调用者判断多久之后能重新获取锁
private Long tryAcquire(long waitTime=-1, long leaseTime=-1, TimeUnit unit=null, long threadId=666) {/*** 有两次调用:1.tryAcquireAsync()返回Future 2.从Future获取异步结果(异步结果就是ttl)* 重点是tryAcquireAsync()*/return get(tryAcquireAsync(waitTime=-1, leaseTime=-1, unit=null, threadId=666));
}// 获取过期时间(非重点)
protected final <V> V get(RFuture<V> future) {return commandExecutor.get(future);
}// 重点,加锁后返回RFuture,内部包含ttl。调用本方法可能加锁成功,也可能加锁失败,外界可以通过ttl判断
private <T> RFuture<Long> tryAcquireAsync(long waitTime=-1, long leaseTime=-1, TimeUnit unit=null, long threadId=666) {// lock()默认leaseTime=-1,所以会跳过ifif (leaseTime != -1) {return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);}// 执行lua脚本,尝试加锁并返回RFuture。这个方法是异步的,其实是把任务提交给线程池RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime=-1,commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()=30秒,TimeUnit.MILLISECONDS, threadId=666, RedisCommands.EVAL_LONG);// 设置回调方法,异步线程与Redis交互得到结果后会回调BiConsumer#accept()ttlRemainingFuture.onComplete((ttlRemaining, e) -> {// 发生异常时直接returnif (e != null) {return;}// 说明加锁成功if (ttlRemaining == null) {// 启动额外的线程,按照一定规则给当前锁续期scheduleExpirationRenewal(threadId);}});// 返回RFuture,里面有ttlRemainingreturn ttlRemainingFuture;
}// 执行lua脚本尝试上锁
<T> RFuture<T> tryLockInnerAsync(long waitTime=-1, long leaseTime=30*1000, TimeUnit unit=毫秒, long threadId=666, RedisStrictCommand<T> command) {internalLockLeaseTime = unit.toMillis(leaseTime);/*** 大家去看一下evalWriteAsync()的参数列表,看看每个参数都代表什么,就能理解KEYS[]和ARGV[]以及整个脚本什么意思了* 如果你仔细看lua脚本,就会明白:加锁成功时返回ttlRemaining=null,加锁失败时返回ttlRemaining=xxx(上一个锁还剩多少时间)** 另外,我们自定义的Redis分布式锁采用了IdUtil生成节点id,和getLockName(threadId)本质是一样的*/return evalWriteAsync(getName(), LongCodec.INSTANCE, command,"if (redis.call('exists', KEYS[1]) == 0) then " +"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +"redis.call('pexpire', KEYS[1], ARGV[1]); " +"return nil; " +"end; " +"if (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(getName()), internalLockLeaseTime, getLockName(threadId));
}// 向Redis服务器发送脚本并返回RFuture,大家可以近似看成:往线程池提交一个任务,然后将异步结果封装到CompletableFuture
protected <T> RFuture<T> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {CommandBatchService executorService = createCommandBatchService();RFuture<T> result = executorService.evalWriteAsync(key, codec, evalCommandType, script, keys, params);if (!(commandExecutor instanceof CommandBatchService)) {executorService.executeAsync();}return result;
}

示意图:

整个流程比较简单,只有两个难点:

  • lua脚本写了啥
  • ttlRemainingFuture.onComplete()有什么作用

lua脚本解读

大家可以通过evalWriteAsync()的参数列表推导出KEYS、ARGV分别是什么:

KEYS[] => Collections.singletonList(getName())

ARGV[] => internalLockLeaseTime, getLockName(threadId)

-- 如果不存在锁:"bravo1988_distributed_lock"
if (redis.call('exists', KEYS[1]) == 0) then-- 使用hincrby设置锁:hincrby bravo1988_distributed_lock a1b2c3d4:666 1redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 设置过期时间。ARGV[1]==internalLockLeaseTimeredis.call('pexpire', KEYS[1], ARGV[1]); -- 返回nullreturn nil; end; -- 如果当前节点已经设置"bravo1988_distributed_lock"(注意,传了ARGV[2]==节点id)
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then -- 就COUNT++,可重入锁redis.call('hincrby', KEYS[1], ARGV[2], 1); -- 设置过期时间。ARGV[1]==internalLockLeaseTimeredis.call('pexpire', KEYS[1], ARGV[1]); -- 返回nullreturn nil;end; -- 已经存在锁,且不是当前节点设置的,就返回锁的过期时间ttl
return redis.call('pttl', KEYS[1]);

总的来说,Redisson设计的分布式锁是采用hash结构:

LOCK_NAME(锁的KEY)+ CLIENT_ID(节点ID)+ COUNT(重入次数)

回调函数的作用

之前我们已经学过CompletableFuture的回调机制:

RFuture#onComplete()和它很相似:

ttlRemainingFuture.onComplete((ttlRemaining, e) -> {// 发生异常时直接returnif (e != null) {return;}// 说明加锁成功if (ttlRemaining == null) {// 启动额外的线程,按照一定规则给当前锁续期scheduleExpirationRenewal(threadId);}});

onComplete()应该也是把回调函数推到stack中,方便后面异步线程弹栈执行。

至此,我们已经解决了之前的两个问题:

  • lua脚本是什么意思(见注释)
  • ttlRemainingFuture.onComplete()有什么作用(设置回调函数,等会儿会有线程调用)

虽然在CompletableFuture中已经强调过,这里还是要提一下:被回调的不是onComplete(BiConsumer),而是BiConsumer#accept()。主线程在调用onComplete(BiConsumer)时把它作为参数传入,然后被推入栈中

BiConsumer consumer = (ttlRemaining, e) -> {// 发生异常时直接returnif (e != null) {return;}// 说明加锁成功if (ttlRemaining == null) {// 启动额外的线程,按照一定规则给当前锁续期scheduleExpirationRenewal(threadId);}
}

Redisson异步回调机制

现在已经确定了尝试加锁后会返回RFuture,并且我们可以通过RFuture做两件事:

  • 通过RFuture获取ttlRemaining,也就是上一个锁的过期时间,如果为null则本次加锁成功,否则加锁失败,需要等待
  • 通过RFuture设置回调函数

现在疑问是:

  • 异步线程是谁,哪来的?
  • onComplete()设置的回调函数是干嘛的?
  • 回调时的参数(ttlRemaining, e)哪来的?

1、3两个问题非常难,源码比较绕,这里就带大家感性地体验一下,有兴趣可以自己跟源码了解。清除刚才的全部断点,只留下:

再次DEBUG,线程会先到达return ttlRemainingFuture,随后回调BiConsumer#accept():

回调时线程变了:

大家有兴趣可以自己顺着调用栈逆推回去,还是比较复杂的,涉及到NIO、Promise等,源头还是在线程池,但其中又设计了Listeners的收集和循环唤醒:

protected <T> RFuture<T> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType, String script, List<Object> keys, Object... params) {CommandBatchService executorService = createCommandBatchService();RFuture<T> result = executorService.evalWriteAsync(key, codec, evalCommandType, script, keys, params);if (!(commandExecutor instanceof CommandBatchService)) {executorService.executeAsync();}return result;
}

总之,目前为止我们只需要知道:

我们虽然不知道onComplete()具体如何实现回调(比CompletableFuture复杂得多),但是我们知道锁续期和RFuture的回调机制相关!

Redisson如何实现锁续期

最终会进入:

private void renewExpiration() {ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ee == null) {return;}/*** 启动一个定时器:Timeout newTimeout(TimerTask task, long delay, TimeUnit unit);* 执行规则是:延迟internalLockLeaseTime/3后执行* 注意啊,每一个定时任务只执行一遍,而且是延迟执行。* * 那么问题就来了:* 1.internalLockLeaseTime/3是多久呢?* 2.如果定时任务只执行一遍,似乎解决不了问题啊,本质上和我们手动设置过期时间一样:多久合适呢?*/ Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());if (ent == null) {return;}Long threadId = ent.getFirstThreadId();if (threadId == null) {return;}// 定时任务的目的是:重新执行一遍lua脚本,完成锁续期,把锁的ttl拨回到30sRFuture<Boolean> future = renewExpirationAsync(threadId);// 设置了一个回调future.onComplete((res, e) -> {if (e != null) {log.error("Can't update lock " + getName() + " expiration", e);// 如果宕机了,就不会续期了return;}// 如果锁还存在(没有unLock,说明业务还没结束),递归调用当前方法,不断续期if (res) {// reschedule itselfrenewExpiration();}});}}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);ee.setTimeout(task);
}/**
* 重新执行evalWriteAsync(),和加锁时的lua脚本比较类似,但有点不同
* 这里设置expire的参数也是internalLockLeaseTime
*
* 看来我们不得不去调查一下internalLockLeaseTime了!
*/
protected RFuture<Boolean> renewExpirationAsync(long threadId) {return evalWriteAsync(getName(), 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(getName()),internalLockLeaseTime, getLockName(threadId));
}

如果你给renewExpirationAsync()打上断点,会发现每隔10秒,定时任务就会执行一遍:

联想到定时任务的delay是internalLockLeaseTime/3,所以推测internalLockLeaseTime为30秒。

点击internalLockLeaseTime,很容易跳转到对应的字段:

再顺着getLockWatchdogTimeout()跳转,很快就会发现

确实是30秒。

梳理一下所谓的Watchdog锁续期机制:

  • lock()第一次成功加锁时,设置的锁过期时间默认30秒,这个值来自Watchdog变量
// 重点
private <T> RFuture<Long> tryAcquireAsync(long waitTime=-1, long leaseTime=-1, TimeUnit unit=null, long threadId=666) {// lock()默认leaseTime=-1,所以会跳过ifif (leaseTime != -1) {return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);}// 执行lua脚本加锁,返回RFuture。第二个参数就是leaseTime,来自LockWatchdogTimeout!!!RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime=-1,commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()=30秒,TimeUnit.MILLISECONDS, threadId=666, RedisCommands.EVAL_LONG);// 设置回调方法ttlRemainingFuture.onComplete((ttlRemaining, e) -> {// 发生异常时直接returnif (e != null) {return;}// 说明加锁成功if (ttlRemaining == null) {// 启动额外的线程,按照一定规则给当前锁续期scheduleExpirationRenewal(threadId);}});// 返回RFuture,里面有ttlRemainingreturn ttlRemainingFuture;
}// 执行lua脚本上锁
<T> RFuture<T> tryLockInnerAsync(long waitTime=-1, long leaseTime=30*1000, TimeUnit unit=毫秒, long threadId=666, RedisStrictCommand<T> command) {// 略...
}
  • onComplete()设置回调,等Redis调用回来后,异步线程回调BiConsumer#accept(),进入scheduleExpirationRenewal(threadId),开始每隔internalLockLeaseTime/3时间就给锁续期

和加锁一样,执行lua脚本其实很快,所以这里的future.onComplete()虽说是异步,但很快就会被调用,然后就会递归调用renewExpiration(),然后又是一个TimerTask(),internalLockLeaseTime/3后又给锁续期。

也就是说,Redisson的Watchdog定时任务虽然只延迟执行一次,但每次调用都会递归,所以相当于:重复延迟执行。

还记得之前学习CompletableFuture时我写的一行注释吗:

也就是说,只要主线程的任务不结束,就会一直给锁续期。

锁释放有两种情况:

  • 任务结束,主动unLock()删除锁
redisson.lock();
task();
redisson.unLock();
  • 任务结束,不调用unLock(),但由于守护线程已经结束,不会有后台线程继续给锁续期,过了30秒自动过期

上面我们探讨的都是加锁成功的流程,直接ttl=null就返回了,后面一大坨都是加锁失败时的判断逻辑,其中涉及到:

  • while(true)死循环
  • 阻塞等待
  • 释放锁时Redis的Publish通知(在后面的unLock流程会看到)
  • 其他节点收到锁释放的信号后重新争抢锁

整个过程还是非常复杂的,大家有精力可以自行百度了解,后面介绍unLock()时也会涉及一部分加锁失败相关内容。

unLock()源码解析

有了lock()的经验,unLock()就简单多了:

相信大家还是能推断出KEYS[]和ARGV[],这里就直接给出答案了:

-- 参数解释:
-- KEYS[1] => "bravo1988_distributed_lock"
-- KEYS[2] => getChannelName()
-- ARGV[1] => LockPubSub.UNLOCK_MESSAGE
-- ARGV[2] => internalLockLeaseTime
-- ARGV[3] => getLockName(threadId)-- 锁已经不存在,返回null
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) thenreturn nil;end;-- 锁还存在,执行COUNT--(重入锁的反向操作)
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);-- COUNT--后仍然大于0(之前可能重入了多次)
if (counter > 0) then-- 设置过期时间redis.call('pexpire', KEYS[1], ARGV[2]);return 0;
-- COUNT--后小于等于0,删除锁,并向对应的Channel发送消息(NIO),消息类型是LockPubSub.UNLOCK_MESSAGE(锁释放啦,快来抢~)
else redis.call('del', KEYS[1]);redis.call('publish', KEYS[2], ARGV[1]);return 1;end;return nil;

也就是说,当一个锁被释放时,原先持有锁的节点会通过NIO的Channel发送LockPubSub.UNLOCK_MESSAGE,告诉其他订阅的Client:我已经释放锁啦,快来抢啊!此时原本阻塞的其他节点就会重新竞争锁。

而所谓重入和反重入,简单来说就是:

// 加锁三次
redisson.lock();
redisson.lock();
redisson.lock();
// 执行业务
executeTask();
// 相应的,就要解锁三次
redisson.unLock();
redisson.unLock();
redisson.unLock();

实际开发不会这样调用,但有时会出现子父类方法调用或者同一个线程反复调用使用同一把锁的多个方法,就会发生锁的重入(COUNT++),而当这些方法执行完毕逐个弹栈的过程中就会逐个unLock()解锁(COUNT--)。

lock(leaseTime, unit):自定义过期时间、且不续期

lock()默认会开启定时任务对锁进行续期,但Redisson还提供了另一个lock方法:

两个lock()唯一的区别是,内部调用lock()时,一个传了leaseTime=-1,另一个传了我们自己的leaseTime。对于外部调用者来说:

redisson.lock();
redisson.lock(-1, null);

这两种写法其实一样。

当然了,通常会传入有意义的leaseTime:

这种写法除了更改了锁的默认ttl时间外,还阉割了锁续期功能。也就是说,10秒后如果任务还没执行完,就会和我们手写的Redis分布式锁一样,自动释放锁。

为什么锁续期的功能失效了呢?留给大家自己解答,这里只给出参考答案:

// 重点
private <T> RFuture<Long> tryAcquireAsync(long waitTime=-1, long leaseTime=-1, TimeUnit unit=null, long threadId=666) {// lock()默认leaseTime=-1,会跳过这个if执行后面的代码。但如果是lock(10, TimeUnit.SECONDS),会执行if并跳过后面的代码。if (leaseTime != -1) {// 其实和下面的tryLockInnerAsync()除了时间不一样外,没什么差别return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);}// 但由于上面直接return了,所以下面的都不会执行!!/*RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime=-1,commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()=30秒,TimeUnit.MILLISECONDS, threadId=666, RedisCommands.EVAL_LONG);// 设置回调方法(不会执行!!)ttlRemainingFuture.onComplete((ttlRemaining, e) -> {// 发生异常时直接returnif (e != null) {return;}// 说明加锁成功if (ttlRemaining == null) {// 启动额外的线程,按照一定规则给当前锁续期scheduleExpirationRenewal(threadId);}});// 不会执行!!return ttlRemainingFuture;*/
}// 执行lua脚本加锁
<T> RFuture<T> tryLockInnerAsync(long waitTime=-1, long leaseTime=30*1000, TimeUnit unit=毫秒, long threadId=666, RedisStrictCommand<T> command) {// 略...
}

也就是说,直接执行lua加锁就返回了,没有机会启动定时任务和递归...

tryLock()系列:让调用者自行决定加锁失败后的操作

之前我们已经观察到,如果多个节点都调用lock(),那么没获取到锁的节点线程会阻塞,直到原先持有锁的节点删除锁并publish LockPubSub.UNLOCK_MESSAGE 。

但如果调用者不希望阻塞呢?他有可能想着:如果加锁失败,我就直接放弃。

是啊,毕竟尝试加锁的目的可能完全相反:

  • 在保证线程安全的前提下,尽量让所有线程都执行成功
  • 在保证线程安全的前提下,只让一个线程执行成功

前者适用于秒杀、下单等操作,希望尽最大努力达成;后者适用于定时任务,只要让一个节点去执行,没有获取锁的节点应该fast-fail(快速失败)。

也就是说,节点获锁失败后,理论上可以有各种各样的处理方式:

  • 阻塞等待
  • 直接放弃
  • 试N次再放弃
  • ...

但lock、lock(leaseTime, timeUnit)替我们写死了:阻塞等待。即使lock(leaseTime, unit),其实也是阻塞等待,只不过不会像lock()一样不断续期。

究其原因,主要是lock()这些方法对于加锁失败的判断是在内部写死的:

而tryLock()方法则去掉了这层中间判断,把结果直接呈递到调用者面前,让调用者自己决定加锁失败后如何处理:

tryLock()直接返回true(加锁成功)和false(加锁失败),后续如何处理,全凭各个节点自己做出决定。

@Test
public void testTryLock() {RLock lock = redissonClient.getLock("bravo1988_distributed_lock");boolean b = lock.tryLock();if (b) {// 业务操作...}// 调用立即结束,不阻塞
}

这样讲可能有点抽象,大家可以分别点进lock()和tryLock(),自行体会。总之,tryLock()中间少了一大块逻辑,因为它不插手结果的判断。

另外,tryLock()在加锁成功的情况下,其实和lock()是一样的,也会触发锁续期:

如果你不希望触发锁续期,可以像lock(leaseTime, unit)一样指定过期时间,还可以指定加锁失败后等待多久:

@Test
public void testLockSuccess() throws InterruptedException {RLock lock = redissonClient.getLock("bravo1988_distributed_lock");// 基本等同于lock(),加锁成功也【会自动锁续期】,但获锁失败【立即返回false】,交给调用者判断是否阻塞或放弃lock.tryLock();// 加锁成功仍然【会自动锁续期】,但获锁失败【会等待10秒】,看看这10秒内当前锁是否释放,如果是否则尝试加锁lock.tryLock(10, TimeUnit.SECONDS);// 加锁成功【不会锁续期】,加锁失败【会等待10秒】,看看这10秒内当前锁是否释放,如果是否则尝试加锁lock.tryLock(10, 30, TimeUnit.SECONDS);
}

注意哈,只传两个参数时,那个time其实是传给waitTime的:

我们之前操作的都是leaseTime,此时还是-1,也就是说如果加锁成功,还是会锁续期。

那waitTime是用来控制什么的呢?

简而言之:

  • tryLock()加锁失败会立即返回false,而加了waitTime可以手动指定阻塞等待的时间(等一等,万一行呢)
  • leaseTime的作用没变,控制的是加锁成功后要不要续期

至此,分布式锁章节暂时告一段段落。大家有兴趣的话,可以把上一篇花里胡哨的定时任务用Redisson改写,去掉Redis Message Queue(但定时任务最好还是用xxl-job等)。

Redisson分布式锁的缺陷

在哨兵模式或者主从模式下,如果master实例宕机,可能导致多个节点同时完成加锁。

以主从模式为例,由于所有的写操作都是先在master上进行,然后再同步给各个slave节点,所以master与各个slave节点之间的数据具有一定的延迟性。对于Redisson分布式锁而言,比如客户端刚对master写入Redisson锁,然后master异步复制给各个slave节点,但这个过程中master节点宕机了,其中一个slave节点经过选举变成了master节点,好巧不巧,这个slave还没同步到Reddison锁,所以其他客户端可能再次加锁。

具体情况,大家可以百度看看,解决方案也比较多。

还是那句话,但凡涉及到分布式,都没那么简单。有时引入一个解决方案后,我们不得不面对另一个问题。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

相关文章:

浅谈Redis分布式锁(下)

作者简介&#xff1a;大家好&#xff0c;我是smart哥&#xff0c;前中兴通讯、美团架构师&#xff0c;现某互联网公司CTO 联系qq&#xff1a;184480602&#xff0c;加我进群&#xff0c;大家一起学习&#xff0c;一起进步&#xff0c;一起对抗互联网寒冬 自定义Redis分布式锁的…...

Django Rest Framework框架的安装

Django Rest Framework框架的安装 Django Rest Framework框架的安装 1.DRF简介2.安装依赖3.安装使用pip安装添加rest_framework应用 1.DRF简介 Django REST Framework是Web api的工具包。它是在Django框架基础之上&#xff0c;进行了二次开发。 2.安装依赖 链接python安装 …...

深度学习(七):bert理解之输入形式

传统的预训练方法存在一些问题&#xff0c;如单向语言模型的局限性和无法处理双向上下文的限制。为了解决这些问题&#xff0c;一种新的预训练方法随即被提出&#xff0c;即BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;。通过在大规模…...

如何用Excel制作一张能在网上浏览的动态数据报表

前言 如今各类BI产品大行其道&#xff0c;“数据可视化”成为一个热门词汇。相比价格高昂的各种BI软件&#xff0c;用Excel来制作动态报表就更加经济便捷。今天小编就将为大家介绍一下如何使用葡萄城公司的纯前端表格控件——SpreadJS来实现一个Excel动态报表&#xff1a; 实…...

双向数据绑定是什么

一、什么是双向绑定 我们先从单向绑定切入单向绑定非常简单&#xff0c;就是把Model绑定到View&#xff0c;当我们用JavaScript代码更新Model时&#xff0c;View就会自动更新双向绑定就很容易联想到了&#xff0c;在单向绑定的基础上&#xff0c;用户更新了View&#xff0c;Mo…...

鱼眼标定方式

鱼眼作用 人单眼水平视角最大可达156度&#xff0c;垂直方向150度。为了增加可视范围&#xff0c;摄像头可以通过畸变参数扩大视野&#xff0c;一般100度到200度的fov。所以鱼眼是为了看的视野更大&#xff0c;注意在一定分辨率下&#xff0c;fov边缘的像素点稀疏&#xff0c;…...

详解Keras3.0 KerasNLP Models: GPT2 GPT2Tokenizer

1、GPT2Tokenizer 用于将文本数据转换为适合训练和预测的格式&#xff0c;主要功能是将输入的文本进行分词、编码等操作&#xff0c;以便在神经网络中使用 keras_nlp.models.GPT2Tokenizer(vocabulary, merges, **kwargs) 参数说明 vocabulary&#xff1a;一个字典&#x…...

2016年第五届数学建模国际赛小美赛B题直达地铁线路解题全过程文档及程序

2016年第五届数学建模国际赛小美赛 B题 直达地铁线路 原题再现&#xff1a; 在目前的大都市地铁网络中&#xff0c;在两个相距遥远的车站之间运送乘客通常需要很长时间。我们可以建议在两个长途车站之间设置直达班车&#xff0c;以节省长途乘客的时间。   第一部分&#xf…...

三秦通ETC续航改造

前些天开车时ETC每隔2分钟滴滴响一下&#xff0c;重插卡提示电池电压低 2.8V。看来应该是电池不行了。去银行更换ETC应该是需要费用的。还有一种办法是注销掉&#xff0c;然后去别的银行办一个。不过我想自己更换电池试一下。 首先拆下ETC&#xff0c;我使用的办法是开水烫。烧…...

使用Python实现发送Email电子邮件【第19篇—python发邮件】

文章目录 &#x1f47d;使用Python实现发送Email电子邮件&#x1f3b6;实现原理&#x1f3c3;Python实现发送Email电子邮件-基础版&#x1f46b;实现源码&#x1f646;源码解析 &#x1f487;Python实现发送Email电子邮件-完善版&#x1f46b;实现源码&#x1f646;源码解析&am…...

Docker基本命令和Docker怎么自己制作镜像

基本命令 启动新的容器&#xff08;指定容器名称和端口映射【主机端口&#xff1a;容器端口】) docker run --name 容器名 -p 8080:80 镜像名 启动新的容器&#xff08;交互式&#xff09; docker run -it centos7-with-jdk /bin/bash 特权方式启动容器 docker run -d --…...

Netty-2-数据编解码

解析编解码支持的原理 以编码为例&#xff0c;要将对象序列化成字节流&#xff0c;你可以使用MessageToByteEncoder或MessageToMessageEncoder类。 这两个类都继承自ChannelOutboundHandlerAdapter适配器类&#xff0c;用于进行数据的转换。 其中&#xff0c;对于MessageToMe…...

伽马校正:FPGA

参考资料&#xff1a; Tone Mapping 与 Gamma Correction - 知乎 (zhihu.com) Book_VIP: 《基于MATLAB与FPGA的图像处理教程》此书是业内第一本基于MATLAB与FPGA的图像处理教程&#xff0c;第一本真正结合理论及算法加速方案&#xff0c;在Matlab验证&#xff0c;以及在FPGA上…...

【SpringCloud笔记】(8)服务网关之GateWay

GateWay 概述简介 官网地址&#xff1a; 上一代网关Zuul 1.x&#xff1a;https://github.com/Netflix/zuul/wiki&#xff08;有兴趣可以了解一下&#xff09; gateway&#xff1a;https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.2.1.RELEASE/reference/…...

Compose常用布局

Compose布局基础知识 上一节对Compose做了简单的介绍&#xff0c;本章节主要介绍Compose中常用的布局&#xff0c;其中包括三个基础布局&#xff08;Colmun、Row、Box&#xff09;&#xff1b;以及其他常用布局&#xff08;ConstraintLayout 、BoxWithConstraints、HorizontalP…...

使用keytool查看Android APK签名

文章目录 一、找到JDK位置二、使用方法2.1 打开windows命令行工具2.2 查看签名 三、如何给APK做系统签名呢? 一、找到JDK位置 安卓AS之后&#xff0c;可选择继续安装JDK&#xff0c;如本文使用amazon版本默认位置&#xff1a;C:\Users\66176.jdks\corretto-1.8.0_342可通过自…...

数据库学习日常案例20231221-oracle libray cache lock分析

1 问题概述&#xff1a; 阻塞的源头为两个ddl操作导致大量的libray cache lock 其中1133为gis sde的create table as语句。 其中697为alter index语句。...

【数据结构】最短路径算法实现(Dijkstra(迪克斯特拉),FloydWarshall(弗洛伊德) )

文章目录 前言一、Dijkstra&#xff08;迪克斯特拉&#xff09;1.方法&#xff1a;2.代码实现 二、FloydWarshall&#xff08;弗洛伊德&#xff09;1.方法2.代码实现 完整源码 前言 最短路径问题&#xff1a;从在带权有向图G中的某一顶点出发&#xff0c;找出一条通往另一顶点…...

算法模板之队列图文详解

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;算法模板、数据结构 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. ⛳️模拟队列1.1 &#x1f514;用数组模拟实现队列1.1.1 &#x1f47b;队列的定…...

[node]Node.js 中REPL简单介绍

[node]Node.js 中REPL简单介绍 什么是REPL为什么使用REPL如何使用REPL 命令REPL模式node的全局内容展示node全局所有模块查看全局模块具体内容其它命令 实践 什么是REPL Node.js REPL(Read Eval Print Loop:交互式解释器) 表示电脑的环境&#xff0c;类似 Windows 系统的终端或…...

AtomHub 开源容器镜像中心开放公测,国内服务稳定下载

由开放原子开源基金会主导&#xff0c;华为、浪潮、DaoCloud、谐云、青云、飓风引擎以及 OpenSDV 开源联盟、openEuler 社区、OpenCloudOS 社区等成员单位共同发起建设的 AtomHub 可信镜像中心正式开放公测。AtomHub 秉承共建、共治、共享的理念&#xff0c;旨在为开源组织和开…...

java8实战 lambda表达式、函数式接口、方法引用双冒号(中)

前言 书接上文&#xff0c;上一篇博客讲到了lambda表达式的应用场景&#xff0c;本篇接着将java8实战第三章的总结。建议读者先看第一篇博客 其他函数式接口例子 上一篇有讲到Java API也有其他的函数式接口&#xff0c;书里也举了2个例子&#xff0c;一个是java.util.functi…...

FPGA高端项目:UltraScale GTH + SDI 视频编解码,SDI无缓存回环输出,提供2套工程源码和技术支持

目录 1、前言免责声明 2、相关方案推荐我这里已有的 GT 高速接口解决方案我目前已有的SDI编解码方案 3、详细设计方案设计框图3G-SDI摄像头LMH0384均衡EQUltraScale GTH 的SDI模式应用UltraScale GTH 基本结构参考时钟的选择和分配UltraScale GTH 发送和接收处理流程UltraScale…...

为什么react call api in cDidMount

为什么react call api in cDM 首先&#xff0c;放到constructor或者cWillMount不是语法错误 参考1 参考2 根据上2个参考&#xff0c;总结为&#xff1a; 1、官网就是这么建议的&#xff1a; 2、17版本后的react 由于fiber的出现导致 cWM 会调用多次&#xff01; cWM 方法已…...

openGauss学习笔记-171 openGauss 数据库运维-备份与恢复-导入数据-深层复制

文章目录 openGauss学习笔记-171 openGauss 数据库运维-备份与恢复-导入数据-深层复制171.1 使用CREATE TABLE执行深层复制171.1.1 操作步骤 171.2 使用CREATE TABLE LIKE执行深层复制171.2.1 操作步骤 171.3 通过创建临时表并截断原始表来执行深层复制171.3.1 操作步骤 openGa…...

[kubernetes]控制平面ETCD

什么是ETCD CoreOS基于Raft开发的分布式key-value存储&#xff0c;可用于服务发现、共享配置以及一致性保障&#xff08;如数据库选主、分布式锁等&#xff09;etcd像是专门为集群环境的服务发现和注册而设计&#xff0c;它提供了数据TTL失效、数据改变监视、多值、目录监听、…...

序列化类的高级用法

1.3.3 模型类序列化器 如果我们想要使用序列化器对应的是Django的模型类&#xff0c;DRF为我们提供了ModelSerializer模型类序列化器来帮助我们快速创建一个Serializer类。 ModelSerializer与常规的Serializer相同&#xff0c;但提供了&#xff1a; 基于模型类自动生成一系列…...

4.svn版本管理工具使用

1. 什么是SVN 版本控制 它可以记录每一次文件和目录的修改情况,这样就可以借此将数据恢复到以前的版本,并可以查看数据的更改细节! Subversion(简称SVN)是一个自由开源的版本控制系统。在Subversion管理下,文件和目录可以超越时空 SVN的优势 统一的版本号 Subversi…...

ZKP Algorithms for Efficient Cryptographic Operations 1 (MSM Pippenger)

MIT IAP 2023 Modern Zero Knowledge Cryptography课程笔记 Lecture 6: Algorithms for Efficient Cryptographic Operations (Jason Morton) Multi-scalar Multiplication(MSM) Naive: nP (((P P) P) P)… (2(2P))…Binary expand $n e_0e_1\alphae_2\alpha2\dots\e_{\…...

Windows系统安装 ffmpeg

下载及解压 ffmpeg官方下载地址&#xff1a;https://ffmpeg.org/download.html 下载好后将其解压至你想保存的位置中。 环境变量设置 打开Windows设置&#xff0c;在搜索框输入&#xff1a;系统高级设置。 新建环境变量&#xff0c;并输入bin目录具体位置。 安装检查 按住 w…...

油猴脚本教程案例【键盘监听】-编写 ChatGPT 快捷键优化

文章目录 1. 元数据namenamespaceversiondescriptionauthormatchgranticon 2. 编写函数.1 函数功能2.1.1. input - 聚焦发言框2.1.2. stop - 取消回答2.1.3. newFunction - 开启新窗口2.1.4. scroll - 回到底部 3. 监听键盘事件3.1 监听X - 开启新对话3.2 监听Z - 取消回答3.3 …...

数据结构 | 查漏补缺

目录 数据的基本单位 冒泡排序 DFS和BFS中文 Prim 比较 中序线索二叉树 顺序栈 链栈 时间复杂度 循环队列 求第K个结点的值 数据的基本单位 数据元素 循环队列sq中&#xff0c;用数组elem[0‥25]存放数据元素&#xff0c;设当前sq->front为20&#xff0c;sq-&g…...

回溯算法练习题

78. 子集 中等 1.9K 相关企业 给你一个整数数组 nums &#xff0c;数组中的元素 互不相同 。返回该数组所有可能的子集&#xff08;幂集&#xff09;。 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。 示例 1&#xff1a; 输入&#xff1a;nums [1,2,3] 输出&#x…...

代码随想录算法训练营 | day60 单调栈 84.柱状图中最大的矩形

刷题 84.柱状图中最大的矩形 题目链接 | 文章讲解 | 视频讲解 题目&#xff1a;给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 1 < heights.len…...

vscode中vue项目报错

当在vscode中写代码时&#xff0c;报错报错报错......... 已经头大&#xff0c;还没写就报错&#xff0c; 这是因为eslint对语法的要求太过严格导致的编译时&#xff0c;出现各种语法格式错误 我们打开vue.config.js&#xff0c;加上这句代码&#xff0c;就OK啦 lintOnSave:…...

「数据结构」二叉树2

&#x1f387;个人主页&#xff1a;Ice_Sugar_7 &#x1f387;所属专栏&#xff1a;初阶数据结构 &#x1f387;欢迎点赞收藏加关注哦&#xff01; 文章目录 &#x1f349;前言&#x1f349;链式结构&#x1f349;遍历二叉树&#x1f34c;前序遍历&#x1f34c;中序遍历&#x…...

数据处理系列课程 01:谈谈数据处理在数据分析中的重要性

一、数据分析 可能很多朋友第一次听到这个名词&#xff0c;那么我们先来谈一谈什么是数据分析。 数据分析是指用适当的统计分析方法对收集来的大量数据进行分析&#xff0c;将它们加以汇总和理解&#xff0c;以求最大化地开发数据的功能&#xff0c;发挥数据的作用。数据分析是…...

C++卡码网题目55--右旋字符串

卡码网题目链接 字符串的右旋转操作是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k&#xff0c;请编写一个函数&#xff0c;将字符串中的后面 k 个字符移到字符串的前面&#xff0c;实现字符串的右旋转操作。 例如&#xff0c;对于输入字符…...

八股文打卡day8——计算机网络(8)

面试题&#xff1a;什么是强缓存和协商缓存&#xff1f; 我的回答&#xff1a; 强缓存&#xff1a;浏览器不需要发送请求到服务器&#xff0c;直接从浏览器缓存中获取数据。浏览器不需要和服务器进行交互就可以获取数据&#xff0c;这样极大提高了页面访问速度。 协商缓存&am…...

亚马逊推出 Graviton4:具有 536.7 GBps 内存带宽的 96 核 ARM CPU

如今&#xff0c;许多云服务提供商都设计自己的芯片&#xff0c;但亚马逊网络服务 (AWS) 开始领先于竞争对手&#xff0c;目前其子公司 Annapurna Labs 开发的处理器可以与 AMD 和英特尔的处理器竞争。本周&#xff0c;AWS 推出了 Graviton4 SoC&#xff0c;这是一款基于 ARM 的…...

跨域问题的解决

1.什么是跨域&#xff1f; 浏览器从一个域名的网页去请求另外一个域名的资源时&#xff0c;域名、端口或者协议不同都是跨域 2.跨域的解决方案 设置CORS响应头∶后端可以在HTTP响应头中添加相关的CORS标头&#xff0c;允许特定的源&#xff08;域名、协议、端口)访问资源。S…...

Typro+PicGo自动上传图片(图床配置)

文章目录 所需工具主要配置 TyproPicGo自动上传图片&#xff08;图床配置&#xff09; 使用Typro编写 的markdown(md)文件如果存在图片&#xff0c;并且想快速发布博文的话&#xff0c;常使用PiGO工具配置图床服务器来管理图片。 所需工具 TyporaPicGo(依赖Nodejs和插件super…...

uniapp实战 -- 个人信息维护(含选择图片 uni.chooseMedia,上传文件 uni.uploadFile,获取和更新表单数据)

效果预览 相关代码 页面–我的 src\pages\my\my.vue <!-- 个人资料 --><view class"profile" :style"{ paddingTop: safeAreaInsets!.top px }"><!-- 情况1&#xff1a;已登录 --><view class"overview" v-if"membe…...

企业如何建立价值评估体系?

企业绩效评价体系是指由一系列与绩效评价相关的评价制度、评价指标体系、评价方法、评价标准以及评价机构等形成的有机整体。企业的评价系统大致可以分为以下四个层次&#xff1a; 第一、岗位评价系统&#xff0c;主要针对不同岗位之间的评估。例如&#xff0c;企业中一般业务…...

华为安防监控摄像头

华为政企42 华为政企 目录 上一篇华为政企城市一张网研究报告下一篇华为全屋wifi6蜂鸟套装标准...

[node] Node.js 缓冲区Buffer

[node] Node.js 缓冲区Buffer 什么是BufferBuffer 与字符编码Buffer 的方法概览Buffer 的实例Buffer 的创建写入缓冲区从 Buffer 区读取数据将 Buffer 转换为 JSON 对象Buffer 的合并Buffer 的比较Buffer 的覆盖Buffer 的截取--sliceBuffer 的长度writeUIntLEwriteUIntBE 什么是…...

【ARM Cortex-M 系列 5 -- RT-Thread renesas/ra4m2-eco 移植编译篇】

文章目录 RT-Thread 移植编译篇编译os.environ 使用示例os.putenv使用示例python from 后指定路径 编译问题_POSIX_C_SOURCE 介绍编译结果 RT-Thread 移植编译篇 本文以瑞萨的ra4m2-eco 为例介绍如何下载rt-thread 及编译的设置。 RT-Thread 代码下载&#xff1a; git clone …...

功能强大的开源数据中台系统 DataCap 1.18.0 发布

推荐一套基于 SpringBoot 开发的简单、易用的开源权限管理平台&#xff0c;建议下载使用: https://github.com/devlive-community/authx 推荐一套为 Java 开发人员提供方便易用的 SDK 来与目前提供服务的的 Open AI 进行交互组件&#xff1a;https://github.com/devlive-commun…...

A Philosophy of Software Design 学习笔记

前言 高耦合&#xff0c;低内聚&#xff0c;降低复杂度&#xff1a;在软件迭代中&#xff0c;不关注软件系统结构&#xff0c;导致软件复杂度累加&#xff0c;软件缺乏系统设计&#xff0c;模块混乱&#xff0c;一旦需求增加、修改或者优化&#xff0c;改变的代价无法评估&…...

设计模式----解释器模式

一、简介 解释器模式使用频率并不高&#xff0c;通常用来构建一个简单语言的语法解释器&#xff0c;它只在一些非常特定的领域被用到&#xff0c;比如编译器、规则引擎、正则表达式、sql解析等。 解释器模式是行为型设计模式之一&#xff0c;它的原始定义为&#xff1a;用于定义…...