h

缓存(cache)

应用

缓存用于保存需要大量计算的操作结果,或者需要快速访问的数据。

比如一个数量庞大的排序,一个耗时的搜索,网站主页内容,等等。

定义

最简单的缓存实现一般只有两个操作,一个是set,另一个是get

更新缓存有两种常用策略:

  1. 手动更新
  2. 自动过期

通常我们可以在数据更新的时候手动更新缓存,比如每当有新数据插入的时候,我们就执行一次set,让新缓存覆盖旧缓存。

另一方面,我们可以用expire操作给缓存设定一个过期时间,当缓存过期时它被自动删除,然后在每次查找数据的时候查看缓存是否存在,如果缓存不存在则重建缓存,并为缓存设定过期时间,然后返回结果;如果缓存存在,则直接取出缓存里的结果并返回。

缓存的删除操作delete通常是可有可无的,其一是因为你可以直接用set覆盖旧缓存,其二是可以让缓存自动过期,所以通常不需要删除缓存,只是偶尔在调试的时候会用上。

实现

缓存可以用Redis的string_structhash_struct来实现。

字符串实现的优点是可以为每个缓存分别设置过期时间,缺点是比哈希表实现占用更多的空间。

而在哈希表实现中,每个哈希表只能共享同一个过期时间(也即是,放在同一个哈希表中的所有缓存会同时过期)。但是你可以利用这一特点为缓存分类,比如你可以将所有排序操作的缓存放到名为sort_cache的哈希表中,而将所有搜索操作的缓存放到名为search_cache的哈希表中,然后分别为sort_cachesearch_cache设置不同的过期时间。

并且哈希表实现比字符串实现更节省空间。

See also

我们通常不对缓存的数量进行限制,如果你需要限制缓存的数量(比如只允许最多100个缓存),请参考日志(log)

如果你需要实现一些复杂的缓存算法,比如Most Recently Used(MRU)Least Recently Used(LRU)请使用sorted_set_struct

关于哈希表比字符串更节约空间的讨论,请参考Redis官方的Memory optimization文档

字符串实现

# file: ./h/cache/string_implement.py

from redis import Redis

def set(name, value, ttl=None, client=Redis()):
    if ttl:
        client.setex(name, value, ttl)
    else:
        client.set(name, value)

def get(name, client=Redis()):
    return client.get(name)

def delete(name, client=Redis()):
    client.delete(name)


# test:
if __name__ == "__main__":

    from time import sleep

    key = 'phone'
    value = '10086'
    expire_time = 3

    set(key, value)
    assert get(key) == value 

    delete(key)
    assert get(key) == None 

    set(key, value, expire_time)
    assert get(key) == value 

    sleep(expire_time * 2)
    assert get(key) == None 

哈希表实现

哈希表实现比字符串实现提供更多功能,因此也相对复杂一些。

我们用category参数给缓存分类,并增加expire操作来设置整个哈希表的过期时间,ttl函数返回哈希表的剩余生存时间,size则返回给定类型的分类缓存的数量。

# file: ./h/cache/hash_implement.py

from redis import Redis

def set(category, name, value, client=Redis()):
    client.hset(category, name, value)

def get(category, name, client=Redis()):
    return client.hget(category, name)

def delete(category, name, client=Redis()):
    client.hdel(category, name)

def expire(category, ttl, client=Redis()):
    client.expire(category, ttl)

def ttl(category, client=Redis()):
    return client.ttl(category)

def size(category, client=Redis()):
    return client.hlen(category)


# test:
if __name__ == "__main__":

    from time import sleep

    category = 'greet'
    key = 'morning'
    value = 'good morning!'
    expire_time = 3

    set(category, key, value)
    assert get(category, key) == value
    assert size(category) == 1

    delete(category, key)
    assert get(category, key) == None
    assert size(category) == 0

    set(category, key,value)
    expire(category, expire_time)
    assert ttl(category) != None

    sleep(expire_time * 2)
    assert get(category, key) == None

实例:用Python装饰器为函数加上缓存

Python中有一个方便好用的特性,就是它的装饰器(decorator)机制,可以无缝地为特定的函数加上新的功能。

我们可以将装饰器、函数和我们的缓存实现三者集合起来,为指定的函数提供方便且通用的缓存机制。

比如现在有一个函数search,这个搜索非常耗时,所以我们想给它加上个缓存,我们利用装饰器cache,为search加上缓存机制。

@cache
def search(key):
    pass

这样,search函数就会在每次执行时查找缓存,如果缓存不命中,就执行一次搜索,将结果保存到缓存并返回。如果搜索命中,则直接返回缓存作为结果。

以下就是cache装饰器的实现方法:

# file: ./h/cache/example.py

from functools import wraps
from string_implement import set, get

def make_unique_id(function, args, kwargs):
    return function.__name__ + repr(args) + repr(kwargs)

def cache(function):
    @wraps(function)
    def _(*args, **kwargs):
        id = make_unique_id(function, *args, **kwargs)
        cache = get(id)

        if cache:
            return cache
        else:
            result = function(*args, **kwargs)
            set(result)
            return result
    return _