>Java >java지도 시간 >Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

Java后端技术全栈
Java后端技术全栈앞으로
2023-08-23 14:54:26715검색

이 글의 주요 내용은 다음과 같습니다.

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

1. 로컬 잠금 문제

우선 로컬 잠금 문제를 검토해 보겠습니다.

현재 마이크로서비스는 질문은 4개의 마이크로서비스로 나누어집니다. 프런트엔드 요청이 들어오면 다른 마이크로서비스로 전달됩니다. 프런트 엔드가 10W 요청을 받고 각 마이크로서비스가 2.5W 요청을 받는 경우, 캐시가 실패하면 각 마이크로서비스는 잠금(동기화 또는 잠금) 잠금 캐시 분석. synchronziedlock)来锁住自己的线程资源,从而防止缓存击穿

这是一种本地加锁的方式,在分布式이것은 로컬 잠금, 분산은 데이터 불일치 문제를 야기합니다. 예를 들어 서비스 A가 데이터를 얻은 후 캐시 키 = 100을 업데이트하지만 서비스 B는 그렇지 않습니다. 서비스 A의 잠금에 의해 제한되고 동시에 캐시 키 = 99를 업데이트합니다. 최종 결과는 99 또는 100일 수 있지만 이는 알 수 없는 상태이며 예상 결과와 일치하지 않습니다. 흐름도는 다음과 같습니다.

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

2. 분산 잠금이란 무엇입니까

위의 로컬 잠금 문제를 기반으로 분산 클러스터 환경을 지원하는 잠금이 필요합니다. DB를 쿼리할 때 하나의 스레드만 액세스할 수 있고 다른 스레드는 실행을 계속하기 전에 첫 번째 스레드가 잠금 리소스를 해제할 때까지 기다려야 합니다.

사례: 자물쇠는 문 밖에 있는 자물쇠라고 생각하시면 됩니다,所有并发线程比作 모두가 방에 들어가고 싶어하는데 한 사람만 들어갈 수 있습니다. 누군가 들어오면 문을 잠그고 다른 사람은 들어온 사람이 나올 때까지 기다려야 합니다.

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

아래 그림과 같이 분산 잠금의 기본 원리를 살펴보겠습니다.

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

위 그림의 분산 잠금을 분석해 보겠습니다.

  • 1. 프런트엔드는 10W의 높은 동시성 요청을 4개의 주제 마이크로서비스로 전달합니다.
  • 2. 각 마이크로서비스는 2.5W 요청을 처리합니다.
  • 3. 요청을 처리하는 각 스레드는 비즈니스를 실행하기 전에 잠금을 확보해야 합니다. '구덩이를 점유하다'로 이해될 수 있다.
  • 4. 잠금을 획득한 스레드는 업무 완료 후 잠금을 해제합니다. 그것은 "구덩이를 풀어준다"는 뜻으로 이해될 수 있다.
  • 5. 획득하지 못한 스레드는 잠금이 해제될 때까지 기다려야 합니다.
  • 6. 잠금이 해제된 후 다른 스레드가 잠금을 점유합니다.
  • 7. 4, 5, 6단계를 반복합니다.

현지 설명: 요청된 모든 스레드는 같은 장소로 이동"구덩이를 차지하세요" , 피트가 있으면 비즈니스 로직이 실행됩니다. 피트가 없으면 다른 스레드가 "피트"를 해제해야 합니다. 이 피트는 모든 스레드에 표시됩니다. 이 문서는 Redis를 사용하여 "분산 피트". “占坑”,如果有坑位,就执行业务逻辑,没有坑位,就需要其他线程释放“坑位”。这个坑位是所有线程可见的,可以把这个坑位放到 Redis 缓存或者数据库,这篇讲的就是如何用 Redis 做“分布式坑位”

三、Redis 的 SETNX

Redis 作为一个公共可访问的地方,正好可以作为“占坑”的地方。

用 Redis 实现分布式锁的几种方案,我们都是用 SETNX 命令(设置 key 等于某 value)。只是高阶方案传的参数个数不一样,以及考虑了异常情况。

我们来看下这个命令,SETNXset If not exist的简写。意思就是当 key 不存在时,设置 key 的值,存在时,什么都不做。

在 Redis 命令行中是这样执行的:

set <key> <value> NX

我们可以进到 redis 容器中来试下 SETNX

3. Redis의 SETNX

Redis는 공개적으로 접근 가능한 장소로서 "활용"할 수 있는 장소로 사용될 수 있습니다.

