>데이터 베이스 >Redis >Redis와 Memcached의 차이점은 무엇입니까?

Redis와 Memcached의 차이점은 무엇입니까?

王林
王林앞으로
2023-06-03 09:14:041347검색

redis는 데이터베이스이지만 기존 데이터베이스와 달리 Redis 데이터가 메모리에 저장되기 때문에 읽기 및 쓰기 속도가 매우 빠르므로 캐싱에는 Redis가 널리 사용됩니다. memcached는 고성능 분산 메모리 캐시 서버입니다. 일반적인 사용 목적은 데이터베이스 쿼리 결과를 캐싱하고 데이터베이스 액세스 횟수를 줄여 동적 웹 애플리케이션의 속도와 확장성을 높이는 것입니다.

Redis와 Memcached의 차이점은 무엇입니까?

권위 있는 비교

Redis의 저자인 Salvatore Sanfilippo는 다음 두 가지 메모리 기반 데이터 스토리지 시스템을 비교한 적이 있습니다.

  1. Redis는 서버 측 데이터 작업을 지원합니다. Memcached에 비해 Redis는 더 많은 데이터 구조를 가지고 있습니다. 더 풍부한 데이터 작업을 지원합니다. 일반적으로 Memcached에서는 유사한 수정을 수행하려면 데이터를 클라이언트로 가져온 다음 다시 설정해야 합니다. 이로 인해 네트워크 IO 수와 데이터 볼륨이 크게 늘어납니다. 일반적인 GET/SET과 비교할 때 이러한 복잡한 작업은 일반적으로 Redis에서 동일하게 효율적입니다. 따라서 더 복잡한 구조와 작업을 지원하기 위해 캐시가 필요한 경우 Redis가 좋은 선택이 될 것입니다.

  2. 메모리 사용 효율성 비교: 단순 키-값 저장을 사용하면 Memcached의 메모리 사용률이 더 높습니다. Redis가 키-값 저장에 해시 구조를 사용하면 결합된 압축으로 인해 메모리 사용률이 낮아집니다. Memcached보다 높습니다.

  3. 성능 비교: Redis는 단일 코어만 사용하는 반면 Memcached는 여러 코어를 사용할 수 있으므로 평균적으로 Redis는 각 코어에 작은 데이터를 저장할 때 Memcached보다 성능이 더 높습니다. 100,000개 이상의 데이터에 대해서는 Memcached의 성능이 Redis보다 높습니다. Redis는 최근 빅데이터 저장 성능에 최적화되어 있지만 여전히 Memcached에 비해 약간 열등합니다.

구체적으로 위와 같은 결론이 나온 이유는 다음과 같습니다.

1. 다양한 데이터 유형 지원

단순한 키-값 구조의 데이터 레코드만 지원하는 Memcached와 달리 Redis는 훨씬 더 다양한 데이터 유형을 지원합니다. . 가장 일반적인 데이터 유형에는 문자열, 해시, 목록, 집합 및 정렬된 집합이 포함됩니다. Redis는 redisObject 객체를 사용하여 모든 키와 값을 나타냅니다. redisObject의 주요 정보는 그림에 나와 있습니다.

type은 값 개체의 특정 데이터 유형을 나타내며 인코딩은 다양한 데이터 유형이 redis 내에 저장되는 방식입니다. 예: type=string은 값이 값으로 저장됨을 나타냅니다. 일반 문자열이면 해당 인코딩은 raw 또는 int일 수 있습니다. int라면 실제 redis가 숫자 클래스에 따라 문자열을 내부적으로 저장하고 표현한다는 의미입니다. 물론 문자열 자체를 표현할 수 있다는 전제가 있습니다. "123″ "456"과 같은 문자열. Redis의 가상 메모리 기능이 켜져 있을 때만 vm 필드가 실제로 메모리를 할당합니다. 이 기능은 기본적으로 꺼져 있습니다. 1) String

일반적인 명령: set/get./decr/incr/mget 등

응용 시나리오: 문자열은 가장 일반적으로 사용되는 데이터 유형이며 일반 키/값 저장소는 이 범주로 분류될 수 있습니다. redisObject에서 참조하는 기본 문자열은 incr, decr 및 기타 작업이 발생할 때 계산을 위해 숫자 유형으로 변환됩니다. 이때 redisObject의 인코딩 필드는 int입니다.

