17天搞定GRE单词

1.背单词最好安排在早晨,最迟在中午,以便在12小时后的晚上的复习。每天三个小时,360个新词。

2.记忆周期:5分钟、30分钟、12小时,1、2、4、7、15天

3. 10个单词为一个记忆单元,背5分钟;开始下一个单元时,先复习本单元;背过6个单元后从头开始复习一遍。 晚上睡觉前复习一遍。

4. 17天后开始做题。

5. 15天的记忆周期过后,每天复习3个list(360个单词)(一个list120个单词,初次背需要一个小时的量)。

6. 对于每一次复习时遮住中文释义,想单词意思。

7. 对于没记住的单词可以在前面画星号,下次重点记忆。

8. 本方法的中间阶段,是最为关键也最为痛苦的时期,极易半途而废,请一定咬牙挺过去。如果实在坚持不下去了,就想一想:“如果放弃,前面的单词就全都白背了”。

Go基础

1. Golang 中常用的并发模型

  • 通过channel通知实现并发控制
  • 通过sync包中的WaitGroup实现并发控制

在WaitGroup里主要有三个方法:

  • Add, 可以添加或减少 goroutine的数量.
  • Done, 相当于Add(-1).
  • Wait, 执行后会堵塞主线程,直到WaitGroup 里的值减至0。

 

 

注:在 WaitGroup 第一次使用后,不能被拷贝

应用示例:


 

 

上面运行会提示所有的 goroutine 都已经睡眠了,出现了死锁。这是因为 wg 给拷贝传递到了 goroutine 中,导致只有 Add 操作,其实 Done操作是在 wg 的副本执行的。因此 Wait 就死锁了。

  • 在Go 1.7 以后引进的强大的Context上下文,实现并发控制

通常,在一些简单场景下使用 channel 和 WaitGroup 已经足够了,但是当面临一些复杂多变的网络并发场景下 channel 和 WaitGroup 显得有些力不从心了。 比如一个网络请求 Request,每个 Request 都需要开启一个 goroutine 做一些事情,这些 goroutine 又可能会开启其他的 goroutine,比如数据库和RPC服务。 所以我们需要一种可以跟踪 goroutine 的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的 Context,称之为上下文非常贴切,它就是goroutine 的上下文。

2. nil slice和空slice

var slice []int     是一个nil slice,直接用会越界

slice := []int{}    是一个空slice

3. 进程、线程和协程

  • 进程

进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

  • 线程

线程是进程的一个实体,是CPU调度和分派的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

  • 协程

协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

线程和协程的区别:

  • 线程的切换由操作系统负责调度,协程由用户自己进行调度,因此减少了上下文切换,提高了效率。
  • 线程的默认Stack大小是1M,而协程更轻量,接近1K。因此可以在相同的内存中开启更多的协程。
  • 由于在同一个线程上,因此可以避免竞争关系而使用锁。
  • 适用于被阻塞的,且需要大量并发的场景。但不适用于大量计算的多线程,遇到此种情况,更好实用线程去解决。

4. 什么是channel,为什么它可以做到线程安全

Channel是Go中的一个核心类型,可以把它看成一个管道,Channel也可以理解是一个先进先出的队列,通过管道进行通信。

Golang的Channel,发送一个数据到Channel 和 从Channel接收一个数据 都是 原子性的。而且Go的设计思想就是:不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。也就是说,设计Channel的主要目的就是在多任务间传递数据的,这当然是安全的。

解决数据竞争(Data race),也可以使用管道解决,使用管道的效率要比互斥锁Mutex高。

Go垃圾回收机制

1. 当前Golang使用的垃圾回收机制是三色标记法配合写屏障辅助GC,三色标记法是标记-清除法的一种增强版本。

2. 从root根出发扫描所有根对象,将他们引用的对象标记为灰色,将灰色对象置为黑色,将灰色对象引用的对象再置为灰色;以此循环,知道灰色对象队列为空。此时白色对象即为垃圾。

注:root区域主要是程序运行到当前时刻的栈和全局数据区域。

3. GC优化思路

通常小对象过多会导致GC三色法消耗过多的GPU。优化思路是,减少对象分配。

a.避免string与[]byte转化;

两者发生转换的时候,底层数据结结构会进行复制,因此导致 gc 效率会变低。

b.对于string的连接操作,少量小文本拼接,用 “+” ;大量小文本拼接,用 strings.Join;大量大文本拼接,用 bytes.Buffer。

 

4. GC触发条件

a. 超过内存大小阈值

b. 达到定时时间,阈值是由一个gcpercent的变量控制的,当新分配的内存占已在使用中的内存的比例超过gcprecent时就会触发。

c. 调用runtime.GC()时,主动触发,如果GC已经启动则跳过。

比如一次回收完毕后,内存的使用量为5M,那么下次回收的时机则是内存分配达到10M的时候。也就是说,并不是内存分配越多,垃圾回收频率越高。 如果一直达不到内存大小的阈值呢?这个时候GC就会被定时时间触发,比如一直达不到10M,那就定时(默认2min触发一次)触发一次GC保证资源的回收。

原文:https://juejin.im/post/6844903917650722829

Mysql join操作

1.left join 会以左表为全量数据,然后on匹配右表的,所以left join里面对左表的on条件无效。right join同理。

2.left join和right join的on和where区别:on作为连接的条件(左连接时,on左表的条件无效,on右表的条件使得右表对应不满足条件的行为NULL,行数和没有条件还是一样),on先生成临时表,where再在临时表中筛选。

3.左连接时,尽量选择数据量小的表作为左表,可以减少筛选次数。右连接同理。

4.inner join的on和where作用相同,但是inner join只是自己判断选择哪个表作为左表,临时表的数据量不会变小。

 

5.在使用left join时,on和where条件的区别如下:

a. on条件是在生成临时表时使用的条件,它不管on中的条件是否为真,都会返回左边表中的记录。

b.where条件是在临时表生成好后,再对临时表进行过滤的条件。这时已经没有left join的含义(必须返回左边表的记录)了,条件不为真的就全部过滤掉。

CPU缓存一致性协议(MESI Protocol)

  • 通过 MESI protocol,任何一个 CPU 要写入资料前,首先会给其它CPU发送Invalid消息,在其它 CPU 回 invalidate ack后, 才能将资料到自己的 cache,并在稍后将资料写回 memory。但是因为其他CPU可能回ack慢,所以该CPU会不等ack,先写入store buffer,然后继续做事,之后收到ack再更新cache的状态。所以CPU 读资料的顺序: store buffer → cache → memory。
  • 其他CPU 收到invalid消息后会先记录到invalidate queue里然后立即回 invalidate ack,稍后再处理 invalidate。

原文:https://medium.com/fcamels-notes/%E5%BE%9E%E7%A1%AC%E9%AB%94%E8%A7%80%E9%BB%9E%E4%BA%86%E8%A7%A3-memry-barrier-%E7%9A%84%E5%AF%A6%E4%BD%9C%E5%92%8C%E6%95%88%E6%9E%9C-416ff0a64fc1

Golang细节

1. 复数

 

结果:

2.

机器学习 – machine learning | ML

总结:

  • 什么是机器学习

把现实生活中的问题抽象成数学模型,然后利用数学方法对这个数学模型进行求解,从而解决现实生活中的问题。

  • 机器学习原理

通过训练集,不断识别特征,不断建模,最后形成有效的模型,这个过程就叫“机器学习”

  • 机器学习训练方法
  1. 监督学习:给算法一个数据集,并且给定正确答案。机器通过数据来学习正确答案的计算方法。
  2. 非监督学习:给定的数据集没有“正确答案”,无监督学习的任务是从给定的数据集中,挖掘出潜在的结构。
  3. 强化学习:它关注的是智能体如何在环境中采取一系列行为,从而获得最大的累积回报。通过强化学习,一个智能体应该知道在什么状态下应该采取什么行为。比如:机器学习玩游戏
  • 机器学习的七个步骤

