基础公共库之本地缓存

1 项目概述

1.1 背景介绍及目标

1.1.1 背景

  • (1) 目前有些接口请求耗时高,而且不是特别稳定,需要重试多次才能获取到结果

  • (2) 存在大量请求集中请求同一接口情况

1.1.2 目标

通过装饰器将某些接口结果缓存

1.2 名词说明

1.3 Roadmap

2 需求分析

2.1 功能需求

  • (1) 通过装饰器将某些接口结果缓存

  • (2) 可以通过接口统计缓存命中率

2.2 非功能需求

2.3 调研

2.3.1 cache_page(django)

cache_page 是 django 自带的装饰器,用起来很方便。

from django.views.decorators.cache import cache_page
@cache_page(24*3600)
def test(request):
    ...

直接在 view 的 function 中添加装饰器 chche_page(过期时间),就可以对该页面进行缓存。

但是这个 cache 函数管杀不管埋,如果缓存了列表页 那么修改 / 创建新的 item 的时候,如何更新列表页的缓存是个大问题。

2.3.2 lru_cache(Python lib)

lru_cache 就是 Python 3.2 开始在 functools 中增加一个函数,通过装饰器的方式来缓存一个函数的执行结果。

https://docs.python.org/3/library/functools.html#functools.lru_cache

@lru_cache
def count_vowels(sentence):
    sentence = sentence.casefold()
    return sum(sentence.count(vowel) for vowel in 'aeiou')

2.3.3 python-diskcache

python-diskcache

DiskCache 是一个纯 Python 的缓存包,基于 SQLite , 文档可访问 http://www.grantjenks.com/docs/diskcache/

DiskCache 可以有效的只用上 G 空间用于缓存,通过利用坚如磐石的数据库和内存映射文件,缓存性能可以匹配并超越行业标准解决方案。

DiskCache 的主要功能如下:

  • 纯 Python 构造

  • 完整的文档

  • 100% 单元测试覆盖

  • 数小时的压力测试

  • Django 兼容的 API

  • 线程安全和进程安全

  • 支持多种缓存算法 (包括 LRU 和 LFU)

  • Keys 支持标签、元数据等

  • 基于 Python 2.7 开发,在 CPython 2.7, 3.4, 3.5, 3.6 和 PyPy 上测试

  • 支持 Linux, Mac OS X 和 Windows

  • 通过 Travis CI 和 AppVeyor CI 的集成测试

2.3.3.1 缓存装饰器

重复使用相同参数的函数调用将在缓存中查找结果,并避免函数求值。

code

import time
import diskcache as dc

cache = dc.Cache()

@cache.memoize(expire=1)
def generate_landing_page():
    print "exe"
    time.sleep(0.2)
    return 3


print generate_landing_page()
print generate_landing_page()
print generate_landing_page()
print generate_landing_page()
print generate_landing_page()
time.sleep(0.5)
print generate_landing_page()
print generate_landing_page()
print generate_landing_page()
print generate_landing_page()
print generate_landing_page()
print generate_landing_page()
time.sleep(0.5)
print generate_landing_page()
print generate_landing_page()
print generate_landing_page()

output, 可以发现多次请求此接口,结果会被缓存

exe
3
3
3
3
3
3
3
3
3
3
3
exe
3
3
3

2.3.3.2 常规使用

场景:启动单独的线程进行更新缓存中数据,其他线程只读数据

from diskcache import Cache
cache = Cache()
cache.set('key', 'value')
cache.get('key')

2.3.3.3 进阶功能

DiskCache 包括一些用于跨线程和跨进程通信的方法:

  • Lock,RLock 和 BoundedSemaphore –– 围绕关键部分进行同步的方法

  • throttle –-函数装饰器,以对函数的调用进行速率限制。

  • barrier –-函数装饰器,用于同步对函数的调用。

  • memoize_stampede–-具有缓存踩踏保护的记忆功能装饰器。

2.3.4 cache.py

如果想既有 lru_cache 这样简单,又暂时不想用 DiskCache 这样的大家伙,但是其文件可以实例化还是不错的一种解决方案,我们还可以尝试用一下 cache.py

出处在这里 https://github.com/bwasti/cache.py

3 总体设计

总体设计重点是设计与折衷

3.1 系统架构

一般来说会有个简单的架构图,并配以文字对架构进行简要说明;

3.2 模块简介

架构图中如果有很多模块,需要对各个模块的功能进行简要介绍;

3.3 设计与折衷

设计与折衷是总体设计中最重要的部分;

3.3.1 如何防止 Cache stampede 问题

Cache stampede 问题又叫做 cache miss storm,是指在高并发场景中,缓存同时失效导致大量请求透过缓存同时访问数据库的问题。

如果同时有多个并发请求,会重复更新缓存,导致重复的写请求。

可以参照【论文】Optimal Probabilistic Cache Stampede Prevention

主要思路是通过在客户端中,通过给每个键的过期时间引入随机因子来避免大量的客户端请求在同一时间检测到缓存过期并向数据库发送读数据请求。

在之前,我们定义键过期的条件为:

Timestamp+ttl > now()

