이 기사에서는 Redis의 분산 잠금, 분산 잠금이 필요한 이유, Redis가 분산 잠금을 구현하는 방법을 소개합니다. 도움이 되길 바랍니다.
분산 잠금이 필요한 이유
분산 잠금을 사용하는 목적은 동시에 하나의 클라이언트만 공유 리소스에서 작동할 수 있도록 보장하는 것입니다.
분산 애플리케이션에서 논리적 처리를 수행할 때 동시성 문제가 자주 발생합니다. [관련 권장 사항: Redis 동영상 튜토리얼]
예를 들어 작업에 사용자 상태 수정이 필요한 경우 상태 수정을 위해서는 먼저 사용자 상태를 읽고 메모리에 수정한 다음 수정이 완료된 후 다시 저장해야 합니다. . 이러한 작업이 동시에 수행되면 상태 읽기 및 저장이라는 두 가지 작업이 원자적이지 않기 때문에 동시성 문제가 발생합니다.
이때, 프로그램의 동시 실행을 제한하기 위해 분산 잠금을 사용해야 합니다. 캐싱 미들웨어 시스템으로서 Redis는 이러한 종류의 분산 잠금 메커니즘을 제공할 수 있습니다. 그 핵심은 Redis에서 피트를 점유하고 싶어하고 그것이 점유되었음을 발견하면 잠시 기다렸다가 나중에 다시 시도하십시오
일반적으로 프로덕션 환경에서 사용할 수 있는 분산 잠금은 다음 사항을 충족해야 합니다.
상호 배제는 잠금의 기본 기능이며 동시에 하나의 스레드만 보유할 수 있습니다.
innodb_lock_wait_timeout
구성과 비교하여 타임아웃 릴리스를 통한 불필요한 스레드 대기를 방지할 수 있습니다. innodb_lock_wait_timeout
配置,通过超时释放,防止不必要的线程等待和资源浪费;使用SETNX实现
SETNX的使用方式为:SETNX key value
,只在键key不存在的情况下,将键key的值设置为value,若键key存在,则SETNX不做任何动作。
boolean result = jedis.setnx("lock-key",true)== 1L; if (result) { try { // do something } finally { jedis.del("lock-key"); } }
这种方案有一个致命问题,就是某个线程在获取锁之后由于某些异常因素(比如宕机)而不能正常的执行解锁操作,那么这个锁就永远释放不掉了。
为此,我们可以为这个锁加上一个超时时间
执行 SET key value EX seconds
的效果等同于执行 SETEX key seconds value
执行 SET key value PX milliseconds
的效果等同于执行 PSETEX key milliseconds value
String result = jedis.set("lock-key",true, 5); if ("OK".equals(result)) { try { // do something } finally { jedis.del("lock-key"); } }
方案看上去很完美,但实际上还是会有问题
试想一下,某线程A获取了锁并且设置了过期时间为10s,然后在执行业务逻辑的时候耗费了15s,此时线程A获取的锁早已被Redis的过期机制自动释放了
在线程A获取锁并经过10s之后,改锁可能已经被其它线程获取到了。当线程A执行完业务逻辑准备解锁(DEL key
)的时候,有可能删除掉的是其它线程已经获取到的锁。
所以最好的方式是在解锁时判断锁是否是自己的,我们可以在设置key
的时候将value设置为一个唯一值uniqueValue
(可以是随机值、UUID、或者机器号+线程号的组合、签名等)。
当解锁时,也就是删除key的时候先判断一下key对应的value是否等于先前设置的值,如果相等才能删除key
String velue= String.valueOf(System.currentTimeMillis()) String result = jedis.set("lock-key",velue, 5); if ("OK".equals(result)) { try { // do something } finally { //非原子操作 if(jedis.get("lock-key")==value){ jedis.del("lock-key"); } } }
这里我们一眼就可以看出问题来:GET
和DEL
是两个分开的操作,在GET执行之后且在DEL执行之前的间隙是可能会发生异常的。
如果我们只要保证解锁的代码是原子性的就能解决问题了
这里我们引入了一种新的方式,就是Lua脚本,示例如下:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
其中ARGV[1]
表示设置key时指定的唯一值。
由于Lua脚本的原子性,在Redis执行该脚本的过程中,其他客户端的命令都需要等待该Lua脚本执行完才能执行。
确保过期时间大于业务执行时间
为了防止多个线程同时执行业务代码,需要确保过期时间大于业务执行时间
增加一个boolean类型的属性isOpenExpirationRenewal
,用来标识是否开启定时刷新过期时间
在增加一个scheduleExpirationRenewal
方法用于开启刷新过期时间的线程
加锁代码在获取锁成功后将isOpenExpirationRenewal置为true,并且调用scheduleExpirationRenewal
SETNX를 사용하여 구현하세요. SETNX 사용 방법은 SETNX 키 값
입니다. 키 키가 없는 경우에만 키 키의 값을 값으로 설정합니다. 키 키가 있으면 SETNX는 아무 것도 만들지 않습니다. 행동.
이 솔루션의 치명적인 문제는 잠금을 획득한 후 스레드가 비정상적인 요인(예: 가동 중지 시간)으로 인해 잠금 해제 작업을 정상적으로 수행할 수 없는 경우 잠금이 절대 해제되지 않는다는 것입니다.
🎜이를 위해 이 잠금에 시간 초과 기간을 추가할 수 있습니다🎜🎜SET 키 값 EX 초
를 실행하는 효과는 SETEX 키 초 값
을 실행하는 것과 동일합니다🎜🎜 SET 키 값 PX 밀리초를 실행하는 것은 PSETEX 키 밀리초 값
을 실행하는 것과 같습니다🎜rrreee🎜🎜해결책은 완벽해 보이지만 실제로는 여전히 문제가 있습니다🎜🎜🎜 스레드 A가 잠금을 획득하고 만료 시간을 10초로 설정한 후 비즈니스 로직을 실행하는 데 15초가 걸렸습니다. 이때 스레드 A가 획득한 잠금은 이미 Redis의 만료 메커니즘에 의해 자동으로 해제되었습니다. 🎜스레드 A가 잠금을 획득하고 통과한 후 10초가 지나면 변경된 잠금이 다른 스레드에서 획득되었을 수 있습니다. 스레드 A가 비즈니스 로직 실행을 마치고 잠금 해제(DEL 키
)를 준비하면 다른 스레드에서 획득한 잠금을 삭제할 수 있습니다. 🎜🎜따라서 가장 좋은 방법은 잠금을 해제할 때 자물쇠가 본인 소유인지 확인하는 것입니다. key
를 설정할 때 값을 고유한 값인 uniqueValue
로 설정할 수 있습니다. 임의의 값, UUID 또는 기계 번호 + 스레드 번호, 서명 등의 조합). 🎜🎜잠금 해제 시, 즉 키를 삭제할 때 먼저 해당 키에 해당하는 값이 이전에 설정한 값과 같은지 확인하세요. 동일하면 키를 삭제할 수 있습니다🎜rrreee🎜여기에서 문제를 확인할 수 있습니다. 한 눈에 보기: GET code>와 <code>DEL
은 두 개의 별도 작업입니다. GET 실행 사이와 DEL 실행 전 사이에 예외가 발생할 수 있습니다. 🎜🎜잠금 해제 코드가 원자성인지 확인하기만 하면 문제가 해결될 수 있습니다🎜🎜여기서 🎜Lua 스크립트🎜라는 새로운 방법을 소개합니다. 예는 다음과 같습니다. 🎜rrreee🎜여기서 ARGV [1]
은 키 설정 시 지정한 고유 값을 나타냅니다. 🎜🎜Lua 스크립트의 원자성으로 인해 Redis가 스크립트를 실행하는 과정에서 다른 클라이언트 명령은 Lua 스크립트가 실행될 때까지 기다려야 실행될 수 있습니다. 🎜🎜🎜만료 시간이 비즈니스 실행 시간보다 길어야 합니다🎜🎜🎜여러 스레드가 동시에 비즈니스 코드를 실행하는 것을 방지하려면 만료 시간이 비즈니스 실행 시간보다 길어야 합니다🎜 🎜예약된 새로 고침 만료 시간이 활성화되었는지 여부를 식별하기 위한 부울 유형 속성 isOpenExpirationRenewal
을 추가합니다.🎜🎜만료 시간을 새로 고치기 위해 스레드를 열려면 scheduleExpirationRenewal
메서드를 추가합니다.🎜🎜잠금 코드는 잠금 획득에 성공한 후 isOpenExpirationRenewal을 true로 설정하고 ScheduleExpirationRenewal
메서드를 호출하고 만료 시간을 새로 고치는 스레드를 시작합니다🎜🎜잠금 해제 코드에 코드 줄을 추가하고 isOpenExpirationRenewal 속성을 false로 설정합니다. 만료 시간을 새로 고치는 스레드 폴링을 중지하세요🎜🎜🎜Redisson 구현🎜🎜🎜잠금을 성공적으로 획득한 후 열립니다. 예약된 작업, 예약된 작업은 갱신을 위해 정기적으로 확인됩니다🎜이 예정된 일정의 각 호출 간의 시간 차이는 internalLockLeaseTime / 3
이며, 이는 10초입니다.internalLockLeaseTime / 3
,也就10秒
默认情况下,加锁的时间是30秒.如果加锁的业务没有执行完,那么到 30-10 = 20
秒的时候,就会进行一次续期,把锁重置成30秒
在集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生
Redlock算法就是为了解决这个问题
使用 Redlock,需要提供多个 Redis
实例,这些实例之前相互独立没有主从关系。同很多分布式算法一样,redlock 也使用大多数机制
加锁时,它会向过半节点发送 set指令,只要过半节点 set
成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。不过 Redlock 算法还需要考虑出错重试、时钟漂移等很多细节问题,同时因为 Redlock
需要向多个节点进行读写,意味着相比单实例 Redis 性能会下降一些
Redlock 算法是在单 Redis 节点基础上引入的高可用模式,Redlock 基于 N 个完全独立的 Redis 节点,一般是大于 3 的奇数个(通常情况下 N 可以设置为 5),可以基本保证集群内各个节点不会同时宕机。
假设当前集群有 5 个节点,运行 Redlock 算法的客户端依次执行下面各个步骤,来完成获取锁的操作
也就是说,假设锁30秒过期,三个节点加锁花了31秒,自然是加锁失败了
在 Redis 官方推荐的 Java 客户端 Redisson
中,内置了对 RedLock
30-10 = 20
초, 갱신이 수행되고 잠금이 30초로 재설정됩니다Redlock 알고리즘은 단일 Redis 노드를 기반으로 도입된 고가용성 모드입니다. Redlock은 일반적으로 3보다 큰 홀수인 N개의 완전히 독립적인 Redis 노드를 기반으로 합니다. N은 5로 설정할 수 있습니다. 이는 기본적으로 클러스터의 각 노드가 동시에 다운되지 않도록 보장할 수 있습니다.RedLock
클러스터에서 마스터 노드가 끊깁니다. 이번에는 슬레이브 노드가 인계를 받지만 클라이언트에서는 뚜렷한 인식이 없습니다. 첫 번째 클라이언트가 마스터 노드에 잠금을 성공적으로 적용했지만 잠금이 슬레이브 노드에 동기화되기 전에 마스터 노드가 갑자기 종료된 것으로 나타났습니다. 그러면 슬레이브 노드가 마스터 노드가 됩니다. 이 새 노드에는 내부에 이 잠금이 없으므로 다른 클라이언트가 잠금을 요청하면 즉시 승인됩니다. 이로 인해 두 클라이언트가 동시에 시스템의 동일한 잠금을 보유하게 되어 보안이 불안정해집니다. Redlock 알고리즘은 이 문제를 해결하기 위해 여러
Redis를 제공해야 합니다. 예를 들어, 이러한 인스턴스는 이전에 서로 독립적이었으며 마스터-슬레이브 관계가 없었습니다. 많은 분산 알고리즘과 마찬가지로 redlock도 대부분의 메커니즘을 사용합니다. 잠금 시 노드의 절반 이상이 <code>set
성공하면 잠금이 고려됩니다. 성공적인. 잠금을 해제할 때 모든 노드에 del 명령을 보내야 합니다. 그러나 Redlock 알고리즘은 오류 재시도 및 클럭 드리프트와 같은 많은 세부적인 문제도 고려해야 합니다. 동시에Redlock
은 여러 노드를 읽고 써야 하기 때문에 이는 Redis의 성능을 의미합니다.
현재 클러스터에 5개의 노드가 있다고 가정하면 Redlock 알고리즘을 실행하는 클라이언트는 다음 단계를 순차적으로 수행하여 잠금 획득 작업을 완료합니다.
클라이언트는 현재 시스템 시간을 밀리초 단위로 기록합니다. 5개의 Redis 인스턴스에서는 잠금을 획득하기 위해 동일한 키가 사용됩니다. Redis에 잠금 획득을 요청할 때 클라이언트는 네트워크 연결 및 응답 시간 초과를 설정해야 네트워크로 인한 문제를 방지할 수 있습니다. 클라이언트는 잠금을 획득하기 시작한 시간을 뺀 현재 시간을 사용하여 잠금을 획득하는 데 사용된 시간을 가져옵니다. 잠금은 절반 이상의 Redis 노드에서 획득된 경우에만 계산됩니다. , 사용된 시간이 잠금 만료 시간보다 작을 경우 획득이 성공합니다.
잠금이 획득되면 키의 실제 유효 시간은 유효 시간에서 잠금을 획득하는 데 사용된 시간을 뺀 것과 같습니다.
Redisson
에서. , 내장된 RedLock
https://redis.io/topics/distlockhttps://github.com/redisson/redisson/wiki🎜 🎜RedLock은 잠금의 고가용성을 보장할 뿐 잠금의 정확성을 보장하지는 않습니다. 🎜🎜RedLock은 🎜시스템 시계에 크게 의존하는 분산 시스템입니다.🎜🎜🎜RedLock에 대한 Martin의 비판: 🎜🎜🎜For 효율성 향상 시나리오에서 RedLock은 너무 무겁습니다. 🎜🎜매우 높은 정확성이 필요한 시나리오의 경우 RedLock은 정확성을 보장할 수 없습니다. 🎜🎜🎜🎜이 기사는 https://juejin.cn/post/7018968452136173576🎜🎜저자: With a Distant Mind🎜🎜🎜더 많은 프로그래밍 관련 지식을 보려면 🎜프로그래밍 비디오🎜를 방문하세요! ! 🎜
위 내용은 Redis에 분산 잠금이 필요한 이유는 무엇입니까? 그것을 달성하는 방법?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!