收集数据—>数据准备—>选择一个模型—>训练—>评估—>参数调整—>预测(开始使用)

机器学习、人工智能、深度学习是什么关系?

1956 年提出 AI 概念,短短3年后(1959) Arthur Samuel 就提出了机器学习的概念:

Field of study that gives computers the ability to learn without being explicitly programmed.

机器学习研究和构建的是一种特殊算法(而非某一个特定的算法),能够让计算机自己在数据中学习从而进行预测。

所以,机器学习不是某种具体的算法,而是很多算法的统称。

机器学习包含了很多种不同的算法,深度学习就是其中之一,其他方法包括决策树,聚类,贝叶斯等。

深度学习的灵感来自大脑的结构和功能,即许多神经元的互连。人工神经网络(ANN)是模拟大脑生物结构的算法。

不管是机器学习还是深度学习,都属于人工智能(AI)的范畴。所以人工智能、机器学习、深度学习可以用下面的图来表示:能、机器学习、深度学习的关系

详细了解人工智能:《「2019更新」什么是人工智能?(AI的本质+发展史+局限性)

详细了解深度学习:《一文看懂深度学习(白话解释+8个优缺点+4个典型算法)

 

什么是机器学习?

在解释机器学习的原理之前,先把最精髓的基本思路介绍给大家,理解了机器学习最本质的东西,就能更好的利用机器学习,同时这个解决问题的思维还可以用到工作和生活中。

机器学习的基本思路

  1. 把现实生活中的问题抽象成数学模型,并且很清楚模型中不同参数的作用
  2. 利用数学方法对这个数学模型进行求解,从而解决现实生活中的问题
  3. 评估这个数学模型,是否真正的解决了现实生活中的问题,解决的如何?

无论使用什么算法,使用什么样的数据,最根本的思路都逃不出上面的3步!

机器学习的基本思路

当我们理解了这个基本思路,我们就能发现:

不是所有问题都可以转换成数学问题的。那些没有办法转换的现实问题 AI 就没有办法解决。同时最难的部分也就是把现实问题转换为数学问题这一步。

 

机器学习的原理

下面以监督学习为例,给大家讲解一下机器学习的实现原理。

假如我们正在教小朋友识字(一、二、三)。我们首先会拿出3张卡片,然后便让小朋友看卡片,一边说“一条横线的是一、两条横线的是二、三条横线的是三”。

不断重复上面的过程,小朋友的大脑就在不停的学习。

当重复的次数足够多时,小朋友就学会了一个新技能——认识汉字:一、二、三。

我们用上面人类的学习过程来类比机器学习。机器学习跟上面提到的人类学习过程很相似。

  • 上面提到的认字的卡片在机器学习中叫——训练集
  • 上面提到的“一条横线,两条横线”这种区分不同汉字的属性叫——特征
  • 小朋友不断学习的过程叫——建模
  • 学会了识字后总结出来的规律叫——模型

通过训练集,不断识别特征,不断建模,最后形成有效的模型,这个过程就叫“机器学习”!

 

监督学习、非监督学习、强化学习

机器学习根据训练方法大致可以分为3大类:

  1. 监督学习
  2. 非监督学习
  3. 强化学习

除此之外,大家可能还听过“半监督学习”之类的说法,但是那些都是基于上面3类的变种,本质没有改变。

 

监督学习

监督学习是指我们给算法一个数据集,并且给定正确答案。机器通过数据来学习正确答案的计算方法。

举个栗子:

我们准备了一大堆猫和狗的照片,我们想让机器学会如何识别猫和狗。当我们使用监督学习的时候,我们需要给这些照片打上标签。

将打好标签的照片用来训练

我们给照片打的标签就是“正确答案”,机器通过大量学习,就可以学会在新照片中认出猫和狗。

当机器遇到新的小狗照片时就能认出他

这种通过大量人工打标签来帮助机器学习的方式就是监督学习。这种学习方式效果非常好,但是成本也非常高。


非监督学习

非监督学习中,给定的数据集没有“正确答案”,所有的数据都是一样的。无监督学习的任务是从给定的数据集中,挖掘出潜在的结构。

举个栗子:

我们把一堆猫和狗的照片给机器,不给这些照片打任何标签,但是我们希望机器能够将这些照片分分类。

将不打标签的照片给机器

通过学习,机器会把这些照片分为2类,一类都是猫的照片,一类都是狗的照片。虽然跟上面的监督学习看上去结果差不多,但是有着本质的差别:

非监督学习中,虽然照片分为了猫和狗,但是机器并不知道哪个是猫,哪个是狗。对于机器来说,相当于分成了 A、B 两类。

机器可以将猫和狗分开,但是并不知道哪个是猫,哪个是狗


强化学习

强化学习更接近生物学习的本质,因此有望获得更高的智能。它关注的是智能体如何在环境中采取一系列行为,从而获得最大的累积回报。通过强化学习,一个智能体应该知道在什么状态下应该采取什么行为。

最典型的场景就是打游戏。

2019年1月25日,AlphaStar(Google 研发的人工智能程序,采用了强化学习的训练方式) 完虐星际争霸的职业选手职业选手“TLO”和“MANA”。

 

机器学习实操的7个步骤

通过上面的内容,我们对机器学习已经有一些模糊的概念了,这个时候肯定会特别好奇:到底怎么使用机器学习?

机器学习在实际操作层面一共分为7步:

  1. 收集数据
  2. 数据准备
  3. 选择一个模型
  4. 训练
  5. 评估
  6. 参数调整
  7. 预测(开始使用)
机器学习的7个步骤

假设我们的任务是通过酒精度和颜色来区分红酒和啤酒,下面详细介绍一下机器学习中每一个步骤是如何工作的。

案例目标:区分红酒和啤酒

 

步骤1:收集数据

我们在超市买来一堆不同种类的啤酒和红酒,然后再买来测量颜色的光谱仪和用于测量酒精度的设备。

这个时候,我们把买来的所有酒都标记出他的颜色和酒精度,会形成下面这张表格。

颜色 酒精度 种类
610 5 啤酒
599 13 红酒
693 14 红酒

这一步非常重要,因为数据的数量和质量直接决定了预测模型的好坏。

 

步骤2:数据准备

在这个例子中,我们的数据是很工整的,但是在实际情况中,我们收集到的数据会有很多问题,所以会涉及到数据清洗等工作。

当数据本身没有什么问题后,我们将数据分成3个部分:训练集(60%)、验证集(20%)、测试集(20%),用于后面的验证和评估工作。

数据要分为3个部分:训练集、验证集、测试集

关于数据准备部分,还有非常多的技巧,感兴趣的可以看看《AI 数据集最常见的6大问题(附解决方案)

 

步骤3:选择一个模型

研究人员和数据科学家多年来创造了许多模型。有些非常适合图像数据,有些非常适合于序列(如文本或音乐),有些用于数字数据,有些用于基于文本的数据。

在我们的例子中,由于我们只有2个特征,颜色和酒精度,我们可以使用一个小的线性模型,这是一个相当简单的模型。

 

步骤4:训练

大部分人都认为这个是最重要的部分,其实并非如此~ 数据数量和质量、还有模型的选择比训练本身重要更多(训练知识台上的3分钟,更重要的是台下的10年功)。

这个过程就不需要人来参与的,机器独立就可以完成,整个过程就好像是在做算术题。因为机器学习的本质就是将问题转化为数学问题,然后解答数学题的过程

 

步骤5:评估

一旦训练完成,就可以评估模型是否有用。这是我们之前预留的验证集和测试集发挥作用的地方。评估的指标主要有 准确率、召回率、F值。

这个过程可以让我们看到模型如何对尚未看到的数是如何做预测的。这意味着代表模型在现实世界中的表现。

 

