[일상 사업 개발] 인터페이스 성능 최적화

은닉처

로컬 캐시

로컬 캐싱의 가장 큰 장점은应用和cache동일한 프로세스 내에서 단일 애플리케이션에 클러스터 지원이 필요하지 않거나 과도한 네트워크 오버헤드 등이 없이 요청 캐싱이 매우 빠르다는 것입니다. 클러스터링 노드가 서로 알릴 필요가 없는 시나리오에서는 로컬 캐시를 사용하는 것이 더 적합합니다. 단점은 캐시가 애플리케이션과 결합되어 있기 때문에 여러 애플리케이션이 직접 캐시를 공유할 수 없고, 각 애플리케이션이나 클러스터의 각 노드가 별도의 캐시를 유지해야 하므로 메모리 낭비라는 점이다.

일반적으로 사용되는 로컬 캐싱 프레임워크에는 Guava、Caffeine 등이 포함되며 모두 프로젝트로 직접 가져와 사용할 수 있는 별도의 jar 패키지입니다.

우리는 필요에 따라 원하는 프레임워크를 선택할 수 있는 유연성을 가지고 있습니다.

@Configuration
public class CaffeineCacheConfig {
    
    
    @Bean
    public Cache<String, Object> caffeineCache() {
    
    
        return Caffeine.newBuilder()
                // 设置最后一次写入或访问后经过固定时间过期
                .expireAfterWrite(60, TimeUnit.SECONDS)
                // 初始的缓存空间大小
                .initialCapacity(100)
                // 缓存的最大条数
                .maximumSize(1000)
                .build();
    }
}

로컬 캐싱은 다음 두 가지 시나리오에 적합합니다.

  • 캐시된 콘텐츠에 대한 적시성 요구 사항은 높지 않으며 특정 지연은 허용될 수 있습니다. 만료 시간을 더 짧게 설정하고 수동 무효화 업데이트를 통해 데이터의 최신성을 유지할 수 있습니다.
  • 캐시된 콘텐츠는 변경되지 않습니다. 예를 들어 주문 번호와 uid 간의 매핑 관계는 일단 생성되면 변경되지 않습니다.

메모:

  • 과도한 메모리 사용으로 인한 애플리케이션 마비를 방지하기 위해 메모리 캐시 데이터 입력 상한을 제어합니다.
  • 메모리 내 데이터 제거 전략.
  • 구현은 간단하지만 잠재적인 함정이 많으므로 성숙한 오픈 소스 프레임워크를 선택하는 것이 가장 좋습니다.

분산 캐시

로컬 캐시를 사용하면 애플리케이션 서버에 "상태"를 쉽게 가져올 수 있으며 메모리 크기에 따라 쉽게 제한됩니다.

분산 캐시는 분산, 클러스터 배포, 독립적인 운영 및 유지 관리, 무제한 용량의 개념에 의존하며 네트워크 전송 손실이 있지만 많은 장점에 비해 1~2ms의 지연은 무시할 수 있습니다.

우수한 분산 캐시 시스템은 잘 알려져 있습니다Memcached​ 、Redis. 관계형 데이터베이스와 캐시 스토리지를 비교하면 읽기 및 쓰기 성능의 격차가 큽니다. Redis는 이미 단일 노드로 이를 달성할 수 있습니다.8W+ QPS(系统每秒处理查询的次数) 솔루션을 설계할 때 읽기 및 쓰기 압력을 데이터베이스에서 캐시로 전달하여 취약한 관계형 데이터베이스를 효과적으로 보호하십시오.

메모:

  • 캐시 적중률이 너무 낮아 압력을 견딜 수 없는 경우 압력은 여전히 ​​다운스트림 저장 계층에 있습니다.
  • 캐시 공간의 크기는 특정 비즈니스 시나리오를 기반으로 평가하여 공간 부족으로 인해 일부 핫 데이터가 교체되는 것을 방지해야 합니다.
  • 캐시된 데이터 일관성.
  • 빠른 캐시 확장 문제.
  • 캐시된 인터페이스 평균 RT, 최대 RT, 최소 RT입니다.
  • 캐시된 QPS.
  • 네트워크 송신 트래픽.
  • 클라이언트 연결 수.