Redis를 사용하여 분산 잠금을 구현하기 위한 여러 솔루션은 모두 SETNX 명령(특정 값과 동일한 키 설정)을 사용합니다. 상위 수준 체계에서 전달되는 매개변수의 개수만 다르며 비정상적인 상황도 고려됩니다. 🎜🎜이 명령을 살펴보겠습니다. SETNX존재하지 않는 경우 집합의 단축. 즉, 키가 존재하지 않으면 키의 값을 설정하고, 존재하면 아무것도 하지 않는다는 의미입니다. 🎜🎜Redis 명령줄에서 실행되는 방법은 다음과 같습니다. 🎜
docker exec -it <容器 id> redis-cli
🎜redis 컨테이너로 가서 SETNX 명령. 🎜🎜컨테이너를 먼저 입력하세요: 🎜
docker exec -it <容器 id> redis-cli

然后执行 SETNX 命令:将 wukong 这个 key 对应的 value 设置成 1111

set wukong 1111 NX

返回 OK,表示设置成功。重复执行该命令,返回 nil表示设置失败。

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

四、青铜方案

我们先用 Redis 的 SETNX 命令来实现最简单的分布式锁。

3.1 青铜原理

我们来看下流程图:

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획
  • 多个并发线程都去 Redis 中申请锁,也就是执行 setnx 命令,假设线程 A 执行成功,说明当前线程 A 获得了。
  • 其他线程执行 setnx 命令都会是失败的,所以需要等待线程 A 释放锁。
  • 线程 A 执行完自己的业务后,删除锁。
  • 其他线程继续抢占锁,也就是执行 setnx 命令。因为线程 A 已经删除了锁,所以又有其他线程可以抢占到锁了。

代码示例如下,Java 中 setnx 命令对应的代码为 setIfAbsent

setIfAbsent 方法的第一个参数代表 key,第二个参数代表值。

// 1.先抢占锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123");
if(lock) {
  // 2.抢占成功,执行业务
  List<TypeEntity> typeEntityListFromDb = getDataFromDB();
  // 3.解锁
  redisTemplate.delete("lock");
  return typeEntityListFromDb;
} else {
  // 4.休眠一段时间
  sleep(100);
  // 5.抢占失败,等待锁释放
  return getTypeEntityListByRedisDistributedLock();
}

一个小问题:那为什么需要休眠一段时间?

因为该程序存在递归调用,可能会导致栈空间溢出。

3.2 브론즈 솔루션의 단점

브론즈는 가장 기본적이고 분명 많은 문제를 일으킬 것이기 때문에 브론즈라고 불립니다.

가족 장면을 상상해 보세요: 밤에 샤오콩이 혼자 문을 열고 방에 들어가 불을 켰나요? 그런데 갑자기 전원이 나갔습니다. Xiao Kong이 문을 열고 나가고 싶어하지만 문 잠금 장치 위치를 찾을 수 없습니다. 그런 다음 Xiao Ming은 들어갈 수 없고 둘 다 들어갈 수 없습니다. 밖에 누구든지. 断电了,小空想开门出去,但是找不到门锁位置,那小明就进不去了,外面的人也进不来。

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

从技术的角度看:setnx 占锁成功,业务代码出现异常或者服务器宕机,没有执行删除锁的逻辑,就造成了死锁

那如何规避这个风险呢?

设置锁的自动过期时间,过一段时间后,自动删除锁,这样其他线程就能获取到锁了。

四、白银方案

4.1 生活中的例子

上面提到的青铜方案会有死锁问题,那我们就用上面的规避风险的方案来设计下,也就是我们的白银方案。

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

还是生活中的例子:小空开锁成功后,给这款智能锁设置了一个沙漏倒计时⏳

기술적인 관점에서: setnx가 잠금을 성공적으로 점유했을 때 비즈니스 코드가 비정상이거나 서버가 다운된 경우 잠금 삭제 로직이 실행되지 않아 교착 상태 코드>. <h3 data-tool="mdnice编辑器" style="font-weight: bold;font-size: 20px;line-height: 1.4;padding-top: 10px;margin-top: 10px;margin-bottom: 5px;"> <span style="color: rgb(81, 81, 81);font-size: 1em;padding-left: 20px;border-left: 3px solid rgb(249, 191, 69);"></span>그러면 이 위험을 피하는 방법은 무엇일까요? </h3> <p data-tool="mdnice编辑器" style="margin-bottom: 20px;line-height: 1.8em;color: rgb(58, 58, 58);"></p>잠금 설정<code style="font-size: 14px;border-radius: 4px;font-family: " operator mono consolas monaco menlo monospace break-all rgb background-color:>자동 만료 시간, 일정 시간이 지나면 잠금이 자동으로 삭제됩니다. , 그래서 다른 스레드가 잠금을 획득할 수 있습니다. 🎜