步骤6:参数调整

完成评估后,您可能希望了解是否可以以任何方式进一步改进训练。我们可以通过调整参数来做到这一点。当我们进行训练时,我们隐含地假设了一些参数,我们可以通过认为的调整这些参数让模型表现的更出色。

 

步骤7:预测

我们上面的6个步骤都是为了这一步来服务的。这也是机器学习的价值。这个时候,当我们买来一瓶新的酒,只要告诉机器他的颜色和酒精度,他就会告诉你,这时啤酒还是红酒了。

15种经典机器学习算法

ner“>

算法 训练方式
线性回归 监督学习
逻辑回归 监督学习
线性判别分析 监督学习
决策树 监督学习
朴素贝叶斯 监督学习
K邻近 监督学习
学习向量量化 监督学习
支持向量机 监督学习
随机森林 监督学习
AdaBoost 监督学习
高斯混合模型 非监督学习
限制波尔兹曼机 非监督学习
K-means 聚类 非监督学习
最大期望算法 非监督学习

 

 

原文:https://easyai.tech/ai-definition/machine-learning/

离不开的微服务架构,脱不开的RPC细节

总结:

为什么要使用RPC

  • 自定义协议,减少数据传输(因为http协议包含请求行请求头等数据,在远程调用场景依赖不是很强)
  • 使用长连接:直接基于socket进行连接,不用每个请求都重新走三次握手的流程

什么是RPC框架:跨进程方法调用时,需要考虑序列化、反序列化、连接池管理、服务发现、负载均衡(服务端收到的流量均匀)、故障转移(服务端挂掉时可以及时下线,恢复时可以加入到服务列表)等问题,而rpc框架就是对业务方屏蔽了这些细节。

RPC-client同步调用:rpc框架将请求(包含请求方法名、请求参数等)序列化后,从连接池中拿到一个可用连接后给该连接加锁(置标志位),然后调用服务端,服务端通过反射执行相关方法,然后将返回值通过该连接同步返回,rpc框架再反序列化成具体的response给到client。

RPC-client异步调用:rpc框架将将请求的上下文存储起来,为每一个请求维护一个map<请求id,上下文>(回调使用),然后执行序列化,将请求放入收发队列后,请求同步返回(不会阻塞)。下游收发线程将收发队列的请求,从连接池拿到一个连接后,请求服务端,服务端处理后,将响应放入已接收队列,释放连接。从接收队列取出响应,从上下文中找到相应callback回调业务。

注:会有定时器,定时扫描上下文管理,如果有超时的,则直接执行callback,后续server端的响应到来后直接丢弃。

 


服务化有什么好处?

服务化的一个好处就是,不限定服务的提供方使用什么技术选型,能够实现大公司跨团队的技术解耦,如下图所示:

  • 服务A:欧洲团队维护,技术背景是Java
  • 服务B:美洲团队维护,用C++实现
  • 服务C:中国团队维护,技术栈是go

服务的上游调用方,按照接口、协议即可完成对远端服务的调用。

 

但实际上,大部分互联网公司,研发团队规模有限,大都使用同一套技术体系来实现服务:


这样的话,如果没有统一的服务框架,各个团队的服务提供方就需要各自实现一套序列化、反序列化、网络框架、连接池、收发线程、超时处理、状态机等“业务之外”的重复技术劳动,造成整体的低效。

因此,统一服务框架把上述“业务之外”的工作统一实现,是服务化首要解决的问题。

什么是RPC?

Remote Procedure Call Protocol,远程过程调用。

什么是“远程”,为什么“远”?

先来看下什么是“近”,即“本地函数调用”。

当我们写下:

int result = Add(1, 2);

这行代码的时候,到底发生了什么?

  • 传递两个入参
  • 调用了本地代码段中的函数,执行运算逻辑
  • 返回一个出参

这三个动作,都发生在同一个进程空间里,这是本地函数调用

 

那有没有办法,调用一个跨进程的函数呢?

典型的,这个进程部署在另一台服务器上。


最容易想到的,两个进程约定一个协议格式,使用Socket通信,来传输:

  • 入参
  • 调用哪个函数
  • 出参

如果能够实现,那这就是“远程”过程调用。

Socket通信只能传递连续的字节流,如何将入参、函数都放到连续的字节流里呢?

假设,设计一个11字节的请求报文:

  • 前3个字节填入函数名“add”
  • 中间4个字节填入第一个参数“1”
  • 末尾4个字节填入第二个参数“2”

同理,可以设计一个4字节响应报文:

  • 4个字节填入处理结果“3”

调用方的代码可能变为:

request = MakePacket(“add”, 1, 2);

SendRequest_ToService_B(request);

response = RecieveRespnse_FromService_B();

int result = unMakePacket(respnse);

这4个步骤是:

(1)将传入参数变为字节流;

(2)将字节流发给服务B;

(3)从服务B接受返回字节流;

(4)将返回字节流变为传出参数;

 

服务方的代码可能变为:

request = RecieveRequest();

args/function = unMakePacket(request);

result = Add(1, 2);

response = MakePacket(result);

SendResponse(response);

这个5个步骤也很好理解:

(1)服务端收到字节流;

(2)将字节流转为函数名与参数;

(3)本地调用函数得到结果;

(4)将结果转变为字节流;

(5)将字节流发送给调用方;

 

这个过程用一张图描述如下:


调用方与服务方的处理步骤都是非常清晰。

这个过程存在最大的问题是什么呢?

调用方太麻烦了,每次都要关注很多底层细节:

  • 入参到字节流的转化,即序列化应用层协议细节
  • socket发送,即网络传输协议细节
  • socket接收
  • 字节流到出参的转化,即反序列化应用层协议细节

 

能不能调用层不关注这个细节?

可以,RPC框架就是解决这个问题的,它能够让调用方“像调用本地函数一样调用远端的函数(服务)”。

讲到这里,是不是对RPC,对序列化范序列化有点感觉了?往下看,有更多的底层细节。

 

RPC框架的职责是什么?

RPC框架,要向调用方屏蔽各种复杂性,要向服务提供方也屏蔽各类复杂性:

  • 服务调用方client感觉就像调用本地函数一样,来调用服务
  • 服务提供方server感觉就像实现一个本地函数一样,来实现服务

所以整个RPC框架又分为client部分server部分,实现上面的目标,把复杂性屏蔽,就是RPC框架的职责。


如上图所示,业务方的职责是:

  • 调用方A,传入参数,执行调用,拿到结果
  • 服务方B,收到参数,执行逻辑,返回结果

RPC框架的职责是,中间大蓝框的部分:

  • client端:序列化、反序列化、连接池管理、负载均衡、故障转移、队列管理,超时管理、异步管理等等
  • server端:服务端组件、服务端收发包队列、io线程、工作线程、序列化反序列化等

 

server端的技术大家了解的比较多,接下来重点讲讲client端的技术细节。

先来看看RPC-client部分的“序列化反序列化”部分。

 

为什么要进行序列化?

工程师通常使用“对象”来进行数据的操纵:

class User{

         std::String user_name;

         uint64_t user_id;

         uint32_t user_age;

};

 

User u = new User(“shenjian”);

u.setUid(123);

u.setAge(35);

 

但当需要对数据进行存储或者传输时,“对象”就不这么好用了,往往需要把数据转化成连续空间的“二进制字节流”,一些典型的场景是:

  • 数据库索引的磁盘存储:数据库的索引在内存里是b+树,但这个格式是不能够直接存储到磁盘上的,所以需要把b+树转化为连续空间的二进制字节流,才能存储到磁盘上
  • 缓存的KV存储:redis/memcache是KV类型的缓存,缓存存储的value必须是连续空间的二进制字节流,而不能够是User对象
  • 数据的网络传输:socket发送的数据必须是连续空间的二进制字节流,也不能是对象

 

