Redis와 로컬 캐시를 활용한 높은 동시성 기술 공유

WBOY
풀어 주다: 2022-11-02 20:19:12
앞으로
2436명이 탐색했습니다.

이 글에서는 Redis에 대한 관련 지식을 소개합니다. 캐시 유형 소개, 다양한 사용 시나리오 및 사용 방법을 포함하여 분산 캐시와 로컬 캐시의 사용 기술을 주로 소개합니다. 아래의 실제 사례를 살펴보세요. 모든 사람에게 도움이 되기를 바랍니다.

추천 학습: Redis 비디오 튜토리얼

우리 모두 알고 있듯이 캐싱의 주요 목적은 액세스 속도를 높이고 데이터베이스 부담을 완화하는 것입니다. 가장 일반적으로 사용되는 캐시는 Redis와 같은 분산 캐시입니다. 대부분의 동시성 시나리오나 일부 중소기업의 트래픽이 그다지 높지 않은 상황에 직면했을 때 Redis를 사용하면 기본적으로 문제를 해결할 수 있습니다. 그러나 트래픽이 많은 경우 guava의 LoadingCache 및 Kuaishou의 오픈 소스 ReloadableCache와 같은 로컬 캐시를 사용해야 할 수도 있습니다.

세 가지 캐시의 사용 시나리오

이 부분에서는 guava의 LoadingCache 및 Kuaishou의 오픈 소스 ReloadableCache와 같은 Redis의 사용 시나리오와 제한 사항을 소개합니다. 이 부분의 소개를 통해 어떤 비즈니스 시나리오에서 어떤 캐시를 사용해야 하는지 알 수 있습니다. , 그리고 그 이유.

Redis의 사용 시나리오 및 제한 사항

Redis를 언제 사용해야 하는지 폭넓게 이야기하면 사용자 방문 수가 너무 많은 곳에서 자연스럽게 사용되므로 액세스가 가속화되고 데이터베이스 부담이 완화됩니다. 세분화하면 단일 노드 문제와 단일 노드가 아닌 문제로 나눌 수 있습니다.

페이지의 사용자 방문 수가 상대적으로 많지만 동일한 리소스에 액세스하지 않는 경우. 예를 들어, 사용자 세부 정보 페이지는 방문 횟수가 상대적으로 많지만 사용자마다 데이터가 다릅니다. 이 경우 redis를 사용하면 키가 사용자의 고유함을 알 수 있습니다. 키, 값은 사용자 정보입니다.

redis로 인한 캐시 고장.

하지만 한 가지 주의할 점은 만료 시간을 설정해야 하며, 동시에 만료되도록 설정할 수는 없다는 점입니다. 예를 들어, 사용자에게 활동 페이지가 있고 활동 페이지에서 사용자 활동 중에 수상 경력에 빛나는 데이터를 볼 수 있는 경우 부주의한 사람은 사용자 데이터의 만료 시점을 활동 종료로 설정할 수 있습니다. 단일(핫) 문제 발생

단일 노드 문제는 Redis의 단일 노드의 동시성 문제를 의미합니다. 동일한 키가 Redis 클러스터의 동일한 노드에 속하므로 이 키에 대한 액세스가 너무 높으면 이 Redis 노드에 동시성이 발생합니다. 숨겨진 위험이 있습니다. 이 키를 바로가기 키라고 합니다.

예를 들어 모든 사용자가 동일한 리소스에 액세스하는 경우 Xiao Ai 앱의 홈페이지는 (처음에) 모든 사용자에게 동일한 콘텐츠를 표시하고 서버는 동일한 큰 json을 h5로 반환하므로 분명히 캐싱이 필요합니다. 먼저 redis를 사용하는 것이 가능한지 여부를 고려합니다. redis는 단일 지점 문제가 있으므로 트래픽이 너무 많으면 모든 사용자 요청이 동일한 redis 노드에 도달하게 되며 해당 노드가 견딜 수 있는지 평가해야 합니다. 이렇게 큰 흐름. 우리의 규칙은 단일 노드의 qps가 천 수준에 도달하면 단일 지점 문제를 해결해야 한다는 것입니다(redis가 십만 수준의 qps를 견딜 수 있다고 주장하더라도). 가장 일반적인 방법은 로컬 캐시를 사용하는 것입니다. . 당연히 Xiaoai 앱 홈페이지의 트래픽은 100 미만이므로 redis 사용에는 문제가 없습니다.

LoadingCache의 사용 시나리오 및 제한

