Server

django-redis 패키지 커스텀하기

문석2 2023. 2. 10. 19:42
반응형

진행하고 있는 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 사이의 포맷팅 불일치 문제가 발생할 수 있으니 주의해서 사용할 필요가 있습니다.

반응형