一 引言
在构建和维护业务服务应用时,大多数情况下业务系统的性能瓶颈往往是在数据库,解决应用到数据库之间瓶颈,系统的性能会得到极大提升。系统的数据库性能优化方法有很多:
从底层到上层有数据库模型设计,SQL优化,使用缓存等等。从图中的优化模式来看,其中数据库模型设计的合理程度奠定了应用系统优化的基石,如果模型设计得不合理,那么统随着业务发展,系统后续的优化困难重重,另一方SQL优化也是数据库优化的一个重要方面,慢SQL和 topSQL往往是系统性能杀手,它们是导致系统故障的重要潜在危险;还有一个就是今天和大家一起探讨的是应用缓存。
缓存在系统的性能优化中有着举足轻重的作用,它比sql优化 所带来的效果更直接,更高效。但是相比其他优化手段,缓存的使用并不是零成本的,任何系统使用缓存,都会遇到两大问题:
第一,数据不一致问题;
第二,系统复杂度大幅度增加;
这给系统的维护和测试增加了一定的成本,所以缓存的设计就显得非常重要了,糟糕的缓存设计可能让系统累赘不堪,而且让系统变得极难以维护。另外对于不同类型的数据,由于实时性,准确性,复杂性的不同,缓存的策略也有些不同。
二 如何设计缓存
其实缓存的设计面临三个问题,缓存哪些数据,怎么缓存,怎么保证数据一致性。
2.1 缓存哪些数据
系统优化时有一句话必须切记:“优化是无止境的”,所以如果缓存不是必须的,请去掉它,要知道越是业务上复杂的系统,对Cache的使用反而越简单,这是因为对于一个复杂、多变、历史悠久的系统,在Cache方面做过度设计会让人深陷其中;缓存的数据越多,系统的维护成本就越高,所以找准需要缓存的点尤为重要。一般情况下,我们只会缓存给系统带来巨大瓶颈的IO操作,在普通应用里尤其指由top SQL 或者慢 SQL 所带来的DAO查询;找准需要优化的sql,你可以找DBA帮忙。
2.2 怎么缓存
首先是缓存存储介质的选择: 你可以直接缓存在JVM内存里 或者app 应用服务器本地的localcache,你也可以采用专门的缓存服务器 如果tair、memcache等; 另外DB、文件其实也可以做缓存,他们一般缓存的事复杂计算的中间结果,这我们一般很少用到;如果你的缓存是存放在 jvm本地,那么通常是用 map 实现,如果缓存数据更新比较频繁且对数据正确性比较高,那么你需要考虑为其添加并发控制和失效策略。还有一点比较重要的就是,在集群环境下想要做到数据一致性,比较困难,主动更新比较麻烦而且达不到其想要的降低数据库等IO操作的效果,所以本地缓存的适用场景一般是在读访问非常高,而写操作极少,对数据一致性要求不是特别高的场景;如果采用专门的缓存服务器则避免了很多麻烦,tair (阿里开发的一套缓存系统),是我们经常使用的缓存中间件,它提供了很好的并发控制和失效机制,另外还提供了不同存储引擎可以供我们选择,mdb,rdb,ldb;普通的缓存可以mdb和rdb,两者分别有memcache和redis的影子,不提供持久化,但其响应时间和高QPS的表现都非常好,如果要确保数据不丢失,那么可以采用ldb引擎存储,它提供了对数据持久化的支持,相反牺牲了一点点性能。
缓存的方式:缓存的方式一般都非常简单,很多时候我们都经常缓存一个方法的执行结果:
public void getData()
{
Object data = null;
data = getDataFromCache();
if(data == null){
data = getDataFromDB();
putDataToCache(data);
}
}
如果缓存的添加比较多,重复的为每个方法都添加一模样的缓存代码非常不方便,我们很容易会想到用AOP去做方法的缓存框架,然后用@注解去为方法添加缓存。其实Spring3.1已经有现成的缓存框架了,使用@Cacheable 注释可以很方便的为某个方法添加缓存(有兴趣的读者可以去查阅资料研究)
@Cacheable
public Object getData(){
return getDataFromDB();
}
当然实际上添加缓存没有那么简单,你还有很多事要去做,比如我们下面要谈到的,怎么保证数据的一致性。
2.3 怎么保证数据一致性
数据的一致性,其实就是缓存与数据源的同步机制,这非常重要,它其实直接确定了你的缓存策略是什么样的,一般缓存的同步模式我把它分为以下几种:
a. Cache Miss Reload
b. Update Then SetCache
c. 补偿机制
d. Reload(Rebuilt)All 等,每种同步机制各有优缺点,在我们的实践中,经常把这几种方法相结合;
【Cache Miss Reload】:最简单,就是上面getData()函数的方式,它的好处是代码较为简洁,数据同步时在数据库CUD的时候,将缓存失效即可,如果业务对数据实时性要求不高,可以直接设置缓存失效时间,而不需要去手动失效它,这可以让代码达到最简的地步;坏处是当缓存失效瞬间,所有的请求都会经过数据库(调用getDataFromDB()函数),可能导致数据库压力过大,导致缓存一直加不上,可能会引发DB故障。
【Update Then SetCache】:顾名思义就是在缓存的数据更新的同时也触发程序更新缓存:
public void updateData(Object data){
boolean success = doUpdateData(data);
if(success){
data = getDataFromDB();
putDataToCache(data);
}
}
这可以在很大程度上避免上述所说的缓存失效雪崩效应;坏处是由于并发的原因,存在极小几率你更新的缓存会导致脏数据进入缓存中,可以采用tair提供的 version 参数避免脏数据进入tair中,不过编码复杂度又上升了。
【补偿机制】有些时候缓存的更新不一定能够成功,也有可能会有脏数据进入缓存,如果要确保数据‘绝对’一致性,我们可以采取适当的补偿机制,如 定时从数据库的值更新到缓存,或者在更新缓存失败时,插入失败日志,定时重新执行缓存更新等
【Reload(Rebuilt)All】 有些时候你会发现上面的所有同步模式都不生效了,因为有些查询对象的写远大于读(可能是因为最初的数据库设计并没有读写分离),那么这是如果一定要缓存它的话,那可能就要以牺牲一部分的数据实时性为代价了,我们一般采用定时程序 Reload或Rebuilt所有的缓存;
三 小结
在系统优化的过程中,缓存的使用也是一门艺术。本文阐述了应用系统使用cache的一般性原则,希望对各位读者在使用缓存方面有一定的帮助。