성능 향상을위한 일반 캐싱 서비스 개발

동료가 전 세계의 모든 국가 목록을 요청한다고 가정 해 보겠습니다. 당신은 지리 전문가가 아니기 때문에 당신은 UN 웹 사이트를 검색하고 목록을 다운로드하고 그녀를 위해 인쇄합니다. 그러나 그녀는 목록을 검토하기만을 원합니다. 그녀는 실제로 그것을 가지고 가지 않습니다. 마지막으로 필요한 것은 책상 위에있는 또 다른 종이이기 때문에 목록을 분쇄기에 공급합니다.

하루 후 다른 동료가 같은 것을 요청합니다. 전 세계 모든 국가의 목록입니다. 목록을 유지하지 않은 것에 대한 저주를 받고 다시 UN 웹 사이트로 돌아갑니다. 이번 웹 사이트 방문에서 UN은 6 개월마다 국가 목록을 업데이트합니다. 동료의 목록을 다운로드하고 인쇄합니다. 그는 그것을보고, 당신에게 감사하고, 다시 당신과 함께 목록을 남깁니다. 이번에는 첨부 된 포스트잇 메모에 메시지와 함께 목록을 정리하여 6 개월 후에 삭제하도록 상기시킵니다.

다음 몇 주 동안 동료들은 계속해서 목록을 계속해서 요청합니다. 웹 사이트에서 추출 할 수있는 것보다 더 빨리 파일 캐비닛에서 문서를 추출 할 수 있으므로 문서 제출을 축하합니다. 당신의 서류 캐비넷 컨셉이 잘 잡 힙니다. 곧 모든 사람들이 당신의 캐비닛에 물건을 넣기 시작합니다. 캐비닛이 무질서 해지는 것을 방지하기 위해 사용 지침을 설정합니다. 파일 캐비넷 관리자 로서의 공식 자격으로 동료들에게 모든 문서에 라벨과 포스트잇 메모를 배치하도록 지시하여 문서와 폐기 / 만료 날짜를 식별합니다. 레이블은 동료가 찾고있는 문서를 찾는 데 도움이되며 Post-it 노트는 정보가 최신 상태인지 여부를 확인합니다.

서류 캐비넷은 너무나 인기가 많아서 곧 새 문서를 제출할 수 없습니다. 무엇을 버리고 무엇을 보관할지 결정해야합니다. 만료 된 문서를 모두 버리더라도 캐비닛에는 여전히 종이가 넘칩니다. 폐기 할 만료되지 않은 문서를 어떻게 결정합니까? 가장 오래된 문서를 버리십니까? 가장 자주 사용하지 않거나 가장 적게 사용한 것을 버릴 수 있습니다. 두 경우 모두 각 문서에 액세스 할 때 나열된 로그가 필요합니다. 또는 다른 결정자를 기반으로 삭제할 문서를 결정할 수 있습니다. 결정은 순전히 개인적인 것입니다.

위의 실제 비유를 컴퓨터 세계와 연관시키기 위해 파일 캐비넷은 캐시 로 작동 합니다. 가끔 유지 관리가 필요한 고속 메모리입니다. 캐시의 문서는 캐시 된 객체이며, 모두 캐시 관리자가 설정 한 표준을 따릅니다 . 캐시를 정리하는 프로세스를 제거 라고 합니다. 캐시 된 항목은 일정 시간이 지나면 제거되기 때문에 캐시를 시간 캐시 라고 합니다.

이 기사에서는 만료 된 항목을 제거하기 위해 익명 백그라운드 스레드를 사용하는 100 % 순수 Java 캐시를 만드는 방법을 배웁니다. 다양한 디자인과 관련된 장단점을 이해하면서 이러한 캐시를 설계하는 방법을 볼 수 있습니다.

캐시 구축