2) Hash.

일반적인 명령: hget/hset/hgetall.Wait

응용 시나리오: 사용자 ID, 사용자 이름, 나이 및 생일을 포함한 사용자 정보 개체 데이터를 저장하려고 합니다. , age 또는 birthday;

구현 방법: Redis Hash 내부에 저장된 Value는 실제로 HashMap이며, 그림과 같이 키는 사용자 ID이고 값은 이 Map의 멤버에 직접 액세스할 수 있는 인터페이스를 제공합니다. 이 Map의 키는 멤버의 속성 이름이고, 값은 Value입니다. 이렇게 하면 내부 Map의 키(내부의 키)를 통해 데이터를 직접 수정하고 액세스할 수 있습니다. Map은 Redis에서는 field라고 한다. 즉, 해당 attribute는 key(사용자 ID) + field(속성 label) Data를 통해 동작할 수 있다. 현재 HashMap을 구현하는 방법은 두 가지가 있다: HashMap의 멤버가 상대적으로 적을 경우, Redis 메모리 절약을 위해 실제 HashMap 구조를 사용하는 대신 1차원 배열을 사용하여 콤팩트하게 저장합니다. 이 경우 redisObject의 해당 값은 zipmap으로 인코딩됩니다. 이때 인코딩은 ht입니다.

일반적인 명령: lpush/rpush/lpop/rpop/lrange 등: 다양한 응용 시나리오가 있습니다. Redis 목록의 경우 Redis의 가장 중요한 데이터 구조 중 하나이기도 합니다. 예를 들어 Twitter의 팔로우 목록, 팬 목록 등은 Redis의 목록 구조를 사용하여 구현할 수 있습니다.

구현 방법: Redis 목록은 Doubly로 구현됩니다. 연결된 목록은 역방향 검색 및 순회를 지원할 수 있어 작동이 더 편리하지만 전송 버퍼 큐를 포함하여 Redis 내의 많은 구현에서도 이 데이터 구조를 사용합니다.

4) Set

일반적인 명령: sadd/spop/smembers/sunion 등

응용 시나리오: Redis 세트에서 제공하는 외부 기능은 목록의 기능과 유사합니다. 특별한 점은 목록 데이터를 저장해야 하고 중복 데이터가 표시되지 않도록 할 때 세트는 A입니다. 좋은 선택이며, set은 목록이 제공할 수 없는 특정 멤버가 set 컬렉션에 있는지 판단하는 데 중요한 인터페이스를 제공합니다.

구현 방법: set의 내부 구현은 실제로 값이 항상 null인 HashMap입니다. 해시를 계산하여 중복 항목을 빠르게 정렬하기 때문에 set은 멤버가 세트에 있는지 확인하는 방법을 제공할 수 있습니다.

5) Sorted Set

일반적인 명령: zadd/zrange/zrem/zcard 등;

응용 시나리오: Redis sorted set의 사용 시나리오는 set과 유사하지만 차이점은 set이 자동으로 정렬되지 않는다는 것입니다. , while sorted set can 사용자가 추가 우선순위(점수) 매개변수를 제공하여 멤버를 정렬하고 삽입을 기준으로 멤버를 정렬, 즉 자동으로 정렬합니다. 순서가 있고 중복되지 않은 세트 목록이 필요한 경우 정렬된 세트 데이터 구조를 선택할 수 있습니다. 예를 들어 Twitter의 공개 타임라인은 게시 시간을 점수로 저장하여 검색 시 자동으로 시간별로 정렬되도록 할 수 있습니다.

구현 방법: Redis 정렬 세트는 내부적으로 HashMap 및 건너뛰기 목록(SkipList)을 사용하여 데이터 저장 및 순서를 보장합니다. HashMap은 멤버에서 점수로의 매핑을 저장하는 반면, 건너뛰기 목록은 모든 멤버를 저장하고 점수를 기준으로 정렬합니다. HashMap에 저장된 점프 테이블 구조를 사용하면 검색 효율성이 높아지고 구현이 비교적 간단합니다.

2. 다양한 메모리 관리 메커니즘

