高性能缓存架构

目录

一、缓存的价值

二、缓存的架构设计要点

1.缓存穿透

2.缓存雪崩

3.缓存热点

三、实现方式

四、常见问题

1.数据库Mysql自身不是有缓存么?

(1)Mysq自身有缓存简介

(2)命中条件

(3)工作流程

(4)缓存失败

(5)缓存的内存管理

(6)缓存的使用时机

(7)缓存参数配置

(8)减少缓存碎片策略

(9)InnoDB查询缓存


一、缓存的价值

某些复杂的业务场景下,单靠存储系统的性能提升是远远不够的,比如:

  • 需要经过复杂的计算

比如论坛首页展示用户同时在线数,MYSQL需要count(*)大量数据得到,此时无论怎么优化 MySQL,性能都不会太高,如果是实时展示的话,当用户数据量较大时,MySQL性能无法支撑。

  • 读多写少的数据,存储系统有心无力

目前的互联网业务大多是读多写少的情况。例如微博、微信、淘宝等。读业务占了整体业务的90%以上。比如某个明星发了一条微博,可能有几千万人来看,写一条微博可能只需要一条insert语句,但是如果每个用户浏览时都要select一次,即使有索引,几千万条select语句对MySQL数据库的压力也会非常大。

缓存的出现为了弥补存储系统在这些复杂业务场景下的不足,其基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统

缓存能够带来性能的大幅提升,以 Memcache 为例,单台 Memcache 服务器简单的 key-value 查询能够达到 TPS 50000 以上。

缓存虽然可以大大减轻存储系统的压力,但是给业务架构引入更多的复杂性。架构设计时如果没有针对缓存的复杂性进行处理,某些场景下甚至会导致整个系统崩溃。

缓存的架构设计要点有哪些呢?

二、缓存的架构设计要点

1.缓存穿透

缓存穿透:缓存没有发挥实际作用,业务系统虽然去缓存查询数据,但缓存中没有对应数据,业务系统需要再次去存储系统查询真实的数据。常见的2种情况:

(1)存储数据不存在

一般情况下,业务读取不存在的数据的请求量不会太大,不然就是设计逻辑不合理了。但是我们也需要考虑一些异常情况,比如被黑客攻击,故意大量访问某些不存在业务的数据,有可能将存储系统拖垮。

解决方法:如果查询存储数据时也没有对应的数据,则可以写个默认值(可以是空值,也可以去具体的值,需要同步给业务并做相应处理)到缓存中,这样就可以避免频繁的访问存储系统。

(2)存数据生成耗费大量时间或者资源,业务访问

这个听起来可能不是很好理解,意思就是存储系统存在数据,但是生成缓存数据需要耗费较长时间或者耗费大量资源。如果刚好在业务访问的时候缓存失效了(比如缓存达到过期时间后自动删除),那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况。

有什么具体的示例呢?

典型的就是电商的商品分页,假设我们在某个电商平台上选择“手机”这个类别查看,由于数据巨大,不能把所有数据都缓存起来,只能按照分页来进行缓存,由于难以预测用户到底会访问哪些分页,因此业务上最简单的就是每次点击分页的时候按分页计算和生成缓存。通常情况下这样实现是基本满足要求的,但是如果被竞争对手用爬虫来遍历的时候,系统性能就可能出现问题

具体的场景有:

  • 分页缓存的有效期设置为 1 天,因为设置太长时间的话,缓存不能反应真实的数据。
  • 通常情况下,用户不会从第 1 页到最后 1 页全部看完,一般用户访问集中在前 10 页,因此第 10 页以后的缓存过期失效的可能性很大。
  • 竞争对手每周来爬取数据,爬虫会将所有分类的所有数据全部遍历,从第 1 页到最后 1 页全部都会读取,此时很多分页缓存可能都失效了。
  • 由于很多分页都没有缓存数据,从数据库中生成缓存数据又非常耗费性能(order by limit 操作),因此爬虫会将整个数据库全部拖慢。

