一次糟糕的缓存的使用与代码的重构

这周被 Vincent 叫去说了一下我之前写过的代码中有几处很糟的地方,这处代码的功能是实现用户添加食物的收藏。

我的实现

把用户收藏的食物作为 User model 的一个 instance method,调用的时候使用 user.favorite_foods。favorite foods 内部是这个 user 收藏的食物的 code 的一个 list 的缓存。我们需要用到的功能有:

  • 添加收藏
  • 取消收藏
  • 检查是否收藏

这种实现的两个很明显的问题

  • 缓存的滥用
  • 代码不 OO

当一个用户的收藏量到一定规模 (不需要很多) 的时候。比如收藏了 500 条的时候,我们每次操作这个缓存就会耗时很久。比如一个检查一个用户是否收藏了某个事物,首先需要从缓存中取出这个用户收藏的 list,然后再将它转成一个 Ruby 的对象,接着再在其中寻找是否存在这个食物的 code。这就导致了整个查询的速度很慢,甚至不如直接在数据库中使用一个 where 查询。在 New Relic 中看日志,最慢的时候甚至到了 6000+ ms,虽然只是一个简单的判断是否收藏的操作,不仅增加了服务器的负担,也大大的降低了用户体验

改进之后

代码不够 OO

用户收藏食物纪录是有一个 FavroiteFoodRecord 的 model 的,对于用户收藏食物的逻辑,自然应该放在这个 model 中,而不是所有的逻辑都塞到 User 中。将逻辑移到 FavoriteFoodRecord 中后,代码也变的清晰了,只需要定义一个 exist_record?(food_code, user_id) 的类方法,就可以判断用户是否收藏了某个食物。

缓存的正确使用

因为判断失误是否收藏是一个经常用到的查询,我们要对它进行缓存。但是一定不能像我之前那种粗暴的缓存的方式,我认为缓存一定要做到

  • 缓存的内容要尽量的简单,之前的代码中缓存维护一个可能很庞大的数组显然不符合这个
  • 即插即用,即使做不到尽量简单,这点也是必须的,比如我们缓存一些 HTML 片段,虽然大,但是可以直接供我们使用

因此这里我们将用户是否缓存了某个食物的结果直接缓存起来,这样当查询过来的时候,直接可以返回结果。代码部署之后,New Relic 的结果直线下降,响应只有 1 ms 左右(汗颜 T||T)

总结

可以说这次的代码中犯了很多低级的错误,所以以后在写代码之前一定要多一些思考,而不是急着去实现功能。