我们常见的 OLTP 类型的 web 应用,性能瓶颈往往是数据库查询,因为应用服务器层面可以水平扩展,但是数据库是单点的,很难水平扩展,当数据库服务器发生磁盘IO,往往无法有效提高性能,因此如何有效降低数据库查询频率,减轻数据库磁盘IO压力,是web应用性能问题的根源。
OLTP:联机事务处理(on-line transaction processing)
对象缓存是所有缓存技术当中适用场景最广泛的,任何 OLTP 应用,即使实时性要求很高,你也可以使用对象缓存,而且好的ORM实现,对象缓存是完全透明的,完全不需要你的程序代码进行硬编码。
用不用对象缓存,怎么用对象缓存,不是一个简单的代码调优技巧,而是整个应用的架构问题。在你开发一个应用之前,你就要想清楚,这个应用最终的场景是什么?会有多大的用户量和数据量?你将采用什么方式来架构这个应用?
也许你偏好对SQL语句级别的优化,数据库设计当大表有很多冗余字段,会尽量消除大表之间的关联关系,当数据量很大以后,选择分库分表的优化方式,这是目前业界常规做法。但是也可以选择使用 ORM 的对象缓存优化方式:数据库设计避免出现大表,比较多的表关联关系,通过 ORM 以对象化方式操作,利用对象缓存提升性能。
举个例子:
论坛的列表页面,需要显示 topic 的分页列表,topic 作者的名字,topic 最后回复帖子的作者,常规做法:
select ... from topic left join user left join post .....
你需要通过 join user 表来取得 topic 作者的名字,然后你还需要 join post 表取得最后回复的帖子,post 再 join user 表取得最后回贴作者名字。也许你说,我可以设计表冗余,在 topic 里面增加 username,在 post 里面增加username,所以通过大表冗余字段,消除了复杂的表关联:
select ... from topic left join post...
且不说冗余字段的维护问题,现在仍然是两张大表的关联查询。
然后让我们看看ORM怎么做
select * from topic where ... -- 分页条件
就这么一条SQL搞定,比上面的关联查询对数据库的压力小多了。
也许你说,不对阿,作者信息呢?回贴作者信息呢?
这些难道不会发送SQL吗?如果发送SQL,这不就是臭名昭著的n+1条问题吗?
你说的对,最坏情况下,会有很多条SQL:
select * from user where id = topic_id...;
....
select * from user where id = topic_id...;
select * from post where id = last_topic_id...;
....
select * from post where id = last_topic_id...;
select * from user where id = post_id...;
....
select * from user where id = post_id...;
事实上何止 n+1,根本就是 3n+1 条SQL了。
那你怎么还说ORM性能高呢? 因为对象缓存在起作用,你可以观察到后面的 3n 条 SQL 语句全部都是基于主键的单表查询,这 3n条语句在理想状况下(比较繁忙的web网站的热点数据),全部都可以命中缓存。
所以事实上只有一条SQL,就是:
select * from topic where ...--分页条件
这条单表的条件查询和直接使用 join 查询 SQL 通过字段冗余简化过后的大表关联查询相比,当数据量大到一定程度以后对数据库磁盘IO的压力很小,这就是对象缓存的真正威力!
更进一步分析,使用ORM,我们不考虑缓存的情况,那么就是 3n+1 条 SQL。
但是这 3n+1 条 SQL 的执行速度一定比SQL的大表关联查询慢吗?
不一定!因为使用ORM的情况下,第一条SQL是单表的条件查询,在有索引的情况下,速度很快,后面的3n条SQL都是单表的主键查询,在繁忙的数据库系统当中,3n 条 SQL几乎可以全部命中数据库的data buffer。
但是使用SQL的大表关联查询,很可能会造成大范围的表扫描,造成频繁的数据库服务器磁盘IO,性能有可能是非常差的。
因此,即使不使用对象缓存,ORM 的 n+1 条 SQL 性能仍然很有可能超过 SQL 的大表关联查询,而且对数据库磁盘 IO 造成的压力要小很多。
这个结论貌似令人难以置信,但经过我的实践证明,就是事实。前提是数据量和访问量都要比较大,否则看不出来这种效果。
应用场景
是 OLTP 还是 OLAP 应用,即使是 OLTP,也要看访问的频度,一个极少被访问到的缓存等于没有什么效果。一般来说,互联网网站是非常适合缓存应用的场景。
缓存的粒度
毫无疑问,缓存的粒度越小,命中率就越高,对象缓存是目前缓存粒度最小的,因此被命中的几率更高。
举个例子来说吧:你访问当前这个页面,浏览帖子,那么对于 ORM 来说,需要发送 n 条 SQL,取各自帖子 user 的对象。很显然,如果这个 user 在其他帖子里面也跟贴了,那么在访问那个帖子的时候,就可以直接从缓存里面取这个 user 对象了。
架构的设计
架构的设计对于缓存命中率也有至关重要的影响。
例如你应该如何去尽量避免缓存失效的问题,如何尽量提供频繁访问数据的缓存问题,这些都是考验架构师水平的地方。
再举个例子来说,对于论坛,需要记录每个topic的浏览次数,所以每次有人访问这个topic,那么topic表就要update一次,这意味着什么呢?
对于topic的对象缓存是无效的,每次访问都要更新缓存。那么可以想一些办法,例如增加一个中间变量记录点击次数,每累计一定的点击,才更新一次数据库,从而减低缓存失效的频率。
缓存的容量和缓存的有效期
缓存太小,造成频繁的 LRU,也会降低命中率,缓存的有效期太短也会造成缓存命中率下降。
所以缓存命中率问题不能一概而论,一定说命中率很低或者命中率很高。但是如果你对于缓存的掌握很精通,有意识的去调整应用的架构,去分解缓存的粒度,总是会带来很高的命中率的。
文章作者: robbin, 转载自 csdn