这种情况并没有太好的解决方案,因为爬虫会遍历所有的数据,而且什么时候来爬取也是不确定的,可能是每天都来,也可能是每周,也可能是一个月来一次,我们也不可能为了应对爬虫而将所有数据永久缓存。通常的应对方案要么就是识别爬虫然后禁止访问,但这可能会影响 SEO 和推广;要么就是做好监控,发现问题后及时处理因为爬虫不是攻击,不会进行暴力破坏,对系统的影响是逐步的,监控发现问题后有时间进行处理

2.缓存雪崩

缓存雪崩:当缓存失效(过期)后引起系统性能急剧下降的情况。

当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。而对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。由于旧的缓存已经被清除,新的缓存还未生成,并且处理这些请求的线程都不知道另外有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。

解决方法更新锁机制或后台更新机制

(1)更新锁

对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

对于采用分布式集群的业务系统,由于存在几十上百台服务器,即使单台服务器只有一个线程更新缓存,但几十上百台服务器一起算下来也会有几十上百个线程同时来更新缓存,同样存在雪崩的问题。因此分布式集群的业务系统要实现更新锁机制,需要用到分布式锁,如 ZooKeeper

(2)后台更新

由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。

后台定时机制需要考虑一种特殊的场景当缓存系统内存不够时,会“踢掉”一些缓存数据,从缓存被“踢掉”到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值,而业务线程本身又不会去更新缓存,因此业务上看到的现象就是数据丢了。解决的方式有两种:

  • 后台线程除了定时更新缓存,还要频繁地去读取缓存(例如,1 秒或者 100 毫秒读取一次),如果发现缓存被“踢了”就立刻更新缓存,这种方式实现简单,但读取时间间隔不能设置太长,因为如果缓存被踢了,缓存读取间隔时间又太长,这段时间内业务访问都拿不到真正的数据而是一个空的缓存值,用户体验一般。
  • 业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。可能会出现多个业务线程都发送了缓存更新消息,但其实对后台线程没有影响,后台线程收到消息后更新缓存前可以判断缓存是否存在,存在就不执行更新操作。这种方式实现依赖消息队列,复杂度会高一些,但缓存更新更及时,用户体验更好。

后台更新既适应单机多线程的场景,也适合分布式集群的场景,相比更新锁机制要简单一些。

后台更新机制还适合业务刚上线的时候进行缓存预热。缓存预热指系统上线后,将相关的缓存数据直接加载到缓存系统,而不是等待用户访问才来触发缓存加载。

3.缓存热点

虽然缓存系统本身的性能比较高,但对于一些特别热点的数据,如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大。例如,某明星微博发布“我们”来宣告恋爱了,短时间内上千万的用户都会来围观。

缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。以微博为例,对于粉丝数超过 100 万的明星,每条微博都可以生成 100 份缓存,缓存的数据是一样的,通过在缓存的 key 里面加上编号进行区分,每次读缓存时都随机读取其中某份缓存。

缓存副本设计有一个细节需要注意,就是不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值。

三、实现方式

由于缓存的各种访问策略和存储的访问策略是相关的,因此上面的各种缓存设计方案通常情况下都是集成在存储访问方案中,可以采用“程序代码实现”的中间层方式,也可以采用独立的中间件来实现。

四、常见问题

1.数据库Mysql自身不是有缓存么?