现在我们定义一个 gap 值,表示每个客户端键最大的提前过期时间,并通过随机化将每个客户端的提前过期时间映射到 0 到 gap 之间的一个值,这样一来,新的过期条件为:

Timestamp+ttl +(rand()*gap)> now()

通过这种方式,由于不同客户端请求拿到的键过期时间不一样,在缓存没有被更新的情况下,可以在一定程度上避免同时有很多请求访问数据库。从而导致比较大的系统延时。

另一种更好的方法是将提前过期时间做一个小的更改,通过取随机函数的对数来将每个客户端检查的键提前过期时间更均匀的分布在 0 到 gap 的区间内

(因为随机函数取对数为负值,所以整个提前过期的时间也需要取反),从而获得更好的性能提升

random.random() 方法返回随机生成的一个实数,它在 [0,1) 范围内。

if ttl_ms + (beta * expire_gap * log(random())) > 0:
    return redis.get(key)
        | |
        | |
         V
if (-delta * beta * math.log(random.random())) < ttl:
    return result  # Cache hit.
  • beta (0-1)

  • -delta 也就是 expire_gap ,表示每个客户端键最大的提前过期时间

3.4 潜在风险

4 详细设计

详细设计重点在“详细”

4.1 模块 xx

(有了数据库 + 接口 + 流程,别的同学拿到详设文档,基本也能够搞定了)

4.1.1 交互流程

简要的交互可用文字说明,复杂的交互建议使用流程图,交互图或其他图形进行说明

4.1.2 数据库设计

4.1.3 接口形式

5 使用文档

5.1 使用在非 handler 方法上

5.1.1 使用

对 xxx 函数结果缓存 10s

from xlib.middleware.cache import cache_page

@cache_page(expire=10)
def xxx(args):
    ...

xxx 方法应该是个单一的读方法,可能是以下内容

  • 数据库只读请求封装

  • HTTP 只读请求封装

5.1.2 数据持久化到哪了?

上面例子没有设置存储目录,则服务启动后会存储在 /tmp/ 目录(会随机生成一个目录),使用 lsof -p {PID} 可以看到

lsof -p 21368 | grep cache

python2.7 21368 meetbill  mem-r  REG        8,7     32768    32260 /tmp/diskcache-GKhd3R/cache.db-shm
python2.7 21368 meetbill   16ur  REG        8,7     32768    32258 /tmp/diskcache-GKhd3R/cache.db
python2.7 21368 meetbill   17u   REG        8,7         0    32259 /tmp/diskcache-GKhd3R/cache.db-wal
python2.7 21368 meetbill   18ur  REG        8,7     32768    32260 /tmp/diskcache-GKhd3R/cache.db-shm

5.2 SQLite 使用

5.2.1 查看表结构

打开

$ cd data/diskcache
$ sqlite3

sqlite> .open cache.db

sqlite> .databases

main: /home/meetbill/butterfly/data/diskcache/cache.db

sqlite> .tables

Cache     Settings

sqlite> .schema Cache

CREATE TABLE Cache ( rowid INTEGER PRIMARY KEY, key BLOB, raw INTEGER, store_time REAL, expire_time REAL, access_time REAL, access_count INTEGER DEFAULT 0, tag BLOB, size INTEGER DEFAULT 0, mode INTEGER DEFAULT 0, filename TEXT, value BLOB);
CREATE UNIQUE INDEX Cache_key_raw ON Cache(key, raw);
CREATE INDEX Cache_expire_time ON Cache (expire_time);
CREATE INDEX Cache_store_time ON Cache (store_time);
CREATE TRIGGER Settings_count_insert AFTER INSERT ON Cache FOR EACH ROW BEGIN UPDATE Settings SET value = value + 1 WHERE key = "count"; END;
CREATE TRIGGER Settings_count_delete AFTER DELETE ON Cache FOR EACH ROW BEGIN UPDATE Settings SET value = value - 1 WHERE key = "count"; END;
CREATE TRIGGER Settings_size_insert AFTER INSERT ON Cache FOR EACH ROW BEGIN UPDATE Settings SET value = value + NEW.size WHERE key = "size"; END;
CREATE TRIGGER Settings_size_update AFTER UPDATE ON Cache FOR EACH ROW BEGIN UPDATE Settings SET value = value + NEW.size - OLD.size WHERE key = "size"; END;
CREATE TRIGGER Settings_size_delete AFTER DELETE ON Cache FOR EACH ROW BEGIN UPDATE Settings SET value = value - OLD.size WHERE key = "size"; END;

sqlite> .schema Settings

CREATE TABLE Settings ( key TEXT NOT NULL UNIQUE, value);

5.2.2 查看命中率

sqlite> SELECT * FROM Settings;

statistics|1                                # 是否开启了统计
sqlite_cache_size|8192
sqlite_journal_mode|wal
disk_min_file_size|32768
disk_pickle_protocol|2
cull_limit|10
size_limit|1073741824
tag_index|0
sqlite_mmap_size|67108864
eviction_policy|least-recently-stored
sqlite_synchronous|1
sqlite_auto_vacuum|1
count|1
hits|10                                     # 命中次数
misses|4                                    # 未命中次数
size|0

6 传送门

Last updated