이 글의 주요 내용은 다음과 같습니다.
우선 로컬 잠금 문제를 검토해 보겠습니다.
현재 마이크로서비스는 질문은 4개의 마이크로서비스로 나누어집니다. 프런트엔드 요청이 들어오면 다른 마이크로서비스로 전달됩니다. 프런트 엔드가 10W 요청을 받고 각 마이크로서비스가 2.5W 요청을 받는 경우, 캐시가 실패하면 각 마이크로서비스는 잠금(동기화
또는 잠금
) 잠금 캐시 분석
. synchronzied
或 lock
)来锁住自己的线程资源,从而防止缓存击穿
。
这是一种本地加锁
的方式,在分布式
이것은 로컬 잠금
, 분산
은 데이터 불일치 문제를 야기합니다. 예를 들어 서비스 A가 데이터를 얻은 후 캐시 키 = 100을 업데이트하지만 서비스 B는 그렇지 않습니다. 서비스 A의 잠금에 의해 제한되고 동시에 캐시 키 = 99를 업데이트합니다. 최종 결과는 99 또는 100일 수 있지만 이는 알 수 없는 상태이며 예상 결과와 일치하지 않습니다. 흐름도는 다음과 같습니다.
위의 로컬 잠금 문제를 기반으로 분산 클러스터 환경을 지원하는 잠금이 필요합니다. DB를 쿼리할 때 하나의 스레드만 액세스할 수 있고 다른 스레드는 실행을 계속하기 전에 첫 번째 스레드가 잠금 리소스를 해제할 때까지 기다려야 합니다.
사례: 자물쇠는 문 밖에 있는 자물쇠라고 생각하시면 됩니다锁
,所有并发线程比作人
모두가 방에 들어가고 싶어하는데 한 사람만 들어갈 수 있습니다. 누군가 들어오면 문을 잠그고 다른 사람은 들어온 사람이 나올 때까지 기다려야 합니다.
아래 그림과 같이 분산 잠금의 기본 원리를 살펴보겠습니다.
위 그림의 분산 잠금을 분석해 보겠습니다.
현지 설명: 요청된 모든 스레드는 같은 장소로 이동"구덩이를 차지하세요"
, 피트가 있으면 비즈니스 로직이 실행됩니다. 피트가 없으면 다른 스레드가 "피트"를 해제해야 합니다. 이 피트는 모든 스레드에 표시됩니다. 이 문서는 Redis를 사용하여 "분산 피트"
. “占坑”
,如果有坑位,就执行业务逻辑,没有坑位,就需要其他线程释放“坑位”。这个坑位是所有线程可见的,可以把这个坑位放到 Redis 缓存或者数据库,这篇讲的就是如何用 Redis 做“分布式坑位”
。
Redis 作为一个公共可访问的地方,正好可以作为“占坑”的地方。
用 Redis 实现分布式锁的几种方案,我们都是用 SETNX 命令(设置 key 等于某 value)。只是高阶方案传的参数个数不一样,以及考虑了异常情况。
我们来看下这个命令,SETNX
是set If not exist
的简写。意思就是当 key 不存在时,设置 key 的值,存在时,什么都不做。
在 Redis 命令行中是这样执行的:
set <key> <value> NX
我们可以进到 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 的 SETNX 命令来实现最简单的分布式锁。
我们来看下流程图:
代码示例如下,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(); }
一个小问题:那为什么需要休眠一段时间?
因为该程序存在递归调用,可能会导致栈空间溢出。
브론즈는 가장 기본적이고 분명 많은 문제를 일으킬 것이기 때문에 브론즈라고 불립니다.
가족 장면을 상상해 보세요: 밤에 샤오콩이 혼자 문을 열고 방에 들어가 불을 켰나요? 그런데 갑자기 전원이 나갔습니다
. Xiao Kong이 문을 열고 나가고 싶어하지만 문 잠금 장치 위치를 찾을 수 없습니다. 그런 다음 Xiao Ming은 들어갈 수 없고 둘 다 들어갈 수 없습니다. 밖에 누구든지. 断电
了,小空想开门出去,但是找不到门锁位置,那小明就进不去了,外面的人也进不来。
从技术的角度看: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.2 기술 도식 🎜🎜🎜 브론즈 솔루션과의 차이점은 자물쇠 점유에 성공한 후 자물쇠 만료 시간 설정이 단계별로 수행된다는 점입니다. 아래 그림과 같이: 🎜清理 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; }
白银方案看似解决了线程异常或服务器宕机造成的锁未释放的问题,但还是存在其他问题:
因为占锁和设置过期时间是分两步执行的,所以如果在这两步之间发生了异常,则锁的过期时间根本就没有设置成功。
所以和青铜方案有一样的问题:锁永远不能过期。
上面的白银方案中,占锁和设置锁过期时间是分步两步执行的,这个时候,我们可以联想到什么:事务的原子性(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(已过期)。
黄金方案和白银方案的不同之处:获取锁的时候,也需要设置锁的过期时间,这是一个原子操作,要么都成功执行,要么都不执行。如下图所示:
设置 lock
的值等于 123
,过期时间为 10 秒。如果 10
秒 以后,lock 还存在,则清理 lock。
setIfAbsent("lock", "123", 10, TimeUnit.SECONDS);
我们还是举生活中的例子来看下黄金方案的缺陷。
123
. 123
。123
,并设置了过期时间 10 秒
。产生了冲突
。15 s
后,完成了任务,此时 用户 B 还在执行任务。123
갈등이 발생했습니다
. 🎜🎜🎜🎜 15초
후에 작업이 완료되고 사용자 B는 아직 임무 중이야. 🎜🎜🎜🎜사용자 A가 주도적으로 123
의 잠금. 🎜🎜🎜🎜사용자 B는 여전히 작업을 수행하고 있으며 잠금 장치가 열려 있음을 발견합니다. 🎜🎜🎜🎜사용자 B는 매우 화가났습니다. 🎜아직 작업을 완료하지 않았는데 왜 자물쇠가 열렸나요? 🎜🎜🎜🎜🎜5.4.3 사용자 C가 자물쇠를 탈취합니다🎜🎜🎜🎜🎜🎜🎜A가 사용자 B의 자물쇠를 적극적으로 연 후, B가 작업을 수행하는 동안 A는 방을 나갑니다.위의 경우를 보면 사용자 A가 작업을 처리하는 데 걸리는 시간이 자물쇠가 자동으로 해제(잠금 해제)되는 시간보다 길기 때문에 자물쇠가 자동으로 해제된 후, 다른 사용자가 잠금을 선점했습니다. 사용자 A가 작업을 완료하면 다른 사용자가 확보한 잠금을 적극적으로 엽니다.
왜 남의 자물쇠는 여기서 열려요? 잠금번호가 모두 호출되었기 때문에“123”
,用户 A 只认锁编号,看见编号为 “123”
잠금이 열렸습니다. 이로 인해 사용자 B의 잠금이 열렸습니다. 이때 사용자 B는 작업을 완료하지 않았으므로 당연히 화를 냅니다.
위의 골드 플랜의 단점도 쉽게 해결할 수 있습니다. 자물쇠마다 다른 숫자를 설정하는 것이 좋습니다~
. 아래 그림에서는 B가 선점한 잠금이 파란색으로 표시되어 A가 선점한 녹색 잠금과 다릅니다. 이렇게 하면 A가 열 수 없습니다. 이해를 돕기 위해 애니메이션을 만들었습니다: 정적 이미지가 더 고화질이므로 살펴볼 수 있습니다.// 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(); }
上面的方案看似很完美,但还是存在问题:第 4 步和第 5 步并不是原子性的。
时刻:0s。线程 A 抢占到了锁。
时刻:9.5s。线程 A 向 Redis 查询当前 key 的值。
时刻:10s。锁自动过期。
时刻:11s。线程 B 抢占到锁。
时刻:12s。线程 A 在查询途中耗时长,终于拿多锁的值。
时刻:13s。线程 A 还是拿自己设置的锁的值和返回的值进行比较,值是相等的,清理锁,但是这个锁其实是线程 B 抢占的锁。
那如何规避这个风险呢?钻石方案登场。
上面的线程 A 查询锁和删除锁的逻辑不是原子性
的,所以将查询锁和删除锁这两步作为原子指令操作就可以了。
如下图所示,红色圈出来的部分是钻石方案的不同之处。用脚本进行删除,达到原子操作。
那如何用脚本进行删除呢?
我们先来看一下这段 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('get',KEYS[1]) == ARGV[1] then return redis.call('del',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。
이 기사에서는 로컬 잠금 문제를 통해 분산 잠금 문제를 확장합니다. 그런 다음 5가지 분산 잠금 솔루션을 소개하고 얕은 솔루션부터 깊은 솔루션까지 다양한 솔루션의 개선 사항을 설명합니다.
위 솔루션의 지속적인 발전을 통해 우리는 시스템에서 비정상적인 상황이 존재할 수 있는 위치와 이를 더 잘 처리하는 방법을 알고 있습니다.
유추하자면, 이 진화하는 사고 모델은 다른 기술에도 적용될 수 있습니다.
다음은 위 5가지 솔루션의 단점과 개선점을 요약한 것입니다.
Bronze Solution:
실버 솔루션:
Golden Plan:
Platinum 솔루션:
다이아몬드 플랜:
The King Plan, 다음 글에서 만나요~
위 내용은 Redis 분산 잠금 | 브론즈에서 다이아몬드까지 5가지 진화 계획의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!