Ps:

  • 其实MySQL8.0已经取消了查询缓存。如果是小型项目,又不想用Reids或者MC等缓存,查询结果相对固定,可以在MySQL5.7以下版本设置缓存。
  • 简单来说什么情况下可以使用缓存?
    • 整个系统以读为主的业务,比如门户型、新闻类、报表型、论坛等网站。
    • 查询语句操作的表对象,非频繁地进行DML操作,可以使用query_cache_type=2模式,然后SQL语句加SQL_CACHE参数指定。
  • 为什么MySql 8弃用了缓存?
    • 参考:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg
      • 首先,查询缓存的效果取决于缓存的命中率,只有命中缓存的查询效果才能有改善,因此无法预测其性能。

      • 其次,查询缓存的另一个大问题是它受到单个互斥锁的保护。在具有多个内核的服务器上,大量查询会导致大量的互斥锁争用。

      • 缓存越靠近客户端,获得的好处越大。关于这份研究请参考:https://proxysql.com/blog/scaling-with-proxysql-query-cache/

      • 除此之外,MySQL8.0新增加了对性能干预的工具,例如,现在可以利用查询重写插件,在不更改应用程序的同时,插入优化器提示语句。另外,还有像ProxySQL这样的第三方工具,它们可以充当中间缓存。

虽然高版本的Mysql(8.0及以上)取消了查询缓存(query-cache),但是我们还是有必要了解下以前低版本的MySql自身缓存机制的。

(1)Mysq自身有缓存简介

【1】MySQL缓存机制

MySQL缓存机制即缓存sql 文本及缓存结果,用KV形式保存再服务器内存中,如果运行相同的sql,服务器直接从缓存中去获取结果,不需要再去解析、优化、执行sql。

【2】MySQL缓存失效

  • 在表的结构或数据发生改变时,查询缓存中的数据不再有效,查询缓存值的相关条目将被清空
  • INSERT、UPDATE、 DELETE、TRUNCATE、ALTER TABLE、DROP TABLE或DROP DATABASE会导致缓存数据失效

【3】 使用场景

  • 对于频繁更新的表,查询缓存不合适
  • 对于一些不变的数据且有大量相同sql查询的表,查询缓存可以大大提高查询的性能

(2)命中条件

  • 缓存的数据结构是hash表
  • 以SQL、数据库名和客户端协议等作为KEY
  • 在判断命中前,MySQL不会解析SQL,而是使用SQL去查询缓存,SQL上的任何字符的不同,如空格、注释等都会导致缓存不命中
  • 如果查询有不确定的数据,如like now()、current_date(),那么查询完成后结果都不会被缓存

(3)工作流程

  1. 服务器接收SQL,以SQL和一些其他条件为key查找缓存表
  2. 如果缓存命中,则直接返回缓存
  3. 如果缓存没有命中,则执行SQL查询,包括SQL解析、优化等。
  4. 执行完SQL查询结果以后,将SQL查询结果写入缓存表

(4)缓存失败

  • 当某个表正在写入数据,则这个表的缓存将会处于失效状态
  • 在InnoDB中,如果某个事务修改了表,则这个表的缓存在事务提交前都会处于失效状态,即在事务提交前,这个表的相关查询都无法被缓存

(5)缓存的内存管理

  • MySQL缓存机制会在内存中开辟一块内存(query_cache_size)区来维护缓存数据,其中大概有40K的空间是用来维护缓存数据的元数据的,例如空间内存、数据表和查询结果的映射,SQL和查询结果的映射。
  • MySQL缓存机制将大内存块分为小内存块(query_cache_min_res_unit),每个小块中存储自身的类型、大小和查询结果数据,还有前后内存块的指针。
  • MySQL缓存机制会在SQL查询开始(还未得到结果)时就去申请一块内存空间,所以即使缓存数据没有达到这个大小也需要占用申请的内存块空间(like linux filesystem’s block)。如果超出申请内存块的大小,则需要再申请一个内存块。当查询完成发现申请的内存有富余,则会将富余的内存空间释放掉,因而可能会造成内存碎片

(6)缓存的使用时机

高性能MySQL中称之为比较能反映性能提升的指数,一般来说达到3:1则算是查询缓存有效,而最好能够达到10:1

  • 通过缓存命中率判断