所谓序列化(Serialization),就是将“对象”形态的数据转化为“连续空间二进制字节流”形态数据的过程。这个过程的逆过程叫做反序列化

 

怎么进行序列化?

这是一个非常细节的问题,要是让你来把“对象”转化为字节流,你会怎么做?很容易想到的一个方法是xml(或者json)这类具有自描述特性的标记性语言:

<class name=”User”>

<element name=”user_name” type=”std::String” value=”shenjian” />

<element name=”user_id” type=”uint64_t” value=”123” />

<element name=”user_age” type=”uint32_t” value=”35” />

</class>

规定好转换规则,发送方很容易把User类的一个对象序列化为xml,服务方收到xml二进制流之后,也很容易将其范序列化为User对象。

画外音:语言支持反射时,这个工作很容易。

 

第二个方法是自己实现二进制协议来进行序列化,还是以上面的User对象为例,可以设计一个这样的通用协议:

  • 头4个字节表示序号
  • 序号后面的4个字节表示key的长度m
  • 接下来的m个字节表示key的值
  • 接下来的4个字节表示value的长度n
  • 接下来的n个字节表示value的值
  • 像xml一样递归下去,直到描述完整个对象

 

上面的User对象,用这个协议描述出来可能是这样的:

  • 第一行:序号4个字节(设0表示类名),类名长度4个字节(长度为4),接下来4个字节是类名(”User”),共12字节
  • 第二行:序号4个字节(1表示第一个属性),属性长度4个字节(长度为9),接下来9个字节是属性名(”user_name”),属性值长度4个字节(长度为8),属性值8个字节(值为”shenjian”),共29字节
  • 第三行:序号4个字节(2表示第二个属性),属性长度4个字节(长度为7),接下来7个字节是属性名(”user_id”),属性值长度4个字节(长度为8),属性值8个字节(值为123),共27字节
  • 第四行:序号4个字节(3表示第三个属性),属性长度4个字节(长度为8),接下来8个字节是属性名(”user_name”),属性值长度4个字节(长度为4),属性值4个字节(值为35),共24字节

整个二进制字节流共12+29+27+24=92字节。

 

实际的序列化协议要考虑的细节远比这个多,例如:强类型的语言不仅要还原属性名,属性值,还要还原属性类型;复杂的对象不仅要考虑普通类型,还要考虑对象嵌套类型等。无论如何,序列化的思路都是类似的。

 

序列化协议要考虑什么因素?

不管使用成熟协议xml/json,还是自定义二进制协议来序列化对象,序列化协议设计时都需要考虑以下这些因素。

  • 解析效率:这个应该是序列化协议应该首要考虑的因素,像xml/json解析起来比较耗时,需要解析doom树,二进制自定义协议解析起来效率就很高
  • 压缩率,传输有效性:同样一个对象,xml/json传输起来有大量的xml标签,信息有效性低,二进制自定义协议占用的空间相对来说就小多了
  • 扩展性与兼容性:是否能够方便的增加字段,增加字段后旧版客户端是否需要强制升级,都是需要考虑的问题,xml/json和上面的二进制协议都能够方便的扩展
  • 可读性与可调试性:这个很好理解,xml/json的可读性就比二进制协议好很多
  • 跨语言:上面的两个协议都是跨语言的,有些序列化协议是与开发语言紧密相关的,例如dubbo的序列化协议就只能支持Java的RPC调用
  • 通用性:xml/json非常通用,都有很好的第三方解析库,各个语言解析起来都十分方便,上面自定义的二进制协议虽然能够跨语言,但每个语言都要写一个简易的协议客户端

 

有哪些常见的序列化方式?

  • xml/json:解析效率,压缩率都较差,扩展性、可读性、通用性较好
  • thrift
  • protobuf:Google出品,必属精品,各方面都不错,强烈推荐,属于二进制协议,可读性差了点,但也有类似的to-string协议帮助调试问题
  • Avro
  • CORBA
  • mc_pack:懂的同学就懂,不懂的就不懂了,09年用过,传说各方面都超越protobuf,懂行的同学可以说一下现状


RPC-client除了:

  • 序列化反序列化的部分(上图中的1、4)

还包含:

  • 发送字节流与接收字节流的部分(上图中的2、3)

这一部分,又分为同步调用与异步调用两种方式,下面一一来进行介绍。

画外音:搞通透RPC-client确实不容易。

 

同步调用的代码片段为:

Result = Add(Obj1, Obj2);// 得到Result之前处于阻塞状态

异步调用的代码片段为:

Add(Obj1, Obj2, callback);// 调用后直接返回,不等结果

处理结果通过回调为:

callback(Result){// 得到处理结果后会调用这个回调函数

         …

}

这两类调用,在RPC-client里,实现方式完全不一样。

 

RPC-client同步调用架构如何?


所谓同步调用,在得到结果之前,一直处于阻塞状态,会一直占用一个工作线程,上图简单的说明了一下组件、交互、流程步骤:

  • 左边大框,代表了调用方的一个工作线程
  • 左边粉色中框,代表了RPC-client组件
  • 右边橙色框,代表了RPC-server
  • 蓝色两个小框,代表了同步RPC-client两个核心组件,序列化组件与连接池组件
  • 白色的流程小框,以及箭头序号1-10,代表整个工作线程的串行执行步骤:

1)业务代码发起RPC调用:

Result=Add(Obj1,Obj2)

2)序列化组件,将对象调用序列化成二进制字节流,可理解为一个待发送的包packet1;

3)通过连接池组件拿到一个可用的连接connection;

4)通过连接connection将包packet1发送给RPC-server;

5)发送包在网络传输,发给RPC-server;

6)响应包在网络传输,发回给RPC-client;

7)通过连接connection从RPC-server收取响应包packet2;

8)通过连接池组件,将conneciont放回连接池;

9)序列化组件,将packet2范序列化为Result对象返回给调用方;

10)业务代码获取Result结果,工作线程继续往下走;

画外音:请对照架构图中的1-10步骤阅读。

连接池组件有什么作用?

RPC框架锁支持的负载均衡、故障转移、发送超时等特性,都是通过连接池组件去实现的。


典型连接池组件对外提供的接口为:

int ConnectionPool::init(…);

Connection ConnectionPool::getConnection();

int ConnectionPool::putConnection(Connection t);

init做了些什么?

和下游RPC-server(一般是一个集群),建立N个tcp长连接,即所谓的连接“池”。

getConnection做了些什么?

从连接“池”中拿一个连接,加锁(置一个标志位),返回给调用方。

putConnection做了些什么?

将一个分配出去的连接放回连接“池”中,解锁(也是置一个标志位)。

 

如何实现负载均衡?

连接池中建立了与一个RPC-server集群的连接,连接池在返回连接的时候,需要具备随机性。

 

如何实现故障转移?

连接池中建立了与一个RPC-server集群的连接,当连接池发现某一个机器的连接异常后,需要将这个机器的连接排除掉,返回正常的连接,在机器恢复后,再将连接加回来。

 

如何实现发送超时?

因为是同步阻塞调用,拿到一个连接后,使用带超时的send/recv即可实现带超时的发送和接收。

 

总的来说,同步的RPC-client的实现是相对比较容易的,序列化组件、连接池组件配合多工作线程数,就能够实现。

遗留问题,工作线程数设置为多少最合适?

这个问题在《工作线程数究竟要设置为多少最合适?》中讨论过,此处不再深究。

RPC-client异步回调架构如何?


