万字长文!不为人所知的分布式锁实现全都在这里了,2024年最新mysql高可用架构

虽然 SETNX 方式能够保证设置锁和过期时间的原子性,但是如果我们设置的过期时间比较短,而执行业务时间比较长,就会存在锁代码块失效的问题,失效后其他客户端也能获取到同样的锁,执行同样的业务,此时可能就会出现一些问题。

我们需要将过期时间设置得足够长,来保证以上问题不会出现,但是设置多长时间合理,也需要依具体业务来权衡。如果其他客户端必须要阻塞拿到锁,需要设计循环超时等待机制等问题,感觉还挺麻烦的是吧。

Spring企业集成模式实现分布式锁

除了使用Jedis客户端之外,完全可以直接用Spring官方提供的企业集成模式框架,里面提供了很多分布式锁的方式,Spring提供了一个统一的分布式锁抽象,具体实现目前支持:

  • Gemfire

  • Jdbc

  • Zookeeper

  • Redis

早期,分布式锁的相关代码存在于Spring Cloud的子项目Spring Cloud Cluster中,后来被迁到Spring Integration中。

Spring Integration 项目地址 :https://github.com/spring-projects/spring-integration

Spring强大之处在于此,对Lock分布式锁做了全局抽象。

抽象结构如下所示:

17179731-ec293918459dea7c.jpg

LockRegistry 作为顶层抽象接口:

/**

  • Strategy for maintaining a registry of shared locks

  • @author Oleg Zhurakousky

  • @author Gary Russell

  • @since 2.1.1

*/

@FunctionalInterface

public interface LockRegistry {

/**

  • Obtains the lock associated with the parameter object.

  • @param lockKey The object with which the lock is associated.

  • @return The associated lock.

*/

Lock obtain(Object lockKey);

}

定义的 obtain() 方法获得具体的 Lock 实现类,分别在对应的 XxxLockRegitry 实现类来创建。

RedisLockRegistry 里obtain()方法实现类为 RedisLock,RedisLock内部,在Springboot2.x(Spring5)版本中是通过SET + PEXIPRE 命令结合lua脚本实现的,在Springboot1.x(Spring4)版本中,是通过SETNX命令实现的。

ZookeeperLockRegistry 里obtain()方法实现类为 ZkLock,ZkLock内部基于 Apache Curator 框架实现的。

JdbcLockRegistry 里obtain()方法实现类为 JdbcLock,JdbcLock内部基于一张INT_LOCK数据库锁表实现的,通过JdbcTemplate来操作。

客户端使用方法:

private final String registryKey = “sb2”;

RedisLockRegistry lockRegistry = new RedisLockRegistry(getConnectionFactory(), this.registryKey);

Lock lock = lockRegistry.obtain(“foo”);

lock.lock();

try {

// doSth…

}

finally {

lock.unlock();

}

}

下面以目前最新版本的实现,说明加锁和解锁的具体过程。

RedisLockRegistry$RedisLock类lock()加锁流程:

17179731-3fd65ba396440abe.jpg

加锁步骤:

1)lockKey为registryKey:path,本例中为sb2:foo,客户端C1优先申请加锁。

2)执行lua脚本,get lockKey不存在,则set lockKey成功,值为clientid(UUID),过期时间默认60秒。

3)客户端C1同一个线程重复加锁,pexpire lockKey,重置过期时间为60秒。

4)客户端C2申请加锁,执行lua脚本,get lockKey已存在,并且跟已加锁的clientid不同,加锁失败

5)客户端C2挂起,每隔100ms再次尝试加锁。

RedisLock#lock()加锁源码实现:

17179731-d755b546e9247dfe.jpg

大家可以对照上面的流程图配合你理解。

@Override

public void lock() {

this.localLock.lock();

while (true) {

try {

while (!obtainLock()) {

Thread.sleep(100); //NOSONAR

}

break;

}

catch (InterruptedException e) {

/*

  • This method must be uninterruptible so catch and ignore

  • interrupts and only break out of the while loop when

  • we get the lock.

*/

}

catch (Exception e) {

this.localLock.unlock();

rethrowAsLockException(e);

}

}

}

