[python] Expiring Dict를 이용한 “in memory caching”

ExpiringDict 패키지는?

ExpiringDict는 Python caching library 중에 하나이며 굉장히 심플하게 사용할 수 있고 https://github.com/mailgun/expiringdict에 그소스 전체가 공개 되어 있습니다. 또한 python의 OrderedDict class를 상속 받아 구현되어 있으며 get/set 메소드 동작시 설정된 TTL(time-to-live) 기준으로 데이터를 자동 삭제 합니다. 또한 저장되어지는 객체는 get/set/contain 메소드 내부의 lock 메카니즘이 작동하여 thread safe한 구조로 만들어져 있습니다.

설치

pip를 이용해서 다음과 같이 설치 합니다.

$ pip install expiringdict

또한 깃헙을 통해 바로 설치가 가능하며 다음과 같이 setup.py를 이용하여 설치 할 수 있습니다.

$ git clone git@github.com:mailgun/expiringdict.git
$ python setup.py install 

Expiring Dict API 사용법

기본적인 생성자

user_cache = ExpiringDict(max_len=50, max_age_seconds=25, items=None)

ExpiringDict 클래스는 기본적으로 세개의 인자를 생성자로 사용될 수 있다. 각각은 다음의 의미를 가집니다.

  • max_len : 저장 가능한 최대 캐시 개수. max_len에 지정된 값 이상을 저장 할 수 없고 get/set/contain 수행 시에 자동으로 오래된 데이터는 삭제 됩니다.
  • max_age_seconds : time-to-live(TTL) 값을 지정하며 단위는 초(second) 입니다.
  • items : 초기화 시킬 캐시 타입의 유형이며, 이 값은 다음의 세가지 객체가 지정되어 질 수 있습니다.
    1. dict
    2. OrderedDict
    3. ExpiringDict

API Methods

  • cache에 데이터를 추가하는 코드
user_cache['user1'] = 'sangwon'
  • 단일 캐쉬 값을 얻을 때는 “get” 메소드를 사용 합니다.
user_name = user_cache.get('user1')
print(user_name) # -> 'sangwon'
  • 단일 캐쉬를 값을 얻을 때 만약 키가 존재 하지 않을 수 있을 경우 default 값을 지정 하여 줄 수 있습니다.
user_name = user_cache.get('user2', default='hwalan')
print(user_name) # -> 'hwalan'
  • 저장 된 캐쉬의 TTL값을 얻을 수 있습니다.
user_object = user_cache.get('user1', with_age=true)
print(user_object) # -> ('sangwon', 10)
  • 또한, TTL 만 조회하기 위한 메소드도 존재하며 다음과 같습니다.
user_ttl_only = user_cache.ttl('user1')
print(user_ttl_only) # -> 10
  • ExpiringDict는 각 key/value/ttl 의 전체 리스트를 items_with_timestamp 메소드를 이용하여 조회 할 수 있습니다.
all_of_user_cahces = user_cache.items_with_timestamp
print(all_of_user_cahces) # -> Returns type list(('user1', sangwon', 10))

Thread-Safe

ExpiringDict 클래스의 괜찮은 점 중 하나가 바로 Thread-Safe한 구조로 메소스들이 설계 되어 있습니다.

다음 주요 핵심 메소드인 __contains__/__getitem__/__setitem__ 의 구현 부의 처음 시작 부분입니다.

def __contains__(self, key):
        """ Return True if the dict has a key, else return False. """
        try:
            with self.lock:
        ...

def __getitem__(self, key, with_age=False):
        """ Return the item of the dict.
        Raises a KeyError if key is not in the map.
        """
        with self.lock:
        ...

def __setitem__(self, key, value, set_time=None):
        """ Set d[key] to value. """
        with self.lock:
        ...

with self.lock 구현 부분은 OrderedDict 클래스 내부의 데이터에 접근할 때 가드를 만들어 줍니다. 이 말은 ExpringDict 클래스를 동시에 두 개 이상의 쓰레드가 접근할 때 안전하게 캐시 된 항목을 set/get 할 수 있다는 의미 입니다. 따라서 ExpringDict를 사용할 때 외부에서 Thread-safe 상태에 대한 별도의 구현이 필요 하지 않고 사용 할 수 있습니다.

ChatOps with ExpiringDict

현재 chatops에서는 동일한 사용자가 command를 중복 및 연속해서 입력하는 것을 방지 하기 위해 적용 되어있으며, 사용자의 첫 요청에 대해 다음과 같이 생성 됩니다.

def initCache():
  """TTL=5분의 시간을 가지는 ExpiringDict를 미리 생성
  self.cache = ExpiringDict(max_len=10000, max_age_seconds=300)

def executeCommandByUserId(userid, command):
  """5분이내에 저장된 캐쉬가 없을 경우 msg 수행
  if !self.cache.get(userid) :
    """userid를 key로 command가 value인 캐쉬를 생성
    self.cache[userid] = command;

Conclusion

ExpiringDict는 경량이면서 간한게 사용할 수 있는 꽤 괜찮은 simple cache library이면서, 동시에 단일 쓰레드 및 다중 쓰레드 환경에서 안심하고 사용 할 수 있도록 만들어 져 있습니다. 또한 TTL 설정을 통한 캐쉬 만료 시간을 잘 적용하면 굉장히 유용하게 작은 메모리 내에서 캐쉬 객체를 생성/삭제 할 수 있습니다. Redis/Memcached 처럼 굉장히 큰사이즈를 저장 하기는 힘들지만 작은 메모리의 데이터를 사용 해야 되는 경우 굉장히 유용하게 사용 할 수 있는 패키지임은 틀림 없습니다.

Leave a Comment