데이터 베이스

하위 데이터베이스 및 하위 테이블

MySQL의 기본 innodb 스토리지 엔진은 B+ 트리 구조를 사용하며 3계층 구조는 수천만 개의 데이터 스토리지를 지원합니다.

물론, 현재 인터넷의 사용자 기반은 매우 넓습니다. 사용자 수가 이렇게 많으면 일반적으로 단일 테이블이 비즈니스 요구를 지원하기 어렵습니다. 큰 테이블을 동일한 구조의 여러 물리적 테이블로 수평 분할하면 크게 향상될 수 있습니다. 스토리지 및 액세스 압력을 완화합니다.

SQL 최적화

하위 데이터베이스와 하위 테이블이 있으면 스토리지 차원의 부담을 많이 줄일 수 있지만 여전히 계산에 주의하는 방법을 배워야 합니다. 예를 들어 모든 데이터베이스 작업은 SQL을 통해 수행됩니다.

잘못된 SQL은 인터페이스 성능에 큰 영향을 미칠 수 있습니다.

예:
1. 깊은 페이지 넘김이 수행되며 데이터베이스 엔진은 매번 많은 양의 데이터를 사전 확인해야 합니다.

 select * from purchase_record where productCode = 'PA9044' and  status=4 order by orderTime desc limit 100000,200 

limit 100000,200즉, 100,200개의 행이 검색되고 200개의 행이 반환되며 처음 100,000개의 행이 삭제됩니다. 그래서 실행 속도가 매우 느립니다. 일반적으로 라벨 기록 방법은 다음과 같은 최적화에 사용될 수 있습니다.

select * from purchase_record where productCode = 'PA9044' and status=4 and id > 100000 limit 200

이 최적화의 장점은 기본 키 인덱스에 도달한다는 것입니다. 페이지 수에 관계없이 성능은 꽤 좋지만 ID가 지속적으로 증가하는 필드가 필요하다는 한계가 있습니다.< a i=1> 2. 인덱스가 누락되어 전체 테이블 스캔을 했습니다. 3. DB에서 메모리로 한꺼번에 많은 양의 데이터를 쿼리하는 경우 메모리 부족 현상이 발생할 수 있으므로 일괄 쿼리와 페이징 쿼리를 사용하는 것이 좋습니다.

업무절차

병렬화

업무 프로세스를 정리하고, 시퀀스 다이어그램을 그리고, 시리얼이 무엇인지 명확하게 구분해 보세요. 어느 것이 평행합니까? 멀티코어 CPU의 병렬 처리 기능을 활용하세요.

아래 그림과 같이 문맥 의존성이 있으면 직렬 처리를 사용하고, 그렇지 않으면 병렬 처리를 사용합니다.
여기에 이미지 설명을 삽입하세요.

JDK의 CompletableFuture​는 우리 시나리오의 요구 사항을 충족할 수 있는 직렬화, 병렬 처리, 조합 및 오류 처리를 처리하기 위한 약 50가지 메서드가 포함된 매우 풍부한 API를 제공합니다.

비동기화

인터페이스의 RT 응답 시간은 내부 비즈니스 로직의 복잡성에 따라 결정되며, 실행 프로세스가 단순할수록 인터페이스에 소요되는 시간은 줄어듭니다.

따라서 일반적인 접근 방식은 인터페이스 내에서 핵심이 아닌 로직을 제거하고 이를 비동기적으로 실행하는 것입니다.
아래 그림은 전자상거래 주문 생성 인터페이스입니다. 주문 기록을 생성하여 데이터베이스에 삽입하는 것이 핵심 요구 사항입니다. 사용자에게 문자 메시지 전송과 같은 후속 사용자 알림에 대해서는 등이 실패하더라도 메인 프로세스의 완료에는 영향을 미치지 않습니다.

이러한 작업을 기본 프로세스와 분리하겠습니다.
여기에 이미지 설명을 삽입하세요.
비동기 구현, 可以用线程池,也可以用消息队列,还可以用一些调度任务框架.

