通常来说,分布式系统中最耗性能的位置在数据库存储部分,虽然我们有许多策略来提高存储的性能,但是由于复杂的业务场景以及快速增长的业务请求,单纯的存储优化难以满足要求。
数据库的DML操作通常不会出现性能问题,除非索引过多,或者数据量过大。
出现性能问题的大多是在select,一方面select的场景非常复杂,它拥有像join、group、order、like、count这样丰富的语义,这些语义非常消耗性能;另一方面大多数的业务都是读多写少的场景,比如发布了一个商品,它的详情信息可能有几万人浏览,只需要insert一次,但是每个用户浏览都会select一次,加剧了慢查询的问题。
缓存的出现即是为了弥补存储系统在复杂业务场景下的不足,它一方面解决了在对应的DB数据没有被改变情况下,多次查询的问题,一方面为前端的API提供了容错,比如网络出现问题时,前端展示缓存内的内容经常要比没有内容展示要好。
缓存是提升性能的优秀方式,单机mysql的qps极限大致在2000以内,而memcached的qps查询性能能够达到50000以上,而事实上,Mysql5.6以后加入了InnoDB memcached Plugin,国内有团队已经对memcached plugin进行过实践,在测试中QPS达到了7万/秒。
下面来说说缓存层的实现方式
Cache Aside 模式
Cache-Aside Pattern是比较常见的一种缓存更新方式,其具体的逻辑如下图所示。
-
查找。Cache-Aside 模式当你需要从DB查询数据时,首先检查缓存中是否存在你需要的数据,如果有则立即返回,如果没有则从DB中查询,并在返回查询结果的过程中将数据放入缓存中。
update.png -
更新。 当你需要向数据库中更新数据时,必须使与你更新的数据相关的缓存中的数据全部失效。
必须注意的是,写完数据后的操作是使缓存失效而不是更新缓存,主要是防止并发写入操作导致脏数据的产生。
Cache Aside有一个经常被问到的并发问题(我在面试时就有被问到过),即当一个读操作,从数据库中读取时,正准备写缓存,此时来了一个写操作,让缓存失效,那么之前的读操作就有可能把旧的数据写入缓存,造成脏数据。
不过实际上上面的案例出现的概率比较低,因为这个条件需要读缓存的时候缓存失效,而且有一个并发写操作,由于写操作需要加锁,且速度比读操作要慢很多,因此读操作必须在写操作之前进行,而读后的更新缓存又必须晚于写操作,因此所有条件都满足的概率并不大。
Read-through/Write-through 模式
read-through.jpg在这种模式下,应用将缓存和DB看做一个整体。在读操作时会更新缓存,当缓存失效时,由缓存服务自己把数据加载回缓存,对应用方是透明的。
write-through.jpg
而在更新数据时,采用write through的方式,如果没有命中缓存,则直接更新数据库然后返回,如果命中了缓存,则更新缓存,由Cache层自己去更新数据库。
采用这种方式使用缓存并不适合所有的业务长场景,它比较适合那些在数据库中读取单行数据或者读取可映射到单个缓存项的数据的情况。
Write Behind 模式
wirte behind模式就是在更新数据的时候,只更新缓存,不更新数据库,缓存会异步地批量更新数据库。这样的操作带来了很高的性能提升,但是同时也导致了数据不一致性的问题,比如在数据还有没有写入数据库的时候发生了宕机,那么重启后数据库的数据就和真实的数据对不上来了,因此在采用这种方案的时候,一定要考虑业务对数据一致性的容忍度。软件的设计从来都是trade-off。
另外write behind的实现逻辑更复杂,它需要跟踪记录哪些数据时被更新了的,需要刷到持久层上的。操作系统也有类似的write back算法,其page cache仅在cache需要失效的时候,才会真正的持久起来,比如内存不足进程退出等情况。
write-behind.png
缓存设计
在总结了常见的缓存设计模式后,下面讲讲缓存架构设计需要注意的地方。我们需要考虑以下几个问题:
-
什么数据应该缓存?
缓存是为了减少DB压力而设置的,通常使用于读多写少的场景,另外既然允许数据缓存,那么在你是可以接受在一定时间区间内的数据不一致性的,即是说即使页面数据和真实数据有一定的不一致,对于用户来说,也是要能接受的。因此比如像证券的价格信息,买卖的委托就不宜缓存,而购物的详情信息,剩余数量信息就属于可以缓存的。 -
什么时机触发缓存和以及触发方式是什么?
触发缓存的时机这个很难把控,需要根据实际的业务需求来设置和调整。缓存触发的方式多样,首先是有前面说过的cache aside,read/write through,write behind模式,也有单独开启一个线程只针对某些热点内容来更新缓存,对于非热点内容让其直接读取数据库的方式,是否是热点需要进行计数统计,到达一定阈值就将其标注为热点内容。这种策略可以防止加载过多的内容在缓存当中(比如有的爬虫进行全站爬取)也可以在业务刚上线的时候进行缓存预热。 -
缓存雪崩
缓存雪崩是指缓存集中在某一时期内失效(过期)后引起系统性能急剧下降的情况。缓存过期失效后,需要重新生成缓存,对于高并发系统,生成缓存的过程中可能有几百上千个请求,而他们并不知道还有别的请求在生成缓存,因此都直接访问DB并生成缓存,从而对DB产生较大压力。
缓存雪崩没有完美的解决方案,但可以减轻其影响,首先就是生成缓存时,让缓存失效的时间不要集中,尽量均匀分布。另外可以考虑使用分布式锁使得单线程来更新缓存,防止大量请求直接落在DB上。 -
缓存的命名规则和失效规则
缓存的命名影响着更新、插入、删除操作时,缓存失效时的键的选取,规则目前没有特别的规范,不过在实际操作中我借鉴了关于缓存命名里面的方式,我将db、table、查询中会用到的唯一索引的值组成了缓存的键,这样在类似getAll,getList的操作,只要有insert和delete就会删除db:table:List结果标识符:*的键的缓存,delete和update操作根据使用到唯一索引的操作会失效db:table:唯一标识:唯一索引值的键的缓存。
网友评论