// 基于Spring封装的RedisTemplate来操作的

private boolean obtainLock() {

Boolean success =

RedisLockRegistry.this.redisTemplate.execute(RedisLockRegistry.this.obtainLockScript,

Collections.singletonList(this.lockKey), RedisLockRegistry.this.clientId,

String.valueOf(RedisLockRegistry.this.expireAfter));

boolean result = Boolean.TRUE.equals(success);

if (result) {

this.lockedAt = System.currentTimeMillis();

}

return result;

}

执行的lua脚本代码:

private static final String OBTAIN_LOCK_SCRIPT =

“local lockClientId = redis.call(‘GET’, KEYS[1])\n” +

“if lockClientId == ARGV[1] then\n” +

" redis.call(‘PEXPIRE’, KEYS[1], ARGV[2])\n" +

" return true\n" +

“elseif not lockClientId then\n” +

" redis.call(‘SET’, KEYS[1], ARGV[1], ‘PX’, ARGV[2])\n" +

" return true\n" +

“end\n” +

“return false”;

RedisLockRegistry$RedisLock类unlock()解锁流程:

17179731-9a2d7483a2ac5fe7.jpg

RedisLock#unlock()源码实现:

@Override

public void unlock() {

if (!this.localLock.isHeldByCurrentThread()) {

throw new IllegalStateException("You do not own lock at " + this.lockKey);

}

if (this.localLock.getHoldCount() > 1) {

this.localLock.unlock();

return;

}

try {

if (!isAcquiredInThisProcess()) {

throw new IllegalStateException("Lock was released in the store due to expiration. " +

“The integrity of data protected by this lock may have been compromised.”);

}

if (Thread.currentThread().isInterrupted()) {

RedisLockRegistry.this.executor.execute(this::removeLockKey);

}

else {

removeLockKey();

}

if (LOGGER.isDebugEnabled()) {

LOGGER.debug("Released lock; " + this);

}

}

catch (Exception e) {

ReflectionUtils.rethrowRuntimeException(e);

}

finally {

this.localLock.unlock();

}

}

// 删除缓存Key

private void removeLockKey() {

if (this.unlinkAvailable) {

try {

RedisLockRegistry.this.redisTemplate.unlink(this.lockKey);

}

catch (Exception ex) {

LOGGER.warn("The UNLINK command has failed (not supported on the Redis server?); " +

“falling back to the regular DELETE command”, ex);

this.unlinkAvailable = false;

RedisLockRegistry.this.redisTemplate.delete(this.lockKey);

}

}

else {

RedisLockRegistry.this.redisTemplate.delete(this.lockKey);

}

}

unlock()解锁方法里发现,并不是直接就调用Redis的DEL命令删除Key,这也是在Springboot2.x版本中做的一个优化,Redis4.0版本以上提供了UNLINK命令。

换句话说,最新版本分布式锁实现,要求是Redis4.0以上版本才能使用。

看下Redis官网给出的一段解释:

This command is very similar to DEL: it removes the specified keys.

Just like DEL a key is ignored if it does not exist. However the

command performs the actual memory reclaiming in a different thread,

so it is not blocking, while DEL is. This is where the command name

comes from: the command just unlinks the keys from the keyspace. The

actual removal will happen later asynchronously.

DEL始终在阻止模式下释放值部分。但如果该值太大,如对于大型LIST或HASH的分配太多,它会长时间阻止Redis,为了解决这个问题,Redis实现了UNLINK命令,即「非阻塞」删除。如果值很小,则DEL一般与UNLINK效率上差不多。

本质上,这种加锁方式还是使用的SETNX实现的,而且Spring只是做了一层薄薄的封装,支持可重入加锁,超时等待,可中断加锁。

但是有个问题,锁的过期时间不能灵活设置,客户端初始化时,创建RedisLockRegistry时允许设置,但是是全局的。

/**

  • Constructs a lock registry with the supplied lock expiration.

  • @param connectionFactory The connection factory.

  • @param registryKey The key prefix for locks.

  • @param expireAfter The expiration in milliseconds.

*/

