lolipop,cache and redis

这篇文章之后,lolipop暂时不会更新了。一个原因是临近期末,似乎我也有了不得不复习科目的必要;而另一个原因是,lolipop本身已经有了不错的完成度,虽然有几个相当重要的功能我很想添加上去—-比如@某人的功能,比如注册的时候发一封验证邮件什么的。但是调研了一下,由于初期架构考虑并不周全,这些功能添加起来还是挺麻烦的,会占用不少复习科目的时间,而且会有让代码腐烂的趋势,所以就暂时搁置了。不过对我而言,写lolipop的初衷就是希望熟悉flask,基本上,这个目的是达到了的。顺带还熟悉了一下redis,算是意外之喜。

这次讨论的几个点是:

  1. 对上次那段trick代码的修正

  2. 缓存与缓存陷阱

  3. redis

ps:这篇文章不会太长,请注意。

trick代码修正

如果您已经读过上一篇文章了,您也许会记得(不记得了么……)上篇文章尾巴上讨论的那段trick代码。但是后来在复用的时候,我发现了,当时那段代码只是为user和profile服务的,代码耦合性太高,根本不可能复用,于是我对这段代码进行了更为晦涩的修改。老实说,代码晦涩到这种程度,我已经并不认为有任何值得借鉴的必要了。不过姑且可以看看,一段本身还算清晰的代码,为了向复用靠拢,完成品是多么不堪入目。

我先上代码好了。

def fill_object(items,objects,*args,**kwargs):
    objects_dict = {}
    for o in objects:
       objects_dict[o.id] = o
    for arg in args:
        items = map(lambda o:_add_attr(o,objects_dict,arg,kwargs),items)
    return items

def _add_attr(item,objects_dict,arg,kwargs):
    name = kwargs.get('name','id')
    attributeName = arg
    _object = objects_dict.get(getattr(item,name))
    if arg in item.__dict__.keys():
        arg = kwargs['rename']
    item.__setattr__(arg,getattr(_object,attributeName))
    return item

这是新的fill_object_add_attr,依然不算长。当然为了阅读,我把注释删掉了,虽然我的原注释也很烂。

注意到新引入了一个聚合参数kwargs,之所以用这个,是我希望扩展能力更强,同时更简洁一点。

kwargs现在只是为了保持两个参数 name 和 rename。当fill_object仅仅施用于user和profile的时候,这里没有必要使用这两个参数。因为user.id 就是 profile.id。但是,如果我需要对topic填充user,这时候,topic.user_id 才是 user.id。所以需要指定用来过滤的属性,所以我用name来记录这个属性到底是什么。

rename更麻烦一点。别忘了,_add_attr只是给类强塞进了属性,当然有可能发生重名的情况,这种情况下,后来的属性值会覆盖掉前面的。比如node和topic,都有title这个属性,进行了fill_with_node操作之后,topic的title值会变成node的title值。于是需要对新的属性进行重命名,重命名的名字自己指定就好。当然,当前的方法并不妥当,比如,有可能是多个值重名,所以,上面写法还是有问题。

调用的话,可以参看fill_with_node的调用,相当简单。

def fill_with_node(items):
    uids = set([item.node_id for item in items])
    nodes = Node.query.filter(Node.id.in_(uids)).all()
    return fill_object(items,nodes,'title',name='node_id',rename='node_title')

缓存与缓存陷阱

flask缓存可以用flask-cache这个扩展实现,提供了几种不同的后端。比如使用python的dict作为缓存,redis作为缓存,memcached作为缓存,这些不说了,看看官方文档就好,本来这篇文章就不是讨论这些的。

为什么要使用缓存?

因为需要更好的浏览体验(我看了一下,基本可以提高两个量级的速度),因为可以减轻数据库负担。总之,貌似使用缓存是一个相当划算的买卖,貌似应该多多益善。

真的是这样么?

缓存很容易让人产生使用越多越好这样的幻觉,但是,请注意,缓存滥用会极大地影响增删改的操作。用户无法知道自己的操作是否生效,这远比不用缓存导致闪屏(由于查询数据库较慢,新的界面还没加载,导致白屏)对浏览体验影响更严重。

