簡單封裝Redis作緩存

基於Redis封裝一個簡單的Python緩存模塊html

0. Docker Redis安裝

參考:python

安裝Docker時錯誤sudo yum-config-manager \ --add-repo \ https://download.docker.com/linux/centos/docker-ce.repolinux

由於配置沒改,當時只改了yum的設置,再改次yum-config-manager的 vim /usr/bin/yum-config-manager-> !/usr/bin/python2.7(升級python時可參考CentOS7升級Python至2.7.13版本)git

  • 啓動dockergithub

    sudo systemctl start dockerredis

  • 拉取Redis鏡像docker

    docker pull redisshell

  • 啓動Redisjson

    docker run --name redis-master -p 6379:6379 -d redisbootstrap

  • 查看容器狀況

    docker ps

  • redis-cli 查看

    docker exec -it 097efa63adef redis-cli

1. Python實例化Redis

import redis

class Cache(object):

    def __init__(self):
        pass

    def _redis(self):
        self._redis_pool = redis.ConnectionPool(host='127.0.0.1', port=6379)
        return redis.Redis(connection_pool=self._redis_pool)

a = Cache()._redis()
a.set('a', '1')

a2 = Cache()._redis()
a2.set('b', '1')

a3 = Cache()._redis()
a3.set('c', '1')

for i in a.client_list():
    print(i)

client_list()返回當前鏈接的客戶端列表。

輸出:

{'id': '73', 'addr': '127.0.0.1:54954', 'fd': '10', 'name': '', 'age': '0', 'idle': '0', 'flags': 'N', 'db': '0', 'sub': '0', 'psub': '0', 'multi': '-1', 'qbuf': '0', 'qbuf-free': '32768', 'obl': '0', 'oll': '0', 'omem': '0', 'events': 'r', 'cmd': 'client'}
{'id': '74', 'addr': '127.0.0.1:54955', 'fd': '8', 'name': '', 'age': '0', 'idle': '0', 'flags': 'N', 'db': '0', 'sub': '0', 'psub': '0', 'multi': '-1', 'qbuf': '0', 'qbuf-free': '32768', 'obl': '0', 'oll': '0', 'omem': '0', 'events': 'r', 'cmd': 'set'}
{'id': '75', 'addr': '127.0.0.1:54956', 'fd': '7', 'name': '', 'age': '0', 'idle': '0', 'flags': 'N', 'db': '0', 'sub': '0', 'psub': '0', 'multi': '-1', 'qbuf': '0', 'qbuf-free': '32768', 'obl': '0', 'oll': '0', 'omem': '0', 'events': 'r', 'cmd': 'set'}

屢次鏈接會新開Redis佔用資源,Redis用單例模式,直接新開文件,由於模塊即單例。

將實例化Redis挪到新文件

import redis
_redis_pool = redis.ConnectionPool(host='127.0.0.1', port=6379)
_instance_redis = redis.Redis(connection_pool=_redis_pool)

注:obj.__dict__輸出全部對象時,鏈接池會出錯,最後沒用鏈接池:_instance_redis = redis.Redis(host='127.0.0.1', port=6379)

使用只須要引入,並在_redis()中返回實例便可

from Instance import _instance_redis


class Cache(object):

    def __init__(self):
        pass

    def _redis(self):
        return _instance_redis
        
a = Cache()._redis()
a.set('a', '1')

a2 = Cache()._redis()
a2.set('b', '1')

a3 = Cache()._redis()
a3.set('c', '1')

for i in a.client_list():
    print(i)

輸出:

{'id': '76', 'addr': '127.0.0.1:55070', 'fd': '11', 'name': '', 'age': '0', 'idle': '0', 'flags': 'N', 'db': '0', 'sub': '0', 'psub': '0', 'multi': '-1', 'qbuf': '0', 'qbuf-free': '32768', 'obl': '0', 'oll': '0', 'omem': '0', 'events': 'r', 'cmd': 'client'}

2. 生成Key

由於key手動指定,可能會指定一個已存在的Key,會覆蓋其餘記錄。緩存須要隨機Key。用uuid1生成隨機ID。

import uuid
class Cache(object):
    def key(self):
        return uuid.uuid1()