Redis에서는 모든 데이터가 항상 메모리에 저장되는 것은 아닙니다. 이것이 Memcached와 비교했을 때 가장 큰 차이점입니다. 물리적 메모리가 부족해지면 Redis는 오랫동안 사용되지 않은 일부 값을 디스크로 스왑할 수 있습니다. Redis는 모든 키 정보만 캐시합니다. Redis는 메모리 사용량이 특정 임계값을 초과하는 것을 발견하면 스왑 작업을 트리거합니다. Redis는 디스크에 대한 "swappability = age*log(size_in_memory)" 스왑을 기반으로 값에 해당하는 키를 계산합니다. . 그런 다음 이러한 키에 해당하는 값은 디스크에 유지되고 메모리에서 지워집니다. 이 기능을 통해 Redis는 머신 자체의 메모리 크기를 초과하는 데이터를 유지할 수 있습니다. 물론, 이러한 데이터는 교환되지 않으므로 모든 주요 데이터를 저장할 수 있을 만큼 기기의 메모리 용량이 충분해야 합니다. 동시에 Redis가 메모리에 있는 데이터를 디스크로 스왑할 때 서비스를 제공하는 메인 스레드와 스왑 작업을 수행하는 하위 스레드가 이 메모리 부분을 공유하게 되므로 필요한 데이터가 swapped가 업데이트되면 Redis는 스왑 작업을 완료한 후에만 하위 스레드 수정이 이루어질 수 있을 때까지 작업을 차단합니다. Redis에서 데이터를 읽을 때 읽기 키에 해당하는 값이 메모리에 없으면 Redis는 스왑 파일에서 해당 데이터를 로드한 다음 요청자에게 반환해야 합니다. 여기에 I/O 스레드 풀 문제가 있습니다. 기본적으로 Redis는 차단됩니다. 즉, 모든 스왑 파일이 로드될 때까지 응답하지 않습니다. 이 전략은 클라이언트 수가 적고 일괄 작업을 수행하는 경우에 더 적합합니다. 동시성이 높은 대규모 웹 사이트 애플리케이션에서 Redis를 사용하려는 경우 분명히 요구 사항을 충족하기에 충분하지 않습니다. 따라서 Redis는 I/O 스레드 풀의 크기를 설정하고 차단 시간을 줄이기 위해 스왑 파일에서 해당 데이터를 로드해야 하는 읽기 요청에 대해 동시 작업을 수행하도록 실행합니다.

Redis 및 Memcached와 같은 메모리 기반 데이터베이스 시스템의 경우 메모리 관리 효율성은 시스템 성능에 영향을 미치는 핵심 요소입니다. 전통적인 C 언어의 malloc/free 함수는 메모리를 할당하고 해제하는 데 가장 일반적으로 사용되는 방법이지만 이 방법에는 심각한 결함이 있습니다. 첫째, 개발자의 경우, malloc과 free가 일치하지 않으면 쉽게 메모리 누수가 발생할 수 있습니다. 재활용 및 재사용이 불가능한 대량의 메모리 조각으로 인해 메모리 활용도가 감소합니다. 마지막으로 시스템 호출로서 시스템 오버헤드가 일반 함수 호출보다 훨씬 큽니다. 따라서 메모리 관리 효율성을 향상시키기 위해 효율적인 메모리 관리 솔루션에서는 malloc/free 호출을 직접 사용하지 않습니다. Redis와 Memcached는 모두 자체 메모리 관리 메커니즘을 사용하지만 구현 방법은 매우 다릅니다. 두 가지의 메모리 관리 메커니즘은 아래에서 별도로 소개됩니다.

Memcached는 기본적으로 Slab 할당 메커니즘을 사용하여 메모리를 관리합니다. 주요 아이디어는 할당된 메모리를 미리 결정된 크기에 따라 특정 길이의 블록으로 나누어 해당 길이의 키-값 데이터 레코드를 저장하여 메모리 조각화 문제를 완전히 해결하는 것입니다. Slab 할당 메커니즘은 외부 데이터만 저장하도록 설계되었습니다. 즉, 모든 키-값 데이터는 Slab 할당 시스템에 저장되는 반면 Memcached에 대한 다른 메모리 요청은 일반 malloc/free를 통해 적용됩니다. 빈도는 전체 시스템의 성능에 영향을 미치지 않을 것이라고 결정합니다. 슬래브 할당의 원리는 매우 간단합니다. 그림과 같이 운영체제로부터 큰 메모리 블록을 먼저 적용하고, 이를 다양한 크기의 청크로 나누고, 같은 크기의 청크를 슬래브 클래스 그룹으로 나눕니다. 청크는 키-값 데이터를 저장하는 가장 작은 단위로 사용됩니다. Memcached가 시작될 때 성장 인자를 지정하여 각 Slab 클래스의 크기를 제어할 수 있습니다. 그림에서 성장 인자 값을 1.25라고 가정합니다. 첫 번째 청크 그룹의 크기가 88바이트인 경우 두 번째 청크 그룹의 크기는 112바이트입니다.

