전통문화대전망 - 전통 미덕 - 데이터베이스 캐시의 최종 일관성을 어떻게 보장하나요?
데이터베이스 캐시의 최종 일관성을 어떻게 보장하나요?
인터넷 비즈니스의 경우 기존 데이터베이스 직접 액세스는 주로 데이터 샤딩, 하나의 마스터와 여러 슬레이브를 사용하여 읽기 및 쓰기 트래픽을 처리하지만 데이터 볼륨이 축적되고 트래픽이 급증함에 따라, 모든 트래픽을 처리하기 위해 데이터베이스에만 의존하는 것은 비용이 많이 들고 비효율적일 뿐만 아니라 안정성이 저하될 위험도 있습니다.
대부분의 기업은 일반적으로 더 많이 읽고 덜 쓴다는 점(읽기 빈도가 업데이트 빈도보다 훨씬 높음)을 고려하면 읽기 작업 수가 쓰기 작업보다 몇 배 더 높은 상황도 있습니다. . 따라서 아키텍처 설계에서는 시스템 응답성을 향상하고, 데이터 읽기 및 쓰기 성능을 향상시키며, 데이터베이스 액세스 부담을 줄여 비즈니스 안정성과 액세스 경험을 향상시키기 위해 캐시 레이어를 추가하는 경우가 많습니다.
CAP 원칙에 따르면 분산 시스템은 가용성, 일관성 및 파티션 내결함성을 모두 가질 수 없습니다. 일반적으로 파티션 내결함성을 피할 수 없기 때문에 일관성과 가용성을 동시에 설정할 수 없습니다. 캐싱 시스템의 경우 데이터 일관성을 어떻게 보장할 것인가는 캐싱을 적용하면서 해결해야 할 문제입니다.
캐시 시스템의 데이터 일관성에는 일반적으로 지속성 계층과 캐시 계층의 일관성뿐만 아니라 다중 레벨 캐시 간의 일관성도 포함됩니다. 여기서는 전자에 대해서만 논의합니다. 지속성 계층과 캐시 계층 간의 일관성 문제는 종종 이중 쓰기 일관성 문제라고도 합니다. "이중 쓰기"는 데이터의 복사본 하나가 데이터베이스에 저장되고 복사본 하나도 캐시에도 저장되는 것을 의미합니다.
일관성에는 Strong Consistency와 Weak Consistency가 있는데, Strong Consistency는 쓴 후에 값을 즉시 읽을 수 있다는 것을 보장하는 반면, 약한 일관성은 쓴 후의 값을 즉시 읽을 수 있다는 것을 보장하지 않습니다. 약한 일관성에서 가장 널리 사용되는 모델은 일정 기간 후에 쓰기와 읽기가 일관된 상태에 도달하도록 보장하는 최종 일관성 모델입니다. 대부분의 애플리케이션 캐싱 시나리오에서는 최종 일관성이 추구되며 매우 높은 데이터 일관성이 필요한 소수의 시나리오에서는 강력한 일관성이 추구됩니다.
최종 일관성을 달성하기 위해 업계에서는 다양한 시나리오에 대해 다음과 같은 애플리케이션 캐싱 전략을 점진적으로 형성해 왔습니다.
— 1 —
캐시 배제
캐시 배제는 캐시 우회 모드를 의미합니다. 가장 널리 사용되는 캐싱 전략입니다. 아래 다이어그램은 최종 일관성을 보장하는 방법을 알아보기 위해 읽기 및 쓰기 프로세스를 보여줍니다. 읽기 요청에서는 캐시가 먼저 요청되며, 캐시가 적중하면 캐시에 있는 데이터가 직접 반환되고, 캐시가 누락되면 데이터베이스가 쿼리되고 쿼리 결과가 캐시에 업데이트됩니다. (수요가 가득한 모습). 쓰기 요청에서는 데이터베이스가 먼저 업데이트된 다음 캐시가 삭제됩니다(쓰기 무효화).
1. 캐시를 업데이트하지 않고 삭제하는 이유는 무엇입니까?
Cache-Aside에서는 읽기 요청 처리가 상대적으로 이해하기 쉽지만 쓰기 요청에서는 독자가 질문을 할 수 있습니다. 왜 캐시를 업데이트하는 대신 삭제해야 합니까? 직관적인 관점에서 보면 캐시 업데이트는 이해하기 쉬운 솔루션이지만 성능 및 보안 측면에서 캐시 업데이트는 몇 가지 나쁜 결과를 초래할 수 있습니다.
첫 번째는 성능입니다. 캐시에 해당하는 결과를 얻기 위해 많은 양의 계산 프로세스가 필요한 경우, 예를 들어 여러 데이터베이스 테이블에 액세스하여 공동 계산해야 하는 경우 업데이트 작업이 수행됩니다. 쓰기 작업 중 캐시 비용은 적지 않습니다. 동시에, 쓰기 작업이 많을 때 새로 업데이트된 캐시를 읽지 못하고 다시 업데이트되는 상황이 있을 수 있습니다(이를 캐시 방해라고 함). 분명히 이러한 업데이트는 시스템 성능을 헛되이 소비합니다. 캐시 활용도가 낮아집니다.
업데이트하기 전에 읽기 요청이 캐시를 놓칠 때까지 기다리는 것도 지연 로딩 아이디어와 일치하며 필요할 때 계산이 수행됩니다. 캐시 삭제 작업은 멱등성이 있고 예외 발생 시 재시도될 수 있을 뿐만 아니라 쓰기-삭제 및 읽기-업데이트가 의미상 더 대칭적입니다.
두 번째는 보안입니다. 동시 시나리오에서는 쓰기 요청에서 캐시를 업데이트하면 데이터 불일치가 발생할 수 있습니다. 아래 다이어그램을 참조하면 서로 다른 스레드에서 두 개의 쓰기 요청이 있는 경우 먼저 스레드 1의 쓰기 요청이 데이터베이스를 업데이트하고(1단계) 스레드 2의 쓰기 요청이 다시 데이터베이스를 업데이트하지만(3단계) 네트워크 지연 및 기타 이유로 인해 스레드 1은 스레드 2보다 나중에 캐시를 업데이트할 수 있습니다(4단계는 3단계보다 늦음). 이로 인해 데이터베이스에 기록되는 최종 결과는 스레드 2의 새 값이 됩니다. 스레드 2의 새로운 값이 캐시에 기록됩니다. 스레드 1의 이전 값, 즉 캐시가 데이터베이스보다 뒤쳐집니다. 이때 다른 읽기 요청이 캐시에 도달하면(5단계) 이전 값이 됩니다. 읽힌다.
2. 캐시를 먼저 삭제하지 않고 데이터베이스를 먼저 업데이트하는 이유는 무엇입니까?
또한 일부 독자들은 데이터베이스 업데이트 및 캐시 삭제 시점에 대해 질문할 수도 있습니다. 그렇다면 캐시를 먼저 삭제한 다음 데이터베이스를 업데이트하는 것은 어떨까요? 단일 스레드에서는 이 솔루션이 합리적인 것으로 보이며 이러한 합리성은 성공적인 캐시 삭제에 반영됩니다.
그러나 데이터베이스 업데이트가 실패하는 경우 캐시가 삭제되더라도 다음 읽기 작업 중에 올바른 데이터가 캐시에 다시 기록될 수 있습니다. 업데이트는 성공했지만 캐시 삭제는 실패했습니다. 이 시나리오에서는 캐시를 먼저 삭제하는 것이 더 합리적으로 보입니다. 그렇다면 캐시를 먼저 삭제하면 무엇이 문제일까요?
동시 시나리오에서는 문제가 계속 발생합니다. 먼저 스레드 1의 쓰기 요청으로 인해 캐시가 삭제되고(1단계) 스레드 2의 읽기 요청으로 인해 캐시 삭제로 인해 캐시 누락이 발생합니다. 캐시 배제 모드에서는 스레드 2가 데이터베이스를 쿼리하지만(2단계) 쓰기 요청은 일반적으로 읽기 요청보다 느리기 때문에 데이터베이스를 업데이트하는 스레드 1의 작업은 데이터베이스를 쿼리한 후 캐시를 업데이트하는 스레드 2의 작업보다 늦을 수 있습니다. 4단계는 3단계보다 이후입니다. 그러면 캐시에 기록된 최종 결과는 스레드 2에서 쿼리된 이전 값이 되고, 데이터베이스에 기록된 결과는 스레드 1의 새 값, 즉 캐시가 됩니다. 데이터베이스보다 지연되고 이때 다른 읽기 요청이 캐시에 도달하면(5단계) 읽혀지는 것은 이전 값입니다.
또한 캐시에 있는 데이터가 부족하여 데이터베이스에 대한 요청 압력이 높아지므로 캐시를 먼저 삭제해야 합니다. .
3. 캐시를 먼저 삭제한 다음 데이터베이스를 업데이트하기로 선택한 경우 일관성 문제를 해결하는 방법은 무엇입니까?
동시에 읽고 쓸 때 "캐시를 먼저 삭제한 다음 데이터베이스를 업데이트하는" 솔루션으로 인해 발생할 수 있는 캐시된 더티 데이터를 방지하기 위해 업계에서는 지연된 이중 삭제 전략도 제안했습니다. 즉, 데이터베이스를 업데이트한 후 캐시를 다시 삭제하는 것을 일정 시간 동안 지연하여 두 번째 캐시 삭제 시점이 읽기 요청에 의해 캐시가 업데이트된 이후인지 확인하기 위한 경험적 값입니다. 이 지연 시간 중 일반적으로 비즈니스의 읽기 요청에 소요되는 시간보다 약간 더 커야 합니다.
지연 구현은 코드에서 잠을 자거나 지연 대기열을 사용할 수 있습니다. 이 값을 어떻게 추정하더라도 읽기 요청 완료 시점을 정확하게 연결하기 어렵다는 점은 지연 이중 삭제가 비판받는 주된 이유이기도 하다.
4. Cache-Aside에서 데이터 불일치 가능성이 있나요?
Cache-Aside에서는 데이터 불일치 가능성도 있습니다. 다음 읽기-쓰기 동시성 시나리오에서는 먼저 스레드 1의 읽기 요청이 캐시에 도달하지 않고 데이터베이스를 쿼리한 다음(1단계) 스레드 2의 쓰기 요청이 데이터베이스를 업데이트합니다(2단계). 이유는 스레드 1의 읽기 요청에 대한 업데이트 캐시 작업이 스레드 2의 쓰기 요청에 대한 캐시 삭제 작업보다 늦기 때문입니다(4단계가 3단계보다 늦음). 그러면 스레드 1의 이전 값이 최종적으로 데이터베이스에 기록되는 것은 스레드 2의 새 값입니다. 즉, 캐시가 데이터베이스보다 뒤떨어집니다. 이때 또 다른 읽기 요청이 캐시에 도달하여(5단계) 이전 값이 됩니다. 읽다.
이 시나리오의 출현에는 캐시 무효화와 읽기 및 쓰기의 동시 실행이 필요할 뿐만 아니라 데이터베이스를 업데이트하기 위한 쓰기 요청보다 먼저 데이터베이스를 쿼리하기 위한 읽기 요청이 실행되어야 하며, 읽기 요청은 쓰기 요청보다 늦게 완료됩니다. 이렇게 일관되지 않은 시나리오에 대한 조건은 매우 엄격하고 실제 생산에서는 발생할 가능성이 적다는 점만 봐도 충분합니다.
또한 동시 환경에서는 쓰기 요청이 데이터베이스를 업데이트한 후 캐시에 도달하기 전에 캐시 배제(Cache-Aside) 시점도 있습니다. 캐시가 삭제되면 읽기 요청으로 쿼리된 캐시가 데이터베이스보다 지연됩니다.
캐시는 다음 읽기 요청에서 업데이트되지만 비즈니스 수준에서 이 상황에 대한 허용 범위가 낮은 경우 쓰기 요청에서 잠금을 사용하여 다음을 보장할 수 있습니다. "데이터베이스 업데이트 및 캐시 삭제"의 직렬 실행은 원자성 작업입니다(동일한 방식으로 읽기 요청의 캐시된 업데이트도 잠길 수 있습니다). 잠금은 필연적으로 처리량 감소로 이어지므로 잠금 솔루션을 채택할 경우 성능 손실이 예상됩니다.
— 2 —
보상 메커니즘
위에서 Cache-Aside에서 언급한 바 있습니다. 데이터베이스가 성공적으로 업데이트되었지만 캐시 삭제에 실패하는 경우, 캐시에 있는 데이터가 데이터베이스보다 지연되어 데이터 불일치가 발생합니다.
실제로 이 문제는 Cache-Aside뿐만 아니라 지연 이중 삭제와 같은 전략에서도 존재합니다. 가능한 삭제 실패 문제에 대응하여 업계에서는 현재 다음과 같은 보상 메커니즘을 갖추고 있습니다.
1. 삭제 재시도 메커니즘
동기식 재시도 삭제는 성능 측면에서 처리량에 영향을 미치기 때문에 삭제에 실패한 캐시에 해당하는 키를 넣기 위해 메시지 큐가 도입되는 경우가 많습니다. 메시지 큐에서 해당 소비자로부터 삭제에 실패한 키를 얻고 비동기적으로 삭제를 다시 시도합니다. 이 방법은 구현이 비교적 간단하지만, 비즈니스 코드의 트리거를 기반으로 삭제 실패 후 로직을 트리거해야 하기 때문에 비즈니스 코드에 다소 방해가 됩니다.
위의 솔루션은 비즈니스 코드를 침해하기 때문에 캐시 삭제 실패에 대한 보상 메커니즘이 가능한 한 적은 결합으로 뒤에서 실행될 수 있도록 보다 우아한 솔루션이 필요합니다. . 간단한 아이디어는 업데이트 타임스탬프 또는 버전을 백그라운드 작업을 통한 비교로 사용하여 데이터베이스의 증분 데이터를 가져와 캐시에 업데이트하는 것입니다. 이 방법은 소규모 데이터 시나리오에서 특정 역할을 할 수 있지만 확장성과 안정성이 제한되어 있습니다.
비교적 성숙한 솔루션은 MySQL 데이터베이스 증분 로그 구문 분석 및 소비를 기반으로 합니다. 여기서 더 인기 있는 솔루션은 MySQL binlog 증분 수집 및 구문 분석 구성 요소인 Alibaba의 오픈 소스 구성 요소입니다(유사한 오픈 소스 구성 요소는 다음과 같습니다). Maxwell, Databus 등도 있습니다.)
운하 서버는 MySQL 슬레이브의 상호 작용 프로토콜을 시뮬레이션하고 MySQL 슬레이브인 척하며 덤프 프로토콜을 MySQL 마스터로 보냅니다. MySQL 마스터는 덤프 요청을 수신하고 바이너리 로그를 MySQL 마스터로 푸시하기 시작합니다. 슬레이브(즉, 운하 서버)와 운하 서버는 이를 구문 분석합니다. 바이너리 로그 객체(원래 바이트 스트림)는 소비를 위해 운하 클라이언트에서 가져올 수 있습니다. 기본적으로 변경 레코드를 MQ 시스템에 저장하고 사용을 위해 다른 시스템에 적극적으로 푸시합니다.
ack 메커니즘의 지원을 통해 푸시든 풀이든 데이터가 예상대로 소비되는지 효과적으로 확인할 수 있습니다. 현재 버전의 canal은 Kafka 또는 RocketMQ와 같은 MQ를 지원합니다. 또한 Canal은 HA를 구현하기 위한 분산 조정 구성 요소로 ZooKeeper를 사용합니다. Canal의 HA는 두 부분으로 나뉩니다.
그런 다음 캐시에 대한 삭제 작업을 수행할 수 있습니다. 운하 클라이언트 또는 소비자에 관련 비즈니스 코드를 작성하여 완료합니다. 이러한 방식으로 데이터베이스 로그의 증분 구문 분석 및 소비 방식과 Cache-Aside 모델을 결합하여 읽기 요청에서 캐시가 누락되면 캐시가 업데이트되고(여기서는 일반적으로 복잡한 비즈니스 로직이 포함됩니다) 캐시는 이후 삭제됩니다. 데이터베이스는 쓰기 요청 시 업데이트되며 로그를 기반으로 증분 구문 분석을 사용하여 데이터베이스 업데이트 중에 발생할 수 있는 캐시 삭제 오류를 보상합니다. 대부분의 시나리오에서는 캐시의 최종 일관성을 효과적으로 보장할 수 있습니다.
또한, 데이터베이스에 저장된 후 캐시가 삭제되도록 트랜잭션과 캐시를 격리해야 한다는 점에 유의해야 합니다. 예를 들어 데이터베이스의 마스터-슬레이브 아키텍처를 고려하면 마스터-슬레이브 동기화 및 읽기-슬레이브-쓰기 마스터 시나리오로 인해 슬레이브 데이터베이스에서 이전 데이터를 읽은 후 캐시가 업데이트되어 캐시가 데이터베이스보다 지연될 수 있습니다. . 이를 위해서는 데이터베이스 작업이 완료된 후 캐시 삭제가 보장되어야 합니다. 따라서 binlog 증분 로그를 기반으로 하는 데이터 동기화 솔루션의 경우 슬레이브 노드의 binlog를 구문 분석하여 마스터-슬레이브 동기화 시 조기 캐시 삭제 문제를 방지하도록 선택할 수 있습니다.
3. 데이터 전송 서비스 DTS
— 3 —
Read-Through
Read-Through는 읽기 침투 모드를 의미하며, 읽기 요청은 이 액세스 제어 레이어와만 상호 작용한다는 점에서 Cache-Aside와 유사합니다. 장면 캐시 적중 또는 실패 논리는 액세스 제어 계층에 의해 데이터 소스와 상호 작용합니다. 비즈니스 계층의 구현은 더 간단해지고 캐시 계층과 지속성 계층 간의 상호 작용은 더 캡슐화되고 이식하기가 더 쉬워집니다.
— 4 —
Write-Through
Write-Through는 직접 쓰기 모드를 의미합니다. 연속 기입(Write-Through) 연속 기입 모드에서는 더 높은 수준의 캡슐화를 제공하기 위해 액세스 제어 계층도 추가됩니다. Cache-Aside와 달리 Write-Through Write-Through 모드는 쓰기 요청이 데이터베이스를 업데이트한 후 캐시를 삭제하지 않지만 캐시를 업데이트합니다.
이 방법의 장점은 읽기 요청 프로세스가 간단하고 캐시를 업데이트하기 위해 데이터베이스를 쿼리하는 등의 작업이 필요하지 않다는 것입니다. 그러나 위에서 언급한 데이터베이스 업데이트와 캐시 업데이트의 단점 외에도 이 솔루션은 업데이트 효율성이 낮고 두 가지 쓰기 작업이 실패하면 데이터 불일치가 발생한다는 단점도 있습니다.
이 솔루션을 사용하려면 이 두 작업을 동시에 실패하거나 성공할 수 있고 롤백을 지원하며 동시 환경에서 불일치를 방지하는 트랜잭션으로 처리하는 것이 가장 좋습니다. 또한 잦은 캐시 교란 발생을 방지하기 위해 캐시에 TTL을 추가하여 이를 완화할 수도 있습니다.
타당성 측면에서 Write-Through 모드이든 Cache-Aside 모드이든 이상적으로는 분산 트랜잭션을 통해 캐시 계층 데이터와 지속성 계층 데이터의 일관성을 보장할 수 있지만 실제 프로젝트에서는 대부분 그 중 일관성 요구 사항을 어느 정도 허용하므로 계획에서 타협이 발생하는 경우가 많습니다.
Write-Through 직접 쓰기 모드는 쓰기 작업 수가 많고 일관성 요구 사항이 높은 시나리오에 적합합니다. Write-Through 모드를 적용할 때 문제를 해결하려면 특정 보상 메커니즘도 필요합니다. 우선, 동시 환경에서는 데이터베이스를 먼저 업데이트한 후 캐시를 업데이트하면 캐시와 데이터베이스 간의 불일치가 발생한다고 앞에서 언급했습니다. 그렇다면 캐시를 먼저 업데이트한 다음 데이터베이스를 업데이트하면 어떨까요?
이러한 작업 순서는 여전히 스레드 1이 캐시를 먼저 업데이트하고 마지막으로 데이터베이스를 업데이트하는 다음과 같은 상황으로 이어집니다. 즉, 스레드 1과 스레드 2의 실행 불확실성으로 인해 데이터베이스와 데이터베이스 간의 불일치가 발생합니다. 캐시. 스레드 경쟁으로 인해 발생하는 이러한 종류의 캐시 불일치는 분산 잠금을 통해 해결될 수 있으며 캐시와 데이터베이스에 대한 작업은 동일한 스레드에 의해서만 완료될 수 있습니다. 잠금을 획득하지 못한 스레드에 대해 하나는 잠금의 타임아웃 시간을 통해 이를 제어하는 것이고, 다른 하나는 요청을 일시적으로 메시지 큐에 저장하여 순차적으로 소비하는 것입니다.
다음 동시 실행 시나리오에서는 스레드 1의 쓰기 요청이 데이터베이스를 업데이트한 다음 스레드 2의 읽기 요청이 캐시에 도달하고 스레드 1이 캐시를 업데이트합니다. 이로 인해 스레드 2가 읽는 캐시가 데이터베이스보다 지연됩니다. 마찬가지로 캐시를 먼저 업데이트한 다음 데이터베이스를 업데이트하면 쓰기 요청과 읽기 요청이 동시에 발생할 때 비슷한 문제가 발생합니다. 이 시나리오에 직면하면 잠글 수도 있습니다.
또한 Write-Through 모드에서는 캐시가 먼저 업데이트되거나 데이터베이스가 먼저 업데이트되는지에 관계없이 캐시 또는 데이터베이스 업데이트가 실패하는 상황이 발생합니다. 위에서 언급한 재판 메커니즘과 보상 메커니즘도 여기서 작동합니다.
— 5 —
Write-Behind
Write Behind는 비동기 후기입 모드를 의미합니다. 또한 Read-Through/Write-Through와 유사한 액세스 제어 계층이 있습니다. 차이점은 Write Behind 프로세스 쓰기 요청 시 데이터베이스는 업데이트되지 않고 일괄 비동기 업데이트를 통해 업데이트된다는 점입니다. 데이터베이스 부하가 적을 때 쓰기가 가능합니다.
Write-Behind 모드에서는 쓰기 요청의 대기 시간이 짧아 데이터베이스에 대한 부담이 줄어들고 처리량이 향상됩니다. 그러나 데이터베이스와 캐시 간의 일관성이 약합니다. 예를 들어 업데이트된 데이터가 데이터베이스에 기록되지 않은 경우 데이터베이스에서 데이터를 직접 쿼리하는 경우 캐시보다 지연됩니다. 동시에 캐시 로드가 상대적으로 크기 때문에 캐시가 다운되면 데이터가 손실될 수 있으므로 캐시의 가용성이 높아야 합니다. 분명히, Write Behind 모드는 쓰기 작업 수가 많은 시나리오에 적합하며 전자상거래 플래시 판매 시나리오에서 재고 공제에 자주 사용됩니다.
— 6 —
Write-Around
일부 비핵심 비즈니스인 경우 일관성 요구 사항이 약하기 때문에 캐시 제외 읽기 모드에서 캐시 만료 시간을 추가하고 캐시를 삭제하거나 업데이트하지 않고 쓰기 요청에서만 데이터베이스를 업데이트하도록 선택할 수 있습니다. 이러한 방식으로 캐시는 다음을 통해서만 무효화될 수 있습니다. 만료 시간. 이 솔루션은 구현이 간단하지만 캐시의 데이터와 데이터베이스 데이터 간의 일관성이 좋지 않아 사용자 경험이 좋지 않은 경우가 많으므로 신중하게 선택해야 합니다.
— 7 —
요약
캐시 일관성을 해결하는 과정에서 많은 문제가 발생합니다. 이 접근 방식은 캐시의 궁극적인 일관성을 보장할 수 있습니다. 읽기가 많고 쓰기가 적은 시나리오에서는 "보상을 위해 소비 데이터베이스 로그와 결합된 캐시 제외"를 사용하도록 선택할 수 있습니다. 쓰기가 많은 시나리오에서는 "분산 잠금과 결합된 Write-Through" 솔루션을 사용하도록 선택할 수 있습니다. 쓰기가 많은 극단적인 시나리오에서는 "Write-Behind" 솔루션을 사용하도록 선택할 수 있습니다.