所谓异步回调,在得到结果之前,不会处于阻塞状态,理论上任何时间都没有任何线程处于阻塞状态,因此异步回调的模型,理论上只需要很少的工作线程与服务连接就能够达到很高的吞吐量,如上图所示:

  • 左边的框框,是少量工作线程(少数几个就行了)进行调用与回调
  • 中间粉色的框框,代表了RPC-client组件
  • 右边橙色框,代表了RPC-server
  • 蓝色六个小框,代表了异步RPC-client六个核心组件:上下文管理器,超时管理器,序列化组件,下游收发队列,下游收发线程,连接池组件
  • 白色的流程小框,以及箭头序号1-17,代表整个工作线程的串行执行步骤:

1)业务代码发起异步RPC调用;

Add(Obj1,Obj2, callback)

2)上下文管理器,将请求,回调,上下文存储起来;

3)序列化组件,将对象调用序列化成二进制字节流,可理解为一个待发送的包packet1;

4)下游收发队列,将报文放入“待发送队列”,此时调用返回,不会阻塞工作线程;

5)下游收发线程,将报文从“待发送队列”中取出,通过连接池组件拿到一个可用的连接connection;

6)通过连接connection将包packet1发送给RPC-server;

7)发送包在网络传输,发给RPC-server;

8)响应包在网络传输,发回给RPC-client;

9)通过连接connection从RPC-server收取响应包packet2;

10)下游收发线程,将报文放入“已接受队列”,通过连接池组件,将conneciont放回连接池;

11)下游收发队列里,报文被取出,此时回调将要开始,不会阻塞工作线程;

12)序列化组件,将packet2范序列化为Result对象;

13)上下文管理器,将结果,回调,上下文取出;

14)通过callback回调业务代码,返回Result结果,工作线程继续往下走;

 

如果请求长时间不返回,处理流程是:

15)上下文管理器,请求长时间没有返回;

16)超时管理器拿到超时的上下文;

17)通过timeout_cb回调业务代码,工作线程继续往下走;

画外音:请配合架构图仔细看几遍这个流程。

 

序列化组件和连接池组件上文已经介绍过,收发队列与收发线程比较容易理解。下面重点介绍上下文管理器超时管理器这两个总的组件。

为什么需要上下文管理器?

由于请求包的发送,响应包的回调都是异步的,甚至不在同一个工作线程中完成,需要一个组件来记录一个请求的上下文,把请求-响应-回调等一些信息匹配起来。

 

如何将请求-响应-回调这些信息匹配起来?

这是一个很有意思的问题,通过一条连接往下游服务发送了a,b,c三个请求包,异步的收到了x,y,z三个响应包:


怎么知道哪个请求包与哪个响应包对应?

怎么知道哪个响应包与哪个回调函数对应?

可以通过“请求id”来实现请求-响应-回调的串联。


整个处理流程如上,通过请求id,上下文管理器来对应请求-响应-callback之间的映射关系:

1)生成请求id;

2)生成请求上下文context,上下文中包含发送时间time,回调函数callback等信息;

3)上下文管理器记录req-id与上下文context的映射关系;

4)将req-id打在请求包里发给RPC-server;

5)RPC-server将req-id打在响应包里返回;

6)由响应包中的req-id,通过上下文管理器找到原来的上下文context;

7)从上下文context中拿到回调函数callback;

8)callback将Result带回,推动业务的进一步执行;

 

如何实现负载均衡,故障转移?

与同步的连接池思路类似,不同之处在于:

  • 同步连接池使用阻塞方式收发,需要与一个服务的一个ip建立多条连接
  • 异步收发,一个服务的一个ip只需要建立少量的连接(例如,一条tcp连接)

 

如何实现超时发送与接收?

超时收发,与同步阻塞收发的实现就不一样了:

  • 同步阻塞超时,可以直接使用带超时的send/recv来实现
  • 异步非阻塞的nio的网络报文收发,由于连接不会一直等待回包,超时是由超时管理器实现的

 

超时管理器如何实现超时管理?


超时管理器,用于实现请求回包超时回调处理。

每一个请求发送给下游RPC-server,会在上下文管理器中保存req-id与上下文的信息,上下文中保存了请求很多相关信息,例如req-id,回包回调,超时回调,发送时间等。

超时管理器启动timer对上下文管理器中的context进行扫描,看上下文中请求发送时间是否过长,如果过长,就不再等待回包,直接超时回调,推动业务流程继续往下走,并将上下文删除掉。

如果超时回调执行后,正常的回包又到达,通过req-id在上下文管理器里找不到上下文,就直接将请求丢弃。

画外音:因为已经超时处理了,无法恢复上下文。

 

无论如何,异步回调和同步回调相比,除了序列化组件和连接池组件,会多出上下文管理器,超时管理器,下游收发队列,下游收发线程等组件,并且对调用方的调用习惯有影响。

画外音:编程习惯,由同步变为了回调。

异步回调能提高系统整体的吞吐量,具体使用哪种方式实现RPC-client,可以结合业务场景来选取。

总结

什么是RPC调用?

像调用本地函数一样,调用一个远端服务。

为什么需要RPC框架?

RPC框架用于屏蔽RPC调用过程中的序列化,网络传输等技术细节。让调用方只专注于调用,服务方只专注于实现调用。

什么是序列化?为什么需要序列化?

把对象转化为连续二进制流的过程,叫做序列化。磁盘存储,缓存存储,网络传输只能操作于二进制流,所以必须序列化。

同步RPC-client的核心组件是什么?

同步RPC-client的核心组件是序列化组件、连接池组件。它通过连接池来实现负载均衡与故障转移,通过阻塞的收发来实现超时处理。

异步RPC-client的核心组件是什么?

异步RPC-client的核心组件是序列化组件、连接池组件、收发队列、收发线程、上下文管理器、超时管理器。它通过“请求id”来关联请求包-响应包-回调函数,用上下文管理器来管理上下文,用超时管理器中的timer触发超时回调,推进业务流程的超时处理。

思路比结论重要。

原文:https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651961985&idx=1&sn=6f757843f5c159eab00d847e9c2cc995&chksm=bd2d0f5d8a5a864b05fada6919204378134e174f1105a0716dd879845b0d365c913ef8e94a12&scene=21#wechat_redirect

MySQL 加锁处理分析

一、背景

MySQL/InnoDB的加锁分析,一直是一个比较困难的话题。我在工作过程中,经常会有同事咨询这方面的问题。同时,微博上也经常会收到MySQL锁相关的私信,让我帮助解决一些死锁的问题。本文,准备就MySQL/InnoDB的加锁问题,展开较为深入的分析与讨论,主要是介绍一种思路,运用此思路,拿到任何一条SQL语句,都能完整的分析出这条语句会加什么锁?会有什么样的使用风险?甚至是分析线上的一个死锁场景,了解死锁产生的原因。

注:MySQL是一个支持插件式存储引擎的数据库系统。本文下面的所有介绍,都是基于InnoDB存储引擎,其他引擎的表现,会有较大的区别。

1.1  MVCC:Snapshot Read vs Current Read

MySQL InnoDB存储引擎,实现的是基于多版本的并发控制协议——MVCC (Multi-Version Concurrency Control) (注:与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control)。MVCC最大的好处,相信也是耳熟能详:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能,这也是为什么现阶段,几乎所有的RDBMS,都支持了MVCC。

 

在MVCC并发控制中,读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

 

在一个支持MVCC并发控制的系统中,哪些读操作是快照读?哪些操作又是当前读呢?以MySQL InnoDB为例:

 

  • 快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外,下面会分析)
    • select * from table where ?;

     

  • 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
    • select * from table where ? lock in share mode;
    • select * from table where ? for update;
    • insert into table values (…);
    • update table set ? where ?;
    • delete from table where ?;

    所有以上的语句,都属于当前读,读取记录的最新版本。并且,读取之后,还需要保证其他并发事务不能修改当前记录,对读取记录加锁。其中,除了第一条语句,对读取记录加S锁 (共享锁)外,其他的操作,都加的是X锁 (排它锁)。

     