Memcached는 클라이언트가 보낸 데이터를 수신하면 먼저 수신된 데이터의 크기를 기준으로 가장 적합한 Slab 클래스를 선택한 다음 Memcached가 저장한 Slab 클래스의 사용 가능한 청크 목록을 쿼리하여 사용 가능한 Slab 클래스를 찾습니다. 데이터를 저장하는 청크입니다. 데이터베이스가 만료되거나 삭제되면 해당 청크가 있는 청크를 재활용하여 사용 가능 목록에 다시 추가할 수 있습니다.

위의 과정을 통해 Memcached의 메모리 관리 시스템은 매우 효율적이고 메모리 조각화를 일으키지 않는다는 것을 알 수 있지만, 공간 낭비가 발생한다는 것이 가장 큰 단점입니다. 가변 길이 데이터는 각 청크에 할당된 특정 길이의 메모리 공간을 완전히 활용할 수 없습니다. 그림과 같이 100바이트의 데이터가 128바이트의 청크에 캐시되고 나머지 28바이트는 낭비됩니다.

Redis가 메모리 관리를 구현하는 방식은 주로 소스 코드의 zmalloc.h 및 zmalloc.c 두 파일과 관련됩니다. 메모리 관리를 용이하게 하기 위해 Redis는 메모리를 할당한 후 메모리 블록의 헤드에 이 메모리의 크기를 저장합니다. real_ptr은 redis가 malloc을 호출한 후 반환된 메모리 블록을 가리킵니다. Redis는 메모리 블록 크기의 크기를 헤더에 저장하며, size가 차지하는 메모리 크기는 size_t 유형의 길이이며 ret_ptr을 반환합니다. 메모리를 해제해야 할 경우 ret_ptr이 메모리 관리자에 전달됩니다. ret_ptr을 통해 프로그램은 real_ptr의 값을 쉽게 계산한 다음 real_ptr을 전달하여 메모리를 해제할 수 있습니다.

Redis는 배열을 정의하여 모든 메모리 할당을 기록합니다. 이 배열의 길이는 ZMALLOC_MAX_ALLOC_STAT입니다. 각 숫자는 현재 프로그램에 의해 할당된 메모리 블록의 개수를 나타내며, 각 메모리 블록의 크기는 해당 메모리 블록이 위치한 배열 인덱스와 동일합니다. 소스 코드에서 이 배열은 zmalloc_allocations입니다. zmalloc_allocations[16]은 16바이트 길이의 할당된 메모리 블록 수를 나타냅니다. zmalloc.c에는 현재 할당된 메모리의 전체 크기를 기록하는 정적 변수 Used_memory가 있습니다. 따라서 일반적으로 Redis는 Memcached의 메모리 관리 방법보다 훨씬 간단한 packaged mallc/free를 사용합니다.

3. 데이터 지속성 지원

Redis는 메모리 기반 스토리지 시스템이지만 자체적으로 메모리 데이터의 지속성을 지원하고 RDB 스냅샷과 AOF 로그라는 두 가지 주요 지속성 전략을 제공합니다. Memcached는 데이터 지속성 작업을 지원하지 않습니다.

1) RDB 스냅샷

RDB 스냅샷은 Redis의 지속성 메커니즘으로, 사용자가 현재 데이터 스냅샷을 데이터 파일로 저장할 수 있습니다. Redis는 fork 명령의 쓰기 시 복사 메커니즘을 사용하여 데이터베이스에 지속적으로 기록되는 스냅샷을 생성합니다. 스냅샷을 생성할 때 포크 작업을 사용하여 하위 프로세스를 생성하고 하위 프로세스의 모든 데이터를 반복하여 RDB 파일에 씁니다. Redis의 save 명령을 통해 RDB 스냅샷 생성 타이밍을 구성할 수 있습니다. 예를 들어 스냅샷이 10분 안에 생성되도록 구성하거나 1,000회 쓰기 후에 스냅샷을 생성하도록 구성하거나 여러 규칙을 함께 구현할 수 있습니다. 이러한 규칙의 정의는 Redis를 다시 시작하지 않고도 Redis가 실행되는 동안 Redis CONFIG SET 명령을 통해 규칙을 설정할 수도 있습니다.