일반적인 비즈니스 관행은 주문이 성공적으로 이루어진 후 MQ 서버에 비동기 메시지를 보내는 것입니다. 그러면 소비자는 주제를 모니터링하고 비동기 소비를 수행합니다.

풀링 기술

우리는 모두 데이터베이스 연결 풀, 스레드 풀 등을 사용했습니다. 이것이 풀 아이디어의 구체화입니다. 그들이 해결하는 문제는 객체나 연결을 반복적으로 생성하지 않고 재사용할 수 있으며 불필요한 손실을 방지하는 것입니다. 결국 생성과 파괴는 시간도 걸리고.

풀링 기술의 핵심은 리소스의 "사전 할당"과 "재활용"이며 스레드 풀, 메모리 풀, 데이터베이스 연결 풀, HttpClient 연결 풀 등 일반적인 풀링 기술이 사용됩니다.

연결 풀의 몇 가지 중요한 매개변수: 최소 연결 수, 유휴 연결 수 및 최대 연결 수.

예를 들어 스레드 풀을 생성합니다.

new ThreadPoolExecutor(3, 15, 5, TimeUnit.MINUTES,
    new ArrayBlockingQueue<>(10),
    new ThreadFactoryBuilder().setNameFormat("data-thread-%d").build(),
    (r, executor) -> {
    
    
         (r instanceof BaseRunnable) {
    
    
            ((BaseRunnable) r).rejectedExecute();
        }
    });

미리 계산된

웹사이트의 PV를 표시해야 하는 페이지, WeChat의 행운의 빨간 봉투 등 많은 비즈니스의 계산 로직은 상대적으로 복잡합니다.

사용자가 인터페이스에 액세스하는 순간 계산 논리가 트리거되면 이러한 논리 계산은 일반적으로 시간이 오래 걸리므로 사용자의 실시간 요구 사항을 충족하기 어렵습니다.

즉, 프리페칭의 개념은 쿼리 데이터를 미리 계산하여 캐시에 저장하는 것인데, 인터페이스에 액세스할 때 캐시만 읽으면 되므로 인터페이스 성능이 크게 향상됩니다.

예: mysql 인벤토리 데이터를 정기적으로 redis에 동기화합니다. 인벤토리 공제를 요청할 때 먼저 redis setNX 중복 제거/mysql 중복 제거 테이블을 전달한 다음redis decrement 인벤토리 데이터를 줄인 다음 MQ 서버에 비동기 메시지를 보냅니다.

소비자는 주제를 모니터링하고 여러 스레드에서 비동기적으로 소비합니다.
1. 재고를 줄이고(먼저 확인 후 업데이트) 주문서를 작성합니다. 이는 동일해야 합니다. 단일 노드인 경우 synchronized를 사용하여 잠그면 스레드 안전 문제를 해결할 수 있습니다.

동기화된 잘못된 잠금 방법: 锁在事物里面
여기에 이미지 설명을 삽입하세요.
동기화된 올바른 잠금 방법: 锁在事物外面
여기에 이미지 설명을 삽입하세요.
2. 분산 다중 노드인 경우 분산 잠금을 추가해야 합니다. : mysql 행 잠금/redis 잠금.
MySQL 행 잠금(낙관적 잠금): 하향 추출, 동시성이 높지 않으면 사용할 수 있지만 동시성이 높으면 데이터베이스에 큰 부담을 줍니다. 프로그래머가 MySQL 고급 잠금에 대해 알아야 할 사항(3)

update goods set total_stocks = total_stocks-1 where user_id = ? and total_stocks-1>=0

redis 잠금: 상향 추출, redis setnx 분산 잠금, 압력은 redis로 분산되고 db에 대한 압력을 완화하기 위한 프로그램 실행
redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
회전이 필요한지 결정
1. while(true) {}는 스핀을 구현합니다

@Component
@RocketMQMessageListener(topic = "seckillTopic3",
        consumerGroup = "seckill-consumer-group3",
        consumeMode = ConsumeMode.CONCURRENTLY,
        consumeThreadMax = 40
)
public class SeckillListener implements RocketMQListener<MessageExt> {
    
    

    @Autowired
    private GoodsService goodsService;

    @Autowired
    private StringRedisTemplate redisTemplate;

