MySQL的锁到底有多少内容 ?再和腾讯大佬的技术面谈,我还是小看锁了!

1,444 阅读44分钟

对酒当歌,人生几何! 朝朝暮暮,唯有己脱。

人生啊

苦苦寻觅找工作之间,殊不知今日之时乃我心之痛,难到是我不配拥有工作嘛。自面试后他所谓的等待都过去一段时日,可惜在下京东上的小金库都要见低啦。每每想到不由心中一紧。正处为难之间,手机忽然来了个短信预约后续面试。 我即刻三下五除二拎包踢门而出。飞奔而去。

冲鸭

此刻面试门外首先映入眼帘的是一个白色似皮球的东西,似圆非圆。好奇冬瓜落地一般。上半段还有一段湿湿的部分,显得尤为入目。这是什么情况?

紧接着现身一名中年男子。他身着纯白色T桖衫的,一灰色宽松的休闲西裤,腰围至少得三十好几。外加一双夏日必备皮制凉鞋。只见,他正低头看着手上的一张A4纸。透过一头黑色短发。满脸的赘肉横生。外加上那大腹便便快要把那T桖衫给撑爆的肚子。

看得我好生害怕,不由得咽了咽口水,生怕自己说错话。这宛如一颗肉粽呀。不在职场摸滚打拼8、9年,也不会有当前这景象。

中年男子

什么是锁

「面试官:」: 你是来参加面试的吧?「吒吒辉:」  不 不 不,我是来参加复试呢。

「面试官:」: 看到上次别人点评,MySQL优化还阔以。那你先谈谈对锁的理解?

「吒吒辉:」 嘿嘿,还好!

**「锁」是计算机在进行「多 进程、线程」执行调度时强行限制资源访问的同步机制,用于在「并发访问」**时保证数据的一致性、有效性;

锁是在执行多线程时,用于强行限制资源访问的同步机制,即用在并发控制中保证对互斥的要求。

一般的锁是建议锁(advisory lock),每个线程在访问对应资源前都需获取锁的信息,再根据信息决定是否可以访问。若访问对应信息,锁的状态会改变为锁定,因此其它线程此时不会来访问该资源,当资源结束后,会恢复锁的状态,允许其他线程的访问。

有些系统有强制锁(mandatory lock),若有未授权的线程想要访问锁定的数据,在访问时就会产生异常。 ---《维基百科》

锁的类型和应用原理

「面试官:」: 那一般数据库有哪些锁? 一般怎么使用?

此刻,用我那呆若木鸡的眼神看向面试官,内心实属**「尴尬+害怕」**,数据库不就是共享和互斥锁吗? 这样看来,是我太嫩。此处必有坑。殊不知此刻我内心已把你拿捏,定斩不饶。

死磕

「吒吒辉:」 数据库的锁根据不同划分方式有很多种说法,在业务访问上有以下两种:

  • 排他锁 在访问共享资源之前对其进行加锁,在访问完成后进行解锁操作。 加锁成功后,任何其它线程请求来获取锁都会被阻塞,直到当前线自行释放锁。

线程3状态:就绪、阻塞、执行

如解锁时,有一个以上的线程阻塞(资源已释放),那么所有尝试获取该锁的线程都被CPU认为**「就绪状态」**, 如果第一个就绪状态的线程又执行加锁操作,那么其他的线程又会进入就绪状态。 在这种方式下,只能有一个线程访问被互斥锁保护的资源

故此,MySQL的SQL语句加了互斥锁后,只有接受到请求并获取锁的线程才能够访问和修改数据。 因为互斥锁是针对线程访问控制而不是请求本身。

  • 共享锁 被加锁资源是可被共享的,但仅限于读请求。它的写请求只能被获取到锁的请求独占。 也就是加了共享锁的数据,只能够当前线程修改,其它线程只能读数据,并不能修改。

「吒吒辉:」 在 SQL 请求上可分为读、写锁。但本质还是对应对共享锁和排它锁。

「面试官:」 那 SQL 请求上不加锁怎么访问? 为啥说它们属于共享锁和排他锁? 这之间有何联系?

微信搜索【莲花童子哪吒】

「吒吒辉:」  除加锁读外,还有一种不加锁读的情况。这种方式称为**「快照读,读请求加锁称为共享读。」**

针对请求加共享、排它锁的原因在于,读请求天生是**「幂等性」**的,不论你读多少次数据不会发生变化,所以给读请求加上锁就应该为共享锁。 不然怎么保证它的特点呢? 而写请求,本身就需对数据进行修改,所以就需要排它锁来保证数据修改的一致性。

「吒吒辉:」  如果按照锁的颗粒度划分看,就有**「表锁和行锁」**

  • 表锁: 是MySQL中最基本的锁策略,并且是开销最小的策略。并发处理较少。表锁由MySQL服务或存储引擎管理。多数情况由服务层管理,具体看SQL操作。

例如:服务器会为诸如 「ALTER TABLE 之类的语句使用表锁」,而忽略存储引擎的锁。

加锁机制:

它会锁定整张表。一个用户在对表进行写操作(插人、删除、更新等)前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他用户才能获取到读锁。

  • 行锁: 锁定当前访问行的数据,并发处理能力很强。但锁开销最大。具体视行数据多少决定。由innoDB存储引擎支持。

  • 页级锁: 页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。由BDB 存储引擎管理页级锁。

「面试官:」  为啥是表锁开销小,而不是行锁呢? 毕竟表锁锁定是整张表