Redis의 RDB 파일은 쓰기 작업이 새 프로세스에서 수행되므로 손상되지 않습니다. 새 RDB 파일이 생성되면 Redis에서 생성된 하위 프로세스는 먼저 데이터를 임시 파일에 쓴 다음 이름을 바꿉니다. 원자적 이름 바꾸기 시스템 호출을 통해 임시 파일을 RDB 파일로 변환하므로 언제든지 오류가 발생하더라도 Redis RDB 파일을 항상 사용할 수 있습니다. Redis 마스터-슬레이브 동기화의 내부 구현에서 RDB 파일도 중요한 역할을 합니다. RDB에는 단점이 있습니다. 즉, 데이터베이스에 문제가 발생하면 RDB 파일에 저장된 데이터는 마지막 RDB 파일 생성부터 Redis 종료까지의 모든 데이터가 손실됩니다. 일부 기업에서는 이것이 허용될 수 있습니다.

2) AOF 로그

AOF 로그의 전체 이름은 "Append Write File"로, 지속적으로 추가되고 기록되는 로그 파일입니다. AOF 파일은 일반 데이터베이스의 binlog와 달리 식별 가능한 일반 텍스트이며 그 내용은 하나씩 Redis 표준 명령입니다. 데이터를 수정하는 명령만 AOF 파일에 추가됩니다. 데이터를 수정하는 각 명령은 로그를 생성하며 AOF 파일은 점점 커지므로 Redis는 AOF rewrite라는 또 다른 기능을 제공합니다. 그 기능은 AOF 파일을 재생성하는 것입니다. 동일한 값에 여러 작업을 기록할 수 있는 이전 파일과 달리 새 AOF 파일의 레코드에는 하나의 작업만 있습니다. AOF는 RDB와 유사한 방식으로 생성됩니다. 즉, 프로세스를 분기하고 데이터를 직접 탐색하여 새 임시 AOF 파일에 기록합니다. 데이터가 새 파일에 기록되는 동안 모든 쓰기 작업 로그는 원본 AOF 파일에 계속 기록되며 동시에 메모리 버퍼에도 기록됩니다. 중요한 작업을 완료한 후 모든 버퍼 로그는 일괄적으로 임시 파일에 기록됩니다. 다음으로, 원자적 "이름 바꾸기" 명령을 사용하여 이전 AOF 파일을 새 AOF 파일로 바꿉니다.

AOF는 파일 쓰기 작업입니다. 그 목적은 작업 로그를 디스크에 쓰는 것이므로 위에서 언급한 쓰기 작업 프로세스도 발생합니다. Redis에서 AOF에 대한 쓰기를 호출한 후,appendfsync 옵션을 사용하여 fsync를 호출하여 디스크에 쓰는 데 걸리는 시간을 제어하면 다음 세 가지appendfsync 설정의 보안 강도가 점차 강화됩니다.

  • appendfsync no appendfsync가 no로 설정되면 Redis는 AOF 로그 콘텐츠를 디스크에 동기화하기 위해 fsync를 적극적으로 호출하지 않으므로 이 모든 것은 전적으로 운영 체제 디버깅에 달려 있습니다. 대부분의 Linux 운영 체제에서는 버퍼 데이터를 디스크에 쓰기 위해 30초마다 fsync 작업이 수행됩니다.

  • appendfsynceverysecappendfsync가everysec로 설정되면 Redis는 기본적으로 매초마다 fsync 호출을 수행하여 버퍼의 데이터를 디스크에 씁니다. 하지만 이 fsync 호출이 1초 이상 지속되는 경우. Redis는 fsync를 지연하고 1초 더 기다리는 전략을 채택합니다. 즉, fsync는 2초 후에 수행됩니다. 이번에는 실행 시간에 관계없이 fsync가 수행됩니다. fsync 작업이 진행되는 동안 파일 설명자가 차단되므로 현재 쓰기 작업이 차단됩니다. 따라서 일반적인 상황에서 Redis는 매초마다 fsync 작업을 수행합니다. 최악의 경우 fsync 작업이 2초마다 발생합니다. 이 작업을 대부분의 데이터베이스 시스템에서는 그룹 커밋이라고 하며 여러 쓰기 작업의 데이터를 결합하고 로그를 한 번에 디스크에 씁니다.

  • appednfsync 항상appendfsync를 항상으로 설정하면 쓰기 작업마다 fsync가 한 번 호출되므로 이때 데이터가 가장 안전합니다. 물론 fsync가 매번 실행되므로 성능도 저하됩니다. 체하는.