为什么将 插入/更新/删除 操作,都归为当前读?可以看看下面这个 更新 操作,在数据库中的执行流程:

 

 

从图中,可以看到,一个Update操作的具体流程。当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁 (current read)。待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了一个当前读。同理,Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查,也会进行一个当前读。

 

:根据上图的交互,针对一条当前读的SQL语句,InnoDB与MySQL Server的交互,是一条一条进行的,因此,加锁也是一条一条进行的。先对一条满足条件的记录加锁,返回给MySQL Server,做一些DML操作;然后在读取下一条加锁,直至读取完毕。

1.2 Cluster Index:聚簇索引

InnoDB存储引擎的数据组织方式,是聚簇索引表:完整的记录,存储在主键索引中,通过主键索引,就可以获取记录所有的列。关于聚簇索引表的组织方式,可以参考MySQL的官方文档:Clustered and Secondary Indexes 。本文假设读者对这个,已经有了一定的认识,就不再做具体的介绍。接下来的部分,主键索引/聚簇索引 两个名称,会有一些混用,望读者知晓。

1.3  2PL:Two-Phase Locking

传统RDBMS加锁的一个原则,就是2PL (二阶段锁):Two-Phase Locking。相对而言,2PL比较容易理解,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。下面,仍旧以MySQL为例,来简单看看2PL在MySQL中的实现。

 

从上图可以看出,2PL就是将加锁/解锁分为两个完全不相交的阶段。加锁阶段:只加锁,不放锁。解锁阶段:只放锁,不加锁。

 

1.4  Isolation Level

隔离级别:Isolation Level,也是RDBMS的一个关键特性。相信对数据库有所了解的朋友,对于4种隔离级别:Read Uncommited,Read Committed,Repeatable Read,Serializable,都有了深入的认识。本文不打算讨论数据库理论中,是如何定义这4种隔离级别的含义的,而是跟大家介绍一下MySQL/InnoDB是如何定义这4种隔离级别的。

 

MySQL/InnoDB定义的4种隔离级别:

  • Read Uncommited

    可以读取未提交记录。此隔离级别,不会使用,忽略。

  • Read Committed (RC)

    快照读忽略,本文不考虑。

    针对当前读,RC隔离级别保证对读取到的记录加锁 (记录锁),存在幻读现象。

  • Repeatable Read (RR)

    快照读忽略,本文不考虑。

    针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象。

  • Serializable

    从MVCC并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。

    Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。

二、一条简单SQL的加锁实现分析

在介绍完一些背景知识之后,本文接下来将选择几个有代表性的例子,来详细分析MySQL的加锁处理。当然,还是从最简单的例子说起。经常有朋友发给我一个SQL,然后问我,这个SQL加什么锁?就如同下面两条简单的SQL,他们加什么锁?

 

  • SQL1:select * from t1 where id = 10;
  • SQL2:delete from t1 where id = 10;

 

针对这个问题,该怎么回答?我能想象到的一个答案是:

 

  • SQL1:不加锁。因为MySQL是使用多版本并发控制的,读不加锁。
  • SQL2:对id = 10的记录加写锁 (走主键索引)。

 

这个答案对吗?说不上来。即可能是正确的,也有可能是错误的,已知条件不足,这个问题没有答案。如果让我来回答这个问题,我必须还要知道以下的一些前提,前提不同,我能给出的答案也就不同。要回答这个问题,还缺少哪些前提条件?

 

  • 前提一:id列是不是主键?

 

  • 前提二:当前系统的隔离级别是什么?
  • 前提三:id列如果不是主键,那么id列上有索引吗?
  • 前提四:id列上如果有二级索引,那么这个索引是唯一索引吗?
  • 前提五:两个SQL的执行计划是什么?索引扫描?全表扫描?

 

没有这些前提,直接就给定一条SQL,然后问这个SQL会加什么锁,都是很业余的表现。而当这些问题有了明确的答案之后,给定的SQL会加什么锁,也就一目了然。下面,我将这些问题的答案进行组合,然后按照从易到难的顺序,逐个分析每种组合下,对应的SQL会加哪些锁?

 

注:下面的这些组合,我做了一个前提假设,也就是有索引时,执行计划一定会选择使用索引进行过滤 (索引扫描)。但实际情况会复杂很多,真正的执行计划,还是需要根据MySQL输出的为准。

 

  • 组合一:id列是主键,RC隔离级别
  • 组合二:id列是二级唯一索引,RC隔离级别
  • 组合三:id列是二级非唯一索引,RC隔离级别
  • 组合四:id列上没有索引,RC隔离级别
  • 组合五:id列是主键,RR隔离级别
  • 组合六:id列是二级唯一索引,RR隔离级别
  • 组合七:id列是二级非唯一索引,RR隔离级别
  • 组合八:id列上没有索引,RR隔离级别
  • 组合九:Serializable隔离级别

 

排列组合还没有列举完全,但是看起来,已经很多了。真的有必要这么复杂吗?事实上,要分析加锁,就是需要这么复杂。但是从另一个角度来说,只要你选定了一种组合,SQL需要加哪些锁,其实也就确定了。接下来,就让我们来逐个分析这9种组合下的SQL加锁策略。

 

注:在前面八种组合下,也就是RC,RR隔离级别下,SQL1:select操作均不加锁,采用的是快照读,因此在下面的讨论中就忽略了,主要讨论SQL2:delete操作的加锁。

2.1 组合一:id主键+RC

这个组合,是最简单,最容易分析的组合。id是主键,Read Committed隔离级别,给定SQL:delete from t1 where id = 10; 只需要将主键上,id = 10的记录加上X锁即可。如下图所示:

结论:id是主键时,此SQL只需要在id=10这条记录上加X锁即可。

2.2 组合二:id唯一索引+RC

这个组合,id不是主键,而是一个Unique的二级索引键值。那么在RC隔离级别下,delete from t1 where id = 10; 需要加什么锁呢?见下图:

此组合中,id是unique索引,而主键是name列。此时,加锁的情况由于组合一有所不同。由于id是unique索引,因此delete语句会选择走id列的索引进行where条件的过滤,在找到id=10的记录后,首先会将unique索引上的id=10索引记录加上X锁,同时,会根据读取到的name列,回主键索引(聚簇索引),然后将聚簇索引上的name = ‘d’ 对应的主键索引项加X锁。为什么聚簇索引上的记录也要加锁?试想一下,如果并发的一个SQL,是通过主键索引来更新:update t1 set id = 100 where name = ‘d’; 此时,如果delete语句没有将主键索引上的记录加锁,那么并发的update就会感知不到delete语句的存在,违背了同一记录上的更新/删除需要串行执行的约束。

 

结论:若id列是unique列,其上有unique索引。那么SQL需要加两个X锁,一个对应于id unique索引上的id = 10的记录,另一把锁对应于聚簇索引上的[name=’d’,id=10]的记录。

2.3  组合三:id非唯一索引+RC

相对于组合一、二,组合三又发生了变化,隔离级别仍旧是RC不变,但是id列上的约束又降低了,id列不再唯一,只有一个普通的索引。假设delete from t1 where id = 10; 语句,仍旧选择id列上的索引进行过滤where条件,那么此时会持有哪些锁?同样见下图:

根据此图,可以看到,首先,id列索引上,满足id = 10查询条件的记录,均已加锁。同时,这些记录对应的主键索引上的记录也都加上了锁。与组合二唯一的区别在于,组合二最多只有一个满足等值查询的记录,而组合三会将所有满足查询条件的记录都加锁。

 

结论:若id列上有非唯一索引,那么对应的所有满足SQL查询条件的记录,都会被加锁。同时,这些记录在主键索引上的记录,也会被加锁。

2.4 组合四:id无索引+RC