위에 언급된 단축키 문제의 경우 가장 직접적인 접근 방식은 가장 친숙한 구아바의 LoadingCache와 같은 로컬 캐시를 사용하는 것이지만 로컬 캐시를 사용하려면 허용할 수 있어야 합니다. 홈페이지를 업데이트하면 로컬 캐시가 업데이트되지 않기 때문에 특정 만료 정책에 따라서만 캐시를 다시 로드하기 때문입니다. 그러나 우리 시나리오에서는 홈페이지가 일단 업데이트되면 완전히 괜찮습니다. 백그라운드로 푸시되면 다시 업데이트되지 않습니다. 변경되더라도 문제가 없습니다. 쓰기 만료를 30분으로 설정하면 30분 후에 캐시를 다시 로드할 수 있습니다.

LoadingCache로 인한 캐시 고장

로컬 캐시는 머신과 밀접한 관련이 있지만 코드 레벨은 30분 안에 만료되도록 작성되어 있지만 각 머신의 시작 시간이 다르기 때문에 캐시 로딩 시간은 만료 시간도 다르기 때문에 캐시가 동시에 만료된 후에는 머신의 모든 요청이 데이터베이스를 요청하지 않습니다. 그러나 캐시 침투는 단일 시스템에 대해서도 발생합니다. 각각 1,000qps를 가진 10개의 시스템이 있는 경우 하나의 캐시가 만료되는 한 이러한 1,000개의 요청이 동시에 데이터베이스에 도달할 수 있습니다. 이런 종류의 문제는 실제로 해결하기 쉽지만 무시하기 쉽습니다. 즉, LoadingCache를 설정할 때 직접 캐시.getIfPresent()== null을 판단한 후 LoadingCache의 load-miss 메서드를 사용합니다. db; 전자는 가상 머신을 추가합니다. 레이어 잠금은 단 하나의 요청만 데이터베이스에 전달되도록 하여 이 문제를 완벽하게 해결합니다.

그러나 일정 기간 동안 빈번한 활동 등 실시간 요구 사항이 높은 경우에는 운영자가 활동 정보를 구성한 후 활동 페이지가 거의 실시간으로 업데이트될 수 있도록 하고 싶습니다. 백그라운드에서는 거의 실시간으로 C 측에서 업데이트되어야 합니다. 이 구성의 활동 정보를 실시간으로 표시하려면 현재로서는 LoadingCache를 사용하는 것만으로는 충분하지 않습니다.

ReloadableCache의 사용 시나리오 및 제한

LoadingCache로 해결할 수 없는 위에서 언급한 실시간 문제의 경우 Kuaishou에서 오픈 소스로 제공하는 로컬 캐싱 프레임워크인 ReloadableCache를 사용하는 것을 고려할 수 있습니다. 가장 큰 특징은 여러 시스템을 지원한다는 것입니다. 동시에 캐시를 업데이트하기 위해 홈페이지 정보를 수정한 다음 요청이 머신 A에 도달한다고 가정합니다. 이때 ReloadableCache가 다시 로드되고 동일한 내용을 듣고 있는 다른 머신에 알림이 전송됩니다. zk 노드는 알림을 받은 후 캐시를 다시 업데이트합니다. 이 캐시를 사용하기 위한 일반적인 요구 사항은 전체 데이터 양을 로컬 캐시에 로드하는 것이므로 데이터 양이 너무 많으면 분명히 gc에 부담을 주게 되며 이 경우에는 사용할 수 없습니다. Xiao Ai의 홈페이지에는 상태가 있고 일반적으로 온라인 상태가 두 개뿐이므로 ReloadableCache를 사용하여 온라인 상태 홈페이지만 로드할 수 있습니다.

요약

여기에는 기본적으로 세 가지 유형의 캐시가 도입되었습니다. 요약은 다음과 같습니다.

  • 사용자 차원 데이터와 같은 비핫스팟 데이터 액세스의 경우 Redis를 직접 사용하세요.
  • 핫스팟 데이터 액세스의 경우 트래픽이 그다지 높지 않으니 아무 생각 없이 Redis를 사용하세요.
  • 핫스팟 데이터의 경우 특정 기간 내에 더티 데이터가 허용되면 LoadingCache를 사용하세요.
  • 핫스팟 데이터의 경우 일관성 요구 사항이 높고 양이 많은 경우; 데이터가 크지 않은 경우에는 ReloadableCache를 사용하세요.

Tips

어떤 종류의 로컬 캐시에 고장 문제를 해결하기 위한 가상 머신 수준 잠금이 있더라도 사고는 항상 예상치 못한 방식으로 발생할 수 있습니다. , 2단계 캐시, 즉 로컬 캐시 + redis + db를 사용할 수 있습니다.

캐시 사용법에 대한 간략한 소개

여기서 redis 사용법에 대해서는 더 이상 언급하지 않겠습니다. 많은 분들이 저보다 API 사용법에 더 익숙하실 거라 믿습니다.

LoadingCache 사용법은

