-
django-redis 패키지 커스텀하기Server 2023. 2. 10. 19:42반응형
진행하고 있는 Django 프로젝트에서 redis 를 사용하고 있습니다. 편리한 사용을 위해 python redis 모듈이 아닌 django-redis 패키지를 사용하고 있고 django-redis 패키지는 python redis 모듈의 interface 같은 역할을 수행한다고 보시면 됩니다.
https://github.com/jazzband/django-redis
하지만 간혹 python 의 redis 라이브러리에서는 지원하는 redis-command 가 django-redis 에는 구현이 되어있지 않았는데요. jazzband 분들이 풀리퀘 응답이 없어 forking 하여 직접 구현해서 사용하기로 결정하였습니다.
먼저 당장 필요했던 command 들 부터 구현해주기로 하였습니다.
-hset
-hget
-hdel
-hgetall
-hincrby
모두 redis 의 자료구조 중 hash set 을 다루는 녀석들입니다.
우선 기존 패키지의 GET 구현을 보겠습니다.
def get( self, key: Any, default=None, version: Optional[int] = None, client: Optional[Redis] = None, ) -> Any: """ Retrieve a value from the cache. Returns decoded value if key is found, the default if not. """ if client is None: client = self.get_client(write=False) key = self.make_key(key, version=version) try: value = client.get(key) except _main_exceptions as e: raise ConnectionInterrupted(connection=client) from e if value is None: return default return self.decode(value) Copyright (c) 2011-2015 Andrey Antukh <niwi@niwi.nz> Copyright (c) 2011 Sean Bleier
SET 구현은 다음과 같습니다.
def set( self, key: Any, value: Any, timeout: Optional[float] = DEFAULT_TIMEOUT, version: Optional[int] = None, client: Optional[Redis] = None, nx: bool = False, xx: bool = False, ) -> bool: """ Persist a value to the cache, and set an optional expiration time. Also supports optional nx parameter. If set to True - will use redis setnx instead of set. """ nkey = self.make_key(key, version=version) nvalue = self.encode(value) if timeout is DEFAULT_TIMEOUT: timeout = self._backend.default_timeout original_client = client tried: List[int] = [] while True: try: if client is None: client, index = self.get_client( write=True, tried=tried, show_index=True ) if timeout is not None: # Convert to milliseconds timeout = int(timeout * 1000) if timeout <= 0: if nx: # Using negative timeouts when nx is True should # not expire (in our case delete) the value if it exists. # Obviously expire not existent value is noop. return not self.has_key(key, version=version, client=client) else: # redis doesn't support negative timeouts in ex flags # so it seems that it's better to just delete the key # than to set it and than expire in a pipeline return bool( self.delete(key, client=client, version=version) ) return bool(client.set(nkey, nvalue, nx=nx, px=timeout, xx=xx)) except _main_exceptions as e: if ( not original_client and not self._replica_read_only and len(tried) < len(self._server) ): tried.append(index) client = None continue raise ConnectionInterrupted(connection=client) from e Copyright (c) 2011-2015 Andrey Antukh <niwi@niwi.nz> Copyright (c) 2011 Sean Bleier
패키지의 make_key method 에서 version 을 사용하여 key 에 prefix 를 붙여서 관리하여주고 있고, 바이너리 데이터를 decode 해서 응답해주고 있습니다.
SET method 는 다소 복잡해 보이는데 timeout 이 0 이하일 경우에서의 nx(set if not exists) 동작에 대한 예외처리를 해주고 있고 replica 노드들을 위해 while 문을 만들어 주었네요. 별다른 특이사항이 없기에 바로 구현을 시작하였습니다.
hget 을 먼저 구현하면 테스트하기가 까다로워지므로 hset 먼저 구현하겠습니다.
django_redis/default.py def hset( self, name: Any, key: Union[str, bytes], value: Union[bytes, float, int, str], version: Optional[int] = None, mapping: Optional[dict] = None, client: Optional[Redis] = None, ) -> bool: original_client = client tried = [] # type: List[int] name = self.make_key(name, version=version) value = self.encode(value) while True: try: if client is None: client, index = self.get_client( write=True, tried=tried, show_index=True ) return bool(client.hset(name, key, value, mapping)) except _main_exceptions as e: if ( not original_client and not self._replica_read_only and len(tried) < len(self._server) ): tried.append(index) client = None continue raise ConnectionInterrupted(connection=client) from e
구현할 코드는 많지 않습니다. python redis client 에 알맞은 데이터들을 보내주기만 하면 됩니다. hset 은 set 과 달리
name : { key : value }
구조이기에 name 인자를 추가로 받습니다. 그리고 패키지 내부의 인터페이스에도 정의해줍니다.
django_redis/cache.py @omit_exception def hset(self, *args, **kwargs): return self.client.hset(*args, **kwargs)
@omit_exception 데코레이터는 기존 패키지에 정의되어 있으며 , 사용자가 settings.py 에 지정한 예외를 ignore 해주는 기능을 담당합니다.
hget 은 더더욱 간단합니다.
def hget( self, name: str, key: str, default=None, version: Optional[int] = None, client: Optional[Redis] = None, ) -> Any: if client is None: client = self.get_client(write=False) name = self.make_key(name, version=version) try: value = client.hget(name, key) except _main_exceptions as e: raise ConnectionInterrupted(connection=client) from e if value is None: return default return self.decode(value)
마찬가지로 인터페이스를 생성해줍니다,
@omit_exception def hget(self, *args, **kwargs): return self.client.hget(*args, **kwargs)
이제 hset, hget 을 구현했으니 테스트를 작성해야합니다.
def test_hset_hget(self, cache: RedisCache): cache.hset("foo", "bar", "baz") cache.hset("foo", "baz", "bar") value_from_cache = cache.hget("foo", "bar") assert value_from_cache == "baz" value_from_cache = cache.hget("foo", "baz") assert value_from_cache == "bar"
foo 라는 hash_set 에 bar, baz 라는 키를 각각 baz,bar 라는 값으로 hset 을 하고 hget 의 응답을 하고 테스트가 성공하였습니다. 비슷한 방법으로 hdel, hgetall, hsetnx 와 같은 redis-command 들을 구현해주었고 모두 잘 작동합니다.
이렇게 별도로 forking 한 패키지를 만들어 사용하기 귀찮고 관리 포인트가 늘어나 귀찮아지는 것이 걱정된다면, 작업하시는 django 프로젝트에서 raw cache client 를 직접 핸들링 하는 방법도 있습니다.
from django_redis import get_redis_connection redis_connection = get_redis_connection("default") redis_connection.{unsupported_redis_command_in_django_redis} example redis_connection.hget('name','key')
상기와 같이 사용하면 raw cache client 에서 정의되어있는 커맨드들은 모두 사용할 수 있기에 django-redis 에 구현되어있지 않아도 됩니다. 다만, django-redis 에서 wrapping 해주는 version, binary decoding 기능을 직접 구현해주어야 하고, raw-client 와 django-redis 사이의 포맷팅 불일치 문제가 발생할 수 있으니 주의해서 사용할 필요가 있습니다.
반응형'Server' 카테고리의 다른 글
Django 버전에 의존하는 Referrer policy 관련 이슈 (0) 2022.06.03 nginx로 악의적인 반복 요청 방지하기 (0) 2022.06.03 Ubuntu 18.04 Django Python 버전 변경하기 (0) 2022.06.02