django-redis 패키지 커스텀하기
진행하고 있는 Django 프로젝트에서 redis 를 사용하고 있습니다. 편리한 사용을 위해 python redis 모듈이 아닌 django-redis 패키지를 사용하고 있고 django-redis 패키지는 python redis 모듈의 interface 같은 역할을 수행한다고 보시면 됩니다.
https://github.com/jazzband/django-redis
GitHub - jazzband/django-redis: Full featured redis cache backend for Django.
Full featured redis cache backend for Django. Contribute to jazzband/django-redis development by creating an account on GitHub.
github.com
하지만 간혹 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 사이의 포맷팅 불일치 문제가 발생할 수 있으니 주의해서 사용할 필요가 있습니다.