写在前面
读写分离、分库分表、反范式化、采用 NoSQL……若是这些扩展手段全都上了,数据响应依旧愈来愈慢,还有什么解决办法吗?mysql
有,加缓存。利用缓存层来吸取不均匀的负载和流量高峰:sql
Popular items can skew the distribution, causing bottlenecks. Putting a cache in front of a database can help absorb uneven loads and spikes in traffic.
一.在哪加?
理论上,在数据层以前的任意一层加缓存都可以阻挡流量,减小最终抵达数据库的操做请求:数据库
按缓存所处位置分为 4 种:json
客户端缓存:包括HTTP 缓存、浏览器缓存等浏览器
Web 缓存:例如CDN、反向代理服务等缓存
应用层缓存:例如Memcached、Redis等键值存储app
为了减轻数据库的负载,咱们在应用程序和数据存储之间加个键值存储做为缓冲层:异步
A cache is a simple key-value store and it should reside as a buffering layer between your application and your data storage.
经过内存中缓存的数据来响应一部分请求,而没必要实际执行查库操做,从而提高数据响应速度ide
二.存什么?
常见的有两种缓存模式:性能
Cached Database Queries:缓存原始查库结果
缓存原始查库结果
根据查询语句生成key,将查库结果缓存起来,例如:
key = "user.%s" % user_id user_blob = memcache.get(key) if user_blob is None: user = mysql.query("SELECT * FROM users WHERE user_id=\"%s\"", user_id) if user: memcache.set(key, json.dumps(user)) return user else: return json.loads(user_blob)
这种模式的主要缺陷在于难以处理缓存过时,由于数据与key(即查询语句)之间并无明确的关联,数据发生变化后,很难精确地删掉缓存中的全部相关条目。试想,一个单元格发生变化,会影响哪些查询语句?
尽管如此,这仍然是最经常使用的缓存模式,由于能够作出妥协,好比:
只缓存与查询语句有直接关联的数据,排序、统计、筛选之类的计算结果通通都不存了
缓存数据对象
另外一种思路是将应用程序中的数据模型对象缓存起来,这样原始数据与缓存之间就有了逻辑关联,从而轻松解决缓存更新的难题
不管数据是如何查询,如何加工转换的,只把最终获得的数据模型对象缓存起来,原始数据发生变化时,直接把相应的数据对象整个移除
对应用程序而言,数据对象比原始数据更容易管理和维护,所以,建议缓存数据对象,而不是原始数据
三.怎么查?
常见的缓存数据访问策略有 6 种:
Cache-aside/Lazy loading:预留缓存
Write-through:直写式
Write-behind/Write-back:回写式
Write-around:绕写式
预留缓存模式下,缓存与数据库之间没有直接关系(缓存位于一旁,因此叫 Cache-aside),由应用程序将须要的数据从数据库中读出并填充到缓存中
数据请求优先走缓存,未命中缓存时才查库,并把结果缓存起来,因此缓存是按需的(Lazy loading),只有实际访问过的数据才会被缓存起来
主要问题在于:
未命中缓存时须要 3 步,延迟不容忽视(对于冷启动能够手动预热)
Read-through
直读模式下,缓存挡在数据库以前,应用程序不与数据库直接交互,而是直接从缓存中读取数据
未命中缓存时,由缓存负责查库,并本身缓存起来。与预留缓存惟一的区别在于查库的工做由缓存来完成,而不是应用程序
Write-through
相似于直读模式,缓存也挡在数据库以前,数据先写到缓存,再写入数据库。也就是说,全部写操做必须先通过缓存
通常与直读式缓存相结合,虽然写操做多过一层缓存(存在额外的延迟),但保证了缓存数据的一致性(避免缓存变旧)。此时,缓存就像数据库的代理,读写都走缓存,缓存再查库或将写操做同步到数据库
Write-behind/Write-back
回写式缓存与直写式很像,写操做一样要先通过缓存,惟一的区别在于异步写入数据库,进而容许批处理以及写操做合并
一样可以与直读式缓存结合使用,并且不存在直写式中写操做的性能问题,但仅保证最终一致性
Write-around
所谓绕写式缓存就是写操做不通过(绕过)缓存,由应用程序直接写入数据库,仅缓存读操做。可与预留缓存或直读缓存结合使用:
Refresh-ahead
提早刷新,在缓存过时以前,自动刷新(从新加载)最近访问过的条目。甚至能够经过预加载来减小延迟,但若是预测不许反而会致使性能降低
四.塞满了怎么办?
固然,缓存空间是极其有限的,因此还要有逐出策略(Eviction Policy),从缓存中剔除一些不太可能用到的条目,经常使用策略以下:
LRU(Least Recently Used):最经常使用的一种策略,根据程序运行时的局部性原理,在一段时间内,大几率访问相同的数据,因此将最近没有用到的数据剔除出去,好比订机票,一段时间内大几率查询同一路线
LFU(Least Frequently Used):根据使用频率,将最不经常使用的数据剔除出去,好比输入法大可能是根据词频联想的
MRU(Most Recently Used):在有些场景下,须要删掉最近用过的条目,好比已读、再也不提醒、不感兴趣等
这些策略还能够结合使用,好比 LRU + LFU 综合考虑,取决于具体场景
参考资料
Caching Overview
Scalability for Dummies – Part 3: Cache
Things You Should Know About Database Caching
All things caching- use cases, benefits, strategies, choosing a caching technology, exploring some popular products