可是這樣用到緩存中是不行的,要作到相同的操做下獲得的結果是相同的。因此能夠將相同的操做這個動做轉化成一個key存起來,下次再有此操做則使用緩存。好比get_all_store(status=1)函數在status==1時獲取全部開業門店,status==2時獲取全部打烊門店,就可有兩個key分別作緩存。因此須要獲得調用堆棧和最後的函數參數,分別哈希拼接起來便可作Key。(僅僅拿最後函數是不夠的,以前的調用也須要拿到。由於函數可能不只僅由於參數不同數據結果就不同,但調用堆棧相同狀況下返回值應該是相同的。 好比同名函數和同名參數請求可能會重複。因此得有堆棧前綴。)

使用Python記錄詳細調用堆棧日誌的方法的代碼,獲取到調用堆棧,單文件測試

F:\py\RedisCache> python .\Cache.py
Cache.py(<module>:103)->Cache.py(key:42)-> test

還算正常,從103行調用key()函數中(42行)的堆棧函數,參數爲test

從其餘文件引入Cache就壞了:

F:\py\RedisCache> python .\TestCache.py
TestCache.py(<module>:1)-><frozen importlib._bootstrap>(_find_and_load:983)-><frozen importlib._bootstrap>(_find_and_load_unlocked:967)-><frozen importlib._bootstrap>(_load_unlocked:677)-><frozen importlib._bootstrap_external>(exec_module:728)-><frozen importlib._bootstrap>(_call_with_frames_removed:219)->Cache.py(<module>:103)->Cache.py(key:42)-> test
TestCache.py(<module>:12)->Cache.py(key:42)-> test

輸出了兩次,第一次多了好多forzen importlib 開頭的模塊,第二次爲想要的結果。

The module was found as a frozen module. imp.PY_FROZEN

後來發現第一次輸出那麼可能是由於TestCache調用Cache,但Cache文件中也有調用自身,加了__name__=='__main__'就行了。

修復一下,將每一個過程拼接或者哈希拼接,而後:分隔。若是級別較多,只取三條便可,[-1] [-2]和以前的。等調用小節時候再說。

3. 基本操做

def set(self, key=None, value=None):
    """ 設置緩存 """
    if not key:
        key = str(self.key())
    self._redis.set(key, value, self._ttl)

def get(self, key):
    """ 獲取緩存 """
    return self._redis.get(key)

def delete(self, key):
        """ 刪除緩存 """
        return self._redis.delete(key)
def remove(self, pattern):
    """ 批量刪除緩存 """
    del_count = 0
    keys = self._redis.keys(pattern)
    for key in keys:
        if self.delete(key):
            del_count += 1
    return del_count

_ttl爲在__init__()的設置過時時間,默認7200。其餘屬性還有:_redis()函數去掉,用_redis屬性存儲便可;_update_delete請日後看。

def __init__(self):
    # redis實例
    self._redis = _instance_redis
    # 過時時間
    self._ttl = 7200
    # 更新標誌
    self._update = False
    # 刪除標誌
    self._delete = False

那麼,相應的就有設置屬性操做:

def set_attr(self, **attr):
    """ 設置屬性 """
    allows = ['update', 'delete', 'ttl']
    for k in attr:
        if k in allows:
            name = str("_"+k)
            setattr(self, name, attr[k])

設置屬性示例:a1 用默認屬性,a2用設置後的屬性

c = Cache()
c.set("a1", 1)
print(c.__dict__)
c.set_attr(update=True, ttl=600).set('a2', 2)
print(c.__dict__)

查看:

# 程序輸出:
{'_redis': Redis<ConnectionPool<Connection<host=127.0.0.1,port=6379,db=0>>>, '_ttl': 7200, '_update': False, '_delete': False}
{'_redis': Redis<ConnectionPool<Connection<host=127.0.0.1,port=6379,db=0>>>, '_ttl': 600, '_update': True, '_delete': False}

# 查看redis客戶端中a一、a2鍵當前的剩餘時間
127.0.0.1:6379> ttl a1
(integer) 7187
127.0.0.1:6379> ttl a2
(integer) 585

4. 定義調用緩存方法

初版定義長這樣:

def call(self, func):
    """ 調用緩存方法 """
    key = self.key()
    cache = self.get(key)

    # 刪除緩存
    if self._delete:
        self._delete = True
        return self.delete(key)

    # 更新緩存
    if not cache or self._update:
        self._update = False
        data = func()
        value = json.dumps(data)

        if self.set(key, value):
            return data
        return False

    return json.loads(cache)

當設置更新_update或者無緩存時,執行函數更新緩存。

使用:

from Cache import Cache


class StoreCache(Cache):
    """ 門店緩存類 """
    def all_data(self, store_status):
        """ 獲取數據 """
        def _(status=store_status):
            print(f'func args status =  {status}')
            return [1, 2, 3, 4, 5]
        return super().call(_)


if __name__ == '__main__':
    s = StoreCache()
    data = s.all_data(5)
    print(data)

這樣一看都是PHP的思想,用匿名函數作參數,傳過去,須要定義緩存的時候執行函數。在Python用的時候,這樣有弊端,函數參數得有默認值,最後生成Key獲取參數也不方便。因此,改用裝飾器。

def __init__(self, func=None):
    # 獲取緩存函數
    self._cache_func = func

def __call__(self, *args, **kwargs):
    """ 調用緩存方法 """

    # 存儲函數參數
    self._cache_func.args = args
    self._cache_func.kwargs = kwargs

    # 獲取key,取緩存
    key = self.key()
    cache = self.get(key)

    # 刪除緩存
    if self._delete:
        self._delete = True
        return self.delete(key)

    # 更新緩存
    if not cache or self._update:
        self._update = False
        data = self._cache_func(*args, **kwargs)
        value = json.dumps(data)

        if self.set(key, value):
            return data
        return False

    return json.loads(cache)

生成Key優化

生成Key時過濾前兩次,第一次爲key函數自己,第二次爲__call__調用函數,均可忽略。

'Cache.py(key:35)', 'Cache.py(__call__:91)',
f = sys._getframe()
f = f.f_back    # 第一次是key函數自身,忽略
f = f.f_back    # 第二次是Cache文件的__call__,忽略

等價於f = sys._getframe(2)

Key最終生成規則:

使用有效的(除了最近兩次緩存文件自身調用(key()__call__())以及緩存函數之外)調用堆棧字符串(細化到函數名),堆棧哈希值(粒度細化到函數不須要堆棧哈希值或者說不須要細化到行號),緩存函數,參數哈希值(*args, **kwargs):

有效堆棧字符串:緩存函數:參數哈希值

形如:

TestCache:func:xxxxxxxxxxxxxxxxxxxxx

5. 示例

5.1 函數緩存示例

新建文件TestCache.py

使用Cache裝飾了all_data()方法,此方法下次使用會生成緩存。

from Cache import Cache


@Cache
def all_data(status):
    """ 緩存數據緩存 """
    print(f"this is all data, args={status}")
    return list(range(status)) # range(status)用來生成模擬數據

class TestC(object):
    def get(self):
        t1 = all_data(10)
        return t1


if __name__ == '__main__':
    a1 = all_data(10)
    print(a1)
    a2 = all_data(10)
    print(a2)
    a3 = all_data(1)
    print(a3)
    a4 = TestC().get()
    print(a4)

輸出結果:a1首先寫緩存,進入了函數,a2a1Key相同,使用a1緩存。

a3參數不一樣,生成的Key也不一樣,寫緩存。a4調用棧不一樣因此Key不一樣,寫緩存。

F:\py\RedisCache> python .\TestCache.py
key:TestCache:<module>:all_data:A66EEA7FC6E56FCAA27D26DF40CE5F2C
this is all data, args=10
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
key:TestCache:<module>:all_data:A66EEA7FC6E56FCAA27D26DF40CE5F2C
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
key:TestCache:<module>:all_data:B9E5D26E4217C1CB496844E233F59E17
this is all data, args=1
[0]
key:TestCache:<module>:TestCache:get:all_data:A66EEA7FC6E56FCAA27D26DF40CE5F2C
this is all data, args=10
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

redis 輸出

127.0.0.1:6379> keys *
1) "TestCache:<module>:all_data:A66EEA7FC6E56FCAA27D26DF40CE5F2C"
2) "TestCache:<module>:all_data:B9E5D26E4217C1CB496844E233F59E17"
3) "TestCache:<module>:TestCache:get:all_data:A66EEA7FC6E56FCAA27D26DF40CE5F2C"