이것입니다. 는 Guava에서 제공하는 포괄적인 온라인입니다. 그러나 주의할 두 가지 사항이 있습니다.

  • load-miss를 사용하려면 다음 중 하나를 사용하세요. V get(K key, Callable<? extends V> loader);要么使用build的时候使用的是build(CacheLoader<? super K1, V1> loader)이때 get()을 직접 사용할 수 있습니다. 또한, getIfPresent==null일 때 데이터베이스를 확인하는 대신 load-miss를 사용하는 것이 좋습니다. 그러면 캐시가 손상될 수 있습니다.
  • 캐시가 실패하면 여러 스레드가 호출되므로 load-miss를 사용하세요. 가져올 때 하나의 스레드만 db를 쿼리하고 다른 스레드는 기다려야 하므로 스레드로부터 안전합니다.
LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(1000L)
                .expireAfterAccess(Duration.ofHours(1L)) // 多久不访问就过期
                .expireAfterWrite(Duration.ofHours(1L))  // 多久这个key没修改就过期
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) throws Exception {
                        // 数据装载方式,一般就是loadDB
                        return key + " world";
                    }
                });
String value = cache.get("hello"); // 返回hello world
로그인 후 복사

ReloadableCache 사용법

타사 종속성을 가져오려면

<dependency>
  <groupId>com.github.phantomthief</groupId>
  <artifactId>zknotify-cache</artifactId>
  <version>0.1.22</version>
</dependency>
로그인 후 복사

문서를 읽어야 합니다. 그렇지 않으면 작동하지 않습니다. 관심이 있으면 직접 작성해도 됩니다.

public interface ReloadableCache<T> extends Supplier<T> {

    /**
     * 获取缓存数据
     */
    @Override
    T get();

    /**
     * 通知全局缓存更新
     * 注意:如果本地缓存没有初始化,本方法并不会初始化本地缓存并重新加载
     *
     * 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()}
     */
    void reload();

    /**
     * 更新本地缓存的本地副本
     * 注意:如果本地缓存没有初始化,本方法并不会初始化并刷新本地的缓存
     *
     * 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()}
     */
    void reloadLocal();
}
로그인 후 복사

흔한 캐시 고장/침투/눈사태 문제

이 세 가지는 정말 영원한 문제이고, 트래픽이 많은 경우에는 정말 고려해야 할 문제입니다.

캐시 분석

간단히 말하면, 캐시가 실패하여 동시에 많은 수의 요청이 데이터베이스에 도달하게 됩니다. 캐시 고장 문제에 대해 위에 많은 솔루션이 제공되었습니다.

  • 예를 들어, 로컬 캐시를 사용하세요
  • 로컬 캐시는 로드-미스 방식을 사용합니다.
  • 캐시를 로드하려면 타사 서비스를 사용하세요.

1.2와 1.2 모두 그렇게 말했는데 주로 3을 보세요. 예를 들어 기업이 Redis를 사용하고 싶지만 로컬 캐시를 사용할 수 없는 경우 데이터 양이 너무 많고 실시간 요구 사항이 상대적으로 높습니다. 그런 다음 캐시가 실패하면 소수의 요청만 데이터베이스에 도달하도록 하는 방법을 찾아야 합니다. 분산 잠금을 사용하는 것을 생각하는 것은 당연합니다. 이론적으로는 가능하지만 실제로는 숨겨진 위험이 있습니다. 우리는 많은 사람들이 redis+lua를 사용하여 분산 잠금을 구현하고 그 동안 순환 훈련을 수행한다고 믿습니다. 요청량이 크고 데이터가 크다면 redis는 숨겨진 위험이 되어 너무 많은 공간을 차지할 것입니다. 비즈니스 스레드는 분산 잠금을 도입하여 복잡성을 증가시킬 뿐입니다. 우리의 원칙은 사용할 수 있으면 사용하지 않는 것입니다.

그럼 분산 잠금과 유사하지만 더 안정적인 RPC 서비스를 설계할 수 있을까요? get 메소드를 호출할 때 이 rpc 서비스는 동일한 키가 동일한 노드에 적중되었는지 확인하고 동기화를 사용하여 잠근 다음 데이터 로드를 완료합니다. Kuaishou는 캐시세터(cacheSetter)라는 프레임워크를 제공합니다. 아래에 단순화된 버전이 제공되며, 직접 작성하여 구현하기 쉽습니다.

import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;

/**
 * @Description 分布式加载缓存的rpc服务,如果部署了多台机器那么调用端最好使用id做一致性hash保证相同id的请求打到同一台机器。
 **/
public abstract class AbstractCacheSetterService implements CacheSetterService {

    private final ConcurrentMap<String, CountDownLatch> loadCache = new ConcurrentHashMap<>();

    private final Object lock = new Object();