缓存命中率 = 缓存命中次数 (Qcache_hits) / 查询次数 (Com_select)

  • 通过缓存写入率判断

写入率 = 缓存写入次数 (Qcache_inserts) / 查询次数 (Qcache_inserts)

  • 通过命中写入率判断

比率 = 命中次数 (Qcache_hits) / 写入次数 (Qcache_inserts)

(7)缓存参数配置

看缓存相关配置相关命令:

SHOW VARIABLES LIKE '%query_cache%';

比如:

1.query_cache_type
是否打开缓存,可选参数有:
- OFF(0):关闭 ,不使用查询缓存
- ON(1):总是打开 ,始终使用查询缓存
- DEMAND(2):按需使用查询缓存,只有明确写了SQL_CACHE的查询才会写入缓存
如果query_cache_type为1而又不想利用查询缓存中的数据,可以用下面的SQL:
- SELECT SQL_NO_CACHE * FROM my_table WHERE condition;
如果值为2,要使用缓存的话,需要使用SQL_CACHE开关参数:
- SELECT SQL_CACHE * FROM my_table WHERE condition;

2.query_cache_size
缓存使用的总内存空间大小,单位是字节,这个值必须是1024的整数倍;否则MySQL实际分配可能跟这个数值不同(感觉这个应该跟文件系统的blcok大小有关)
默认情况下query_cache_size为0,表示为查询缓存预留的内存为0,则无法使用查询缓存
设置query_cache_size的值
- SET GLOBAL query_cache_size = 134217728; -- 注意值如果设得太小不会生效

3.query_cache_min_res_unit
分配内存块时的最小单位大小

4.query_cache_limit
MySQL能够缓存的最大结果,如果超出,则增加 Qcache_not_cached的值,并删除查询结果

5.query_cache_wlock_invalidate
如果某个数据表被锁住,是否仍然从缓存中返回数据,默认是OFF,表示仍然可以返回

6.GLOBAL STATUS 中关于缓存的参数解释
Qcache_free_blocks:缓存池中空闲块的个数
Qcache_free_memory:缓存中空闲内存量
Qcache_hits:缓存命中次数
Qcache_inserts:缓存写入次数
Qcache_lowmen_prunes:因内存不足删除缓存次数
Qcache_not_cached:查询未被缓存次数,例如查询结果超出缓存块大小,查询中包含可变函数等
Qcache_queries_in_cache:当前缓存中缓存的SQL数量
Qcache_total_blocks:缓存总block数


(8)减少缓存碎片策略

  1. 选择合适的block大小
  2. 使用 FLUSH QUERY CACHE 命令整理碎片,这个命令在整理缓存期间,会导致其他连接无法使用查询缓存

清空缓存的命令:

RESET QUERY CACHE; // 从查询缓存中移出所有查询。
FLUSH TABLES; //关闭所有打开的表,同时该操作将会清空查询缓存中的内容。

(9)InnoDB查询缓存

InnoDB存储引擎会对每个表设置一个事务计数器,里面存储当前最大的事务ID
当一个事务提交时,InnoDB会使用MVCC中系统最大的事务ID更新当前表的计数器
只有比这个最大ID大的事务能使用查询缓存,其他比这个ID小的事务则不能使用查询缓存
在InnoDB中,所有加锁操作的事务都不使用任何查询缓存
查询必须是完全相同的(逐字节相同)才能够被认为是相同的。
查询字符串由于其它原因使用不同的数据库、不同的协议版本或者不同的默认字符集都会被认为是不同的查询而分别进行缓存。

参考:

【1】李运华老师《从0开始学架构》(极客时间)

【2】MySQL缓存机制https://www.cnblogs.com/yueyun00/p/10898677.html

【3】MySQL为什么取消了Query Cache:https://mp.weixin.qq.com/s/_EXXmciNdgXswSVzKyO4xg

猜你喜欢

转载自blog.csdn.net/sinat_33718563/article/details/119982646