4. 실버 계획 🎜

🎜🎜4.1 실제 사례🎜🎜🎜 위에서 언급한 브론즈 계획에는 교착 문제가 있으므로 위의 위험 회피 계획을 사용하여 설계하겠습니다. 실버 계획. 🎜
🎜실생활의 예: Xiao Kong이 성공적으로 문을 잠금 해제한 후 , 이 스마트 잠금 장치에 대해 모래시계 카운트다운⏳, 모래시계가 끝나면 자동으로 문이 잠깁니다. 열려 있는. 방에 갑작스러운 정전이 발생하더라도 잠시 후 자동으로 자물쇠가 열리고 다른 사람이 들어올 수 있습니다. 🎜🎜🎜4.2 기술 도식 🎜🎜🎜 브론즈 솔루션과의 차이점은 자물쇠 점유에 성공한 후 자물쇠 만료 시간 설정이 단계별로 수행된다는 점입니다. 아래 그림과 같이: 🎜
Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

4.3 示例代码

清理 redis key 的代码如下

// 在 10s 以后,自动清理 lock
redisTemplate.expire("lock", 10, TimeUnit.SECONDS);

完整代码如下:

// 1.先抢占锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "123");
if(lock) {
    // 2.在 10s 以后,自动清理 lock
    redisTemplate.expire("lock", 10, TimeUnit.SECONDS);
    // 3.抢占成功,执行业务
    List<TypeEntity> typeEntityListFromDb = getDataFromDB();
    // 4.解锁
    redisTemplate.delete("lock");
    return typeEntityListFromDb;
}

4.4 白银方案的缺陷

白银方案看似解决了线程异常或服务器宕机造成的锁未释放的问题,但还是存在其他问题:

因为占锁和设置过期时间是分两步执行的,所以如果在这两步之间发生了异常,则锁的过期时间根本就没有设置成功。

所以和青铜方案有一样的问题:锁永远不能过期

五、黄金方案

5.1 原子指令

上面的白银方案中,占锁和设置锁过期时间是分步两步执行的,这个时候,我们可以联想到什么:事务的原子性(Atom)。

原子性:多条命令要么都成功执行,要么都不执行。

将两步放在一步中执行:占锁+设置锁过期时间。

Redis 正好支持这种操作:

# 设置某个 key 的值并设置多少毫秒或秒 过期。
set <key> <value> PX <多少毫秒> NX
或
set <key> <value> EX <多少秒> NX

然后可以通过如下命令查看 key 的变化

ttl <key>

下面演示下如何设置 key 并设置过期时间。注意:执行命令之前需要先删除 key,可以通过客户端或命令删除。

# 设置 key=wukong,value=1111,过期时间=5000ms
set wukong 1111 PX 5000 NX
# 查看 key 的状态
ttl wukong

执行结果如下图所示:每运行一次 ttl 命令,就可以看到 wukong 的过期时间就会减少。最后会变为 -2(已过期)。

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

5.2 技术原理图

黄金方案和白银方案的不同之处:获取锁的时候,也需要设置锁的过期时间,这是一个原子操作,要么都成功执行,要么都不执行。如下图所示:

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

5.3 示例代码

设置 lock 的值等于 123,过期时间为 10 秒。如果 10 秒 以后,lock 还存在,则清理 lock。

setIfAbsent("lock", "123", 10, TimeUnit.SECONDS);

5.4 黄金方案的缺陷

我们还是举生活中的例子来看下黄金方案的缺陷。

5.4.1 사용자 A가 잠금을 선점합니다

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획
  • 사용자 A가 먼저 잠금을 선점하고 10초 후에 자동으로 잠금이 해제되도록 설정했습니다. 잠금 번호는 123. 123
  • 10 秒以后,A 还在执行任务,此时锁被自动打开了。

5.4.2 用户 B 抢占锁

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획
  • 用户 B 看到房间的锁打开了,于是抢占到了锁,设置锁的编号为 123,并设置了过期时间 10 秒
  • 因房间内只允许一个用户执行任务,所以用户 A 和 用户 B 执行任务产生了冲突
  • 用户 A 在 15 s 后,完成了任务,此时 用户 B 还在执行任务。
  • 用户 A 主动打开了编号为 123
  • 10초 후에도 A는 여전히 작업을 수행하고 있으며 자물쇠는 자동으로 열립니다.
  • 5.4.2 사용자 B는 잠금 Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

