이 글에서는 Redis를 사용할 때 발생할 수 있는 15가지 함정을 소개합니다. 도움이 필요한 친구들이 모두 참고할 수 있기를 바랍니다.
안녕하세요 여러분, 제 이름은 카이토입니다.
이 기사에서는 Redis를 사용할 때 발생할 수 있는 "구덩이"에 대해 이야기하고 싶습니다.
Redis를 사용할 때 다음과 같은 "이상한" 시나리오에 직면했다면 "구덩이"에 빠졌을 가능성이 높습니다.
키에 만료 시간이 설정되어 있는데 어떻게 만료되지 않을 수 있습니까? 이미?
O(1) 복잡도 SETBIT 명령을 사용하여 Redis가 OOM되었습니다.
RANDOMKEY를 실행하고 무작위로 키를 선택하면 Redis가 차단되나요?
동일한 명령을 사용하면 왜 마스터 데이터베이스는 데이터를 찾을 수 없는데 슬레이브 데이터베이스는 찾을 수 있나요?
슬레이브 데이터베이스가 마스터 데이터베이스보다 메모리를 더 많이 사용하는 이유는 무엇인가요?
Redis에 기록된 데이터가 설명할 수 없는 이유로 손실되는 이유는 무엇인가요?
...
[관련 추천: Redis 영상 튜토리얼]
이러한 문제가 발생하는 이유는 정확히 무엇인가요?
이 기사에서는 Redis를 사용할 때 발생할 수 있는 함정과 이를 방지하는 방법을 함께 검토하겠습니다.
저는 이 질문을 세 부분으로 나누었습니다.
일반적인 명령의 함정은 무엇입니까?
데이터 지속성의 함정은 무엇입니까?
마스터-슬레이브 데이터베이스 동기화의 함정은 무엇입니까?
이러한 문제의 원인은 여러분의 이해를 "전복"시킬 가능성이 높습니다. 준비가 되었다면 제 생각을 따르고 시작하세요!
이 기사에는 유용한 정보가 많이 포함되어 있으므로 인내심을 가지고 읽어보시기 바랍니다.
먼저 Redis를 사용할 때 "예기치 않은" 결과가 발생할 수 있는 몇 가지 일반적인 명령을 살펴보겠습니다.
1) 실수로 유효기간을 잃어버리셨나요?
Redis를 사용하다 보면 SET 명령어를 자주 사용하게 되는데 아주 간단합니다.
키-값 설정 외에도 SET은 다음과 같이 키의 만료 시간을 설정할 수도 있습니다.
127.0.0.1:6379> SET testkey val1 EX 60 OK 127.0.0.1:6379> TTL testkey (integer) 59
이때 키 값을 수정하고 싶지만 간단히 SET 명령을 사용하면 됩니다. "만료 시간" 매개변수를 추가하면 이 키의 만료 시간이 "삭제"됩니다.
127.0.0.1:6379> SET testkey val2 OK 127.0.0.1:6379> TTL testkey // key永远不过期了! (integer) -1
보셨나요? testkey는 만료되지 않습니다!
Redis를 이제 막 사용하기 시작했다면 이 함정을 밟았을 것입니다.
이 문제의 원인은 다음과 같습니다. 만료 시간이 SET 명령에 설정되지 않은 경우 Redis는 키의 만료 시간을 자동으로 "삭제"합니다.
Redis의 메모리가 계속 증가하고 많은 키에 원래 만료 시간이 설정되어 있었지만 나중에 만료 시간이 손실된 것을 발견한 경우 이러한 이유 때문일 가능성이 높습니다.
이제 Redis에는 만료되지 않은 키가 너무 많아 너무 많은 메모리 리소스를 소비하게 됩니다.
그래서 SET 명령을 사용할 때 처음에 만료 시간을 설정했다면 나중에 키를 수정할 때 만료 시간 매개변수도 추가해야 만료 시간이 손실되는 문제를 피할 수 있습니다.
2) DEL도 Redis를 차단할 수 있나요?
키를 삭제하려면 반드시 DEL 명령을 사용하게 됩니다. 시간 복잡도에 대해 생각해본 적이 없으신가요?
O(1)? 반드시 그런 것은 아닙니다.
Redis 공식 문서를 주의 깊게 읽으면 다음 내용을 확인할 수 있습니다. 키 삭제에 걸리는 시간은 키 유형과 관련이 있습니다.
Redis 공식 문서에서는 DEL 명령에 대해 다음과 같이 설명합니다.
key는 String 유형이고 DEL 시간 복잡도는 O(1)
key는 List/Hash/Set/ZSet 유형입니다. DEL의 복잡도는 O(M), M은 요소의 개수입니다
즉, 문자열이 아닌 유형의 키를 삭제하려면 키에 요소가 많을수록 시간이 더 많이 걸립니다. DEL을 실행하는 데 시간이 오래 걸립니다!
이게 왜죠?
이런 종류의 키를 삭제할 때 Redis는 요소가 많을수록 이 프로세스에 더 많은 시간이 소요되기 때문에 각 요소의 메모리를 차례로 해제해야 합니다.
그리고 이러한 긴 작업은 필연적으로 전체 Redis 인스턴스를 차단하고 Redis 성능에 영향을 미치게 됩니다.
따라서 List/Hash/Set/ZSet 유형의 키를 삭제하는 경우에는 아무 생각 없이 DEL을 실행할 수 없으므로 다음과 같은 방법으로 삭제해야 합니다.
Query 요소 수: LLEN/HLEN/SCARD/ZCARD 명령
을 실행하여 요소 수를 결정합니다. 요소 수가 적으면 직접 DEL 삭제를 실행할 수 있고, 그렇지 않으면 일괄 삭제
삭제 일괄 처리: LRANGE/HSCAN/SSCAN/ZSCAN + LPOP/RPOP/HDEL/SREM/ZREM 삭제 실행
이제 DEL이 List/Hash/Set/ZSet 유형 데이터에 미치는 영향을 이해했으니 다시 분석해 보겠습니다. 문자열 유형 키를 삭제하면 이 문제가 발생합니까?
어? 앞서 Redis 공식 문서에서 String 유형 키 삭제의 시간 복잡도가 O(1)이라고 설명하지 않았나요? 이로 인해 Redis가 차단되지는 않습니다. 그렇죠?
사실 꼭 그렇지는 않아요!
생각해보세요. 이 키가 아주 많은 양의 메모리를 차지한다면 어떨까요?
예를 들어 이 키에 500MB의 데이터가 저장되어 있다면(물론 빅키임) DEL을 실행할 때 시간이 더 오래 걸립니다!
Redis가 이렇게 큰 메모리를 운영체제에 해제하는 데 시간이 걸리기 때문에 작업 시간이 더 오래 걸리기 때문입니다.
그래서 String 유형의 경우 너무 큰 데이터를 저장하지 않는 것이 좋습니다. 그렇지 않으면 삭제할 때 성능 문제가 발생합니다.
이 시점에서 다음과 같은 생각이 들 수도 있습니다. Redis 4.0에 게으른 자유 메커니즘이 도입되지 않았나요? 이 메커니즘을 켜면 메모리 해제 작업이 백그라운드 스레드에서 실행되어 메인 스레드를 차단하지 않나요?
아주 좋은 질문입니다.
정말 그런가요?
결론을 먼저 말씀드리자면, Redis에서lazy-free를 켜도 String타입의 bigkey를 삭제하면 여전히 백그라운드 스레드에서 실행되지 않고 메인 스레드에서 처리됩니다. 따라서 여전히 Redis를 차단할 위험이 있습니다!
이게 왜죠?
먼저 힌트를 드릴게요. 관심 있는 학생들은 먼저 게으른 관련 정보를 확인하여 답을 찾아보세요. :)
사실 레이지프리에 대한 지식 포인트가 너무 많아서 나중에 특집글을 쓸 예정이니 계속해서 관심 가져주세요~
3) RANDOMKEY도 가능합니다. Redis를 차단하시겠습니까?
Redis에서 키를 무작위로 확인하려면 일반적으로 RANDOMKEY 명령을 사용합니다.
이 명령은 Redis에서 키를 "무작위로" 추출합니다.
랜덤이라 실행속도가 엄청 빠르겠죠?
사실 그렇지 않아요.
이 문제를 명확하게 설명하려면 Redis의 만료 전략과 결합해야 합니다.
Redis의 만료 전략에 대해 알고 있다면 Redis가 예약된 정리와 지연 정리를 조합하여 만료된 키를 정리한다는 것을 알아야 합니다.
RANDOMKEY는 무작위로 키를 꺼낸 후 먼저 키가 만료되었는지 확인합니다.
키가 만료된 경우 Redis는 이를 삭제합니다. 이 프로세스는 지연 정리입니다.
하지만 정리는 아직 끝나지 않았습니다. Redis는 여전히 "만료되지 않은" 키를 찾아 클라이언트에 반환해야 합니다.
이때 Redis는 만료되지 않은 키를 찾아 클라이언트에 반환할 때까지 계속해서 무작위로 키를 가져온 다음 만료 여부를 확인합니다.
전체 프로세스는 다음과 같습니다.
마스터가 무작위로 키를 선택하고 만료되었는지 확인합니다
키가 만료된 경우 삭제하고 계속해서 무작위로 키를 선택합니다
그리고 반복합니다 만료되지 않은 키를 찾을 때까지 이 주기는 만료되지 않은 키가 반환됩니다.
하지만 여기에 문제가 있습니다. 현재 Redis에서 많은 수의 키가 만료되었지만 아직 삭제되지 않은 경우 이 주기는 지속됩니다. 종료되기까지 오랜 시간이 걸리고, 이 시간은 만료된 키를 정리하고 만료되지 않은 키를 찾는 데 소비됩니다.
导致的结果就是,RANDOMKEY 执行耗时变长,影响 Redis 性能。
以上流程,其实是在 master 上执行的。
如果在 slave 上执行 RANDOMEKY,那么问题会更严重!
为什么?
主要原因就在于,slave 自己是不会清理过期 key。
那 slave 什么时候删除过期 key 呢?
其实,当一个 key 要过期时,master 会先清理删除它,之后 master 向 slave 发送一个 DEL 命令,告知 slave 也删除这个 key,以此达到主从库的数据一致性。
还是同样的场景:Redis 中存在大量已过期,但还未被清理的 key,那在 slave 上执行 RANDOMKEY 时,就会发生以下问题:
slave 随机取出一个 key,判断是否已过期
key 已过期,但 slave 不会删除它,而是继续随机寻找不过期的 key
由于大量 key 都已过期,那 slave 就会寻找不到符合条件的 key,此时就会陷入「死循环」!
也就是说,在 slave 上执行 RANDOMKEY,有可能会造成整个 Redis 实例卡死!
是不是没想到?在 slave 上随机拿一个 key,竟然有可能造成这么严重的后果?
这其实是 Redis 的一个 Bug,这个 Bug 一直持续到 5.0 才被修复。
修复的解决方案是,在 slave 上执行 RANDOMKEY 时,会先判断整个实例所有 key 是否都设置了过期时间,如果是,为了避免长时间找不到符合条件的 key,slave 最多只会在哈希表中寻找 100 次,无论是否能找到,都会退出循环。
这个方案就是增加上了一个最大重试次数,这样一来,就避免了陷入死循环。
虽然这个方案可以避免了 slave 陷入死循环、卡死整个实例的问题,但是,在 master 上执行这个命令时,依旧有概率导致耗时变长。
所以,你在使用 RANDOMKEY 时,如果发现 Redis 发生了「抖动」,很有可能是因为这个原因导致的!
4) O(1) 复杂度的 SETBIT,竟然会导致 Redis OOM?
在使用 Redis 的 String 类型时,除了直接写入一个字符串之外,还可以把它当做 bitmap 来用。
具体来讲就是,我们可以把一个 String 类型的 key,拆分成一个个 bit 来操作,就像下面这样:
127.0.0.1:6379> SETBIT testkey 10 1 (integer) 1 127.0.0.1:6379> GETBIT testkey 10 (integer) 1
其中,操作的每一个 bit 位叫做 offset。
但是,这里有一个坑,你需要注意起来。
如果这个 key 不存在,或者 key 的内存使用很小,此时你要操作的 offset 非常大,那么 Redis 就需要分配「更大的内存空间」,这个操作耗时就会变长,影响性能。
所以,当你在使用 SETBIT 时,也一定要注意 offset 的大小,操作过大的 offset 也会引发 Redis 卡顿。
这种类型的 key,也是典型的 bigkey,除了分配内存影响性能之外,在删除它时,耗时同样也会变长。
5) 执行 MONITOR 也会导致 Redis OOM?
这个坑你肯定听说过很多次了。
当你在执行 MONITOR 命令时,Redis 会把每一条命令写到客户端的「输出缓冲区」中,然后客户端从这个缓冲区读取服务端返回的结果。
但是,如果你的 Redis QPS 很高,这将会导致这个输出缓冲区内存持续增长,占用 Redis 大量的内存资源,如果恰好你的机器的内存资源不足,那 Redis 实例就会面临被 OOM 的风险。
따라서 특히 QPS가 높은 경우에는 MONITOR를 주의해서 사용해야 합니다.
위의 문제 시나리오는 모두 일반적인 명령을 사용할 때 발생하며 "의도하지 않게" 실행될 가능성이 높습니다.
Redis "데이터 지속성"의 함정을 살펴볼까요?
Redis 데이터 지속성은 RDB와 AOF의 두 가지 방법으로 나뉩니다.
그 중 RDB는 데이터 스냅샷이며 AOF는 모든 쓰기 명령을 로그 파일에 기록합니다.
데이터 지속성 문제는 주로 이 두 블록에 집중되어 있습니다.
1) 마스터가 다운되고 슬레이브 데이터도 손실된다?
Redis가 다음 모드로 배포되면 데이터 손실이 발생합니다.
master-slave + sentinel 배포 인스턴스
master는 데이터 지속성을 활성화하지 않습니다.
Redis 프로세스는 감독자 관리를 사용합니다. "프로세스 가동 중지 시간, 자동 다시 시작"으로 구성됨
이때 마스터가 다운되면 다음과 같은 문제가 발생합니다.
마스터가 다운되고 센티널이 스위치를 시작하지 않았으며 마스터 프로세스가 즉시 Supervisor에 의해 자동으로 풀업
하지만 마스터는 데이터 지속성을 활성화하지 않고 시작 후 "빈" 인스턴스입니다
이때 마스터와 일관성을 유지하기 위해 슬레이브 인스턴스의 모든 데이터가 자동으로 "삭제"되고 슬레이브도 "빈" 인스턴스가 됩니다
보셨나요? 이 시나리오에서는 모든 마스터/슬레이브 데이터가 손실됩니다.
이때 비즈니스 애플리케이션이 Redis에 액세스하여 캐시에 데이터가 없음을 발견하면 모든 요청을 백엔드 데이터베이스로 보내며, 이는 "캐시 사태"를 더욱 유발하고 시스템에 큰 영향을 미칩니다. 사업.
따라서 이러한 상황이 발생하지 않도록 해야 합니다.
Redis 인스턴스는 프로세스 관리 도구를 사용하지 않고도 자동으로 풀업됩니다.
마스터가 다운된 후 센티널이 스위치를 시작하고 슬레이브 전환 마스터
로의 업그레이드가 완료된 후 마스터를 다시 시작하고 슬레이브로 성능이 저하되도록 하세요
데이터 지속성을 구성할 때 이 문제를 피해야 합니다.
2) AOF Everysec은 실제로 메인 스레드를 차단하지 않나요?
Redis가 AOF를 켜면 AOF 플러싱 전략을 구성해야 합니다.
성능과 데이터 보안의 균형을 바탕으로 귀하는 확실히appendfsynceverysec솔루션을 채택하게 될 것입니다.
이 솔루션의 작동 모드는 Redis의 백그라운드 스레드가 AOF 페이지 캐시의 데이터를 1초마다 디스크(fsync)로 플러시하는 것입니다.
이 솔루션의 장점은 시간이 많이 소요되는 AOF 디스크 브러싱 작업이 메인 스레드에 대한 영향을 피하면서 백그라운드 스레드에서 실행된다는 것입니다.
그런데 정말 메인 스레드에는 영향을 주지 않나요?
답은 '아니오'입니다.
실제로 다음과 같은 시나리오가 있습니다. Redis 백그라운드 스레드가 AOF 페이지 캐시 플러시(fysnc)를 수행할 때 이때 디스크 IO 로드가 너무 높으면 fsync에 대한 호출이 차단됩니다.
이때 메인 스레드는 여전히 쓰기 요청을 받고 있으므로 이때 메인 스레드는 먼저 마지막 백그라운드 스레드가 디스크를 성공적으로 플러시했는지 여부를 확인합니다.
판단하는 방법은 무엇인가요?
디스크 플래시가 성공하면 백그라운드 스레드가 플래시 시간을 기록합니다.
메인 스레드는 이 시간을 사용하여 마지막 디스크 플러시 이후 경과된 시간을 확인합니다. 전체 프로세스는 다음과 같습니다.
AOF 페이지 캐시를 작성(시스템 호출 작성)하기 전에 메인 스레드는 먼저 백그라운드 fsync가 완료되었는지 확인합니다.
fsync가 완료되고 메인 스레드가 AOF 페이지 캐시에 직접 씁니다
fsync가 완료되지 않은 다음 마지막 fsync 이후 얼마나 시간이 지났는지 확인하세요.
마지막 fysnc가 성공한 지 2초 이내라면 메인 스레드는 AOF 페이지 캐시를 쓰지 않고 직접 반환됩니다.
마지막 fysnc가 성공한 지 2초 이상이면 메인 스레드는 AOF 페이지 캐시를 강제로 작성합니다(시스템 호출 작성)
높은 디스크 IO 로드로 인해 이때 백그라운드 스레드 fynsc가 차단되고 AOF 페이지 캐시를 작성할 때 메인 스레드도 차단되어 대기합니다( 동일한 fd, fsync 및 write를 작동하는 것은 상호 배타적입니다. 한 쪽은 다른 쪽이 성공할 때까지 기다려야 실행을 계속할 수 있습니다. 그렇지 않으면 차단하고 기다립니다.)
분석을 통해 구성한 AOF 플러싱 전략이 Appendfsync Everysec인 경우에도 여전히 메인 스레드를 차단할 위험이 있음을 알 수 있습니다.
사실 이 문제의 핵심은 디스크 IO 로드가 너무 높아서 fynsc가 차단되고, 이로 인해 AOF 페이지 캐시에 쓸 때 메인 스레드가 차단된다는 것입니다.
따라서 이 문제를 방지하려면 디스크에 충분한 IO 리소스가 있는지 확인해야 합니다.
3) AOF Everysec은 실제로 1초의 데이터만 손실되나요?
위 질문을 계속 분석해 보세요.
위에서 언급했듯이 여기서는 위의 4단계에 집중해야 합니다.
즉, 메인 스레드가 AOF 페이지 캐시를 쓸 때 마지막 fsync가 성공한 시간을 먼저 결정합니다. 마지막 fysnc가 2초 이내에 성공하면 메인 스레드가 직접 반환되고 더 이상 캐시에 쓰지 않습니다. AOF 페이지 캐시.
이는 백그라운드 스레드가 fsync를 실행하여 디스크를 플러시할 때 기본 스레드가 최대 2초 동안 대기하고 AOF 페이지 캐시에 쓰지 않음을 의미합니다.
이때 Redis가 충돌하면 AOF 파일에서 1초가 아닌 2초의 데이터가 손실됩니다!
계속 분석해 보겠습니다. Redis 메인 스레드가 AOF 페이지 캐시를 쓰지 않고 2초 동안 기다리는 이유는 무엇입니까?
실제로 Redis AOF를 Appendfsync Everysec으로 구성하면 일반적으로 백그라운드 스레드가 1초마다 fsync 플러시를 실행하므로 디스크 리소스가 충분하면 차단되지 않습니다.
즉, AOF 페이지 캐시가 아무 생각 없이 기록되는 한 Redis 메인 스레드는 실제로 백그라운드 스레드가 디스크를 성공적으로 플러시하는지 여부에 대해 신경 쓸 필요가 없습니다.
그러나 Redis 작성자는 이때 디스크 IO 리소스가 상대적으로 부족할 경우 백그라운드 스레드 fsync가 차단될 수 있다고 생각합니다.
그래서 Redis 작성자는 메인 스레드에 AOF 페이지 캐시를 작성하기 전에 먼저 마지막 fsync가 성공한 이후의 시간을 확인합니다. 성공하지 못한 채 1초 이상이면 이때 메인 스레드는 fsync를 알게 됩니다. 차단될 수 있습니다.
따라서 메인 스레드는 AOF 페이지 캐시에 쓰지 않고 2초 동안 기다립니다. 목적은 다음과 같습니다.
메인 스레드 차단 위험을 줄입니다(AOF 페이지 캐시를 부주의하게 작성하면 메인 스레드가 즉시 차단됩니다)
fsync가 차단되면 백그라운드 스레드가 fsync가 성공할 때까지 기다리도록 1초 동안 메인 스레드를 남겨둡니다
하지만 이때 다운타임이 발생하면 AOF는 2초를 잃게 됩니다. 1초가 아닌 초 단위의 데이터입니다.
이 솔루션은 Redis 작성자의 성능과 데이터 보안 간의 추가적인 절충안이어야 합니다.
어쨌든 여기서 알아야 할 것은 AOF가 1초마다 디스크를 플러시하도록 구성하더라도 위의 극단적인 상황이 발생하면 AOF로 인해 손실되는 데이터는 실제로 2초라는 것입니다.
4) RDB 및 AOF를 다시 작성할 때 Redis에서 OOM이 발생합니까?
마지막으로 Redis가 RDB 스냅샷 및 AOF 재작성을 수행할 때 발생하는 문제를 살펴보겠습니다.
Redis가 RDB 스냅샷 및 AOF 재작성을 수행하면 인스턴스의 데이터를 디스크에 유지하기 위한 하위 프로세스가 생성됩니다.
하위 프로세스를 생성하면 운영 체제의 포크 기능이 호출됩니다.
Fork 실행이 완료된 후 상위 프로세스와 하위 프로세스는 동시에 동일한 메모리 데이터를 공유합니다.
그러나 현재 메인 프로세스는 여전히 쓰기 요청을 받을 수 있으며, 들어오는 쓰기 요청은 Copy On Write를 사용하여 메모리 데이터를 작동합니다.
즉, 기본 프로세스에 수정해야 할 데이터가 있으면 Redis는 기존 메모리의 데이터를 직접 수정하지 않고 먼저 메모리 데이터를 복사한 다음 새 메모리의 데이터를 수정합니다. "기록 중 복사"라고 불리는 것입니다.
기록 중 복사는 글을 써야 하는 사람이 먼저 복사한 다음 수정한다는 의미로 이해할 수도 있습니다.
부모 프로세스가 키를 수정하려면 원래 메모리 데이터를 새 메모리에 복사해야 한다는 사실을 발견했어야 합니다. 이 프로세스에는 "새 메모리" 적용이 포함됩니다.
귀하의 비즈니스 특성이 "쓰기는 많고 읽기는 적다"이고 OPS가 매우 높으면 RDB 및 AOF 재작성 중에 많은 메모리 복사 작업이 생성됩니다.
이게 뭐가 문제야?
쓰기 요청이 많기 때문에 Redis 상위 프로세스가 많은 메모리를 적용하게 됩니다. 이 기간 동안 키 수정 범위가 넓어질수록 더 많은 새로운 메모리 애플리케이션이 필요합니다.
머신에 메모리 리소스가 부족하면 Redis가 OOM 위험에 처하게 됩니다!
这就是你会从 DBA 同学那里听到的,要给 Redis 机器预留内存的原因。
其目的就是避免在 RDB 和 AOF rewrite 期间,防止 Redis OOM。
以上这些,就是「数据持久化」会遇到的坑,你踩到过几个?
下面我们再来看「主从复制」会存在哪些问题。
Redis 为了保证高可用,提供了主从复制的方式,这样就可以保证 Redis 有多个「副本」,当主库宕机后,我们依旧有从库可以使用。
在主从同步期间,依旧存在很多坑,我们依次来看。
1) 主从复制会丢数据吗?
首先,你需要知道,Redis 的主从复制是采用「异步」的方式进行的。
这就意味着,如果 master 突然宕机,可能存在有部分数据还未同步到 slave 的情况发生。
这会导致什么问题呢?
如果你把 Redis 当做纯缓存来使用,那对业务来说没有什么影响。
master 未同步到 slave 的数据,业务应用可以从后端数据库中重新查询到。
但是,对于把 Redis 当做数据库,或是当做分布式锁来使用的业务,有可能因为异步复制的问题,导致数据丢失 / 锁丢失。
关于 Redis 分布式锁可靠性的更多细节,这里先不展开,后面会单独写一篇文章详细剖析这个知识点。这里你只需要先知道,Redis 主从复制是有概率发生数据丢失的。
2) 同样命令查询一个 key,主从库却返回不同的结果?
不知道你是否思考过这样一个问题:如果一个 key 已过期,但这个 key 还未被 master 清理,此时在 slave 上查询这个 key,会返回什么结果呢?
slave 正常返回 key 的值
slave 返回 NULL
你认为是哪一种?可以思考一下。
答案是:不一定。
嗯?为什么会不一定?
这个问题非常有意思,请跟紧我的思路,我会带你一步步分析其中的原因。
其实,返回什么结果,这要取决于以下 3 个因素:
Redis 的版本
具体执行的命令
机器时钟
先来看 Redis 版本。
如果你使用的是 Redis 3.2 以下版本,只要这个 key 还未被 master 清理,那么,在 slave 上查询这个 key,它会永远返回 value 给你。
也就是说,即使这个 key 已过期,在 slave 上依旧可以查询到这个 key。
// Redis 2.8 版本 在 slave 上执行 127.0.0.1:6479> TTL testkey (integer) -2 // 已过期 127.0.0.1:6479> GET testkey "testval" // 还能查询到!
但如果此时在 master 上查询这个 key,发现已经过期,就会把它清理掉,然后返回 NULL。
// Redis 2.8 版本 在 master 上执行 127.0.0.1:6379> TTL testkey (integer) -2 127.0.0.1:6379> GET testkey (nil)
发现了吗?在 master 和 slave 上查询同一个 key,结果竟然不一样?
其实,slave 应该要与 master 保持一致,key 已过期,就应该给客户端返回 NULL,而不是还正常返回 key 的值。
为什么会发生这种情况?
其实这是 Redis 的一个 Bug:3.2 以下版本的 Redis,在 slave 上查询一个 key 时,并不会判断这个 key 是否已过期,而是直接无脑返回给客户端结果。
这个 Bug 在 3.2 版本进行了修复,但是,它修复得「不够彻底」。
什么叫修复得「不够彻底」?
这就要结合前面提到的,第 2 个影响因素「具体执行的命令」来解释了。
Redis 3.2 虽然修复了这个 Bug,但却遗漏了一个命令:EXISTS。
也就是说,一个 key 已过期,在 slave 直接查询它的数据,例如执行 GET/LRANGE/HGETALL/SMEMBERS/ZRANGE 这类命令时,slave 会返回 NULL。
但如果执行的是 EXISTS,slave 依旧会返回:key 还存在。
// Redis 3.2 版本 在 slave 上执行 127.0.0.1:6479> GET testkey (nil) // key 已逻辑过期 127.0.0.1:6479> EXISTS testkey (integer) 1 // 还存在!
原因在于,EXISTS 与查询数据的命令,使用的不是同一个方法。
Redis 作者只在查询数据时增加了过期时间的校验,但 EXISTS 命令依旧没有这么做。
直到 Redis 4.0.11 这个版本,Redis 才真正把这个遗漏的 Bug 完全修复。
如果你使用的是这个之上的版本,那在 slave 上执行数据查询或 EXISTS,对于已过期的 key,就都会返回「不存在」了。
这里我们先小结一下,slave 查询过期 key,经历了 3 个阶段:
3.2 以下版本,key 过期未被清理,无论哪个命令,查询 slave,均正常返回 value
3.2 - 4.0.11 版本,查询数据返回 NULL,但 EXISTS 依旧返回 true
4.0.11 以上版本,所有命令均已修复,过期 key 在 slave 上查询,均返回「不存在」
这里要特别鸣谢《Redis开发与运维》的作者,付磊。
这个问题我是在他的文章中看到的,感觉非常有趣,原来 Redis 之前还存在这样的 Bug 。随后我又查阅了相关源码,并对逻辑进行了梳理,在这里才写成文章分享给大家。
虽然已在微信中亲自答谢,但在这里再次表达对他的谢意~
最后,我们来看影响查询结果的第 3 个因素:「机器时钟」。
假设我们已规避了上面提到的版本 Bug,例如,我们使用 Redis 5.0 版本,在 slave 查询一个 key,还会和 master 结果不同吗?
答案是,还是有可能会的。
这就与 master / slave 的机器时钟有关了。
无论是 master 还是 slave,在判断一个 key 是否过期时,都是基于「本机时钟」来判断的。
如果 slave 的机器时钟比 master 走得「快」,那就会导致,即使这个 key 还未过期,但以 slave 上视角来看,这个 key 其实已经过期了,那客户端在 slave 上查询时,就会返回 NULL。
是不是很有意思?一个小小的过期 key,竟然藏匿这么多猫腻。
如果你也遇到了类似的情况,就可以通过上述步骤进行排查,确认是否踩到了这个坑。
3) 主从切换会导致缓存雪崩?
这个问题是上一个问题的延伸。
我们假设,slave 的机器时钟比 master 走得「快」,而且是「快很多」。
此时,从 slave 角度来看,Redis 中的数据存在「大量过期」。
如果此时操作「主从切换」,把 slave 提升为新的 master。
它成为 master 后,就会开始大量清理过期 key,此时就会导致以下结果:
master 大量清理过期 key,主线程发生阻塞,无法及时处理客户端请求
Redis 中数据大量过期,引发缓存雪崩
你看,当 master / slave 机器时钟严重不一致时,对业务的影响非常大!
所以,如果你是 DBA 运维,一定要保证主从库的机器时钟一致性,避免发生这些问题。
4) master / slave 大量数据不一致?
还有一种场景,会导致 master / slave 的数据存在大量不一致。
这就涉及到 Redis 的 maxmemory 配置了。
Redis 的 maxmemory 可以控制整个实例的内存使用上限,超过这个上限,并且配置了淘汰策略,那么实例就开始淘汰数据。
但这里有个问题:假设 master / slave 配置的 maxmemory 不一样,那此时就会发生数据不一致。
例如,master 配置的 maxmemory 为 5G,而 slave 的 maxmemory 为 3G,当 Redis 中的数据超过 3G 时,slave 就会「提前」开始淘汰数据,此时主从库数据发生不一致。
또한 마스터/슬레이브의 최대 메모리 설정은 동일하지만 상한을 조정하려는 경우에도 특별한 주의를 기울여야 합니다. 그렇지 않으면 슬레이브도 데이터가 제거됩니다.
증가하는 경우 maxmemory는 슬레이브를 먼저 조정하고 마스터를 조정하세요
최대 메모리를 조정할 때는 마스터를 먼저 조정하고 슬레이브를 조정하세요
이렇게 하면 슬레이브가 사전에 최대 메모리를 초과하는 문제가 발생합니다 방지됩니다.
사실 생각해보면 이러한 문제의 핵심은 무엇일까요?
근본적인 이유는 슬레이브가 최대 메모리를 초과하면 "자체" 데이터를 제거한다는 것입니다.
슬레이브가 스스로 데이터를 제거하는 것이 허용되지 않으면 이 모든 문제를 피할 수 있나요?
맞습니다.
이 문제에 대해 Redis 관계자도 많은 사용자로부터 피드백을 받았어야 했습니다. Redis 5.0 버전에서는 공식적으로 이 문제가 완전히 해결되었습니다!
Redis 5.0에는 구성 항목인 Replica-ignore-maxmemory가 추가되었으며 기본값은 yes입니다.
이 매개변수는 슬레이브 메모리가 최대 메모리를 초과하더라도 자체적으로 데이터를 제거하지 않는다는 것을 의미합니다!
이런 방식으로 슬레이브는 항상 마스터와 동등하며 마스터가 보낸 데이터만 충실히 복사하며 자체적으로 "작은 속임수"를 만들지 않습니다.
이 시점에서 마스터/슬레이브의 데이터는 완전한 일관성을 보장할 수 있습니다!
버전 5.0을 사용하고 있다면 이 문제에 대해 걱정할 필요가 없습니다.
5) 슬레이브가 실제로 메모리 누수 문제를 겪고 있나요?
예, 읽으신 것이 맞습니다.
어떻게 이런 일이 일어났나요? 자세히 살펴보겠습니다.
Redis를 사용할 때 다음 시나리오가 충족되면 슬레이브 메모리 누수가 발생합니다.
Redis는 4.0 이하 버전을 사용하고 있습니다
슬레이브 구성 항목이 읽기 전용=아니요(라이브러리에서 쓰기 가능)
만료 시간이 포함된 키를 슬레이브에 쓰기
이 때 슬레이브에는 메모리 누수가 발생합니다. 슬레이브에 있는 키는 만료 시간에 도달하더라도 자동으로 정리되지 않습니다.
적극적으로 삭제하지 않으면 이러한 키는 슬레이브 메모리에 남아 슬레이브의 메모리를 소모하게 됩니다.
가장 귀찮은 점은 명령을 사용하여 이러한 키를 쿼리하지만 여전히 결과를 찾을 수 없다는 것입니다!
슬레이브 "메모리 누수" 문제입니다.
이것은 실제로 Redis의 버그입니다. 이 문제는 Redis 4.0에서만 수정되었습니다.
해결책은 쓰기 가능한 슬레이브에서 만료 시간이 있는 키를 쓸 때 슬레이브가 이러한 키를 "기록"한다는 것입니다.
그러면 슬레이브는 이 키를 정기적으로 스캔하고 만료 시간에 도달하면 삭제됩니다.
비즈니스에서 슬레이브에 데이터를 임시로 저장해야 하고 이러한 키에 만료 시간이 설정되어 있는 경우 이 문제에 주의해야 합니다.
Redis 버전이 4.0 미만인 경우 이러한 함정을 피해야 합니다.
사실 가장 좋은 해결책은 Redis 사용 사양을 공식화하는 것입니다. 슬레이브는 읽기 전용이어야 하며 쓰기는 허용되지 않습니다. 이렇게 하면 마스터/슬레이브의 데이터 일관성을 보장할 수 있을 뿐만 아니라 슬레이브 메모리 누수 문제.
6) 마스터-슬레이브 전체 동기화가 계속 실패하는 이유는 무엇입니까?
전체 마스터-슬레이브 동기화 중에 동기화 실패 문제가 발생할 수 있습니다. 구체적인 시나리오는 다음과 같습니다.
슬레이브가 마스터에 대한 전체 동기화 요청을 시작하고, 마스터가 RDB를 생성하여 슬레이브에 보냅니다. 슬레이브는 RDB를 로드합니다.
RDB 데이터가 너무 크기 때문에 슬레이브 로딩 시간도 매우 길어집니다.
이 시점에서 슬레이브가 RDB 로딩을 완료하지 않았지만 마스터와 슬레이브 간의 연결이 끊어져 데이터 동기화에 실패한 것을 확인할 수 있습니다.
이후에는 슬레이브가 다시 전체 동기화를 시작하고 마스터가 RDB를 생성하여 슬레이브로 보내는 것을 볼 수 있습니다.
마찬가지로 슬레이브가 RDB를 로드할 때 마스터/슬레이브 동기화가 다시 실패하는 등의 현상이 발생합니다.
무슨 일이에요?
사실 이게 바로 Redis의 "replication storm" 문제입니다.
복사 폭풍이란 무엇인가요?
방금 설명한 것과 같습니다. 마스터-슬레이브 전체 동기화가 실패하고, 동기화가 다시 시작되고, 다시 동기화가 실패하는 악순환이 계속되며 시스템 리소스가 계속 낭비됩니다.
이 문제가 발생하는 이유는 무엇입니까?
Redis가 다음과 같은 특성을 가지고 있는 경우 이 문제가 발생할 수 있습니다.
마스터의 인스턴스 데이터가 너무 크고 슬레이브가 RDB를 로드하는 데 너무 오랜 시간이 걸립니다.
버퍼 복사(슬레이브 클라이언트 출력 -buffer-limit)이 너무 작게 구성되었습니다
마스터에 많은 양의 쓰기 요청이 있습니다
마스터와 슬레이브가 데이터를 완전히 동기화하면 마스터가 받은 쓰기 요청이 먼저 마스터에 기록됩니다 -slave "복사 버퍼". 이 버퍼의 "상한"은 구성에 따라 결정됩니다.
슬레이브가 RDB를 너무 느리게 로드하면 슬레이브가 "복제 버퍼"의 데이터를 제때 읽을 수 없게 되어 복제 버퍼가 "오버플로"됩니다.
지속적인 메모리 증가를 방지하기 위해 마스터는 이때 슬레이브의 연결을 "강제로" 연결 해제하며 전체 동기화는 실패합니다.
이후 동기화에 실패한 슬레이브는 전체 동기화를 "다시" 시작하게 되고, 이후 위에서 설명한 문제에 빠지게 되며, 이것이 소위 "복제 폭풍"이 반복됩니다.
이 문제를 해결하는 방법은 무엇입니까? 다음과 같은 제안을 드립니다.
Redis 인스턴스는 너무 커서는 안 되며, 너무 큰 RDB도 피하세요.
복사 버퍼를 최대한 크게 구성하고, 슬레이브에 RDB를 로드할 수 있는 충분한 시간을 주며, 확률을 줄이세요.
이 함정에 빠졌다면 이 솔루션을 통해 해결할 수 있습니다.
알겠습니다. 요약하자면 이 문서에서는 주로 "명령 사용", "데이터 지속성" 및 "마스터-슬레이브 동기화"라는 세 가지 측면에서 Redis의 가능한 함정에 대해 설명합니다.
어때요? 그것이 당신의 이해를 뒤엎었나요?
이 글에는 상대적으로 많은 양의 정보가 포함되어 있습니다. 지금 여러분의 생각이 다소 "복잡"하다면 걱정하지 마세요. 여러분의 더 나은 이해와 기억을 돕기 위해 마인드 맵도 준비했습니다.
Redis를 사용하실 때 이러한 함정을 미리 피하시고 Redis가 더 나은 서비스를 제공할 수 있도록 해주시기 바랍니다.
마지막으로 개발 과정에서 함정을 밟았던 경험과 생각에 대해 말씀드리고 싶습니다.
사실 새로운 분야를 접하게 되면 낯설음, 익숙함, 함정 밟기, 경험 흡수, 편안함의 여러 단계를 거치게 됩니다.
이 함정 단계에서 어떻게 함정을 피할 수 있을까요? 아니면 함정에 빠진 후 문제를 효율적으로 해결하는 방법은 무엇입니까?
여기에는 도움이 될 4가지 측면이 요약되어 있습니다.
1) 더 많은 공식 문서 및 구성 파일에 대한 설명을 읽어보세요
구성 파일에 대한 더 많은 공식 문서와 설명을 읽어보세요. 실제로 훌륭한 소프트웨어는 문서와 주석에 있을 수 있는 많은 위험을 상기시켜 줍니다. 주의 깊게 읽으면 많은 기본적인 문제를 사전에 피할 수 있습니다.
2) 질문의 내용을 놓치지 말고 왜 그런지 더 생각해 보세요.
항상 호기심을 가지세요. 문제에 직면했을 때 고치를 벗겨 점차적으로 문제를 찾아내는 능력을 익히고, 항상 문제의 본질을 탐구하는 정신을 유지하십시오.
3) 과감하게 질문을 해보세요. 소스 코드는 거짓말을 하지 않습니다.
문제가 이상하다고 생각되면 버그일 수도 있으니 과감하게 질문을 해보세요.
소스 코드를 통해 문제의 진실을 찾는 것이 인터넷에서 서로 표절한 기사 백 개를 읽는 것보다 낫습니다(계속 복사하면 오류가 발생할 가능성이 매우 높습니다).
4) 완벽한 소프트웨어는 없습니다. 우수한 소프트웨어는 단계별로 반복됩니다.
모든 우수한 소프트웨어는 단계별로 반복됩니다. 반복 과정에서 버그가 존재하는 것은 당연하며, 이를 올바른 사고방식으로 바라볼 필요가 있습니다.
이러한 경험과 통찰력은 모든 학문 분야에 적용될 수 있기를 바랍니다.
더 많은 프로그래밍 관련 지식을 보려면 프로그래밍 교육을 방문하세요! !
위 내용은 Redis를 사용할 때 발생할 수 있는 15가지 함정을 수집하여 번개를 피하세요! !의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!