충분한 서류 정리 비유 : 웹 사이트로 이동합시다. 웹 사이트 서버도 캐싱을 처리해야합니다. 서버는 다른 요청과 동일한 정보 요청을 반복적으로받습니다. 다음 작업을 위해 세계 최대 기업 중 하나를위한 인터넷 애플리케이션을 구축해야합니다. 많은 잠 못 이루는 밤과 너무 많은 Jolt 콜라를 포함하여 4 개월의 개발 끝에 애플리케이션은 1,000 명의 사용자를 대상으로 개발 테스트에 들어갑니다. 5,000 명의 사용자 인증 테스트와 이후 20,000 명의 사용자 프로덕션 롤아웃이 개발 테스트를 따릅니다. 그러나 200 명의 사용자 만 애플리케이션을 테스트하는 동안 메모리 부족 오류가 발생하면 개발 테스트가 중단됩니다.

성능 저하의 원인을 식별하기 위해 프로파일 링 제품을 사용하고 서버가 ResultSet각각 수천 개의 레코드가있는 여러 개의 데이터베이스 사본을로드 함을 발견 합니다. 레코드는 제품 목록을 구성합니다. 또한 제품 목록은 모든 사용자에게 동일합니다. 제품 목록이 매개 변수화 된 쿼리의 결과 인 경우처럼 목록은 사용자에 따라 달라지지 않습니다. 목록의 사본 하나가 모든 동시 사용자에게 서비스를 제공 할 수 있는지 신속하게 결정하여 캐시합니다.

그러나 다음과 같은 복잡성을 포함한 많은 질문이 발생합니다.

  • 제품 목록이 변경되면 어떻게됩니까? 캐시가 목록을 어떻게 만료시킬 수 있습니까? 제품 목록이 만료되기 전에 캐시에 얼마나 오래 남아 있어야하는지 어떻게 알 수 있습니까?
  • 두 개의 개별 제품 목록이 있고 두 목록이 다른 간격으로 변경되면 어떻게됩니까? 각 목록을 개별적으로 만료 할 수 있습니까? 아니면 모두 동일한 유효 기간을 가져야합니까?
  • 캐시가 비어 있고 두 명의 요청자가 정확히 동시에 캐시를 시도하면 어떻게됩니까? 둘 다 빈 것을 발견하면 자신의 목록을 만든 다음 둘 다 복사본을 캐시에 넣으려고할까요?
  • 항목에 액세스하지 않고 몇 달 동안 캐시에 보관하면 어떻게됩니까? 그들은 기억을 먹지 않을까요?

이러한 문제를 해결하려면 소프트웨어 캐싱 서비스를 구축해야합니다.

파일 캐비닛 비유에서 사람들은 문서를 검색 할 때 항상 캐비닛을 먼저 확인했습니다. 소프트웨어는 동일한 절차를 구현해야합니다. 요청은 데이터베이스에서 새 목록을로드하기 전에 캐싱 서비스를 확인해야합니다. 소프트웨어 개발자로서 귀하의 책임은 데이터베이스에 액세스하기 전에 캐시에 액세스하는 것입니다. 제품 목록이 이미 캐시에로드 된 경우 만료되지 않은 경우 캐시 된 목록을 사용합니다. 제품 목록이 캐시에 없으면 데이터베이스에서로드하고 즉시 캐시합니다.

참고 : 캐싱 서비스의 요구 사항 및 코드를 진행하기 전에 아래의 "캐싱 대 풀링"사이드 바를 확인하는 것이 좋습니다. 관련 개념 인 풀링을 설명합니다 .

요구 사항

좋은 디자인 원칙에 따라이 기사에서 개발할 캐싱 서비스에 대한 요구 사항 목록을 정의했습니다.

  1. 모든 Java 애플리케이션은 캐싱 서비스에 액세스 할 수 있습니다.
  2. 개체는 캐시에 배치 할 수 있습니다.
  3. 캐시에서 개체를 추출 할 수 있습니다.
  4. 캐시 된 개체는 만료시기를 스스로 결정할 수 있으므로 유연성을 극대화 할 수 있습니다. 동일한 만료 공식을 사용하여 모든 개체를 만료하는 캐싱 서비스는 캐시 된 개체를 최적으로 사용하지 못합니다. 예를 들어, 제품 목록은 매일 변경되는 반면 매장 위치 목록은 한 달에 한 번만 변경 될 수 있으므로이 접근 방식은 대규모 시스템에서는 적합하지 않습니다.
  5. 낮은 우선 순위로 실행되는 백그라운드 스레드는 만료 된 캐시 된 개체를 제거합니다.
  6. 캐싱 서비스는 LRU (Least-Recently Used) 또는 LFU (Least-Frequently Used) 제거 메커니즘을 사용하여 나중에 향상 될 수 있습니다.

