使用 Memcached

作者 耿嘉豪 日期 2019-01-18 阅读量
使用 Memcached

使用 Memcached

Memcached 是一个高性能的分布式内存对象缓存系统, 用在动态的 web 应用中减轻数据库负载, 通过在内存中缓存数据来减少读取数据库的次数, 提高 web 应用的速度, 除了数据库的数据, 其它场景的数据也能缓存下来, 比如一些计算量较大的数据。

Memcached 的守护进程由 C 编写, 客户端则可以使用任何语言通过 memcached 的协议与守护进程通信, 这里使用的是豆瓣开源的 libmc

安装 Memcached

~$ sudo apt install memcached  # 安装 Memcached
~$ ps -ef | grep memcached | grep -v grep
memcache 17023 1 0 12:38 ? 00:00:03
/usr/bin/memcached -m 64 -p 11211 -u memcache -l 127.0.0.1 -P
/var/run/memcached/memcached.pid # 安装之后已经自动启动了

使用 Memcached 的分布式去处理缓存

# 现在有 3 个 Memcached 的进程
~$ /usr/bin/memcached -m 64 -p 11212 -u memcache -l 127.0.0.1 -d
~$ /usr/bin/memcached -m 64 -p 11213 -u memcache -l 127.0.0.1 -d
~$ ps -ef|grep memcached
jiangya+ 4182 2605 0 17:22 ? 00:00:00 /usr/bin/memcached -m 64 -p 11212 -u memcache -l 127.0.0.1 -d
jiangya+ 4193 2605 0 17:22 ? 00:00:00 /usr/bin/memcached -m 64 -p 11213 -u memcache -l 127.0.0.1 -d
jiangya+ 4210 3910 0 17:22 pts/0 00:00:00 grep --color=auto memcached
memcache 17023 1 0 12:38 ? 00:00:03 /usr/bin/memcached -m 64 -p 11211 -u memcache -l 127.0.0.1 -P /var/run/memcached/memcached.pid

配置 libmc

# cache.py

from libmc import (
Client,
MC_HASH_MD5,
MC_POLL_TIMEOUT,
MC_CONNECT_TIMEOUT,
MC_RETRY_TIMEOUT,
)

from mc_decorator import create_decorators


mc = Client(
[
'localhost', # 默认, 端口为 112211,
'localhost:11212',
'localhost:11213 mc_213',
],
do_split = True, # 默认为 False, 若为 True, 则小于 10M 的值会被切分为多个块, 但不能存储大于 10M 的值
comp_threshold = 0, # 设置阈值, 如果阈值为 0, 则字符串不会被 zlib 亚索
noreply = False, # 默认值为 False, 如果为 True, 更新缓存的 set 操作可以不需要 memcached 服务器响应
prefix = None, # 缓存值的前缀, 常用于区分不同环境中相同的缓存值
hash_fn = MC_HASH_MD5, # 对键做 hash 的函数的标识, 默认为 MC_HASH_MD5
failover = False # 如果当前服务器不可用, 是否转移到其它的服务器, 默认为 False
)

mc.config(MC_POLL_TIMEOUT, 100) # 执行 memcached 的 set 和 get 操作的超时时间, 单位为毫秒
mc.config(MC_CONNECT_TIMEOUT, 300) # 连接 memcached 的超时时间
mc.config(MC_RETRY_TIMEOUT, 5) # 当 memcached 的服务器不可用等情况不可用, 重试连接的时间, 单位为秒, 这个重试会持续到服务恢复


globals().update(create_decorators(mc)) # 添加到全局命名空间

Cache 的装饰器

import re
import time
import inspect

from functools import wraps


MC_DEFAULT_EXPIRE_IN = 0 # 不过期
__formaters = {}
percent_pattern = re.compile(r'%\w')
brace_pattern = re.compile(r'\{[\w\d\.\[\]_]+]}')


def format(text, *args, **kwargs):
f = __formaters.get(text)
if f is None:
f = formater(text)
__formaters[text] = f
return f(*args, **kwargs)


def formater(text):
'''
把各种需要缓存的参数格式化成一个缓存值
'''
percent = percent_pattern.findall(text)
brace = brace_pattern.search(text)
if percent and brace:
raise Exception('mixed format is not allowed')

if percent:
percent_length = len(percent)
return lambda *args, **kwargs: text % tuple(a[::percent_length])
elif '%(' in text:
return lambda *args, **kwargs: text % kw
else:
return text.format