public RedisLockRegistry(RedisConnectionFactory connectionFactory, String registryKey, long expireAfter) {

Assert.notNull(connectionFactory, “‘connectionFactory’ cannot be null”);

Assert.notNull(registryKey, “‘registryKey’ cannot be null”);

this.redisTemplate = new StringRedisTemplate(connectionFactory);

this.obtainLockScript = new DefaultRedisScript<>(OBTAIN_LOCK_SCRIPT, Boolean.class);

this.registryKey = registryKey;

this.expireAfter = expireAfter;

}

expireAfter参数是全局的,同样会存在问题,可能是锁过期时间到了,但是业务还没有处理完,这把锁又被另外的客户端获得,进而会导致一些其他问题。

经过对源码的分析,其实我们也可以借鉴RedisLockRegistry实现的基础上,自行封装实现分布式锁,比如:

1、允许支持按照不同的Key设置过期时间,而不是全局的?

2、当业务没有处理完成,当前客户端启动个定时任务探测,自动延长过期时间?

自己实现?嫌麻烦?别急别急!业界已经有现成的实现方案了,那就是 Redisson 框架!

站在Redis集群角度看问题

从Redis主从架构上来考虑,依然存在问题。因为 Redis 集群数据同步到各个节点时是异步的,如果在 Master 节点获取到锁后,在没有同步到其它节点时,Master 节点崩溃了,此时新的 Master 节点依然可以获取锁,所以多个应用服务可以同时获取到锁。

基于以上的考虑,Redis之父Antirez提出了一个RedLock算法

RedLock算法实现过程分析:

假设Redis部署模式是Redis Cluster,总共有5个master节点,通过以下步骤获取一把锁:

1)获取当前时间戳,单位是毫秒

2)轮流尝试在每个master节点上创建锁,过期时间设置较短,一般就几十毫秒

3)尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)

4)客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了

5)要是锁建立失败了,那么就依次删除这个锁

6)只要有客户端创建成功了分布式锁,其他客户端就得不断轮询去尝试获取锁

以上过程前文也提到了,进一步分析RedLock算法的实现依然可能存在问题,也是Martain和Antirez两位大佬争论的焦点。

问题1:节点崩溃重启

节点崩溃重启,会出现多个客户端持有锁。

假设一共有5个Redis节点:A、B、 C、 D、 E。设想发生了如下的事件序列:

1)客户端C1成功对Redis集群中A、B、C三个节点加锁成功(但D和E没有锁住)。

2)节点C Duang的一下,崩溃重启了,但客户端C1在节点C加锁未持久化完,丢了。

3)节点C重启后,客户端C2成功对Redis集群中C、D、 E尝试加锁成功了。

这样,悲剧了吧!客户端C1和C2同时获得了同一把分布式锁。

为了应对节点重启引发的锁失效问题,Antirez提出了延迟重启的概念,即一个节点崩溃后,先不立即重启它,而是等待一段时间再重启,等待的时间大于锁的有效时间。

采用这种方式,这个节点在重启前所参与的锁都会过期,它在重启后就不会对现有的锁造成影响。

这其实也是通过人为补偿措施,降低不一致发生的概率。

问题2:时钟跳跃

假设一共有5个Redis节点:A、B、 C、 D、 E。设想发生了如下的事件序列:

1)客户端C1成功对Redis集群中A、B、 C三个节点成功加锁。但因网络问题,与D和E通信失败。

2)节点C上的时钟发生了向前跳跃,导致它上面维护的锁快速过期。

3)客户端C2对Redis集群中节点C、 D、 E成功加了同一把锁。

此时,又悲剧了吧!客户端C1和C2同时都持有着同一把分布式锁。

为了应对时钟跳跃引发的锁失效问题,Antirez提出了应该禁止人为修改系统时间,使用一个不会进行「跳跃式」调整系统时钟的ntpd程序。这也是通过人为补偿措施,降低不一致发生的概率。

但是…,RedLock算法并没有解决,操作共享资源超时,导致锁失效的问题。

存在这么大争议的算法实现,还是不推荐使用的。

一般情况下,本文锁介绍的框架提供的分布式锁实现已经能满足大部分需求了。

小结:

上述,我们对spring-integration-redis实现原理进行了深入分析,还对RedLock存在争议的问题做了分析。

