django model在访问外键时的缓存是内存缓存,

class Post(models.Model):
  category = models.ForeignKey(Category, null=True, default=None)

post = Post.objects.latest()
post.category ## 第一次访问,从数据库读取
post.category ## 第二次访问,从内存直接获取

posts = Post.objects.all()
for post in posts:
  print post.category ## 第一次访问,从数据库获取

上面代码片段中category是post model中定义的一个foreign key,访问时会判断是从数据库读取还是从cache中读取。单个访问时,这种策略是很有效的,但访问列表时却并不一定。

假设每篇post的category都相同,那么访问每一个post对象都去数据库查询就显得没有效率了。django的解决之道是采用prefetch_related,

posts = Post.objects.all().prefetch_related('cateory')
for post in posts:
  print post.category ## 第一次访问,直接从内存获取

prefetch_related会生成一条单独的sql去获取相关联的字段,这样类似这种列表遍历只会生成两条sql,一条用于获取posts,一条用于获取post相关的所有category。这个做法应当可以满足大部分需求场景,不过却也有几个问题,

  • prefetch_related是django 1.4才加入的,对于还在用1.3.x的项目来说就不那么方便。类似的逻辑当然也可以自己实现,就是略麻烦了些。
  • 不能进一步减少数据库查询。两句查询肯定是免不了的,有些时候还是会想着尽量减少数据库操作。

较少数据库操作意味着需要将数据获取尽可能在缓存这一层完成,通过外键去查找对象是非常符合缓存使用场景的。假设已经有一个category_cache实现,那么上述代码就可以变成,

posts = Post.objects.all()
for post in posts:
  category_id = post.category_id
  category = category_cache.get(category_id) ## 从缓存中获取,缓存对象的有效期根据需求进行设定

倘若category_cache中对象有效期足够,且缓存大小足够,那么上述循环几乎不会触及数据库层。减少数据库操作的目的达到了,但上述代码看着却繁琐,

# 用缓存
category_id = post.cagegory_id
category = category_cache.get(category_id)

# 不用缓存
category = post.category

这两种调用方法无疑是用后一种更为便利。因此我们需要进一步考虑简化缓存的使用,一个直观的想法就是拦截model中定义的foreign key字段访问,将django默认的访问逻辑改为从缓存中读取数据。

class Mixin(object):

   def __getattribute__(self, name):
       self_type = type(self)
       attr = self_type.__dict__.get(name)
       # 判断是否是关联域
       if isinstance(attr, ReverseSingleRelatedObjectDescriptor):
           rel_to = attr.field.rel.to
           pk_id = getattr(self, name + '_id')
           return cache_impl.get(rel_to, pk_id)

       return super(Mixin, self).__getattribute__(name)

 # 定义model类时需要将Mixin添加在父类当中
 class Post(Mixin, models.Model):
     pass

在这个简单实现中,cache_impl根据model类型和主键从缓存中读取对象,Mixin负责拦截model对象的属性访问。经过这样修改后post.category总是会先访问缓存。这样修改的优点有,

  • 充分利用外部缓存。避免列表访问中可能出现的低效性能。
  • 外键关联脏数据可控。在django admin中进行的修改可以实时的去刷新缓存,避免外键关联直接访问时候的数据失效。

同时,也罗列下缺点,

  • 增加复杂性。很多地方性能尚不是问题,增加复杂性就是增加出bug的几率。
  • 无法利用内存缓存。django默认会在内存中存储一份外键访问过的对象,按上述修改这个内存缓存是无法利用上了。