	@Override
	public void onMessage(MessageExt message) {
    
    
	        String msg = new String(message.getBody());
	        Integer userId = Integer.parseInt(msg.split("-")[0]);
	        Integer goodsId = Integer.parseInt(msg.split("-")[1]);
	        
	        while (true) {
    
    
	            // 这里给一个key的过期时间,可以避免死锁的发生
	            Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock:" + goodsId, "", Duration.ofSeconds(30));
	            if (flag) {
    
    
	                // 拿到锁成功
	                try {
    
    
	                    goodsService.realSeckill(userId, goodsId);
	                    return;
	                } finally {
    
    
	                    // 删除
	                    redisTemplate.delete("lock:" + goodsId);
	                }
	            } else {
    
    
	                try {
    
    
	                    Thread.sleep(200L);
	                } catch (InterruptedException e) {
    
    
	                    e.printStackTrace();
	                }
	            }
	        }
	    }

2. 스핀 구현을 위한 재귀 호출

@Override
public void onMessage(MessageExt message) {
    
    
        String msg = new String(message.getBody());
        Integer userId = Integer.parseInt(msg.split("-")[0]);
        Integer goodsId = Integer.parseInt(msg.split("-")[1]);
        
        
       // 这里给一个key的过期时间,可以避免死锁的发生
        Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock:" + goodsId, "", Duration.ofSeconds(30));
        if (flag) {
    
    
            // 拿到锁成功
            try {
    
    
                goodsService.realSeckill(userId, goodsId);
            } finally {
    
    
                // 删除
                redisTemplate.delete("lock:" + goodsId);
            }
        } else {
    
    
            try {
    
    
                Thread.sleep(200L);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            onMessage(message);
        }
    }
@Service
public class GoodsServiceImpl implements GoodsService {
    
    

    @Resource
    private GoodsMapper goodsMapper;

    @Autowired
    private OrderMapper orderMapper;

  /**
     * 行锁(innodb)方案 mysql  不适合用于并发量特别大的场景
     * 因为压力最终都在数据库承担
     *
     * @param userId
     * @param goodsId
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void realSeckill(Integer userId, Integer goodsId) {
    
    
        // update goods set total_stocks = total_stocks - 1 where goods_id = goodsId and total_stocks - 1 >= 0;
        // 通过mysql来控制锁
        int i = goodsMapper.updateStock(goodsId);
        if (i > 0) {
    
    
            Order order = new Order();
            order.setGoodsid(goodsId);
            order.setUserid(userId);
            order.setCreatetime(new Date());
            orderMapper.insert(order);
        }
    }
}

인벤토리 수정

<update id="updateStock">
	update  goods set total_stocks = total_stocks - 1 ,update_time = now() where goods_id = #{value} and total_stocks - 1 >= 0
</update>

주문서 작성

<insert id="insert" keyColumn="id" keyProperty="id"  parameterType="cn.zysheep.domain.Order" useGeneratedKeys="true">
 insert into `order` (userid, goodsid, createtime
   )
 values (#{userid,jdbcType=INTEGER}, #{goodsid,jdbcType=INTEGER}, #{createtime,jdbcType=TIMESTAMP}
   )
</insert>

거래 세분성

많은 비즈니스 논리에는 트랜잭션 요구 사항이 있으며 여러 테이블에 대한 쓰기 작업은 트랜잭션 특성을 보장해야 합니다.

그러나 트랜잭션 자체는 특히 성능 집약적이므로 최대한 빨리 종료하고 데이터베이스 연결 자원을 오랫동안 점유하지 않기 위해 일반적으로 트랜잭션 범위를 줄여야 합니다.

트랜잭션 외부에 많은 쿼리 논리를 추가합니다.

또한 트랜잭션 내에서 원격 RPC 인터페이스 액세스는 일반적으로 시간이 오래 걸리기 때문에 허용되지 않습니다. 발생하는 주요 문제는 교착 상태, 인터페이스 시간 초과, 마스터-슬레이브 지연 등입니다.

일괄 읽기 및 쓰기

현재 컴퓨터 CPU 처리 속도는 여전히 매우 높으며 IO는 일반적으로 디스크 IO 및 네트워크 IO와 같은 병목 현상입니다.

100명의 계좌잔고를 확인하고 싶은 시나리오가 있나요?

두 가지 디자인 옵션이 있습니다.

옵션 1: 단일 쿼리 인터페이스를 열고 호출자가 내부 루프에서 이를 100번 호출합니다.

옵션 2: 서비스 제공자는 일괄 쿼리 인터페이스를 열고 호출자는 한 번만 쿼리하면 됩니다.

어떤 솔루션이 더 좋다고 생각하시나요?

대답은 자명합니다. 옵션 2여야 합니다.

데이터베이스 쓰기 작업도 마찬가지이며, 성능 향상을 위해 일반적으로 일괄 업데이트를 사용합니다.

잠금 세분성

잠금은 일반적으로 동시성이 높은 시나리오에서 공유 리소스를 보호하기 위한 수단이지만 잠금 세분성이 너무 낮으면 인터페이스 성능에 심각한 영향을 미칩니다.

잠금 세분성 정보: 잠그려는 범위의 크기를 의미합니다. synchronized 또는 redis 분산 잠금이든 관계없이 다음 사항만 수행하면 됩니다. 중요한 리소스에 추가하고 잠그기만 하면 됩니다. 공유 리소스가 아닌 경우에는 잠글 필요가 없습니다. 화장실에 가고 싶은 것처럼 화장실 문만 잠그면 되지만, 거실 문을 잠글 필요가 없습니다.

잠금 범위를 제어하는 ​​것이 우리가 고려해야 할 핵심 사항입니다.

잘못된 잠금 방법:

//非共享资源
 private void notShare(){
    
    
 }
 //共享资源
 private void share(){
    
    
 }
 private int wrong(){
    
    
     synchronized (this) {
    
    
         share();
         notShare();
     }
 }

올바른 잠금 방법:

//非共享资源
private void notShare(){
    
    
}
//共享资源
private void share(){
    
    
}
private int right(){
    
    
    notShare();
    synchronized (this) {
    
    
        share();

    }
} 

가능한 한 빨리 돌아와

비즈니스 로직을 시작하기 전에 필요한 매개변수나 세트를 판단하고, 확립되지 않은 경우 최대한 빨리 반환/던지십시오.

if(CollectionUtils.isEmpty(list)) {
    
    
    throw new RuntimeException("数据不合法");
}

컨텍스트 전달

데이터의 일부가 필요한 경우, 사용자 정보 등의 일반 인터페이스 등 이를 확인할 수 있는 RPC 인터페이스가 없는 경우.

이전에 사용하게 될 것이기 때문에 꼭 확인해 보셨을 텐데요. 하지만 우리는 메소드 호출이 스택 프레임 형태로 전달된다는 것을 알고 있으며, 메소드가 실행된 후 스택에서 튀어나오면서 메소드 내부의 지역 변수도 재활용됩니다.

나중에 이 정보를 다시 사용해야 하는 경우 다시 확인만 하면 됩니다.

Context 객체(ThreadLocal)를 정의하고 일부 중간 정보를 저장하고 전달할 수 있으면 후속 프로세스에서 다시 쿼리해야 하는 부담이 크게 줄어듭니다.

시간을 위한 공간

공간과 시간을 교환하는 잘 알려진 예는 캐시의 합리적인 사용입니다.자주 사용하고 자주 변경되지 않는 일부 데이터의 경우 미리 캐시하고 필요할 때 직접 캐시를 확인하여 빈번한 데이터베이스 쿼리나 반복 계산을 피할 수 있습니다.

컬렉션 공간 크기

컬렉션에 저장할 요소 수를 미리 알고 있다면 컬렉션을 초기화할 때 크기를 지정해 보세요. 특히 용량이 더 큰 컬렉션의 경우 더욱 그렇습니다.

ArrayList의 초기 크기는 10입니다. 임계값을 초과하면 크기가 1.5배로 확장되며, 이로 인해 이전 컬렉션의 데이터가 새 컬렉션으로 복사되므로 성능이 낭비됩니다.

Guess you like

Origin blog.csdn.net/qq_45297578/article/details/133799970