除此以外,我们还提到了spring-integration中集成了 Jdbc、Zookeeper、Gemfire实现的分布式锁,Gemfire和Jdbc大家感兴趣可以自行去看下。

为啥还要提供个Jdbc分布式锁实现?

猜测一下,当你的应用并发量也不高,比如是个后台业务,而且还没依赖Zookeeper、Redis等额外的组件,只依赖了数据库。

但你还想用分布式锁搞点事儿,那好办,直接用spring-integration-jdbc即可,内部也是基于数据库行锁来实现的,需要你提前建好锁表,创建表的SQL长这样:

CREATE TABLE INT_LOCK (

LOCK_KEY CHAR(36) NOT NULL,

REGION VARCHAR(100) NOT NULL,

CLIENT_ID CHAR(36),

CREATED_DATE DATETIME(6) NOT NULL,

constraint INT_LOCK_PK primary key (LOCK_KEY, REGION)

) ENGINE=InnoDB;

具体实现逻辑也非常简单,大家自己去看吧。

集成的Zookeeper实现的分布式锁,因为是基于Curator框架实现的,不在本节展开,后续会有分析。

基于Redisson实现分布式锁

Redisson 是 Redis 的 Java 实现的客户端,其 API 提供了比较全面的 Redis 命令的支持。

Jedis 简单使用阻塞的 I/O 和 Redis 交互,Redission 通过 Netty 支持非阻塞 I/O。

Redisson 封装了锁的实现,让我们像操作我们的本地 Lock 一样去使用,除此之外还有对集合、对象、常用缓存框架等做了友好的封装,易于使用。

截止目前,Github上 Star 数量为 11.8k,说明该开源项目值得关注和使用。

Redisson分布式锁Github:

https://github.com/redisson/redisson/wiki/8.-Distributed-locks-and-synchronizers

Redisson 可以便捷的支持多种Redis部署架构:

  1. Redis 单机

  2. Master-Slave + Sentinel 哨兵

  3. Redis-Cluster集群

// Master-Slave配置

Config config = new Config();

MasterSlaveServersConfig serverConfig = config.useMasterSlaveServers()

.setMasterAddress(“”)

.addSlaveAddress(“”)

.setReadMode(ReadMode.SLAVE)

.setMasterConnectionPoolSize(maxActiveSize)

.setMasterConnectionMinimumIdleSize(maxIdleSize)

.setSlaveConnectionPoolSize(maxActiveSize)

.setSlaveConnectionMinimumIdleSize(maxIdleSize)

.setConnectTimeout(CONNECTION_TIMEOUT_MS) // 默认10秒

.setTimeout(socketTimeout)

;

RedissonClient redisson = Redisson.create(config);

RLock lock = redisson.getLock(“myLock”);

// 获得锁

lock.lock();

// 等待10秒未获得锁,自动释放

lock.lock(10, TimeUnit.SECONDS);

// 等待锁定时间不超过100秒

// 10秒后自动释放锁

boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);

if (res) {

try {

} finally {

lock.unlock();

}

}

使用上非常简单,RedissonClient客户端提供了众多的接口实现,支持可重入锁、、公平锁、读写锁、锁超时、RedLock等都提供了完整实现。

lock()加锁流程:

为了兼容老的版本,Redisson里都是通过lua脚本执行Redis命令的,同时保证了原子性操作。

加锁执行的lua脚本:

17179731-94d6fdec3c62cfeb.jpg

Redis里的Hash散列结构存储的。

参数解释:

KEY[1]:要加锁的Key名称,比如示例中的myLock。

ARGV[1]:针对加锁的Key设置的过期时间

ARGV[2]:Hash结构中Key名称,lockName为UUID:线程ID

protected String getLockName(long threadId) {

return id + “:” + threadId;

}

1)客户端C1申请加锁,key为myLock。

2)如果key不存在,通过hset设置值,通过pexpire设置过期时间。同时开启Watchdog任务,默认每隔10秒中判断一下,如果key还在,重置过期时间到30秒。

开启WatchDog源码:

17179731-0bfb163b97def419.jpg