如此,就能夠裝飾函數使用緩存了。

注:若是Redis的Key形如

2) "ptvsd_launcher:<module>:__main__:main:__main__:handle_args:_local:debug_main:_local:run_file:_local:_run:pydevd:main:pydevd:run:pydevd:_exec:_pydev_execfile:execfile:TestCache:<module>:all_data:A66EEA7FC6E56FCAA27D26DF40CE5F2C"
3) "ptvsd_launcher:<module>:__main__:main:__main__:handle_args:_local:debug_main:_local:run_file:_local:_run:pydevd:main:pydevd:run:pydevd:_exec:_pydev_execfile:execfile:TestCache:<module>:all_data:B9E5D26E4217C1CB496844E233F59E17"

多了不少莫名其妙的調用,那是由於VsCode調試模式使用了ptvsd模塊。

6. 待優化

如今又有問題了:

  1. 如今直接用裝飾器了,沒有繼承Cache類,如何設置過時時間或者標誌位。
  2. 怎麼修飾類呢?形如這樣的:
class Manager(object):
    @Cache
    def search_manager(self, district=1):
        print("into search manager func.")
        return list(range(district))

下週好好研究下裝飾器再優化吧,別忘了弄這個緩存是爲了給公衆號添加功能的工具。

6.1 指定Key

發現有個必要的問題還得改:指定Key。否則Token沒辦法存,多個地方調用的不同,必須有惟一Key。

須要指定Key的話,裝飾器就要這麼寫了:

def __init__(self, key=None):
    # 指定key
    self._key = key
    # 緩存函數
    self._cache_func = None
    # redis實例
    self._redis = _instance_redis
    # 過時時間
    self._ttl = 7200
    # 更新標誌
    self._update = False
    # 刪除標誌
    self._delete = False
    
def __call__(self, func):
    """ 調用緩存 """
    self._cache_func = func

    def wrapper(*args, **kwargs):
        # 存儲函數參數
        self._cache_func.args = args
        self._cache_func.kwargs = kwargs

        # 獲取key,獲取緩存
        key = self.key()
        cache = self.get(key)

        # 刪除緩存
        if self._delete:
            self._delete = True
            return self.delete(key)

        # 更新緩存
        if not cache or self._update:
            self._update = False
            data = func(*args, **kwargs)
            value = json.dumps(data)

            if self.set(key, value):
                return data
            return False

        return json.loads(cache)
    return wrapper

def key(self):
    """ 生成Key """

    if self._key:
        """ 使用指定Key """
        key = self._key
        logging.debug("key: %s" % key)
        return key
    ......

調用時指定all_data_key函數有惟一Key:

from Cache import Cache


@Cache()
def all_data(status):
    """ 緩存數據 """
    print(f"this is all data, args={status}")
    return list(range(status))


class TestC(object):
    """ 類中調用查看堆棧不一樣 """
    def get(self):
        t1 = all_data(10)
        return t1


@Cache(key='X0011')
def all_data_key(status):
    """ 指定Key的緩存數據 """
    print(f"this is all data (use key), args={status}")
    return list(range(status))


if __name__ == '__main__':
    a1 = all_data(10)
    print(a1)
    a2 = all_data(10)
    print(a2)
    a3 = all_data(1)
    print(a3)
    a4 = TestC().get()
    print(a4)
    print("use key: -------------------------- ")
    a5 = all_data_key(4)
    print(a5)
    a6 = all_data_key(5)
    print(a6)

輸出:

DEBUG:root:key:TestCache:<module>:all_data:A66EEA7FC6E56FCAA27D26DF40CE5F2C
this is all data, args=10
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
DEBUG:root:key:TestCache:<module>:all_data:A66EEA7FC6E56FCAA27D26DF40CE5F2C
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
DEBUG:root:key:TestCache:<module>:all_data:B9E5D26E4217C1CB496844E233F59E17
this is all data, args=1
[0]
DEBUG:root:key:TestCache:<module>:TestCache:get:all_data:A66EEA7FC6E56FCAA27D26DF40CE5F2C
this is all data, args=10
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
use key: --------------------------
DEBUG:root:key: X0011
this is all data (use key), args=4
[0, 1, 2, 3]
DEBUG:root:key: X0011
[0, 1, 2, 3]