def gen_key_factory(key_pattern, arg_names, defaults):
args = dict(zip(arg_names[-len(defaults):], defaults)) if defaults else {}
if callable(key_pattern):
names = inspect.getargspec(key_pattern)[0]

def gen_key(*args, **kwargs):
copy_args = args.copy()
copy_args.update(zip(args_names, a))
copy_args.update(kwargs)
if callable(key_pattern):
key = key_pattern(*[copy_args[n] for n in names])
else:
key = format(key_pattern, *[copy_args[n] for n in arg_names], **copy_names)
return key and key.replace(' ', '_'), copy_args
return gen_key


def cache(key_pattern, mc, expire=MC_DEFAULT_EXPIRE_IN, max_retry=0):
def deco(f):
arg_names, var_args, var_kwargs, defaults = inspect.getargspec(f)
if var_args or var_kwargs:
raise Exception("do not support varargs")
gen_key = gen_key_factory(key_pattern, arg_names, defaults)

@wraps(f)
def _(*args, **kwargs):
key, args = gen(*args, **kwargs)
if not key:
return f(*args, **kwargs)
force = kwargs.pop('force', False)
r = mc.get(key) if not force else None

retry = max_retry
while r is None and retry > 0:
if mc.add(key + '#mutex', 1, int(max_retry * 0.1)):
break
time.sleep(0.1)
r = mc.get(key)
retry -= 1

if r is None:
r = f(*args, **kwargs)
if r is not None:
mc.set(key, r, expire)
if max_retry > 0:
mc.delete(key + '#mutex')
return r
_.original_function = f
return _
return deco


def create_decorators(mc):
def _cache(key_pattern, expire=0, mc=mc, max_retry=0):
return cache(key_pattern, mc, expire=expire, max_retry=max_retry)
return {'cache': _cache}

Flask + Flask-sqlalchemy 应用 libmc

import os

from flask import Flask, request
from flask_sqlalchemy import SQLAlchemy

from cache import cache, mc


app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(os.path.dirname(app.root_path), 'test.db')
db = SQLAlchemy(app)
USER_KEY = 'users:{}'


class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True, nullable=False)

@classmethod
def add(cls, name):
user = cls(name=name)
db.session.add(user)
db.session.commit()
mc.delete(USER_KEY.format(user.id))
return user

@classmethod
@cache(USER_KEY.format('{ident}'))
def get(cls, ident):
return super().get(ident)


@app.route('/add', methods=['POST'])
def add():
name = request.form.get('name', None)
if name is None:
return 'name is None!'
user = User.add(name)
return f'User {user.id} created!'


@app.route('/user/<id>')
def get_user(id):
user = User.query.get_or_404(id)
return f'User[{user.id}]: {user.name}'


@app.before_first_request
def before_first_request():
db.drop_all()
db.create_all()


if __name__ == '__main__':
app.run(
host='0.0.0.0',
port=3000,
)

运行

~$ http http://localhost:3000/user/1
HTTP/1.0 404 NOT FOUND
Content-Length: 233
Content-Type: text/html
Date: Fri, 18 Jan 2019 10:07:20 GMT
Server: Werkzeug/0.14.1 Python/3.6.7

~$ http -f POST http://localhost:3000/add name=jiangyang
HTTP/1.0 200 OK
Content-Length: 15
Content-Type: text/html; charset=utf-8
Date: Fri, 18 Jan 2019 10:08:24 GMT
Server: Werkzeug/0.14.1 Python/3.6.7

User 1 created!

~$ http http://localhost:3000/user/1
HTTP/1.0 200 OK
Content-Length: 18
Content-Type: text/html; charset=utf-8
Date: Fri, 18 Jan 2019 10:08:43 GMT
Server: Werkzeug/0.14.1 Python/3.6.7

User[1]: jiangyang

缓存更新策略

  1. 懒惰式加载。客户端先查询 Memcached, 如果命中, 则返回结果; 如果没命中(没有数据或已经过期), 则从数据库中获得最新数据, 并写回到 Memcached 中, 最后返回结果。这种策略在高并发的场景下如果数据突然失效会使数据库的压力骤增。

  2. 主动更新。默认的缓存永不失效, 当有数据需要更新时, 同时也会把最新数据写回到 Memcached 中。这样的更新如果遇到耗时过长的情况应该使用异步, 比如放到消息队列中。这种方案的优点是用户拿到的永远都是缓存, 不会去查询后后端的数据库。Memcached 永不失效是设置超时时间为 0, 当内存不足的时候, 会触发 LRU 机制, 删除最近最久未使用的内存空间。