일반적인 비즈니스 요구에는 지속성을 위해 RDB를 사용하는 것이 좋습니다. 그 이유는 RDB의 오버헤드가 AOF 로그보다 훨씬 낮기 때문입니다. 데이터 손실을 허용할 수 없는 애플리케이션의 경우 AOF를 사용하는 것이 좋습니다. 로그.

4. 클러스터 관리의 차이점

Memcached는 전체 메모리 데이터 버퍼링 시스템이지만 Redis는 데이터 지속성을 지원하지만 결국 고성능의 핵심은 전체 메모리입니다. 메모리 기반 스토리지 시스템으로서, 머신의 물리적 메모리 크기는 시스템이 수용할 수 있는 최대 데이터 양입니다. 스토리지 용량을 확장하려면 처리할 데이터의 양이 단일 머신의 물리적 메모리 제한을 초과하는 경우 분산 클러스터를 구축해야 합니다.

Memcached 자체는 배포를 지원하지 않으므로 Memcached의 분산 스토리지는 컨시스턴트 해싱(Consistency Hashing)과 같은 분산 알고리즘을 통해서만 클라이언트에서 구현될 수 있습니다. 아래 그림은 Memcached의 분산 스토리지 구현 아키텍처를 보여줍니다. 클라이언트가 Memcached 클러스터에 데이터를 보내기 전에 먼저 내장된 분산 알고리즘을 통해 데이터의 대상 노드를 계산한 다음 데이터를 노드에 직접 전송하여 저장합니다. 클라이언트가 데이터를 쿼리할 때 먼저 쿼리할 데이터가 있는 노드를 계산한 다음 해당 노드에 쿼리 요청을 보내 데이터를 가져와야 합니다.

클라이언트만 사용하여 분산 스토리지를 구현할 수 있는 Memcached에 비해 Redis는 서버 측에서 분산 스토리지를 구축하는 것을 선호합니다. 최신 버전의 Redis는 이미 분산 스토리지 기능을 지원합니다. Redis Cluster는 분산을 구현하고 단일 장애 지점을 허용하는 Redis의 고급 버전입니다. 중앙 노드가 없으며 선형 확장성을 갖습니다. 아래 그림은 노드간은 바이너리 프로토콜로 통신하고, 노드와 클라이언트간은 ASCII 프로토콜로 통신하는 Redis Cluster의 분산 스토리지 아키텍처를 보여준다. 데이터 배치 전략 측면에서 Redis Cluster는 전체 키 값 필드를 4096개의 해시 슬롯으로 나누고 각 노드는 하나 이상의 해시 슬롯을 저장할 수 있습니다. 즉, 현재 Redis Cluster에서 지원하는 최대 노드 수는 4096개입니다. Redis Cluster에서 사용하는 분산 알고리즘도 매우 간단합니다: crc16(key) % HASH_SLOTS_NUMBER.

Redis Cluster는 단일 장애 지점이 발생하는 경우에도 데이터를 계속 사용할 수 있도록 보장하기 위해 마스터 노드와 슬레이브 노드를 도입합니다. Redis 클러스터에서 각 마스터 노드에는 중복성을 위한 두 개의 해당 슬레이브 노드가 있습니다. 이러한 방식으로 전체 클러스터에서 두 노드의 가동 중지 시간으로 인해 데이터 가용성이 저하되지 않습니다. 마스터 노드가 오프라인 상태가 되면 클러스터는 자동으로 슬레이브 노드에서 새 마스터 노드를 선택합니다.

위 내용은 Redis와 Memcached의 차이점은 무엇입니까?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 yisu.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제