를 압수합니다. Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획 사용자 B는 방의 자물쇠가 열려 있는 것을 보고 자물쇠를 탈취하고 자물쇠 번호를 123, 만료 시간 설정10초.
  • 방에서는 한 명의 사용자만 작업을 수행할 수 있으므로 사용자 A와 사용자 B가 작업을 수행합니다 갈등이 발생했습니다. 🎜🎜🎜🎜 15초 후에 작업이 완료되고 사용자 B는 아직 임무 중이야. 🎜🎜🎜🎜사용자 A가 주도적으로 123의 잠금. 🎜🎜🎜🎜사용자 B는 여전히 작업을 수행하고 있으며 잠금 장치가 열려 있음을 발견합니다. 🎜🎜🎜🎜사용자 B는 매우 화가났습니다. 🎜아직 작업을 완료하지 않았는데 왜 자물쇠가 열렸나요? 🎜🎜🎜🎜🎜5.4.3 사용자 C가 자물쇠를 탈취합니다🎜🎜🎜🎜🎜🎜🎜A가 사용자 B의 자물쇠를 적극적으로 연 후, B가 작업을 수행하는 동안 A는 방을 나갑니다.
  • 사용자 C가 잠금을 획득하고 C가 작업 실행을 시작합니다.
  • 방에서는 한 명의 사용자만 작업을 수행할 수 있으므로 사용자 B와 사용자 C가 수행하는 작업 간에 충돌이 발생합니다.

위의 경우를 보면 사용자 A가 작업을 처리하는 데 걸리는 시간이 자물쇠가 자동으로 해제(잠금 해제)되는 시간보다 길기 때문에 자물쇠가 자동으로 해제된 후, 다른 사용자가 잠금을 선점했습니다. 사용자 A가 작업을 완료하면 다른 사용자가 확보한 잠금을 적극적으로 엽니다.

왜 남의 자물쇠는 여기서 열려요? 잠금번호가 모두 호출되었기 때문에“123”,用户 A 只认锁编号,看见编号为 “123” 잠금이 열렸습니다. 이로 인해 사용자 B의 잠금이 열렸습니다. 이때 사용자 B는 작업을 완료하지 않았으므로 당연히 화를 냅니다.

6. 플래티넘 플랜

6.1 실생활의 예

위의 골드 플랜의 단점도 쉽게 해결할 수 있습니다. 자물쇠마다 다른 숫자를 설정하는 것이 좋습니다~

. 아래 그림에서는 B가 선점한 잠금이 파란색으로 표시되어 A가 선점한 녹색 잠금과 다릅니다. 이렇게 하면 A가 열 수 없습니다.

이해를 돕기 위해 애니메이션을 만들었습니다:

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획애니메이션 데모
정적 이미지가 더 고화질이므로 살펴볼 수 있습니다.

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

6.2 기술 회로도의 차이점

그리고 황금 해결책 :

  • 设置锁的过期时间时,还需要设置唯一编号。
  • 主动删除锁的时候,需要判断锁的编号是否和设置的一致,如果一致,则认为是自己设置的锁,可以进行主动删除。
Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

6.3 代码示例

// 1.生成唯一 id
String uuid = UUID.randomUUID().toString();
// 2. 抢占锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 10, TimeUnit.SECONDS);
if(lock) {
    System.out.println("抢占成功:" + uuid);
    // 3.抢占成功,执行业务
    List<TypeEntity> typeEntityListFromDb = getDataFromDB();
    // 4.获取当前锁的值
    String lockValue = redisTemplate.opsForValue().get("lock");
    // 5.如果锁的值和设置的值相等,则清理自己的锁
    if(uuid.equals(lockValue)) {
        System.out.println("清理锁:" + lockValue);
        redisTemplate.delete("lock");
    }
    return typeEntityListFromDb;
} else {
    System.out.println("抢占失败,等待锁释放");
    // 4.休眠一段时间
    sleep(100);
    // 5.抢占失败,等待锁释放
    return getTypeEntityListByRedisDistributedLock();
}
  • 1.生成随机唯一 id,给锁加上唯一值。
  • 2.抢占锁,并设置过期时间为 10 s,且锁具有随机唯一 id。
  • 3.抢占成功,执行业务。
  • 4.执行完业务后,获取当前锁的值。
  • 5.如果锁的值和设置的值相等,则清理自己的锁。

6.4 铂金方案的缺陷