相对于前面三个组合,这是一个比较特殊的情况。id列上没有索引,where id = 10;这个过滤条件,没法通过索引进行过滤,那么只能走全表扫描做过滤。对应于这个组合,SQL会加什么锁?或者是换句话说,全表扫描时,会加什么锁?这个答案也有很多:有人说会在表上加X锁;有人说会将聚簇索引上,选择出来的id = 10;的记录加上X锁。那么实际情况呢?请看下图:

由于id列上没有索引,因此只能走聚簇索引,进行全部扫描。从图中可以看到,满足删除条件的记录有两条,但是,聚簇索引上所有的记录,都被加上了X锁。无论记录是否满足条件,全部被加上X锁。既不是加表锁,也不是在满足条件的记录上加行锁。

 

有人可能会问?为什么不是只在满足条件的记录上加锁呢?这是由于MySQL的实现决定的。如果一个条件无法通过索引快速过滤,那么存储引擎层面就会将所有记录加锁后返回,然后由MySQL Server层进行过滤。因此也就把所有的记录,都锁上了。

 

注:在实际的实现中,MySQL有一些改进,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录放锁 (违背了2PL的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。

 

结论:若id列上没有索引,SQL会走聚簇索引的全扫描进行过滤,由于过滤是由MySQL Server层面进行的。因此每条记录,无论是否满足条件,都会被加上X锁。但是,为了效率考量,MySQL做了优化,对于不满足条件的记录,会在判断后放锁,最终持有的,是满足条件的记录上的锁,但是不满足条件的记录上的加锁/放锁动作不会省略。同时,优化也违背了2PL的约束。

2.5 组合五:id主键+RR

上面的四个组合,都是在Read Committed隔离级别下的加锁行为,接下来的四个组合,是在Repeatable Read隔离级别下的加锁行为。

 

组合五,id列是主键列,Repeatable Read隔离级别,针对delete from t1 where id = 10; 这条SQL,加锁与组合一:[id主键,Read Committed]一致。

2.6  组合六:id唯一索引+RR

与组合五类似,组合六的加锁,与组合二:[id唯一索引,Read Committed]一致。两个X锁,id唯一索引满足条件的记录上一个,对应的聚簇索引上的记录一个。

2.7   组合七:id非唯一索引+RR

还记得前面提到的MySQL的四种隔离级别的区别吗?RC隔离级别允许幻读,而RR隔离级别,不允许存在幻读。但是在组合五、组合六中,加锁行为又是与RC下的加锁行为完全一致。那么RR隔离级别下,如何防止幻读呢?问题的答案,就在组合七中揭晓。

 

组合七,Repeatable Read隔离级别,id上有一个非唯一索引,执行delete from t1 where id = 10; 假设选择id列上的索引进行条件过滤,最后的加锁行为,是怎么样的呢?同样看下面这幅图:

此图,相对于组合三:[id列上非唯一锁,Read Committed]看似相同,其实却有很大的区别。最大的区别在于,这幅图中多了一个GAP锁,而且GAP锁看起来也不是加在记录上的,倒像是加载两条记录之间的位置,GAP锁有何用?

 

其实这个多出来的GAP锁,就是RR隔离级别,相对于RC隔离级别,不会出现幻读的关键。确实,GAP锁锁住的位置,也不是记录本身,而是两条记录之间的GAP。所谓幻读,就是同一个事务,连续做两次当前读 (例如:select * from t1 where id = 10 for update;),那么这两次当前读返回的是完全相同的记录 (记录数量一致,记录本身也一致),第二次的当前读,不会比第一次返回更多的记录 (幻象)。

 

如何保证两次当前读返回一致的记录,那就需要在第一次当前读与第二次当前读之间,其他的事务不会插入新的满足条件的记录并提交。为了实现这个功能,GAP锁应运而生。

 

如图中所示,有哪些位置可以插入新的满足条件的项 (id = 10),考虑到B+树索引的有序性,满足条件的项一定是连续存放的。记录[6,c]之前,不会插入id=10的记录;[6,c]与[10,b]间可以插入[10, aa];[10,b]与[10,d]间,可以插入新的[10,bb],[10,c]等;[10,d]与[11,f]间可以插入满足条件的[10,e],[10,z]等;而[11,f]之后也不会插入满足条件的记录。因此,为了保证[6,c]与[10,b]间,[10,b]与[10,d]间,[10,d]与[11,f]不会插入新的满足条件的记录,MySQL选择了用GAP锁,将这三个GAP给锁起来。

 

Insert操作,如insert [10,aa],首先会定位到[6,c]与[10,b]间,然后在插入前,会检查这个GAP是否已经被锁上,如果被锁上,则Insert不能插入记录。因此,通过第一遍的当前读,不仅将满足条件的记录锁上 (X锁),与组合三类似。同时还是增加3把GAP锁,将可能插入满足条件记录的3个GAP给锁上,保证后续的Insert不能插入新的id=10的记录,也就杜绝了同一事务的第二次当前读,出现幻象的情况。

 

有心的朋友看到这儿,可以会问:既然防止幻读,需要靠GAP锁的保护,为什么组合五、组合六,也是RR隔离级别,却不需要加GAP锁呢?

 

首先,这是一个好问题。其次,回答这个问题,也很简单。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。而组合五,id是主键;组合六,id是unique键,都能够保证唯一性。一个等值查询,最多只能返回一条记录,而且新的相同取值的记录,一定不会在新插入进来,因此也就避免了GAP锁的使用。其实,针对此问题,还有一个更深入的问题:如果组合五、组合六下,针对SQL:select * from t1 where id = 10 for update; 第一次查询,没有找到满足查询条件的记录,那么GAP锁是否还能够省略?此问题留给大家思考。

 

结论:Repeatable Read隔离级别下,id列上有一个非唯一索引,对应SQL:delete from t1 where id = 10; 首先,通过id索引定位到第一条满足查询条件的记录,加记录上的X锁,加GAP上的GAP锁,然后加主键聚簇索引上的记录X锁,然后返回;然后读取下一条,重复进行。直至进行到第一条不满足条件的记录[11,f],此时,不需要加记录X锁,但是仍旧需要加GAP锁,最后返回结束。

2.8  组合八:id无索引+RR

组合八,Repeatable Read隔离级别下的最后一种情况,id列上没有索引。此时SQL:delete from t1 where id = 10; 没有其他的路径可以选择,只能进行全表扫描。最终的加锁情况,如下图所示:

 

如图,这是一个很恐怖的现象。首先,聚簇索引上的所有记录,都被加上了X锁。其次,聚簇索引每条记录间的间隙(GAP),也同时被加上了GAP锁。这个示例表,只有6条记录,一共需要6个记录锁,7个GAP锁。试想,如果表上有1000万条记录呢?

 

在这种情况下,这个表上,除了不加锁的快照度,其他任何加锁的并发SQL,均不能执行,不能更新,不能删除,不能插入,全表被锁死。

 

当然,跟组合四:[id无索引, Read Committed]类似,这个情况下,MySQL也做了一些优化,就是所谓的semi-consistent read。semi-consistent read开启的情况下,对于不满足查询条件的记录,MySQL会提前放锁。针对上面的这个用例,就是除了记录[d,10],[g,10]之外,所有的记录锁都会被释放,同时不加GAP锁。semi-consistent read如何触发:要么是read committed隔离级别;要么是Repeatable Read隔离级别,同时设置了innodb_locks_unsafe_for_binlog 参数。更详细的关于semi-consistent read的介绍,可参考我之前的一篇博客:MySQL+InnoDB semi-consitent read原理及实现分析 。

 

结论:在Repeatable Read隔离级别下,如果进行全表扫描的当前读,那么会锁上表中的所有记录,同时会锁上聚簇索引内的所有GAP,杜绝所有的并发 更新/删除/插入 操作。当然,也可以通过触发semi-consistent read,来缓解加锁开销与并发影响,但是semi-consistent read本身也会带来其他问题,不建议使用。

2.9  组合九:Serializable

针对前面提到的简单的SQL,最后一个情况:Serializable隔离级别。对于SQL2:delete from t1 where id = 10; 来说,Serializable隔离级别与Repeatable Read隔离级别完全一致,因此不做介绍。

 

Serializable隔离级别,影响的是SQL1:select * from t1 where id = 10; 这条SQL,在RC,RR隔离级别下,都是快照读,不加锁。但是在Serializable隔离级别,SQL1会加读锁,也就是说快照读不复存在,MVCC并发控制降级为Lock-Based CC。

 

结论:在MySQL/InnoDB中,所谓的读不加锁,并不适用于所有的情况,而是隔离级别相关的。Serializable隔离级别,读不加锁就不再成立,所有的读操作,都是当前读。

三、一条复杂的SQL

写到这里,其实MySQL的加锁实现也已经介绍的八八九九。只要将本文上面的分析思路,大部分的SQL,都能分析出其会加哪些锁。而这里,再来看一个稍微复杂点的SQL,用于说明MySQL加锁的另外一个逻辑。SQL用例如下:

 

如图中的SQL,会加什么锁?假定在Repeatable Read隔离级别下 (Read Committed隔离级别下的加锁情况,留给读者分析。),同时,假设SQL走的是idx_t1_pu索引。

 

在详细分析这条SQL的加锁情况前,还需要有一个知识储备,那就是一个SQL中的where条件如何拆分?具体的介绍,建议阅读我之前的一篇文章:SQL中的where条件,在数据库中提取与应用浅析 。在这里,我直接给出分析后的结果:

 

  • Index key:pubtime > 1 and puptime < 20。此条件,用于确定SQL在idx_t1_pu索引上的查询范围。

     

  • Index Filter:userid = ‘hdc’ 。此条件,可以在idx_t1_pu索引上进行过滤,但不属于Index Key。
  • Table Filter:comment is not NULL。此条件,在idx_t1_pu索引上无法过滤,只能在聚簇索引上过滤。

 

在分析出SQL where条件的构成之后,再来看看这条SQL的加锁情况 (RR隔离级别),如下图所示:

 

从图中可以看出,在Repeatable Read隔离级别下,由Index Key所确定的范围,被加上了GAP锁;Index Filter锁给定的条件 (userid = ‘hdc’)何时过滤,视MySQL的版本而定,在MySQL 5.6版本之前,不支持Index Condition Pushdown(ICP),因此Index Filter在MySQL Server层过滤,在5.6后支持了Index Condition Pushdown,则在index上过滤。若不支持ICP,不满足Index Filter的记录,也需要加上记录X锁,若支持ICP,则不满足Index Filter的记录,无需加记录X锁 (图中,用红色箭头标出的X锁,是否要加,视是否支持ICP而定);而Table Filter对应的过滤条件,则在聚簇索引中读取后,在MySQL Server层面过滤,因此聚簇索引上也需要X锁。最后,选取出了一条满足条件的记录[8,hdc,d,5,good],但是加锁的数量,要远远大于满足条件的记录数量。

 

结论:在Repeatable Read隔离级别下,针对一个复杂的SQL,首先需要提取其where条件。Index Key确定的范围,需要加上GAP锁;Index Filter过滤条件,视MySQL版本是否支持ICP,若支持ICP,则不满足Index Filter的记录,不加X锁,否则需要X锁;Table Filter过滤条件,无论是否满足,都需要加X锁。

四、死锁原理与分析 

本文前面的部分,基本上已经涵盖了MySQL/InnoDB所有的加锁规则。深入理解MySQL如何加锁,有两个比较重要的作用:

 

  • 可以根据MySQL的加锁规则,写出不会发生死锁的SQL;

     

  • 可以根据MySQL的加锁规则,定位出线上产生死锁的原因;

下面,来看看两个死锁的例子 (一个是两个Session的两条SQL产生死锁;另一个是两个Session的一条SQL,产生死锁):

 

 

上面的两个死锁用例。第一个非常好理解,也是最常见的死锁,每个事务执行两条SQL,分别持有了一把锁,然后加另一把锁,产生死锁。

 

第二个用例,虽然每个Session都只有一条语句,仍旧会产生死锁。要分析这个死锁,首先必须用到本文前面提到的MySQL加锁的规则。针对Session 1,从name索引出发,读到的[hdc, 1],[hdc, 6]均满足条件,不仅会加name索引上的记录X锁,而且会加聚簇索引上的记录X锁,加锁顺序为先[1,hdc,100],后[6,hdc,10]。而Session 2,从pubtime索引出发,[10,6],[100,1]均满足过滤条件,同样也会加聚簇索引上的记录X锁,加锁顺序为[6,hdc,10],后[1,hdc,100]。发现没有,跟Session 1的加锁顺序正好相反,如果两个Session恰好都持有了第一把锁,请求加第二把锁,死锁就发生了。

 

结论:死锁的发生与否,并不在于事务中有多少条SQL语句,死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。而使用本文上面提到的,分析MySQL每条SQL语句的加锁规则,分析出每条语句的加锁顺序,然后检查多个并发SQL间是否存在以相反的顺序加锁的情况,就可以分析出各种潜在的死锁情况,也可以分析出线上死锁发生的原因。

五、总结

写到这儿,本文也告一段落,做一个简单的总结,要做的完全掌握MySQL/InnoDB的加锁规则,甚至是其他任何数据库的加锁规则,需要具备以下的一些知识点:

 

  • 了解数据库的一些基本理论知识:数据的存储格式 (堆组织表 vs 聚簇索引表);并发控制协议 (MVCC vs Lock-Based CC);Two-Phase Locking;数据库的隔离级别定义 (Isolation Level);
  • 了解SQL本身的执行计划 (主键扫描 vs 唯一键扫描 vs 范围扫描 vs 全表扫描);
  • 了解数据库本身的一些实现细节 (过滤条件提取;Index Condition Pushdown;Semi-Consistent Read);
  • 了解死锁产生的原因及分析的方法 (加锁顺序不一致;分析每个SQL的加锁顺序)

 

有了这些知识点,再加上适当的实战经验,全面掌控MySQL/InnoDB的加锁规则,当不在话下。

原文:http://hedengcheng.com/?p=771

半小时漫画中国史2

一、西汉

  1. 西汉历史:刘邦—>文景之治—>汉武帝—>昭宣中兴—>汉成帝
  2. 汉高祖刘邦的“非刘氏而王,天下共击之”的“白马之盟”,在他死后,吕后掌权,后吕雉去世后,吕家被灭门。
  3. 汉文帝和儿子汉景帝,倡导休养生息,史称文景之治。
  4. 之后的汉武帝刘彻,连年发起战争开疆拓土,家底差点败光。
  5. 汉昭帝汉宣帝,开始给汉武帝还债,比较消停,人民生活水平提高,史称昭宣中兴。
  6. 汉成帝荒淫无度,还导致外戚擅权,为西汉的无能代表。
  7.  因为外戚擅权,王姓的太后提拔自己的亲戚,最后出现了王莽篡汉

二、王莽篡汉

  1. 西汉—>新朝—>东汉。
  2. 因为外戚擅权,所以导致了王莽篡汉,之后因为施政想法太过超前,后来被起义所杀。

三、东汉

  1. 东汉从刘秀开始,与民休息,一直到汉和帝,都是好皇帝。
  2. 从汉和帝开始,外戚政治开始死灰复燃,被几位太后一直把持朝政。
  3. 小皇帝找宦官帮忙,宦官赶走了外戚政治,但是宦官也开始做大,开始与士大夫的党锢之祸。
  4. 在宦官和士大夫时争斗时,开始了黄巾军的黄巾起义。之后黄巾起义被镇压,但在镇压的过程中,产生了许多大将。之后就开始了三国。