Redis 분산 잠금을 정말로 이해하고 있나요? 다음 글에서는 Redis의 분산 잠금에 대해 심도있게 소개하고 잠금 구현 방법, 잠금 해제, 분산 잠금의 결함 등에 대해 설명하겠습니다. 도움이 되셨으면 좋겠습니다!
Redis라고 하면 가장 먼저 생각하는 기능은 데이터를 캐시하는 기능입니다. 또한 Redis는 단일 프로세스 및 고성능 특성으로 인해 배포에 자주 사용됩니다. 잠그다. [관련 권장 사항: Redis 동영상 튜토리얼]
우리 모두는 잠금이 프로그램에서 동시에 하나의 스레드에서만 액세스할 수 있도록 하는 동기화 도구로 작동한다는 것을 알고 있습니다. 동기화 및 잠금과 같은 잠금은 모두 우리가 일반적으로 사용하지만 Java의 잠금은 단일 시스템에서만 효과적이라는 것을 보장할 수 있으며 분산 클러스터 환경에서는 무력합니다. 이때 분산 잠금을 사용해야 합니다.
분산 잠금은 이름에서 알 수 있듯이 분산 프로젝트 개발에 사용되는 잠금입니다. 일반적으로 분산 잠금은 다음 특성을 충족해야 합니다.
1. 독점성: 언제든지 동일한 데이터에 대해 하나의 애플리케이션만 분산 잠금을 얻을 수 있습니다.
2. 고가용성: 분산 시나리오에서는 소수의 서버가 작동 중지되더라도 정상적인 사용에는 영향을 미치지 않습니다. 이 경우 분산 잠금을 제공하는 서비스를 클러스터에 배포해야 합니다.
3. 잠금 시간 초과 방지: 클라이언트가 잠금을 적극적으로 해제하지 않으면 서버는 일정 시간 후에 자동으로 잠금을 해제합니다. 또는 네트워크에 연결할 수 없을 때 교착 상태가 발생합니다.
4. 독점성: 잠금 및 잠금 해제는 동일한 서버에서 수행되어야 합니다. 즉, 잠금 소유자만 잠금을 해제할 수 없습니다. ;
업계에는 분산된 잠금 효과를 얻을 수 있는 많은 도구가 있지만 작업은 잠금, 잠금 해제 및 잠금 시간 초과 방지와 같습니다.
이 기사에서는 Redis 분산 잠금에 대해 이야기하고 있으므로 Redis의 지식 포인트를 확장하는 것이 당연합니다.
먼저 Redis의 몇 가지 명령인
1을 소개합니다. SETNX, 사용법은 SETNX 키 값
입니다.SETNX key value
SETNX是『 SET if Not eXists』(如果不存在,则 SET)的简写,设置成功就返回1,否则返回0。
setnx用法
可以看出,当把key为lock的值设置为"Java"后,再设置成别的值就会失败,看上去很简单,也好像独占了锁,但有个致命的问题,就是key没有过期时间,这样一来,除非手动删除key或者获取锁后设置过期时间,不然其他线程永远拿不到锁。
既然这样,我们给key加个过期时间总可以吧,直接让线程获取锁的时候执行两步操作:
`SETNX Key 1` `EXPIRE Key Seconds`
这个方案也有问题,因为获取锁和设置过期时间分成两步了,不是原子性操作,有可能获取锁成功但设置时间失败,那样不就白干了吗。
不过也不用急,这种事情Redis官方早为我们考虑到了,所以就引出了下面这个命令
2、SETEX,用法SETEX key seconds value
将值 value
关联到 key
,并将 key
的生存时间设为 seconds
(以秒为单位)。如果 key
已经存在,SETEX 命令将覆写旧值。
这个命令类似于以下两个命令:
`SET key value` `EXPIRE key seconds # 设置生存时间`
这两步动作是原子性的,会在同一时间完成。
setex用法
3、PSETEX ,用法PSETEX key milliseconds value
这个命令和SETEX命令相似,但它以毫秒为单位设置 key
`SET key value NX EX seconds`
SETEX 키 초 값
🎜🎜값 변경 < code> 값은 key
와 연결되어 있으며 키
의 수명을 초
(초)로 설정합니다. key
가 이미 존재하는 경우 SETEX 명령은 이전 값을 덮어씁니다. 🎜🎜이 명령은 다음 두 명령과 유사합니다. 🎜`if redis.call("get",KEYS[1]) == ARGV[1]` `then` `return redis.call("del",KEYS[1])` `else` `return 0` `end`
PSETEX 키 밀리초 값
🎜🎜이 명령은 SETEX와 유사합니다. 그러나 key
의 수명을 SETEX 명령처럼 초 단위가 아닌 밀리초 단위로 설정합니다. 🎜🎜그러나 Redis 버전 2.6.12부터 SET 명령은 매개 변수를 사용하여 SETNX, SETEX 및 PSETEX 세 가지 명령과 동일한 효과를 얻을 수 있습니다. 🎜🎜예를 들어, 이 명령은🎜`public class RedisLockUtil {` `private String LOCK_KEY = "redis_lock";` `// key的持有时间,5ms` `private long EXPIRE_TIME = 5;` `// 等待超时时间,1s` `private long TIME_OUT = 1000;` `// redis命令参数,相当于nx和px的命令合集` `private SetParams params = SetParams.setParams().nx().px(EXPIRE_TIME);` `// redis连接池,连的是本地的redis客户端` `JedisPool jedisPool = new JedisPool("127.0.0.1", 6379);` `/**` `* 加锁` `*` `* @param id` `* 线程的id,或者其他可识别当前线程且不重复的字段` `* @return` `*/` `public boolean lock(String id) {` `Long start = System.currentTimeMillis();` `Jedis jedis = jedisPool.getResource();` `try {` `for (;;) {` `// SET命令返回OK ,则证明获取锁成功` `String lock = jedis.set(LOCK_KEY, id, params);` `if ("OK".equals(lock)) {` `return true;` `}` `// 否则循环等待,在TIME_OUT时间内仍未获取到锁,则获取失败` `long l = System.currentTimeMillis() - start;` `if (l >= TIME_OUT) {` `return false;` `}` `try {` `// 休眠一会,不然反复执行循环会一直失败` `Thread.sleep(100);` `} catch (InterruptedException e) {` `e.printStackTrace();` `}` `}` `} finally {` `jedis.close();` `}` `}` `/**` `* 解锁` `*` `* @param id` `* 线程的id,或者其他可识别当前线程且不重复的字段` `* @return` `*/` `public boolean unlock(String id) {` `Jedis jedis = jedisPool.getResource();` `// 删除key的lua脚本` `String script = "if redis.call('get',KEYS[1]) == ARGV[1] then" + " return redis.call('del',KEYS[1]) " + "else"` `+ " return 0 " + "end";` `try {` `String result =` `jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(id)).toString();` `return "1".equals(result);` `} finally {` `jedis.close();` `}` `}` `}`
`if redis.call("get",KEYS[1]) == ARGV[1]` `then` `return redis.call("del",KEYS[1])` `else` `return 0` `end`
KEYS[1]是当前key的名称,ARGV[1]可以是当前线程的ID(或者其他不固定的值,能识别所属线程即可),这样就可以防止持有过期锁的线程,或者其他线程误删现有锁的情况出现。
知道了原理后,我们就可以手写代码来实现Redis分布式锁的功能了,因为本文的目的主要是为了讲解原理,不是为了教大家怎么写分布式锁,所以我就用伪代码实现了。
首先是redis锁的工具类,包含了加锁和解锁的基础方法:
`public class RedisLockUtil {` `private String LOCK_KEY = "redis_lock";` `// key的持有时间,5ms` `private long EXPIRE_TIME = 5;` `// 等待超时时间,1s` `private long TIME_OUT = 1000;` `// redis命令参数,相当于nx和px的命令合集` `private SetParams params = SetParams.setParams().nx().px(EXPIRE_TIME);` `// redis连接池,连的是本地的redis客户端` `JedisPool jedisPool = new JedisPool("127.0.0.1", 6379);` `/**` `* 加锁` `*` `* @param id` `* 线程的id,或者其他可识别当前线程且不重复的字段` `* @return` `*/` `public boolean lock(String id) {` `Long start = System.currentTimeMillis();` `Jedis jedis = jedisPool.getResource();` `try {` `for (;;) {` `// SET命令返回OK ,则证明获取锁成功` `String lock = jedis.set(LOCK_KEY, id, params);` `if ("OK".equals(lock)) {` `return true;` `}` `// 否则循环等待,在TIME_OUT时间内仍未获取到锁,则获取失败` `long l = System.currentTimeMillis() - start;` `if (l >= TIME_OUT) {` `return false;` `}` `try {` `// 休眠一会,不然反复执行循环会一直失败` `Thread.sleep(100);` `} catch (InterruptedException e) {` `e.printStackTrace();` `}` `}` `} finally {` `jedis.close();` `}` `}` `/**` `* 解锁` `*` `* @param id` `* 线程的id,或者其他可识别当前线程且不重复的字段` `* @return` `*/` `public boolean unlock(String id) {` `Jedis jedis = jedisPool.getResource();` `// 删除key的lua脚本` `String script = "if redis.call('get',KEYS[1]) == ARGV[1] then" + " return redis.call('del',KEYS[1]) " + "else"` `+ " return 0 " + "end";` `try {` `String result =` `jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(id)).toString();` `return "1".equals(result);` `} finally {` `jedis.close();` `}` `}` `}`
具体的代码作用注释已经写得很清楚了,然后我们就可以写一个demo类来测试一下效果:
`public class RedisLockTest {` `private static RedisLockUtil demo = new RedisLockUtil();` `private static Integer NUM = 101;` `public static void main(String[] args) {` `for (int i = 0; i < 100; i++) {` `new Thread(() -> {` `String id = Thread.currentThread().getId() + "";` `boolean isLock = demo.lock(id);` `try {` `// 拿到锁的话,就对共享参数减一` `if (isLock) {` `NUM--;` `System.out.println(NUM);` `}` `} finally {` `// 释放锁一定要注意放在finally` `demo.unlock(id);` `}` `}).start();` `}` `}` `}`
我们创建100个线程来模拟并发的情况,执行后的结果是这样的:
代码执行结果
可以看出,锁的效果达到了,线程安全是可以保证的。
当然,上面的代码只是简单的实现了效果,功能肯定是不完整的,一个健全的分布式锁要考虑的方面还有很多,实际设计起来不是那么容易的。
我们的目的只是为了学习和了解原理,手写一个工业级的分布式锁工具不现实,也没必要,类似的开源工具一大堆(Redisson),原理都差不多,而且早已经过业界同行的检验,直接拿来用就行。
虽然功能是实现了,但其实从设计上来说,这样的分布式锁存在着很大的缺陷,这也是本篇文章想重点探讨的内容。
一、客户端长时间阻塞导致锁失效问题
客户端1得到了锁,因为网络问题或者GC等原因导致长时间阻塞,然后业务程序还没执行完锁就过期了,这时候客户端2也能正常拿到锁,可能会导致线程安全的问题。
客户端长时间阻塞
那么该如何防止这样的异常呢?我们先不说解决方案,介绍完其他的缺陷后再来讨论。
二、redis服务器时钟漂移问题
如果redis服务器的机器时钟发生了向前跳跃,就会导致这个key过早超时失效,比如说客户端1拿到锁后,key的过期时间是12:02分,但redis服务器本身的时钟比客户端快了2分钟,导致key在12:00的时候就失效了,这时候,如果客户端1还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的问题。
三、单点实例安全问题
如果redis是单master模式的,当这台机宕机的时候,那么所有的客户端都获取不到锁了,为了提高可用性,可能就会给这个master加一个slave,但是因为redis的主从同步是异步进行的,可能会出现客户端1设置完锁后,master挂掉,slave提升为master,因为异步复制的特性,客户端1设置的锁丢失了,这时候客户端2设置锁也能够成功,导致客户端1和客户端2同时拥有锁。
为了解决Redis单点问题,redis的作者提出了RedLock算法。
该算法的实现前提在于Redis必须是多节点部署的,可以有效防止单点故障,具体的实现思路是这样的:
1、获取当前时间戳(ms);
2、先设定key的有效时长(TTL),超出这个时间就会自动释放,然后client(客户端)尝试使用相同的key和value对所有redis实例进行设置,每次链接redis实例时设置一个比TTL短很多的超时时间,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例。
比如:TTL(也就是过期时间)为5s,那获取锁的超时时间就可以设置成50ms,所以如果50ms内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁;
3、client通过获取所有能获取的锁后的时间减去第一步的时间,还有redis服务器的时钟漂移误差,然后这个时间差要小于TTL时间并且成功设置锁的实例数>= N/2 + 1(N为Redis实例的数量),那么加锁成功
比如TTL是5s,连接redis获取所有锁用了2s,然后再减去时钟漂移(假设误差是1s左右),那么锁的真正有效时长就只有2s了;
4、如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例。
根据这样的算法,我们假设有5个Redis实例的话,那么client只要获取其中3台以上的锁就算是成功了,用流程图演示大概就像这样:
키 유효 시간
알고리즘이 도입되었습니다. 설계 관점에서 볼 때 RedLock 알고리즘의 아이디어는 주로 Redis의 문제를 효과적으로 방지하기 위한 것임에 틀림이 없습니다. 단일 지점 오류 및 TTL을 설계할 때 서버 시계 드리프트 오류도 고려되어 분산 잠금의 보안이 크게 향상됩니다.
그런데 정말 그럴까요? 아무튼 개인적으로 효과는 보통 정도라고 생각하는데,
우선 RedLock 알고리즘에서는 이 과정을 거치면 Redis 인스턴스에 연결하는 데 걸리는 시간만큼 Lock의 유효 시간이 줄어든다는 것을 알 수 있습니다. 네트워크 문제로 인해 시간이 너무 오래 걸립니다. 이 경우 잠금에 남은 유효 시간이 크게 줄어들게 됩니다. 클라이언트가 공유 리소스에 액세스하는 시간은 매우 짧으며 도중에 잠금이 만료될 가능성이 매우 높습니다. 프로그램 처리. 그리고 서버의 클럭 드리프트에서 락의 유효시간을 빼야 하는데, 이 값을 잘 설정하지 않으면 문제가 발생하기 쉽습니다.
두 번째 요점은 이 알고리즘이 Redis 단일 실패 지점을 방지하기 위해 여러 노드의 사용을 고려하더라도 노드가 충돌하고 다시 시작되는 경우 여러 클라이언트가 동시에 잠금을 획득할 수 있다는 것입니다.
총 5개의 Redis 노드(A, B, C, D, E)가 있다고 가정합니다. 클라이언트 1과 2가 각각 잠겨 있습니다.
클라이언트 1이 A, B, C를 성공적으로 잠갔고 성공적으로 잠금을 획득했습니다. D와 E는 잠겨 있지 않습니다.)
노드 C의 마스터가 끊겼는데, 슬레이브가 마스터로 업그레이드된 후 클라이언트 1이 추가한 잠금이 손실되었습니다.
클라이언트 2가 이때 Lock을 획득하였고, C, D, E에 Lock을 걸어 성공적으로 Lock 획득에 성공하였습니다.
이렇게 하면 클라이언트 1과 클라이언트 2가 동시에 잠금을 얻게 되며 프로그램 보안의 숨겨진 위험이 여전히 존재합니다. 또한 이러한 노드 중 하나에서 시간 드리프트가 발생하면 잠금 보안 문제가 발생할 수도 있습니다.
따라서 다중 인스턴스 배포를 통해 가용성과 안정성이 향상되지만 RedLock은 Redis 단일 실패 지점의 숨겨진 위험을 완전히 해결하지 못하며 시계 드리프트 및 장기 클라이언트 차단으로 인한 잠금 시간 초과 오류도 해결하지 않습니다. 잠금장치의 문제점과 보안상의 위험은 여전히 존재합니다.
어떤 사람들은 더 묻고 싶을 수도 있습니다. 자물쇠의 절대적인 안전을 보장하려면 어떻게 해야 합니까?
우리가 Redis를 분산 잠금 도구로 사용하는 이유는 높은 동시성 시나리오에서도 Redis의 높은 효율성과 단일 프로세스 특성 때문입니다. 또한 특정 상황에서는 성능을 잘 보장할 수 있지만 많은 경우 성능과 보안의 균형을 완전히 유지할 수 없습니다. 잠금의 보안을 보장해야 하는 경우 db 및 Zookeeper와 같은 다른 미들웨어를 사용하여 제어할 수 있습니다. 매우 효과적입니다. 좋은 것은 자물쇠의 안전을 보장하지만 그 성능은 만족스럽지 않다고 말할 수 있습니다. 그렇지 않으면 모든 사람이 오래 전에 그것을 사용했을 것입니다.
일반적으로 Redis를 사용하여 공유 리소스를 제어하고 높은 데이터 보안 요구 사항이 필요한 경우 최종 보장 솔루션은 비즈니스 데이터에 대한 멱등성 제어를 구현하는 것입니다. 이러한 방식으로 여러 클라이언트가 잠금을 획득하더라도 영향을 미치지 않습니다. 데이터 일관성. 물론 모든 장면이 이에 적합한 것은 아니다. 구체적인 선택은 결국 심사위원 각자의 몫이다. 결국 완벽한 기술은 없고, 가장 적합한 장면만이 최선이다.
더 많은 프로그래밍 관련 지식을 보려면 프로그래밍 소개를 방문하세요! !
위 내용은 Redis의 분산 잠금에 대한 심층적인 이해 제공의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!