이행

요구 사항 1을 충족하기 위해 100 % 순수한 Java 환경을 채택합니다. 캐싱 서비스에서 퍼블릭 getset메소드 를 제공함으로써 요구 사항 2 및 3도 충족합니다.

요구 사항 4에 대한 논의를 진행하기 전에 캐시 관리자에서 익명 스레드를 생성하여 요구 사항 5를 충족시킬 것임을 간단히 언급하겠습니다. 이 스레드는 정적 블록에서 시작됩니다. 또한 LRU 및 LFU 알고리즘을 구현하기 위해 나중에 코드가 추가 될 지점을 식별하여 요구 사항 6을 충족합니다. 이 문서의 뒷부분에서 이러한 요구 사항에 대해 자세히 설명하겠습니다.

이제 상황이 흥미로워지는 요구 사항 4로 돌아갑니다. 모든 캐시 된 개체가 만료되었는지 자체적으로 확인해야하는 경우 개체가 만료되었는지 여부를 확인할 수있는 방법이 있어야합니다. 즉, 캐시의 개체는 모두 특정 규칙을 준수해야합니다. 인터페이스를 구현하여 Java에서이를 수행합니다.

캐시에있는 개체를 제어하는 ​​규칙부터 시작하겠습니다.

  1. 모든 객체 isExpired()에는 부울 값을 반환하는 라는 공용 메서드가 있어야합니다 .
  2. 모든 개체에는라는 공용 메서드가 있어야합니다.이 메서드 getIdentifier()는 개체를 캐시의 다른 모든 개체와 구별하는 개체를 반환합니다.

참고 : 코드로 바로 들어가기 전에 여러 가지 방법으로 캐시를 구현할 수 있음을 이해해야합니다. 나는 12 개 이상의 다른 구현을 발견했습니다. Enhydra 및 Caucho는 여러 캐시 구현을 포함하는 우수한 리소스를 제공합니다.

이 기사의 캐싱 서비스에 대한 인터페이스 코드는 Listing 1에서 찾을 수있다.

목록 1. Cacheable.java

/ ** * 제목 : 캐싱 설명 :이 인터페이스는 캐시에 배치되기를 원하는 모든 개체에 의해 구현되어야하는 메서드를 정의합니다. * * 저작권 : Copyright (c) 2001 * 회사 : JavaWorld * FileName : Cacheable.java @author Jonathan Lurie @version 1.0 * / public interface Cacheable {/ * 모든 객체가 자체 만료를 결정하도록 요구함으로써 알고리즘은 캐싱 서비스를 제공하므로 각 개체가 서로 다른 만료 전략을 채택 할 수 있으므로 최대한의 유연성을 제공합니다. * / 공개 부울 isExpired (); / *이 방법은 캐싱 서비스가 캐시에있는 개체를 고유하게 식별 할 책임이 없음을 보장합니다. * / 공용 객체 getIdentifier (); }

캐시에있는 모든 객체 ( String예 :)는 Cacheable인터페이스 를 구현하는 객체 내부에 래핑되어야합니다 . 목록 2는라는 일반 래퍼 클래스의 예입니다 CachedObject. 캐싱 서비스에 배치하는 데 필요한 모든 개체를 포함 할 수 있습니다. 이 래퍼 클래스 Cacheable는 목록 1에 정의 된 인터페이스를 구현합니다 .

목록 2. CachedManagerTestProgram.java