# redis查看
1) "TestCache:<module>:all_data:A66EEA7FC6E56FCAA27D26DF40CE5F2C"
2) "TestCache:<module>:all_data:B9E5D26E4217C1CB496844E233F59E17"
3) "X0011"
4) "TestCache:<module>:TestCache:get:all_data:A66EEA7FC6E56FCAA27D26DF40CE5F2C"

能夠看到all_data_key(4)all_data_key(5)函數使用相同的Key X0011因此第二次函數未執行。 不受參數所限制,指定後到過時前就不能更換了,因此適用惟一的函數。固然了,根據業務權衡,是要緩存仍是要Key。能知足各自的需求就能夠了。不知足本身改規則。

6.2 優化類函數緩存

改了上面的裝飾器以後,用

class Manager():
    @Cache()
    def search_manager(self, district=1):
        print("into search manager func.")
        return list(range(district))

能夠跑通,可是key不惟一,由於傳給__call__()args參數形如(<__main__.Manager object at 0x030FDA50>, 3)因此每次都不同。前面那一串<__main__.Manager object at 0x030FDA50>就是Manager實例化的self。過濾掉就能夠了。

第一次嘗試

args_temp = self._cache_func.args
        if isinstance(args_temp[0], object):
            args_temp = args_temp[1:]

這樣能夠,可是萬一人第一個參數原本就是對象呢,豈不誤刪了。

再次嘗試

不只判斷object,還要判斷該函數是否屬於self的類函數:

print(self._cache_func, type(self._cache_func))
        print(self._cache_func.args[0], type(self._cache_func.args[0]))
# 分別輸出:
<function Manager.search_manager at 0x03062F60> <class 'function'>
<__main__.Manager object at 0x03321050> <class '__main__.Manager'>

看來直接if isinstance(self._cache_func, type(self._cache_func.args[0])):也是不行的,一個是函數對象,一個是類對象,統一拿到類名再比較:

# 過濾參數中的self對象
args_temp = self._cache_func.args
# 拿到類函數對象的類名和類對象的類名
func_class_name = os.path.splitext(self._cache_func.__qualname__)[0]
obj_class_name = self._cache_func.args[0].__class__.__name__
if isinstance(args_temp[0], object) and func_class_name == obj_class_name:
    args_temp = args_temp[1:]

測試一波:

""" 類函數緩存測試 """
from Cache import Cache


class Manager():
    """ 測試類函數緩存 """
    @Cache()
    def search_manager(self, district=1):
        print("into search manager func.")
        return list(range(district))


@Cache()
def search_manager(obj, district=1):
    """ 測試對象過濾 """
    print("into search manager func.")
    return list(range(district))


if __name__ == '__main__':
    a1 = Manager().search_manager()
    print(a1)
    a2 = Manager().search_manager(2)
    print(a2)
    a3 = Manager().search_manager(2)
    print(a3)
    print("test object: ---------------")
    m1 = Manager()
    m2 = Manager()
    b1 = search_manager(m1, 2)
    b2 = search_manager(m1, 2)
    b3 = search_manager(m2, 2)

輸出

DEBUG:root:key:TestClassCache:<module>:search_manager:E517FB10ADC90F5B727C5D734FD63EBC
into search manager func.
[0]
DEBUG:root:key:TestClassCache:<module>:search_manager:F575A889334789CA315DF7C855F33BEC
into search manager func.
[0, 1]
DEBUG:root:key:TestClassCache:<module>:search_manager:F575A889334789CA315DF7C855F33BEC
[0, 1]
test object: ---------------
DEBUG:root:key:TestClassCache:<module>:search_manager:933A83EC830CD986E4CA81EA3A9A260C
into search manager func.
DEBUG:root:key:TestClassCache:<module>:search_manager:933A83EC830CD986E4CA81EA3A9A260C
DEBUG:root:key:TestClassCache:<module>:search_manager:4183D446C799065E0DAD7FCC47D934C3
into search manager func.

最後的b1 b2同使用m1對象,b3使用m2對象,能夠觀察出來沒有過濾此對象。

6.3 設置屬性

不一樣的調用都應該能夠設置所調用函數的生存時間,以及是否強制更新/刪除。

失敗嘗試1:裝飾器參數