    @Override
    public void load(Collection<String> needLoadIds) {
        if (CollectionUtils.isEmpty(needLoadIds)) {
            return;
        }
        CountDownLatch latch;
        Collection<CountDownLatch> loadingLatchList;
        synchronized (lock) {
            loadingLatchList = excludeLoadingIds(needLoadIds);

            needLoadIds = Collections.unmodifiableCollection(needLoadIds);

            latch = saveLatch(needLoadIds);
        }
        System.out.println("needLoadIds:" + needLoadIds);
        try {
            if (CollectionUtils.isNotEmpty(needLoadIds)) {
                loadCache(needLoadIds);
            }
        } finally {
            release(needLoadIds, latch);
            block(loadingLatchList);
        }

    }

    /**
     * 加锁
     * @param loadingLatchList 需要加锁的id对应的CountDownLatch
     */
    protected void block(Collection<CountDownLatch> loadingLatchList) {
        if (CollectionUtils.isEmpty(loadingLatchList)) {
            return;
        }
        System.out.println("block:" + loadingLatchList);
        loadingLatchList.forEach(l -> {
            try {
                l.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }

    /**
     * 释放锁
     * @param needLoadIds 需要释放锁的id集合
     * @param latch 通过该CountDownLatch来释放锁
     */
    private void release(Collection<String> needLoadIds, CountDownLatch latch) {
        if (CollectionUtils.isEmpty(needLoadIds)) {
            return;
        }
        synchronized (lock) {
            needLoadIds.forEach(id -> loadCache.remove(id));
        }
        if (latch != null) {
            latch.countDown();
        }
    }

    /**
     * 加载缓存,比如根据id从db查询数据,然后设置到redis中
     * @param needLoadIds 加载缓存的id集合
     */
    protected abstract void loadCache(Collection<String> needLoadIds);

    /**
     * 对需要加载缓存的id绑定CountDownLatch,后续相同的id请求来了从map中找到CountDownLatch,并且await,直到该线程加载完了缓存
     * @param needLoadIds 能够正在去加载缓存的id集合
     * @return 公用的CountDownLatch
     */
    protected CountDownLatch saveLatch(Collection<String> needLoadIds) {
        if (CollectionUtils.isEmpty(needLoadIds)) {
            return null;
        }
        CountDownLatch latch = new CountDownLatch(1);
        needLoadIds.forEach(loadId -> loadCache.put(loadId, latch));
        System.out.println("loadCache:" + loadCache);
        return latch;
    }

    /**
     * 哪些id正在加载数据,此时持有相同id的线程需要等待
     * @param ids 需要加载缓存的id集合
     * @return 正在加载的id所对应的CountDownLatch集合
     */
    private Collection<CountDownLatch> excludeLoadingIds(Collection<String> ids) {
        List<CountDownLatch> loadingLatchList = Lists.newArrayList();
        Iterator<String> iterator = ids.iterator();
        while (iterator.hasNext()) {
            String id = iterator.next();
            CountDownLatch latch = loadCache.get(id);
            if (latch != null) {
                loadingLatchList.add(latch);
                iterator.remove();
            }
        }
        System.out.println("loadingLatchList:" + loadingLatchList);
        return loadingLatchList;
    }
}
로그인 후 복사

비즈니스 구현

import java.util.Collection;
public class BizCacheSetterRpcService extends AbstractCacheSetterService {
    @Override
    protected void loadCache(Collection<String> needLoadIds) {
        // 读取db进行处理
   	// 设置缓存
    }
}
로그인 후 복사

캐시 침투

간단히 말하면, 요청한 데이터가 데이터베이스에 존재하지 않아 잘못된 요청이 데이터베이스에 침투하게 됩니다.

해결 방법도 매우 간단합니다. db(getByKey(K 키))에서 데이터를 가져오는 방법은 기본값을 제공해야 합니다.

예를 들어 상한이 1W인 상금 풀이 있습니다. 사용자가 작업을 완료하면 그에게 돈을 보내고 이를 Redis를 사용하여 기록하고 테이블에 로그인하면 사용자가 남은 금액을 볼 수 있습니다. 작업 페이지에서 실시간으로 상금 풀을 확인할 수 있습니다. 작업 시작 시 상금 풀 금액이 변경되지 않은 것이 분명합니다. redis 및 db에 발행된 금액에 대한 기록이 없으므로 확인이 필요합니다. 이 경우 db에서 데이터를 찾을 수 없으면 캐시에 0 값을 캐시해야 합니다.

Cache avalanche

중앙 집중식 캐시 오류가 다수 발생했다는 의미입니다. 물론 최종적으로 분석해 보면 코드 작성에 문제가 있는 것입니다. 캐시 무효화의 만료 시간을 분할하고 중앙에서 실패하지 않도록 할 수 있습니다.

추천 학습: Redis 비디오 튜토리얼

위 내용은 Redis와 로컬 캐시를 활용한 높은 동시성 기술 공유의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:jb51.net
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