上面的方案看似很完美,但还是存在问题:第 4 步和第 5 步并不是原子性的。

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획
  • 时刻:0s。线程 A 抢占到了锁。

  • 时刻:9.5s。线程 A 向 Redis 查询当前 key 的值。

  • 时刻:10s。锁自动过期。

  • 时刻:11s。线程 B 抢占到锁。

  • 时刻:12s。线程 A 在查询途中耗时长,终于拿多锁的值。

  • 时刻:13s。线程 A 还是拿自己设置的锁的值和返回的值进行比较,值是相等的,清理锁,但是这个锁其实是线程 B 抢占的锁。

那如何规避这个风险呢?钻石方案登场。

七、钻石方案

上面的线程 A 查询锁和删除锁的逻辑不是原子性的,所以将查询锁和删除锁这两步作为原子指令操作就可以了。

7.1 技术原理图

如下图所示,红色圈出来的部分是钻石方案的不同之处。用脚本进行删除,达到原子操作。

Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획

7.2 代码示例

那如何用脚本进行删除呢?

我们先来看一下这段 Redis 专属脚本:

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end

这段脚本和铂金方案的获取key,删除key的方式很像。先获取 KEYS[1] 的 value,判断 KEYS[1] 的 value 是否和 ARGV[1] 的值相等,如果相等,则删除 KEYS[1]。

那么这段脚本怎么在 Java 项目中执行呢?

分两步:先定义脚本;用 redisTemplate.execute 方法执行脚本。

// 脚本解锁
String script = "if redis.call(&#39;get&#39;,KEYS[1]) == ARGV[1] then return redis.call(&#39;del&#39;,KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

上面的代码中,KEYS[1] 对应“lock”,ARGV[1] 对应 “uuid”,含义就是如果 lock 的 value 等于 uuid 则删除 lock。

而这段 Redis 脚本是由 Redis 内嵌的 Lua 环境执行的,所以又称作 Lua 脚本。

那钻石方案是不是就完美了呢?有没有更好的方案呢?

下篇,我们再来介绍另外一种分布式锁的王者方案:Redisson。

8. 요약

이 기사에서는 로컬 잠금 문제를 통해 분산 잠금 문제를 확장합니다. 그런 다음 5가지 분산 잠금 솔루션을 소개하고 얕은 솔루션부터 깊은 솔루션까지 다양한 솔루션의 개선 사항을 설명합니다.

위 솔루션의 지속적인 발전을 통해 우리는 시스템에서 비정상적인 상황이 존재할 수 있는 위치와 이를 더 잘 처리하는 방법을 알고 있습니다.

유추하자면, 이 진화하는 사고 모델은 다른 기술에도 적용될 수 있습니다.

다음은 위 5가지 솔루션의 단점과 개선점을 요약한 것입니다.

Bronze Solution:

  • 결점: 비즈니스 코드가 비정상이거나 서버가 다운되어 적극적으로 잠금을 삭제하는 로직이 실행되지 않아 교착상태가 발생합니다.
  • 개선: 잠금의 자동 만료 시간을 설정하면 일정 시간이 지나면 다른 스레드가 잠금을 얻을 수 있도록 잠금이 자동으로 삭제됩니다.

실버 솔루션:

  • 결함: 잠금 점유 및 잠금 만료 시간 설정은 두 단계로 수행되며 원자적 작업이 아닙니다.
  • 개선: 잠금을 점유하고 잠금 만료 시간을 설정하면 원자적 작업이 보장됩니다.

Golden Plan:

  • 결함: 잠금이 적극적으로 삭제되면 잠금 값이 동일하기 때문에 다른 클라이언트가 차지한 잠금이 삭제됩니다.
  • 개선 사항: 잠금이 점유될 때마다 무작위로 더 큰 값으로 설정됩니다. 잠금이 적극적으로 삭제되면 잠금 값과 직접 설정한 값을 비교하여 동일한지 확인합니다.

Platinum 솔루션:

  • 결함: 잠금 획득, 잠금 값 비교, 잠금 삭제의 세 단계는 비원자적입니다. 잠금이 자동으로 중간에 만료되어 다른 클라이언트에 의해 점유되어 잠금이 삭제되면 다른 클라이언트가 차지한 잠금도 삭제될 수 있습니다.
  • 개선: Lua 스크립트를 사용하여 잠금 획득, 잠금 비교, 잠금 삭제 등의 원자적 작업을 수행합니다.

다이아몬드 플랜:

  • 결함: 비전문적인 분산 잠금 솔루션.
  • 개선: 재분배 잠금.

The King Plan, 다음 글에서 만나요~

위 내용은 Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 Java后端技术全栈에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제