저자: JD Retail Zhang Luyao
1. 적용 시나리오
현재 시스템에는 결제 시간 초과 취소, 대기 시간 초과, SMS 전송 지연, WeChat 및 기타 알림, 토큰 갱신, 멤버십 카드 만료 등 지연 처리가 필요한 많은 기능이 있습니다. 지연된 처리를 통해 시스템 리소스가 크게 절약되고 작업 처리를 위해 데이터베이스를 폴링할 필요가 없습니다.
현재 대부분의 기능은 타이밍 태스크에 의해 완료됩니다.타이밍 태스크에는 석영과 xxljob의 두 가지 유형이 있습니다.폴링 시간이 짧고 초당 한 번 실행되므로 데이터베이스에 일정한 압력을 가하고 1초의 시간이 있습니다. 오류. 폴링 시간이 길어서 예를 들어 30분에 한 번씩 03:01에 데이터가 삽입되고 만료는 보통 3:31에 실행되지만 폴링이 3:30에 수행되면 3:00-3:30이 스캔되지만 스캔이 3보다 작음: 31의 데이터는 4:00에만 스캔할 수 있으며, 이는 29분의 지연에 해당합니다!
2. 지연 처리 방법 연구
1.DelayQueue
1. 시행방법:
jvm에서 제공하는 지연 차단 큐는 지연 시간이 다른 작업을 우선 순위 큐를 통해 정렬하고 조건을 통해 차단하며 sleep dealy 시간 동안 지연된 작업을 획득합니다.
새로운 작업이 추가되면 해당 작업이 가장 먼저 실행해야 하는 작업인지 판단하여 실행해야 하는 요소 중 새로 추가된 요소를 실행으로 정상적으로 획득할 수 없도록 Queue Sleep을 해제합니다. 실.
2. 기존 문제:
1. 독립 실행형 작업, 시스템이 다운된 후 효과적인 재시도를 수행할 수 없습니다.
2. 로깅 및 백업 수행 실패
3. 재시도 메커니즘 없음
4. 시스템이 다시 시작되면 작업이 지워집니다!
5. 조각 소비는 허용되지 않습니다.
3. 장점 : 간단한 구현, 작업이 없을 때 차단, 자원 절약, 정확한 실행 시간
2. 지연 큐 mq
구현 방법: mq에 의존하고 지연 소비 시간을 설정하여 지연 소비 기능을 달성합니다. rabbitMq 및 jmq와 마찬가지로 지연된 소비 시간을 설정할 수 있습니다. RabbitMq는 메시지의 만료 시간을 설정하고 소비를 위해 데드 레터 큐에 넣는 방식으로 구현됩니다.
기존 문제:
1. 시간 설정이 유동적이지 않음 큐마다 만료 시간이 고정되어 있어 지연 큐가 새로 생성될 때마다 새로운 메시지 큐를 생성해야 함
장점: jmq에 의존하여 효과적으로 모니터링, 레코드 사용 및 재시도가 가능하고 동시에 여러 시스템을 사용할 수 있으며 다운타임이 두렵지 않습니다.
3. 예약된 작업
예약된 작업을 통해 검증된 데이터 폴링
결점:
1. 데이터베이스에 특정 압력을 가하는 비즈니스 데이터베이스를 읽을 필요가 있습니다.
2. 지연이 있습니다
3. 스캔 데이터의 양이 너무 많으면 시스템 리소스를 너무 많이 차지합니다.
4. 단편화를 사용할 수 없습니다.
이점:
1. 소비 실패 후 다음 번에 계속 소비할 수 있으며 재시도할 수 있습니다.
2. 안정적인 소비력
4.레디스
작업은 redis에 저장되고 redis의 zset 대기열은 점수에 따라 정렬하는 데 사용됩니다.프로그램은 지연 대기열을 실현하기 위해 스레드를 통해 대기열 데이터 소비를 지속적으로 얻습니다.
이점:
1. redis 쿼리가 데이터베이스보다 빠르고 설정된 큐 길이가 너무 커서 쿼리가 점프 테이블 구조에 따라 수행되므로 효율성이 높습니다.
2. Redis는 타임스탬프에 따라 정렬할 수 있으므로 현재 타임스탬프에서 점수 작업만 쿼리하면 됩니다.
3. 기계 재시작에 대한 두려움 없음
4. 분산 소비
결점:
1. redis 성능 제한, 동시 10W
2. 다중 명령은 원자성을 보장할 수 없습니다.lua 스크립트를 사용하려면 모든 데이터가 하나의 redis 샤드에 있어야 합니다.
5. 타임휠
타임 휠을 통한 지연 작업 실행도 jvm 독립 실행형 작업을 기반으로 합니다.예를 들어 kafka와 netty는 모두 타임 휠을 구현하고 redisson의 watchdog도 netty의 타임 휠을 통해 구현됩니다.
단점: 분산 서비스 사용에 적합하지 않으며 다운타임 후 작업이 손실됩니다.
3. 목표 달성
현재 사용 중인 비동기 이벤트 구성 요소와 호환되며 보다 안정적이고 재시도 가능하며 기록된 경보 모니터링 및 고성능 지연 구성 요소를 제공합니다.
•메시지 전송 신뢰성: 메시지가 지연 대기열에 들어간 후 최소 한 번은 소비됨이 보장됩니다.
•클라이언트는 풍부한 지원: 여러 언어를 지원합니다.
• 고가용성: 다중 인스턴스 배포를 지원합니다. 인스턴스가 일시 중지된 후에도 서비스를 계속 제공하는 백업 인스턴스가 있습니다.
•실시간: 특정 시간 오차가 허용됩니다.
•메시지 삭제 지원: 비즈니스 사용자는 언제든지 지정된 메시지를 삭제할 수 있습니다.
• 소비 쿼리 지원
• 수동 재시도 지원
• 현재 비동기 이벤트 실행 모니터링 증가
4. 건축 설계
5. 지연 컴포넌트 구현
1. 시행 원칙
현재 jimdb를 사용하여 zset을 통해 지연 기능을 구현하고 작업 ID와 해당 실행 시간을 zset 대기열에 점수로 저장합니다.기본적으로 점수별로 정렬되며 작업 ID를 가져올 때마다 현재 시간 내에 0점,
지연된 작업을 보낼 때 타임스탬프 + 머신 ip + queueName + 순서에 따라 고유 ID가 생성되고 메시지 본문이 구성되고 암호화되어 zset 대기열에 저장됩니다.
스레드를 이동하면 실행 시간에 도달한 작업이 릴리스 대기열로 이동하여 소비자가 가져오기를 기다립니다.
모니터링 당사자는 ump를 통합합니다.
Redis 백업 + 데이터베이스 지속성을 통해 소비 기록을 완성합니다.
캐싱으로 구현하는 방식은 구현의 한 종류일 뿐이며 구현 방식은 매개변수를 통해 제어할 수 있으며 spi를 통해 자유롭게 확장할 수 있습니다.
2. 메시지 구조
각 작업에는 다음 속성이 포함되어야 합니다.
•주제: Job type, 즉 QueueName
•Id: 작업의 고유 식별자입니다. 지정된 작업 정보를 검색하고 삭제하는 데 사용됩니다.
•지연: Job이 시간을 지연시켜야 합니다. 단위: 초. (서버에서 절대 시간으로 변환합니다.)
• 본문: 소비자가 특정 비즈니스 처리를 수행할 수 있도록 json 형식으로 저장되는 Job의 내용입니다.
•traceId: 송신 쓰레드의 traceId, 후속 pfinder가 traceId 설정을 지원한 후, 로그 추적에 편리한 송신 쓰레드와 동일한 traceid를 공유할 수 있습니다.
구체적인 구조는 아래 그림과 같습니다.
TTR은 메시지 전송의 신뢰성을 보장하도록 설계되었습니다.
3. 데이터 흐름 및 흐름도
메시지로 사용할 수 있는 redis-disruptor 방식을 기반으로 게시 및 소비 소비자는 소비를 위해 원래의 비동기 이벤트 중단자 잠금 해제 대기열을 사용하며 서로 다른 응용 프로그램과 서로 다른 대기열 사이에 잠금이 없습니다.
1. 응용 프로그램이 메시지 큐의 기능을 사용하지 않고 게시만 할 수 있도록 지원합니다.
2: 버킷팅 지원 큰 키 문제의 경우 이벤트가 많은 경우 지연 대기열 및 작업 대기열 버킷 수를 설정하여 큰 키로 인한 redis 차단 문제를 줄일 수 있습니다.
3: ducc 구성을 통해 성능이 확장됩니다.현재 소비만 활성화되고 소비는 비활성화됩니다.
4: 소비자 스레드가 너무 오래 실행되지 않도록 제한 시간 구성 설정 지원
병목 현상: 소비 속도가 느리고 생산 속도가 너무 빠르면 링 버퍼 대기열이 가득 차게 됩니다.현재 응용 프로그램이 생산자이자 소비자일 때 생산자는 잠자고 성능은 소비 속도에 따라 달라집니다. .기계를 수평으로 확장하여 성능을 직접적으로 향상시킬 수 있습니다. Redis 대기열의 길이를 모니터링합니다. 계속해서 늘어나면 소비자를 추가하여 성능을 직접적으로 개선하는 것이 좋습니다.
가능한 상황: 응용 프로그램이 방해자를 공유하고 64개의 소비자 스레드가 있기 때문에 특정 이벤트의 소비가 너무 느리면 64개의 스레드 모두가 이 이벤트를 소비합니다. 쓰레드도 소비가 차단되어 모든 이벤트의 소비가 차단됩니다.
나중에 그러한 성능 병목 현상이 있는지 관찰하기 위해 각 대기열에 소비자 스레드 풀을 제공할 수 있습니다.
6. 데모 예시
구성 파일 추가
jd.event.enable:true 활성화 여부 결정
<dependency> <groupId>com.jd.car</groupId>
<artifactId>senna-event</artifactId>
<version>1.0-SNAPSHOT</version> </dependency>
구성
jd:
senna:
event:
enable: true
queue:
retryEventQueue:
bucketNum: 1
handleBean: retryHandle
소비 코드:
package com.jd.car.senna.admin.event;
import com.jd.car.senna.event.EventHandler;
import com.jd.car.senna.event.annotation.SennaEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @author zhangluyao
* @description
* @create 2022-02-21-9:54 下午
*/
@Slf4j
@Component("retryHandle")
public class RetryQueueEvent extends EventHandler {
@Override
protected void onHandle(String key, String eventType) {
log.info("Handler开始消费:{}", key);
}
@Override
protected void onDelayHandle(String key, String eventType) {
log.info("delayHandler开始消费:{}", key);
}
}
주석 양식:
package com.jd.car.senna.admin.event;
import com.jd.car.senna.event.EventHandler;
import com.jd.car.senna.event.annotation.SennaEvent;
import lombok.extern.slf4j.Slf4j;
/**
* @author zhangluyao
* @description
* @create 2022-02-21-9:54 下午
*/
@Slf4j
@SennaEvent(queueName = "testQueue", bucketNum = 5,delayBucketNum = 5,delayEnable = true)
public class TestQueueEvent extends EventHandler {
@Override
protected void onHandle(String key, String eventType) {
log.info("Handler开始消费:{}", key);
}
@Override
protected void onDelayHandle(String key, String eventType) {
log.info("delayHandler开始消费:{}", key);
}
}
코드 보내기
package com.jd.car.senna.admin.controller;
import com.jd.car.senna.event.queue.IEventQueue;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.concurrent.CompletableFuture;
/**
* @author zly
*/
@RestController
@Slf4j
public class DemoController {
@Lazy
@Resource(name = "testQueue")
private IEventQueue eventQueue;
@ResponseBody
@GetMapping("/api/v1/demo")
public String demo() {
log.info("发送无延迟消息");
eventQueue.push("no delay 5000 millseconds message 3");
return "ok";
}
@ResponseBody
@GetMapping("/api/v1/demo1")
public String demo1() {
log.info("发送延迟5秒消息");
eventQueue.push(" delay 5000 millseconds message,name",1000*5L);
return "ok";
}
@ResponseBody
@GetMapping("/api/v1/demo2")
public String demo2() {
log.info("发送延迟到2022-04-02 00:00:00执行的消息");
eventQueue.push(" delay message,name to 2022-04-02 00:00:00", new Date(1648828800000));
return "ok";
}
}
Youzan의 디자인 참조: https://tech.youzan.com/queuing_delay/
7. 현재 애플리케이션:
1. Yunxiu는 매장에서 줄을 서서 24시간이 지나면 자동으로 취소됩니다.
2. Meituan은 정기적으로 토큰 갱신을 요청합니다.
3. 보증 카드는 24시간 이내에 생성됩니다.
5. 명세서 생성 연기
6. SMS 발송 지연