/ ** * 제목 : 캐싱 * 설명 : 일반 캐시 개체 래퍼입니다. Cacheable 인터페이스를 구현합니다. * CacheObject 만료를 위해 TimeToLive stategy를 사용합니다. * 저작권 : Copyright (c) 2001 * 회사 : JavaWorld * 파일 이름 : CacheManagerTestProgram.java * @author Jonathan Lurie * @version 1.0 * / 공용 클래스 CachedObject는 Cacheable 구현 {// +++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++ ++++ / *이 변수는 개체가 만료되었는지 확인하는 데 사용됩니다. * / private java.util.Date dateofExpiration = null; 개인 개체 식별자 = null; / * 이것은 실제 "값"을 포함합니다. 이것은 공유해야하는 객체입니다. * / 공용 개체 개체 = null; // ++++++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++++++ public CachedObject (Object obj, Object id, int minutesToLive) {this.object = obj; 이.식별자 = id; // minutesToLive가 0이면 무기한 유지됩니다. if (minutesToLive! = 0) {dateofExpiration = new java.util.Date (); java.util.Calendar cal = java.util.Calendar.getInstance (); cal.setTime (dateofExpiration); cal.add (cal.MINUTE, minutesToLive); dateofExpiration = cal.getTime (); }} // ++++++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++++++++ public boolean isExpired () {// 시간이 0이면 영원히 살 수 있음을 기억하십시오! if (dateofExpiration! = null) {// 만료 날짜를 비교합니다. if (dateofExpiration.before (new java.util.Date ())) {System.out.println ( "CachedResultSet.isExpired : Cache에서 만료되었습니다! EXPIRE TIME :"+ dateofExpiration.toString () + "CURRENT TIME :"+ ( new java.util.Date ()). toString ()); true를 반환하십시오. } else {System.out.println ( "CachedResultSet.isExpired :Expired not from Cache! "); return false;}} else // 이것은 영원함을 의미합니다! return false;} // ++++++++++++++++++++++ +++++++++++++++++++++++++++++++++++++++++++++ public Object getIdentifier () {return identifier;} // +++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++}

CachedObject클래스는 세 개의 매개 변수를 사용하는 생성자 메소드를 노출합니다.

public CachedObject (Object obj, Object id, int minutesToLive) 

아래 표는 이러한 매개 변수를 설명합니다.

CachedObject 생성자의 매개 변수 설명
이름 유형 기술
Obj 목적 공유되는 개체입니다. 최대 유연성을 허용하는 객체로 정의됩니다.
Id 목적 Idobj캐시에있는 다른 모든 개체 와 매개 변수 를 구별하는 고유 식별자를 포함 합니다. 캐싱 서비스는 캐시에있는 개체의 고유성을 보장 할 책임이 없습니다.
minutesToLive Int obj매개 변수가 캐시에서 유효한 시간 (분)입니다 . 이 구현에서 캐싱 서비스는 0 값을 해석하여 개체가 만료되지 않음을 의미합니다. 1 분 이내에 오브젝트를 만료해야하는 경우이 매개 변수를 변경할 수 있습니다.

생성자 메서드는 TTL 전략 사용하여 캐시에있는 개체의 만료 날짜를 결정합니다 . 이름에서 알 수 있듯이 수명은 특정 개체가 종료시 죽은 것으로 간주되는 고정 된 시간을 가지고 있음을 의미합니다. 추가하여 minutesToLive, 생성자의 int현재 시간에 매개 변수를, 유효 기간이 계산된다. 이 만료는 클래스 변수에 할당됩니다 dateofExpiration.

이제 isExpired()메서드는 단순히 dateofExpiration현재 날짜 및 시간 이전인지 이후 인지 확인해야합니다 . 날짜가 현재 시간 이전이고 캐시 된 개체가 만료 된 것으로 간주되면 isExpired()메서드는 true를 반환합니다. 날짜가 현재 시간 이후이면 캐시 된 개체는 만료되지 않고 isExpired()false를 반환합니다. 물론 if dateofExpiration가 null ( minutesToLive0 인 경우)이면 isExpired()메서드는 항상 false를 반환하여 캐시 된 개체가 영구적임을 나타냅니다.