退化时间是一个必要的方法,但是并不能把清空缓存这个操作全部寄托于缓存退化时间,很多时候,手动清空(cache.clear())这个操作是更必要的。比如,在提交一个新话题的时候,调用cache.clear()清空缓存,用户登录的时候,清空缓存,等等。当然这样必要导致,对数据库的负担加重,一个权衡的思路是可以设置一个较长的退化时间。

用精细的缓存操作代替粗放的操作。

缓存数据库中提取的数据是必要的,但是更应该考虑的是,缓存哪些不容易改变的东西,比如,缓存模板。

flask-cache的缓存清理一直让我不爽—-只能一次性把全部缓存一同删掉。我倒是希望它能提供对键删除的操作……

redis

redis是个很好玩的东西,对pythoner来说,这玩意使用起来和dict相当类似,并且提供了一些实用的概念,比如事务和退化时间。lolipop中,主要依赖redis做了两个功能—-如果不是我时间太紧,甚至有几个功能我都希望能用redis实现。

一个经典是实现是统计在线人数。

考虑一下没有redis怎么实现这个功能。

最笨的方法,是在数据库User表中,增加一个叫做is_active的字段,然后一旦一个用户登录,就把他所对应的那个字段改成True,注销则改成False。当点击查看在线用户的时候,一个in操作就好。

但是这种方法很慢,即使做了缓存,还是很慢,而且每次只对一个用户的一个字段进行更改,太浪费资源。

另一种是我以前考虑过的,在flask里面传入一个set以记录登录的用户id,用户登录即在这个set中add,注销就remove。但是问题是,这个set应该保存在哪里?当然应该是服务器上;那怎么储存呢?鬼才知道。

后面翻文档,发现了基于redis的经典实现。

代码:

def mark_online(user_id):
    now = int(time.time())
    expires = now + ONLINE_LAST_MINUTES * 60 + 10
    all_users_key = 'online-users/%d' % (now // 60)
    user_key = 'user-activity/%s' % user_id
    p = redis.pipeline()
    p.sadd(all_users_key,user_id)
    p.set(user_key,now)
    p.expireat(all_users_key,expires)
    p.expireat(user_key,expires)
    p.execute()

def get_online_users():
    current = int(time.time()) // 60
    minutes = xrange(ONLINE_LAST_MINUTES)
    return redis.sunion(['online-users/%d' % (current-x) for x in minutes])

里面的pipeline一方面指管道,更重要的一点是,它保证了到execute为止,之间的操作是原子操作,不可分割。

make_online对任一分钟活跃的用户id生成一个键值,然后设定一个退化时间(五分钟),储存在redis中。这里活跃的定义叙述上比较困难,实现看着会简单一点。

@app.before_request
def mark_current_user_online():
    if current_user is not None and current_user.is_authenticated():
        mark_online(current_user.id)

这就是活跃的定义,即是,有请求发送。只要有请求的发送,那么,就把这个用户id标记为活跃。

get_online_users对五分钟每一分钟在线的用户取了一个交集,五分钟都在线的,就是当前的在线用户。

相当优美的一个实现,比较精确,速度也很快。

第二个用到了redis的,是关注操作。

我希望,用户能从节点中,选出几个关注的节点,然后放在侧边栏上,作为访问捷径。这个操作能用传统数据库么,哼哼,于是您要做外键了?相当蛋疼不是么。

用redis的话,思路和上面那个实现一样,只不过不需要退化时间,over~~

然后实现的话,可以简单地把函数注册进jinja2的上下文处理器,注册上下文处理器我在前前一篇有说,这里不累述。

@bp.route('/add/<int:node_id>')
def add(node_id):
    if current_user is not None and current_user.is_authenticated():
        user_key = 'user-nodes/%d' % current_user.id
        p = redis.pipeline()
        p.sadd(user_key,node_id)
        p.execute()
    cache.clear()
    return redirect(url_for('node.nodes'))

不需要任何的修改,思路很简洁,速度也快,为什么不用呢,恩哼哼~~~

上面就是今天的内容,其实相当少,感觉很多东西也没有特别说开,就这样吧。

谢谢您阅读到这里!

以上!

为了考试正在努力奋斗的AS。