開始想直接帶參數便可。

在裝飾時候加參數:像以前的key參數同樣

@Cache(key='A001', ttl=100, update=True)
def attack_wait():
   pass

在Cache初始化中設置:

class Cache(object):

    def __init__(self, *, key=None, **attr):
        pass
        # 設置屬性
        if attr:
            self.set_attr(**attr)

這樣試了一下,不能夠。

由於裝飾器@Cache(xxxx)在代碼運行到這裏就會執行__init__,因此設置了三次在調用以前就會初始化裝飾器

attr:
{}
attr:
{'ttl': 100}
attr:
{'ttl': 100, 'update': True}

達不到每次函數在多個地方不一樣的需求。

直接在函數參數裏作手腳吧:

失敗嘗試2:緩存函數參數

直接在緩存裏帶約定好的參數

attack_start(cache_attr={'ttl':100, 'update':True})

還要在緩存函數中增長參數:

@Cache()
def attack_start(*, cache_attr=None):

在Cache中修改:

def __call__(self, func):
        """ 調用緩存 """
        self._cache_func = func

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if 'cache_attr' in kwargs:
                print(kwargs['cache_attr'])
                self.set_attr(**kwargs['cache_attr'])

能夠完成可是太蠢了。

有沒有不傳參並且接觸不到原對象但能在運行時影響原對象屬性的?好像作不到。

可自定義屬性的裝飾器

直到我找到了9.5 可自定義屬性的裝飾器

你想寫一個裝飾器來包裝一個函數,而且容許用戶提供參數在運行時控制裝飾器行爲。

添加裝飾器

def attach_wrapper(obj, func=None):
    if func is None:
        return partial(attach_wrapper, obj)
    setattr(obj, func.__name__, func)
    return func

__call__中就能夠用函數了,由於以前有寫set_attr()方法,直接調用一次就行了,精簡後:

@attach_wrapper(wrapper)
        def set_attr(**attr):
            self.set_attr(**attr)

這樣就行了。測試一下:

@Cache()
def attack_start(mul=1):
    print("戰鬥開始......")
    return 'attack ' * mul


if __name__ == "__main__":
    attack_start.set_attr(update=True, ttl=200)
    a1 = attack_start(3)
    print(a1)
    attack_start.set_attr(update=True, ttl=100)
    a2 = attack_start(3)
    print(a2)
    attack_start.set_attr(delete=True)
    a3 = attack_start(3)
    print(a3)

輸出:

DEBUG:root:key:TestCacheAttr:<module>:attack_start:CFDA50BC76FD8A05597004C3B00E927E
DEBUG:root:cache attr:{'_key': None, '_cache_func': <function attack_start at 0x038B07C8>, '_redis': Redis<ConnectionPool<Connection<host=127.0.0.1,port=6379,db=0>>>, '_ttl': 200, '_update': True, '_delete': False, '_attr': {}}
戰鬥開始......
DEBUG:root:redis set: TestCacheAttr:<module>:attack_start:CFDA50BC76FD8A05597004C3B00E927E:"attack attack attack ",(200s)
attack attack attack
DEBUG:root:key:TestCacheAttr:<module>:attack_start:CFDA50BC76FD8A05597004C3B00E927E
DEBUG:root:cache attr:{'_key': None, '_cache_func': <function attack_start at 0x038B07C8>, '_redis': Redis<ConnectionPool<Connection<host=127.0.0.1,port=6379,db=0>>>, '_ttl': 100, '_update': True, '_delete': False, '_attr': {}}
戰鬥開始......
DEBUG:root:redis set: TestCacheAttr:<module>:attack_start:CFDA50BC76FD8A05597004C3B00E927E:"attack attack attack ",(100s)
attack attack attack
DEBUG:root:key:TestCacheAttr:<module>:attack_start:CFDA50BC76FD8A05597004C3B00E927E
1

設置三次,前兩次強制更新並設置ttl,因此前一二次雖然Key相同也能夠更新,從ttl能夠看出來200->100。第三次刪除,查看Redis列表爲空。

改了功能還不影響之前的代碼邏輯,這就是裝飾器的妙處吧。如此簡單和圓滿,美滋滋。

完整代碼GitHub

其餘參考

相關文章
相關標籤/搜索