「吒吒辉:」  表锁锁定的是表没错,但它不是把表里面所有的数据行都上锁,相当于是封锁了表的入口,这样它只是需要判断每个请求是否可以获取到表的锁,没有就不锁定。 而行锁是针对表的每一行数据,数据量一多,锁定内容就多,故开销大。 但因它颗粒度小,锁定行不会影响到别的行。所以并发就高。而如果表锁在一个入口就卡死了,那整体请求处理肯定就会下降。

「面试官:」 我记得行锁里面有几种不同的实现方式,你知道吗?

您可真贴心啊,替我考虑这么多,大佬都是这么心比针细? 我要是说不知道,你老是不是又准备给出穿小鞋啦。强忍内心啃人的冲动

我懂你

ps:读懂图,说明你有故事

「吒吒辉:」  innodb虽支持行锁,但锁实现的算法却和SQL的查询形式有关系:

  • Record Lock(记录锁):单个行记录上的锁。也就是我们日常认为的行锁。由where = 的形式触发

  • Gap Lock(间隙锁):间隙锁,锁定一个范围,但不包括记录本身(它锁住了某个范围内的多个行,包括根本不存在的数据)。

GAP锁的目的,是为了防止事务插入而导致幻读的情况。该锁只会在隔离级别是RR或者以上的级别内存在。间隙锁的目的是为了让其他事务无法在间隙中新增数据。 SQL里面用 where >、>=等范围条件触发,但会根据锁定的范围内,是否包含了表中真实存在的记录进行变化,如果存在真实记录就会进化为 临建锁。反之就为间隙所。

  • Next-Key Lock(临键锁):它是记录锁和间隙锁的结合,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题。next-key 锁是InnoDB默认的。是一个左开右闭的规则

  • IS锁:意向共享锁、Intention Shared Lock。当事务准备在某条记录上加S(读)锁时,需要先在表级别加一个IS锁。

  • IX锁:意向排它锁、Intention Exclusive Lock。当事务准备在某条记录上加X(写)锁时,需要先在表级别加一个IX锁。

「面试官:」 那这个东西是怎么实现的?

t(id PK, name KEY, sex, flag);

表中有四条记录:

1, zhazhahui, m, A3, nezha, m, A5, lisi, m, A9, wangwu, f, B
  • 记录锁 select * from t where id=1 for update; 锁定 id =1的记录

  • 间隙锁select * from t where id > 3 and id < 9 ;

锁定(3,5],(5,9)范围的值,因为当前访问3到9的范围记录,就需要锁定表里面已经存在的数据来解决幻读和不可重复读的问题

  • 临建锁select * from t where id >=9 ;  

会锁定 [9,+∞) 。查询会先选中 9 号记录,所以锁定范围就以9开始到正无穷数据。

「面试官:」 那意向排它、共享锁呢?是怎么个内容

「吒吒辉:」 意向排它锁和意向共享锁,是针对当前SQL请求访问数据行时,会提前进行申请访问,如果最终行锁未命中就会退化为该类型的表锁。

「面试官:」 那有这个意向排它锁有什么好处呢?

「吒吒辉:」  可提前做预判,每次尝试获取行锁之前会检查是否有表锁,如果存在就不会继续申请行锁,从而减少锁的开销。从而整个表就退化为表锁。

「面试官:」 那你动手给我演示下每个场景

嗯。。。(瞳孔放大2倍)我这不说的很明白吗? 难道故意和作对,这是干嘛啊。欺负人嘛不是 只见那面试官忽然翘起来二郎腿,还有节拍的抖动着腿,看向我。一看就是抖音整多了 哎,没办法 官大以及压死人。打碎了牙齿自己咽。你给我看细细看好了,最好眼睛都别眨

搞事情

「吒吒辉:」 因为锁就是解决事务并发的问题,所以记录锁就不演示了,直接游荡在间隙和临建锁里面。

建立语句:

CREATE TABLE `t1`  
(  
`id` int(10) NOT NULL AUTO_INCREMENT, 
 `name` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL, 
 `age` tinyint(3) unsigned DEFAULT NULL,  
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

表数据:

表数据

间隙锁:

  • 关闭 MySQL 默认的事务自动提交机制。

  • 关闭前:

  • 关闭后:

  • 加锁:

直接插入 >8 的数据就阻塞,都会上锁。为的就解决插入新数据而导致幻读。

「啊!幻读不知道呀」。下篇文章给大家安排上】微信搜索【莲花童子哪吒】

「面试官:」 你这条件不是>=8吗? 那等于8呢? 被吃辣?

「吒吒辉:」 别着急嘛,这不还没说完吗。为什么不指定8呢?

因为 「>=8」 的条件会从**「间隙锁升级为临建锁」**,因为你条件里面包含了 8 这个真实存在的数据。所以会把它锁起来。如下:

所以,最终的行锁会和SQL语句的条件触发有关系,一旦范围查询包含了数据库里面真实存在数据,就会升级为临建锁。不要问我为什么? 看前面的定义

「面试官独白」:这小伙多少看来还有有点货,不错。此刻面试官露出一丝笑容。殊不知他内心又开酝酿起了新的想法。就等我入瓮

「面试官:」 那什么场景下行锁不会生效呢?锁 锁定的又是什么?

此刻,我呆了,这都什么跟什么啊。不带这么玩的吧。天杀的,净使坏

微信搜索【莲花童子哪吒】

锁的触发机制

**「吒吒辉:」innodb的行锁是根据索引触发,如果没有相关的索引,那行锁将会退化成表锁(即锁定整个表里的行)。 而 「锁」  锁定的是「索引」**即索引树里面的数据库字段的值。

  • id为主键索引字段。微信搜索【莲花童子哪吒】

  • 给 age 字段上锁微信搜索【莲花童子哪吒】

  • age 字段没索引,退化成表锁。直接查询将失败。微信搜索【莲花童子哪吒】

有索引,用索引字段查询可得数据,其余字段查询将失败。因为获取不到行锁,只能等待。而锁定的是索引,故此其它用其它索引值查询能拿查询数据

  • 索引字段上锁微信搜索【莲花童子哪吒】

  • 索引当前字段锁定,用其余索引字段可查询

微信搜索【莲花童子哪吒】

  • 不是索引字段都差不到。微信搜索【莲花童子哪吒】

「面试官:」 你前面说到的锁可以解决事务并发,然而MVCC也是用于解决并发,那干嘛还用锁来呢?你给说说

「吒吒辉:」 通过MVCC可以解决脏读、不可重复读、幻读这些读一致性问题,但实际上这只是解决了普通select语句的数据读取问题。 事务利用MVCC进行的读取操作称之为快照读,所有普通的SELECT语句在READ COMMITTED、REPEATABLE READ隔离级别下都算是快照读。

除了快照读之外,还有一种是**「锁定读」,即在读取的时候给记录加锁,在「锁定读」**的情况下依然要解决脏读、不可重复读、幻读的问题。

比如:如果 1 4 7 9 的数据。如果条件为 where > 4 的,那如果不锁定到 (4,7] (7,9],(9,+∞)。那势必就会早幻读,不可重复读的问题。

ps:不重复读?脏读是如何产生的?

死锁

「面试官:」 那你说下数据库的死锁是个什么情况?

「吒吒辉:」 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。

当事务试图以不同的顺序锁定资源时,就可能产生死锁。多个事务同时锁定同一个资源时也可能会产生死锁。

一般可通过死锁检测和死锁超时机制来解决该问题。 死锁检查: 像InnoDB存储引擎,就能检测到死锁的循环依赖,并立即返回一个错误。否则死锁会导致出现非常慢的查询。通过参数 innodb_deadlock_detect 设置为on,来开启。

超时机制: 就是当查询的时间达到锁等待超时的设定后放弃锁请求。InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚(这是相对比较简单的死锁回滚算法)。

可通过配置参数 innodb_lock_wait_timeout 用来设置超时时间。如果有些用户使用哪种大事务,就设置**「锁超时时间大于事务执行时间」**。 但这种情况下死锁超时检查的发现时间是无法接受的。

「面试官:」  那你说说InnoDB和MyisAM是如何发现死锁的?

「吒吒辉:」

  • innodb

数据库会把事务单元锁维持的锁和它所等待的锁都记录下来,Innodb提供了wait-for graph算法来主动进行死锁检测,每当加锁请求无法立即满足需要进入等待时,wait-for graph算法都会被触发。当数据库检测到两个事务不同方向地给同一个资源加锁(产生循序),它就认为发生了死锁,触发wait-for graph算法。

比如:事务1给A加锁,事务2给B加锁,同时事务1给B加锁(等待),事务2给A加锁就发生了死锁。那么死锁解决办法就是终止一边事务的执行即可,这种效率一般来说是最高的,也是主流数据库采用的办法。

Innodb目前处理死锁的方法就是将持有最少行级排他锁的事务进行回滚。这是相对比较简单的死锁回滚方式。死锁发生以后,只有部分或者完全回滚其中一个事务,才能打破死锁。

对于事务型的系统,这是无法避免的,所以应用程序在设计必须考虑如何处理死锁。大多数情况下只需要重新执行因死锁回滚的事务即可。

  • MyisAM

MyisAM自身只支持表级锁,故加锁后一次性获取的。所以资源上不会出现多个事务之间互相需要对方释放锁之后再来进行处理。故不会有死锁

「面试官:」  wait-for graph 算法怎么理解?

「吒吒辉:」 如下所示,四辆车就是死锁微信搜索【莲花童子哪吒】

它们相互等待对方的资源,而且形成环路!每辆车可看为一个节点,当节点1需要等待节点2的资源时,就生成一条有向边指向节点2,最后形成一个有向图。我们只要检测这个有向图是否出现环路即可,出现环路就是死锁!这就是wait-for graph算法。

微信搜索【莲花童子哪吒】

Innodb将各个事务看为一个个节点,资源就是各个事务占用的锁,当事务1需要等待事务2的锁时,就生成一条有向边从1指向2,最后行成一个有向图。

「面试官:」  既然死锁无法避免,那如何减少发生呢?

「吒吒辉:」

  • 对应用程序进行调整/修改。某些情况下,你可以通过把大事务分解成多个小事务,使得锁能够更快被释放,从而极大程度地降低死锁发生的频率。在其他情况下,死锁的发生是因为两个事务采用不同的顺序操作了一个或多个表的相同的数据集。需要改成以相同顺序读写这些数据集,换言之,就是对这些数据集的访问采用串行化方式。这样在并发事务时,就让死锁变成了锁等待。

  • 修改表的schema,例如:删除外键约束来分离两张表,或者添加索引来减少扫描和锁定的行。

  • 如果发生了间隙锁,你可以把会话或者事务的事务隔离级别更改为RC(read committed)级别来避免,可以避免掉很多因为gap锁造成的死锁,但此时需要把binlog_format设置成row或者mixed格式。

  • 为表添加合理的索引,不走索引将会为表的每一行记录添加上锁(等同表锁),死锁的概率大大增大。

  • 为了在单个InnoDB 表上执行多个并发写入操作时避免死锁,可以在事务开始时通过为预期要修改的每个元祖(行)使用SELECT ... FOR UPDATE语句来获取必要的锁,即使这些行的更改语句是在之后才执行的。

  • 通过SELECT ... LOCK IN SHARE MODE获取行的读锁后,如果当前事务再需要对该记录进行更新操作,则很有可能造成死锁。因进行获锁读取在修改

这时,只见对面所坐面试官,捋了捋那没有毛发的下巴,故作深思熟虑,像是在端详这什么。 难道 难道 是让我通过了吗? 此刻内心犹如小鹿乱撞,呐喊到我要干它二量。真的是不容易。 就在此时,他起身而立,那白色T桖衫包裹着那甩大肚子,犹如波浪上下翻滚。一看就是没少在酒桌上撸肉。

「只见开口到,小伙子不错啊。」微信搜索【莲花童子哪吒】

这是肯定我吗?  不容易啊,今天不开几把LOL,难消我心头之恨

「面试官:」  其实这数据库嘛 ,内容还是有很多的,你回去准备下,下一次的面试吧

。。。。什么个玩意儿,下次? 那就是这次不行啦, 这还没考够啊,下巴本来没毛,你捋个什么劲儿,整得个神神忽忽的。 此时内心犹如翻江倒海,猛龙过江。白鹤亮翅的冲动打他,奈何我这小身板子不行

「吒吒辉:」 那行吧,下次是多久啊,我这好多天都没整顿好的啦,你给我个准信呗。

我用那水汪汪可怜的小眼神望向他说到。他却很斯文的笑着,说道

「面试官:」 快了,小伙子别着急,我看好你的,加油

我加你那撸啊丝压榨花生油。 面个试,还嫌我脸上出油出的不多,都是被你挤出来的。只有强忍住内心的冲动。 哎 官大一级压死人啊**「吒吒辉:」** 行吧,那我走啦 此刻,露出我那灰溜溜的背影,犹如鲁迅先生笔下的孔乙己

参考:《高性能MySQL》
https://zhuanlan.zhihu.com/p/29150809
https://www.cnblogs.com/yulibostu/articles/9978618.html

这篇想聊的话题是:分布式多级缓存架构的终章,如何解决大流量、高并发这样的业务场景,取决于你能不能成为这个领域金字塔上层的高手? 能不能把这个问题思考清楚决定了你的成长速度。

很多人在一个行业5年、10年,依然未达到这个行业的中层甚至还停留在底层,因为他们从来不关心这样的话题。作为砥砺前行的践行者,我觉得有必要给大家来分享一下。
file

开篇

服务端缓存是整个缓存体系中的重头戏,从开始的网站架构演进中,想必你已看到服务端缓存在系统性能的重要性。

但数据库确是整个系统中的“半吊子|慢性子”,有时数据库调优却能够以小搏大,在不改变架构和代码逻辑的前提下,缓存参数的调整往往是条捷径。

在系统开发的过程中,可直接在平台侧使用缓存框架,当缓存框架无法满足系统对性能的要求时,就需要在应用层自主开发应用级缓存。

缓存常用的就是Redis这东西,那到底什么是平台级、应用级缓存呢?

后面给大家揭晓。但有一点可表明,平台级就是你所选择什么开发语言来实现缓存,而应用级缓存,则是通过应用程序来达到目的。

01数据库缓存

为何说数据库是“慢性子”呢? 对现在喜欢的你来说,慢是解决不了问题的。就好像总感觉感觉妹子回复慢

因为数据库属于IO密集型应用,主要负责数据的管理及存储。数据一多查询本身就有可能变慢, 这也是为啥数据上得了台面时,查询爱用索引提速的原因。当然数据库自身也有“缓存”来解决这个问题。

数据多了查询不应该都慢吗? 小白说吒吒辉你不懂额

。。。这个,你说的也不全是,还得分情况。例如:数据有上亿行

file

原因:

  1. 因为简单的SQL的结果不会特别多。你请求也不大,磁盘跟的上
  2. 并发总量超过磁盘吞吐上限,是谁都没招

就算你们不喜欢吒吒辉,我也要奋笔疾书

数据库缓存是自身一类特殊的缓存机制。大多数数据库不需要配置就可以快速运行,但并没有为特定的需求进行优化。在数据库调优的时候,缓存优化你可以考虑下。

以MySQL为例,MySQL中使用了查询缓冲机制,将SELECT语句和查询结果存放在缓冲区中,以键值对的形式存储。以后对于同样的SELECT语句,将直接从缓冲区中读取结果,以节省查询时间,提高了SQL查询的效率。

1.1.MySQL查询缓存

Query cache作用于整个MySQL实例,主要用于缓存MySQL中的ResultSet,也就是一条SQL语句执行的结果集,所以它只针对select语句。

当打开 Query Cache 功能,MySQL在接收到一条select语句的请求后,如果该语句满足Query Cache的条件,MySQL会直接根据预先设定好的HASH算法将接收到的select语句以字符串方式进行 hash,然后到Query Cache中直接查找是否已经缓存。

file

如果结果集已经在缓存中,该select请求就会直接将数据返回,从而省略后面所有的步骤(如SQL语句的解析,优化器优化以及向存储引擎请求数据等),从而极大地提高了性能。

当然,若数据变化非常频繁的情况下,使用Query Cache可能会得不偿失。

这是为啥,用它不是提速吗?咋还得不偿失

因为MySQL只要涉及到数据更改,就会重新维护缓存。

  1. 如果SQL请求量比较大,你在维护的时候,就透过缓存走磁盘检索。这样数据库的压力肯定大。
  2. 重建缓存数据,它需要mysql后台线程来工作。也会增加数据库的负载。

所以在MySQL8已经取消了它。 故一般在读多写少,数据不怎么变化的场景可用它,例如:博客

Query Cache使用需要多个参数配合,其中最为关键的是query_cache_size和query_cache_type, 前者用于设置缓存ResultSet的内存大小,后者设置在何种场景下使用Query Cache。

file 这样可以通过计算Query Cache的命中率来进行调整缓存大小。

1.2.检验Query Cache的合理性

检查Query Cache设置的是否合理,可以通过在MySQL控制台执行以下命令观察:

  • SHOW VARIABLES LIKE '%query_cache%';
  • SHOW STATUS LIKE 'Qcache%'; 通过检查以下几个参数可以知道query_cache_size设置得是否合理:
    • Qcache_inserts:表示Cache多少次未命中然后插入到缓存
    • Qcache_hits: 表示命中多少次,它可反映出缓存的使用效果。

如果Qcache_hits的值非常大,则表明查询缓冲使用非常频繁,如果该值较小反而会影响效率,那么可以考虑不用查询缓存;

  • Qcache_lowmem_prunes: 表示多少条Query因为内存不足而被清除出Query_Cache。

如果Qcache_lowmem_prunes的值非常大,则表明经常出现缓冲不够的情况,因增加缓存容量。

  • Qcache_free_blocks: 表示缓存区的碎片

Qcache_free_blocks值非常大,则表明缓存区中的碎片很多,可能需要寻找合适的机会进行整理。

通过 Qcache_hitsQcache_inserts 两个参数可以算出Query Cache的命中率:

file

通过 Qcache_lowmem_prunes 和 Qcache_free_memory 相互结合,能更清楚地了解到系统中Query Cache的内存大小是否真的足够,是否频繁的出现因内存不足而有Query被换出的情况。

1.3.InnoDB的缓存性能

当选择 InnoDB 时,innodb_buffer_pool_size 参数可能是影响性能的最为关键的一个参数,它用来设置缓存InnoDB索引及数据块、自适应HASH、写缓冲等内存区域大小,更像是Oracle数据库的 db_cache_size。

简单来说,当操作InnoDB表的时候,返回的所有数据或者查询过程中用到的任何一个索引块,都会在这个内存区域中去查询一遍

MyISAM引擎中的 key_buffer_size 一样,innodb_buffer_pool_size设置了 InnoDB 引擎需求最大的一块内存区域,直接关系到InnoDB存储引擎的性能,所以如果有足够的内存,尽可将该参数设置到足够大,将尽可能多的InnoDB的索引及数据都放入到该缓存区域中,直至全部。

说到缓存肯定少不了,缓存命中率。那innodb该如何计算?

file

计算出缓存命中率后,在根据命中率来对 innodb_buffer_pool_size 参数大小进行优化

除开查询缓存。数据库查询的性能也与MySQL的连接数有关

table_cache 用于设置 table 高速缓存的数量。

show global status like 'open%_tables'; # 查看参数

由于每个客户端连接都会至少访问一个表,因此该参数与max_connections有关。当某一连接访问一个表时,MySQL会检查当前已缓存表的数量。

如果该表已经在缓存中打开,则会直接访问缓存中的表以加快查询速度;如果该表未被缓存,则会将当前的表添加进缓存在进行查询。

在执行缓存操作之前,table_cache参数用于限制缓存表的最大数目:

如果当前已经缓存的表未达到table_cache数目,则会将新表添加进来;若已经达到此值,MySQL将根据缓存表的最后查询时间、查询率等规则释放之前的缓存。

02平台级缓存

什么是平台级缓存,说的这个玄乎?

平台级缓存是指你所用什么开发语言,具体选择的是那个平台,毕竟缓存本身就是提供给上层调用。主要针对带有缓存特性的应用框架,或者可用于缓存功能的专用库。

如:

  • PHP中的Smarty模板库
  • Java中,缓存框架更多,如Ehcache,Cacheonix,Voldemort,JBoss Cache,OSCache等等。

Ehcache是现在最流行的纯Java开源缓存框架,配置简单、结构清晰、功能强大,是从hibernate的缓存开始被广泛使用起来的。EhCache有如下特点:

file Ehcache的系统结构如图所示:
file

什么是分布式缓存呢?好像我还没搞明白,小吒哥

首先得看看恒古不变的“分布式”,即它是独立的部署到多个服务节点上或者独立的进程,彼此之间仅仅通过消息传递进行通信和协调。

也就是说分布式缓存,它要么是在单机上有多个实例,要么就独立的部署到不同服务器,从而把缓存分散到各处

最后通过客户端连接到对应的节点来进行缓存操作。

Voldemort是一款基于Java开发的分布式键-值缓存系统,像JBoss的缓存一样,Voldemort同样支持多台服务器之间的缓存同步,以增强系统的可靠性和读取性能。

Voldemort有如下特点:

file Voldemort的逻辑架构图

file Voldemort相当于是Amazon Dynamo的一个开源实现,LinkedIn用它解决了网站的高扩展性存储问题。

简单来说,就平台级缓存而言,只需要在框架侧配置一下属性即可,而不需要调用特定的方法或函数。

系统中引入缓存技术往往就是从平台级缓存开始,平台级缓存也通常会作为一级缓存使用。

既然平台级缓存都使用框架配置来实现,这咋实现缓存的分布式呢?节点之间都没有互相的消息通讯了

如果单看,框架缓存的调用,那确实没办法做到分布式缓存,因为自身没得像Redis那样分布式的部署方式,通过网络把各节点连接 。
但本地平台缓存可通过远程过程调用,来操作分布在各个节点上的平台缓存数据。

file

在 Ehcache 中: file

03应用级缓存

当平台级缓存不能满足系统的性能时,就要考虑使用应用级缓存。 应用级缓存,需要开发者通过代码来实现缓存机制。

有些许 一方有难,八方支援 的感觉。自己搞不定 ,请教别人

这是NoSQL的战场,不论是Redis还是MongoDB,以及Memcached都可作为应用级缓存的技术支持。
一种典型的方式是每分钟或一段时间后统一生成某类页面存储在缓存中,或者可以在热数据变化时更新缓存。

为啥平台缓存还不能满足系统性能要求呢?它不是还可以减少应用缓存的网络开销吗 那你得看这几点:

file

3.1面向Redis的缓存应用

Redis是一款开源的、基于BSD许可的高级键值对缓存和存储系统,例如:新浪微博有着几乎世界上最大的Redis集群。

为何新浪微博是世界上最大的Redis集群呢?

微博是一个社交平台,其中用户关注与被关注、微博热搜榜、点击量、高可用、缓存穿透等业务场景和技术问题。Redis都有对应的hash、ZSet、bitmap、cluster等技术方案来解决。

在这种数据关系复杂、易变化的场景上面用到它会显得很简单。比如:

用户关注与取消:用hash就可以很方便的维护用户列表,你可以直接找到key,然后更改value里面的关注用户即可。

如果你像 memcache ,那只能先序列化好用户关注列表存储,更改在反序列化。然后再缓存起来,像大V有几百万、上千万的用户,一旦关注/取消。 当前任务的操作就会有延迟。

Reddis主要功能特点

  • 主从同步
    Redis支持主从同步,数据可以从主服务器向任意数量的从服务器同步,从服务器可做为关联其他从服务器的主服务器。这使得Redis可执行单层树状复制。

file

  • 发布/订阅
    由于实现了发布/订阅机制,使得从服务器在任何地方同步树的时候,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。

file

  • 集群
    Redis 3.0版本加入cluster功能,解决了Redis单点无法横向扩展的问题。Redis集群采用无中心节点方式实现,无需proxy代理,客户端直接与Redis集群的每个节点连接,根据同样的哈希算法计算出key对应的slot,然后直接在slot对应的Redis上执行命令。

从Redis视角来看,响应时间是最苛刻的条件,增加一层带来的开销是不能接受的。因此,Redis实现了客户端对节点的直接访问,为了去中心化,节点之间通过Gossip协议交换相互的状态,以及探测新加入的节点信息。Redis集群支持动态加入节点,动态迁移slot,以及自动故障转移。

Redis集群的架构示意如图所示。

file

那什么是 Gossip 协议呢? 感觉好高大上,各种协议频繁出现

Gossip 协议是一个多播协议,基本思想是:
一个节点想要分享一些信息给网络中的其他的一些节点。于是,它周期性的随机选择一些节点,并把信息传递给这些节点。这些收到信息的节点接下来会做同样的事情,即把这些信息传递给其他一些随机选择的节点。直至全部的节点。

即,Redis集群中添加、剔除、选举主节点,都是基于这样的方式。

例如:当加入新节点时(meet),集群中会随机选择一个节点来邀请新节点,此时只有邀请节点和被邀请节点知道这件事,其余节点要等待 ping 消息一层一层扩散。 除了 Fail 是立即全网通知的,其他诸如新节点、节点重上线、从节点选举成为主节点、槽变化等,都需要等待被通知到,所以Gossip协议也是最终一致性的协议。

这种多播的方式,是不是忽然有种好事不出门,坏事传千里的感脚

然而,Gossip协议也有不完美的地方,例如,拜占庭问题(Byzantine)。即,如果有一个恶意传播消息的节点,Gossip协议的分布式系统就会出问题。

注:Redis集群节点通信消息类型

file

所有的Redis节点通过PING-PONG机制彼此互联,内部使用二进制协议优化传输速度和带宽。

这个ping为啥能提高传输速度和带宽? 感觉不大清楚,小吒哥。那这里和OSI网络层级模式有关系了

在OSI网络层级模型下,ping协议隶属网络层,所以它会减少网络层级传输的开销,而二进制是用最小单位0,1表示的位。

带宽是固定的,如果你发送的数据包都很小,那传输就很快,并不会出现数据包很大还要拆包等复杂工作。
相当于别人出差1斤多MacPro。你出差带5斤的战神电脑。

Redis的瓶颈是什么呢? 吒吒辉给安排

Redis本身就是内存数据库,读写I/O是它的强项,瓶颈就在单线程I/O上与内存的容量上。 目前已经有多线程了,

例如:Redis6具备网络传输的多线程模式,keydb直接就是多线程。
啥? 还没了解多Redis6多线程模式,后面单独搞篇来聊聊

集群节点故障如何发现?

节点故障是通过集群中超过半数的节点检测失效时才会生效。客户端与Redis节点直连,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

Redis Cluster把所有的物理节点映射到slot上,cluster负责维护node、slot和value的映射关系。当节点发生故障时,选举过程是集群中所有master参与的,如果半数以上master节点与当前master节点间的通信超时,则认为当前master节点挂掉。

这为何不没得Slave节点参与呢?

集群模式下,请求在集群模式下会自动做到读写分离,即读从写主。但现在是选择主节点。只能由主节点来进行身份参与。

毕竟集群模式下,主节点有多个,每个从节点只对应一个主节点,那这样,你别个家的从节点能够参与选举整个集群模式下的主节点吗?

就好像小姐姐有了对象,那就是名花有主,你还能在有主的情况下,去选一个? 小心遭到社会的毒打

如果集群中超过半数以上master节点挂掉,无论是否有slave集群,Redis的整个集群将处于不可用状态。

当集群不可用时,所有对集群的操作都不可用,都将收到错误信息:

[(error)CLUSTERDOWN The cluster is down]。

支持Redis的客户端编程语言众多,可以满足绝大多数的应用,如图所示。

file

3.2.多级缓存实例

一个使用了Redis集群和其他多种缓存技术的应用系统架构如图所示

file

负载均衡

首先,用户的请求被负载均衡服务分发到Nginx上,此处常用的负载均衡算法是轮询或者一致性哈希,轮询可以使服务器的请求更加均衡,而一致性哈希可以提升Nginx应用的缓存命中率。

什么是一致性hash算法?

hash算法计算出的结果值本身就是唯一的,这样就可以让每个用户的请求都落到同一台服务器。
默认情况下,用户在那台在服务器登录,就生成会话session文件到该服务器,但如果下次请求重新分发给其他服务器就又需要重新登录。

而有了一致性hash算法就可以治愈它,它把请求都专心交给同一台服务器,铁打的专一,从而避免上述问题。 当然这里的一致性hash原理就没给大家讲了。后面安排

nginx本地缓存

请求进入到Nginx应用服务器,首先读取本地缓存,实现本地缓存的方式可以是Lua Shared Dict,或者面向磁盘或内存的 Nginx Proxy Cache,以及本地的Redis实现等,如果本地缓存命中则直接返回。

这本地缓存怎么感觉那么特别呢? 好像你家附近的小姐姐,离得这么近,可惜吃不着。呸呸呸,跑题啦

  • Lua Shard Dict是指在nginx上,通过lua开辟一块内存空间来存储缓存数据。相当于用的是nginx的进程资源
  • nginx Cache指nginx获取上游服务的数据缓存到本地。
  • 本地Redis指nginx和Redis部署在同一台服务上,由nginx直接操作Redis

啥! nginx还可直接操作Redis呀,听我细细到来

这些方式各有千秋,Lua Shard Dict 是通过Lua脚本控制缓存数据的大小并可以灵活的通过逻辑处理来修改相关缓存数据。

而Nginx Proxy Cache开发相对简单,就是获取上游数据到本地缓存处理。 而本地Redis则需要通过lua脚本编写逻辑来设置,虽然操作繁琐了,但解决了本地内存局限的问题。
所以nginx操作Redis是需要借助于 Lua 哒

nginx本地缓存有什么优点?

Nginx应用服务器使用本地缓存可以提升整体的吞吐量,降低后端的压力,尤其应对热点数据的反复读取问题非常有效。

本地缓存未命中时如何解决?

如果Nginx应用服务器的本地缓存没有命中,就会进一步读取相应的分布式缓存——Redis分布式缓存的集群,可以考虑使用主从架构来提升性能和吞吐量,如果分布式缓存命中则直接返回相应数据,并回写到Nginx应用服务器的本地缓存中。

如果Redis分布式缓存也没有命中,则会回源到Tomcat集群,在回源到Tomcat集群时也可以使用轮询和一致性哈希作为负载均衡算法。

file

那我是PHP技术栈咋办?都不会用到java的Tomcat呀
nginx常用于反向代理层。而这里的Tomcat更多是属于应用服务器,如果换成PHP,那就由php-fpm或者swoole服务来接受请求。即不管什么语言,都应该找对应语言接受请求分发的东西。

当然,如果Redis分布式缓存没有命中的话,Nginx应用服务器还可以再尝试一次读主Redis集群操作,目的是防止当从Redis集群有问题时可能发生的流量冲击。

这样的设计方案我在下表示看不懂

如果你网站流量比较大,如果一次在Redis分布式缓存中未读取到的话,直接透过到数据库,那流量可能会把数据库冲垮。这里的一次读主也是考虑到Redis集群中的主从延迟问题,为的就是防止缓存击穿。

在Tomcat | PHP-FPM集群应用中,首先读取本地平台级缓存,如果平台级缓存命中则直接返回数据,并会同步写到主Redis集群,在由主从同步到从Redis集群。

此处可能存在多个Tomcat实例同时写主Redis集群的情况,可能会造成数据错乱,需要注意缓存的更新机制和原子化操作。

如何保证原子化操作执行呢?

当多个实例要同时要写Redis缓存时,为了保持原子化,起码得在涉及这块业务多个的 Key 上采用lua脚本进行封装,然后再通过分布式锁或去重相同请求并入到一个队列来获取,让获取到锁或从队列pop的请求去读取Redis集群中的数据。

如果所有缓存都没有命中,系统就只能查询数据库或其他相关服务获取相关数据并返回,当然,我们已经知道数据库也是有缓存的。 是不是安排得明明白白。

file 这就是多级缓存的使用,才能保障系统具备优良的性能。

什么时候,小姐姐也能明白俺的良苦心。。。。 默默的独自流下了泪水

3.3.缓存算法

缓存一般都会采用内存来做存储介质,使用索引成本相对来说还是比较高的。所以在使用缓存时,需要了解缓存技术中的几个术语。
file

缓存淘汰算法

替代策略的具体实现就是缓存淘汰算法。

使用频率:

file

  1. Least-Recently-Used(LRU) 替换掉最近被请求最少的对象。

在CPU缓存淘汰和虚拟内存系统中效果很好。然而在直接应用与代理缓存中效果欠佳,因为Web访问的时间局部性常常变化很大。
浏览器就一般使用了LRU作为缓存算法。新的对象会被放在缓存的顶部,当缓存达到了容量极限,底部的对象被去除,方法就是把最新被访问的缓存对象放到缓存池的顶部。

  1. Least-Frequently-Used(LFU) 替换掉访问次数最少的缓存,这一策略意图是保留最常用的、最流行的对象,替换掉很少使用的那些数据。

然而,有的文档可能有很高的使用频率,但之后再也不会用到。传统的LFU策略没有提供任何移除这类文件的机制,因此会导致“缓存污染”,即一个先前流行的缓存对象会在缓存中驻留很长时间,这样,就阻碍了新进来可能会流行的对象对它的替代。

  1. Pitkow/Recker 替换最近最少使用的对象

除非所有对象都是今天访问过的。如果是这样,则替换掉最大的对象。这一策略试图符合每日访问Web网页的特定模式。这一策略也被建议在每天结束时运行,以释放被“旧的”、最近最少使用的对象占用的空间。

  1. Adaptive Replacement Cache(ARC) ARC介于LRU和LFU之间,为了提高效果,由2个LRU组成。

第一个包含的条目是最近只被使用过一次的,而第二个LRU包含的是最近被使用过两次的条目,因此,得到了新的对象和常用的对象。ARC能够自我调节,并且是低负载的。

  1. Most Recently Used(MRU) MRU与LRU是相对,移除最近最多被使用的对象。

当一次访问过来的时候,有些事情是无法预测的,并且在存系统中找出最少最近使用的对象是一项时间复杂度非常高的运算,这时会考虑MRU,在数据库内存缓存中比较常见。

访问计数
  1. Least Recently Used2 (LRU2)

LRU的变种,把被两次访问过的对象放入缓存池,当缓存池满了之后,会把有两次最少使用的缓存对象去除。

因为需要跟踪对象2次,访问负载就会随着缓存池的增加而增加。

  1. Two Queues(2Q) Two Queues是LRU的另一个变种。

把被访问的数据放到LRU的缓存中,如果这个对象再一次被访问,就把他转移到第二个、更大的LRU缓存,使用了多级缓存的方式。去除缓存对象是为了保持第一个缓存池是第二个缓存池的1/3。

当缓存的访问负载是固定的时候,把LRU换成LRU2,就比增加缓存的容量更好。

缓存容量算法
  1. SIZE 替换占用空间最大的对象,这一策略通过淘汰一个大对象而不是多个小对象来提高命中率。不过,可能有些进入缓存的小对象永远不会再被访问。SIZE策略没有提供淘汰这类对象的机制,也会导致“缓存污染”。

  2. LRU-Threshold 不缓存超过某一size的对象,其他与LRU相同。

  3. Log(Size)+LRU 替换size最大的对象,当size相同时,按LRU进行替换。

缓存时间
  1. Hyper-G LFU的改进版,同时考虑上次访问时间和对象size。

  2. Lowest-Latency-First 替换下载时间最少的文档。显然它的目标是最小化平均延迟。

缓存评估
  1. Hybrid Hybrid 有一个目标是减少平均延迟。

对缓存中的每个文档都会计算一个保留效用,保留效用最低的对象会被替换掉。位于服务器S的文档f的效用函数定义如下:

file Cs是与服务器s的连接时间;
bs是服务器s的带宽;frf代表f的使用频率;sizef是文档f的大小,单位字节。K1和K2是常量,Cs和bs是根据最近从服务器s获取文档的时间进行估计的。

  1. Lowest Relative Value(LRV) LRV也是基于计算缓存中文档的保留效用,然后替换保留效用最低的文档。
随机与队列算法
  1. First in First out(FIFO)

FIFO通过一个队列去跟踪所有的缓存对象,最近最常用的缓存对象放在后面,而更早的缓存对象放在前面,当缓存容量满时,排在前面的缓存对象会被踢走,然后把新的缓存对象加进去。

  1. Random Cache 随机缓存就是随意的替换缓存数据,比FIFO机制好,在某些情况下,甚至比LRU好,但是通常LRU都会比随机缓存更好些。

还有很多的缓存算法,例如Second Chance、Clock、Simple time-based、Extended time-based expiration、Sliding time-based expiration……各种缓存算法没有优劣之分,不同的实际应用场景,会用到不同的缓存算法。在实现缓存算法的时候,通常会考虑**使用频率、获取成本、缓存容量和时间等因素。 **

04.使用公有云的缓存服务

file 国内的共有云服务提供商如阿里云、青云、百度云等都推出了基于Redis的云存储服务,这些服务的有如下特点:

  • 动态扩容:

用户可以通过控制面板升级所需Redis的存储空间,扩容过程中服务不需要中断或停止,整个扩容过程对用户是透明且无感知的,而自主使用集群解决Redis平滑扩容是个很烦琐的任务,现在需要用你的小手按几下鼠标就能搞定,大大减少了运维的负担。

  • 数据多备:
    数据保存在一主一备两台机器中,其中一台机器宕机了,数据还在另外一台机器上有备份。
  • 自动容灾:
    主机宕机后系统能自动检测并切换到备机上,实现了服务的高可用性。
  • 成本较低:
    在很多情况下,为使Redis的性能更好,需要购买一台专门的服务器用于Redis的存储服务,但这样会导致某些资源的浪费,购买Redis云存储服务就能很好地解决这样的问题。

有了Redis云存储服务,能使后台开发人员从烦琐的运维中解放出来。应用后台服务中,如果自主搭建一个高可用、高性能的Redis集群服务,是需要投入相当的运维成本和精力。

如果使用云服务,就没必要投入这些成本和精力,可以让后台应用的开发人员更专注于业务。

我是吒吒辉,就爱分析进阶相关知识,下期在见。如果觉得文章对你有帮助,欢迎分享+关注额。
同时这边我也整理了后端系统提升的电子书和技术问题的知识卡片,也一并分享给大家,后面将持续更新,你们的关注将是我继续写作下去的最大动力。 需要的小伙伴可微信搜索【莲花童子哪吒】