17179731-bd458bc628afd3ae.jpg

3)客户端C1相同线程再次加锁,如果key存在,判断Redis里Hash中的lockName跟当前线程lockName相同,则将Hash中的lockName的值加1,代表支持可重入加锁。

4)客户单C2申请加锁,如果key存在,判断Redis里Hash中的lockName跟当前线程lockName不同,则执行pttl返回剩余过期时间。

5)客户端C2线程内不断尝试pttl时间,此处是基于Semaphore信号量实现的,有许可立即返回,否则等到pttl时间还是没有得到许可,继续重试。

重试源码:

17179731-929a7b51e3515239.jpg

Redisson这样的实现就解决了,当业务处理时间比过期时间长的问题。

同时,Redisson 还自己扩展 Lock 接口,叫做 RLock 接口,扩展了很多的锁接口,比如给 Key 设定过期时间,非阻塞+超时时间等。

void lock(long leaseTime, TimeUnit unit);

boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;

redisson里的WatchDog(看门狗)逻辑保证了没有死锁发生。

如果客户端宕机了,WatchDog任务也就跟着停掉了。此时,不会对Key重置过期时间了,等挂掉的客户端持有的Key过期时间到了,锁自动释放,其他客户端尝试获得这把锁。

可以进一步看官网的关于WatchDog描述:

If Redisson instance which acquired lock crashes then such lock could hang forever in acquired state. To avoid this Redisson maintains lock watchdog, it prolongs lock expiration while lock holder Redisson instance is alive. By default lock watchdog timeout is 30 seconds and can be changed through Config.lockWatchdogTimeout setting.

unlock()解锁过程也是同样的,通过lua脚本执行一大坨指令的。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

总结:绘上一张Kakfa架构思维大纲脑图(xmind)

image

其实关于Kafka,能问的问题实在是太多了,扒了几天,最终筛选出44问:基础篇17问、进阶篇15问、高级篇12问,个个直戳痛点,不知道如果你不着急看答案,又能答出几个呢?

若是对Kafka的知识还回忆不起来,不妨先看我手绘的知识总结脑图(xmind不能上传,文章里用的是图片版)进行整体架构的梳理

梳理了知识,刷完了面试,如若你还想进一步的深入学习解读kafka以及源码,那么接下来的这份《手写“kafka”》将会是个不错的选择。

  • Kafka入门

  • 为什么选择Kafka

  • Kafka的安装、管理和配置

  • Kafka的集群

  • 第一个Kafka程序

  • Kafka的生产者

  • Kafka的消费者

  • 深入理解Kafka

  • 可靠的数据传递

  • Spring和Kafka的整合

  • SpringBoot和Kafka的整合

  • Kafka实战之削峰填谷

  • 数据管道和流式处理(了解即可)

image

image

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-UjzgIdef-1712695188623)]

总结:绘上一张Kakfa架构思维大纲脑图(xmind)

[外链图片转存中…(img-zmgiFZ6o-1712695188623)]

其实关于Kafka,能问的问题实在是太多了,扒了几天,最终筛选出44问:基础篇17问、进阶篇15问、高级篇12问,个个直戳痛点,不知道如果你不着急看答案,又能答出几个呢?

若是对Kafka的知识还回忆不起来,不妨先看我手绘的知识总结脑图(xmind不能上传,文章里用的是图片版)进行整体架构的梳理

梳理了知识,刷完了面试,如若你还想进一步的深入学习解读kafka以及源码,那么接下来的这份《手写“kafka”》将会是个不错的选择。

  • Kafka入门

  • 为什么选择Kafka

  • Kafka的安装、管理和配置

  • Kafka的集群

  • 第一个Kafka程序

  • Kafka的生产者

  • Kafka的消费者

  • 深入理解Kafka

  • 可靠的数据传递

  • Spring和Kafka的整合

  • SpringBoot和Kafka的整合

  • Kafka实战之削峰填谷

  • 数据管道和流式处理(了解即可)

[外链图片转存中…(img-6daLDKBm-1712695188624)]

[外链图片转存中…(img-4YHXJdCY-1712695188624)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-2cONkRJG-1712695188624)]

  • 20
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值