Redis와 로컬 캐시를 활용한 높은 동시성 기술 공유
이 글에서는 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

Video Face Swap
완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

Redis Cluster Mode는 Sharding을 통해 Redis 인스턴스를 여러 서버에 배포하여 확장 성 및 가용성을 향상시킵니다. 시공 단계는 다음과 같습니다. 포트가 다른 홀수 redis 인스턴스를 만듭니다. 3 개의 센티넬 인스턴스를 만들고, Redis 인스턴스 및 장애 조치를 모니터링합니다. Sentinel 구성 파일 구성, Redis 인스턴스 정보 및 장애 조치 설정 모니터링 추가; Redis 인스턴스 구성 파일 구성, 클러스터 모드 활성화 및 클러스터 정보 파일 경로를 지정합니다. 각 redis 인스턴스의 정보를 포함하는 Nodes.conf 파일을 작성합니다. 클러스터를 시작하고 Create 명령을 실행하여 클러스터를 작성하고 복제본 수를 지정하십시오. 클러스터에 로그인하여 클러스터 정보 명령을 실행하여 클러스터 상태를 확인하십시오. 만들다

Redis 데이터를 지우는 방법 : Flushall 명령을 사용하여 모든 키 값을 지우십시오. FlushDB 명령을 사용하여 현재 선택한 데이터베이스의 키 값을 지우십시오. 선택을 사용하여 데이터베이스를 전환 한 다음 FlushDB를 사용하여 여러 데이터베이스를 지우십시오. del 명령을 사용하여 특정 키를 삭제하십시오. Redis-Cli 도구를 사용하여 데이터를 지우십시오.

Redis의 대기열을 읽으려면 대기열 이름을 얻고 LPOP 명령을 사용하여 요소를 읽고 빈 큐를 처리해야합니다. 특정 단계는 다음과 같습니다. 대기열 이름 가져 오기 : "큐 :"와 같은 "대기열 : my-queue"의 접두사로 이름을 지정하십시오. LPOP 명령을 사용하십시오. 빈 대기열 처리 : 대기열이 비어 있으면 LPOP이 NIL을 반환하고 요소를 읽기 전에 대기열이 존재하는지 확인할 수 있습니다.

CentOS 시스템에서는 Redis 구성 파일을 수정하거나 Redis 명령을 사용하여 악의적 인 스크립트가 너무 많은 리소스를 소비하지 못하게하여 LUA 스크립트의 실행 시간을 제한 할 수 있습니다. 방법 1 : Redis 구성 파일을 수정하고 Redis 구성 파일을 찾으십시오. Redis 구성 파일은 일반적으로 /etc/redis/redis.conf에 있습니다. 구성 파일 편집 : 텍스트 편집기 (예 : VI 또는 Nano)를 사용하여 구성 파일을 엽니 다. Sudovi/etc/redis/redis.conf LUA 스크립트 실행 시간 제한을 설정 : 구성 파일에서 다음 줄을 추가 또는 수정하여 LUA 스크립트의 최대 실행 시간을 설정하십시오 (Unit : Milliseconds).

Redis Command Line 도구 (Redis-Cli)를 사용하여 다음 단계를 통해 Redis를 관리하고 작동하십시오. 서버에 연결하고 주소와 포트를 지정하십시오. 명령 이름과 매개 변수를 사용하여 서버에 명령을 보냅니다. 도움말 명령을 사용하여 특정 명령에 대한 도움말 정보를 봅니다. 종금 명령을 사용하여 명령 줄 도구를 종료하십시오.

Redis Counter는 Redis Key-Value Pair 스토리지를 사용하여 다음 단계를 포함하여 계산 작업을 구현하는 메커니즘입니다. 카운터 키 생성, 카운트 증가, 카운트 감소, 카운트 재설정 및 카운트 얻기. Redis 카운터의 장점에는 빠른 속도, 높은 동시성, 내구성 및 단순성 및 사용 편의성이 포함됩니다. 사용자 액세스 계산, 실시간 메트릭 추적, 게임 점수 및 순위 및 주문 처리 계산과 같은 시나리오에서 사용할 수 있습니다.

REDIS 데이터 만료 전략에는 두 가지 유형이 있습니다. 정기 삭제 : 만료 된 기간 캡-프리브-컨트 컨트 및 만료 된 시간 캡-프레임 딜레이 매개 변수를 통해 설정할 수있는 만료 된 키를 삭제하기위한주기 스캔. LAZY DELETION : 키를 읽거나 쓰는 경우에만 삭제가 만료 된 키를 확인하십시오. 그것들은 게으른 불쾌한 말입니다. 게으른 유발, 게으른 게으른 expire, Lazyfree Lazy-user-del 매개 변수를 통해 설정할 수 있습니다.

Debian Systems에서 ReadDir 시스템 호출은 디렉토리 내용을 읽는 데 사용됩니다. 성능이 좋지 않은 경우 다음과 같은 최적화 전략을 시도해보십시오. 디렉토리 파일 수를 단순화하십시오. 대규모 디렉토리를 가능한 한 여러 소규모 디렉토리로 나누어 읽기마다 처리 된 항목 수를 줄입니다. 디렉토리 컨텐츠 캐싱 활성화 : 캐시 메커니즘을 구축하고 정기적으로 캐시를 업데이트하거나 디렉토리 컨텐츠가 변경 될 때 캐시를 업데이트하며 readDir로 자주 호출을 줄입니다. 메모리 캐시 (예 : Memcached 또는 Redis) 또는 로컬 캐시 (예 : 파일 또는 데이터베이스)를 고려할 수 있습니다. 효율적인 데이터 구조 채택 : 디렉토리 트래버스를 직접 구현하는 경우 디렉토리 정보를 저장하고 액세스하기 위해보다 효율적인 데이터 구조 (예 : 선형 검색 대신 해시 테이블)를 선택하십시오.
