缓存是提升系统性能非常有效的手段,常常起到立竿见影的效果,但是有时不恰当的使用不但起不到优化效果,反而可能让系统更慢。下面总结缓存使用过程中常见的一些陷阱。
大家应该比较熟悉数据库查询时的 N+1 问题,在缓存中同样存在 N+1 问题。当应用中出现需要多次读取缓存的时候,虽然单次读取缓存速度很快,但是多次读取缓存累计时间相当可观,很可能会成为一个性能瓶颈。
直接给一个演示例子,生成 10000 个缓存对象 user:<i>:counter
存储整数,然后分别单次,批量读取缓存,统计每种方式消耗时间。
代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
执行结果如下所示:
1 2 3 4 5 6 |
|
从结果中可以看到,分批读取(每次 30~50时)速度很快,要比每次 1 个对象快 100 多倍,每次读取 10 个对象也要快 100 倍,批次并不是越大越好,每次读取 100 个速度比 10 个更慢。
在薄荷生产系统性能优化中,我们遇到过好几次类似的问题。例如有一个 api 需要返回多个存放缓存的用户资料,单个用户资料缓存读取时间接近 1 ms,50 个用户资料消耗接近 45 ms 时间,它导致这个 api 响应时间很长,把 50 次用户资料缓存读取放到一次批量读取后,缓存读取时间减少为 3 ms 左右,应用性能立即大幅提升。
为什么批量读取时间消耗大幅减少呢?因为每一次缓存读取过程有很多固定开销,包括加锁,系统(网络)调用等等,当使用批量读取时,这些固定开销统统节省了,而缓存服务器单次 key 查找和数据返回消耗时间差别不大,所以整体时间大幅减少。
当然,批量读取增加了应用的复杂度,如果应用性能没有问题,或者缓存读取次数很少,并没有必要改造成批量读取形式。
最后一点是,通常我们以 fetch 方法使用缓存对象,这时批量读取方法如下所示:
1 2 3 4 5 |
|
总结:缓存虽然很快,但它毕竟也是一次 IO 操作,同样需要消耗一定时间,如果某一次特别大量读写缓存,很可能会以前性能问题,通过批量读取方式是解决该问题的有效手段。