ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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

     

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

    반응형

    댓글

Designed by Tistory.