dream

一个菜鸟程序员的成长历程

0%

设计原则之里氏替换原则–子类可以替换父类吗

里氏替换原则(Liskov Substitution Principle)LSP,这个原则是说子类应该可以替换父类进行使用。

这个原则的英文描述是functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。

为什么要使用里氏替换原则

在我们的日常开发中很常用依赖注入,简单来说就是通过外部传入对象而不是内部new对象。传入参数的时候我们可以通过标识父类或者接口。

1
2
3
4
5
function demo(Idemo $demo) {

$demo->run();

}

在这个函数中,我不关心你传进来的是个什么玩意,只要你实现了Idemo这个接口的对象就可以。或者是继承了Idemo这个类的自类对象也可以,只要你实现了run这个方法,并且属于Idemo这个类或者接口。我们经常使用这个方法来增加程序的测试行,扩展性,灵活性。比如我们需要一个支付接口的时候。

1
2
3
4

function buy(Payment $payment) {
$payment->pay();
}

我们购买东西的时候,只要知道我要支付就可以了,至于你传进来的是支付宝支付还是微信支付我不需要知道。

在这里面,支付宝或者微信支付就是具体的自类,而Payment就是他们的父类。这些子类对象可以替换他们的父类对象,并且保证原来程序的逻辑行为不变及正确性不被破坏。

里氏替换和多态

其实我们上面使用的就是面向对象里面的多态。但是多态并不是里氏替换。类的多态特性允许我们重新实现父类的函数,我们可以根据不同的需求来改写。但是里氏替换原则要求不能修改原来的逻辑行为和正确性。

比如父类中这个函数返回了异常而子类没有,那么这就违反了里氏替换原则。

比如原来内容是123,而子类改成了456,那么同样违反了里氏替换原则。

参考资料:

  • 大话设计模式
  • 极客时间设计模式之美

设计原则之开闭原则–对扩展开发对修改关闭

开闭原则(Open Closed Principle) OCP,这个原则是说对扩展可以开放,但是对修改要关闭。

这个原则的英文描述是software entities(modules,classes,functions,etc.) should be open for extension, but closed for modification.

为什么要使用开闭原则

引用大话设计模式书中的故事。在香港澳门回归的时候,我国使用了一国两制的方针。这其实就是没有修改任何地方,但却扩展了我国的领土。这也就是对修改关闭,对扩展开放。

开放封闭原则,是说软件实体应该可以扩展,但是不可修改。

在现在的开发中,改需求是一个很常见的事情,我们经常需要这个需求要改成这样,明天上线!!这种事情。这时候我们加班加点改完一个类实现了新需求。这个时候发现由于这个类的依赖太多,我们的修改引起了其他地方的bug,那我们需要整体测试,然后各种修改,还可能引起更多的bug,如此耗时耗力。

在思考另外一个场景,同样我们实现了需求,这个时候没出其他问题,但是!!!这个需求要改回去,你把之前的代码拿出来吧。我相信你一定很不爽的,如果我们可以通过git来回滚到之前的代码还好,如果我们之前的代码在本地没有提交这个时候我们又修改完了,那,呵呵,再改回去吧,鬼知道之前代码啥样。

如果我们遵循开闭原则,不是通过修改类来完成,而是通过扩展类的方式来完成这个新需求,那么我们就避免了这些问题,扩展不会引起其他的未知bug。不会影响到其他的类。如果要改回去我们原来的代码也还在。这就是开闭原则的好处了。

如何使用开闭原则

无论模块是多么的封闭,都会存在一些无法对之封闭的变化。既然不可能完全封闭,设计人员必须对于他设计的模块应该对那种变化封闭做出选择。它必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离哪些变化

我们开发中,最开始的时候,很难预测接下来的变化。但是,一旦发生变化我们可以根据变化来采取行动,进行抽象重构。使得接下来通过扩展和少量的必要修改来应对需求的变化。

开闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所生成的巨大好处,也就是可维护,可扩展,可复用,灵活性好。我们应该仅对程序中呈现出频繁变化的那些部分做出抽象,然而,对于程序中的每个部分都刻意的进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要。

我们可以在开发的开始阶段对接下来能预测到的变化留出扩展空间,然后再不断的持续重构开发代码中一直遵循开闭原则。

很多设计模式都是以提高扩展性为目的。最常用的方法有:多态,依赖注入,基于接口编程等。

参考资料:

  • 大话设计模式
  • 极客时间设计模式之美

设计原则之单一职责–为什么要设计成单一职责

单一职责原则(Single Responsibility Principle) SRP的意思是,就一个类而言,应该仅有一个引起他变化的原因。

这个原则的英文描述是A class or module should have a single reponsibility。一个类或者模块只有一个职责。

为什么要使用单一职责

借用大话设计模式一书中使用的例子。比如现在科技高度发达,手机成为了日常使用中不可替代的产品。手机的功能很多,可以拍照,可以上网,可以打电话,聊天,打游戏等。假设我们没有手机,那么我们拍照需要摄像机,打游戏需要游戏机,听歌需要mp3,但是现在不用带这么多东西,只需要一个手机就搞定了。觉得很方便。但是呢,手机的拍照功能并没有单反好,游戏性能和游戏数量也比不上ps4和xbox以及ns。

如果放在代码中就是你的所有代码都写在一起,像一团乱麻一样。严重影响了可读性,扩展性,维护性等。相信没有人喜欢看到这样的代码。所以这就是单一职责。对代码要有结构,要清晰明了,其实代码中到处都体现了这个原则。比如mvc的分层架构,比如前后端分离,比如docker的兴起,比如我们日常使用的框架也分离了我们的职责,使得我们只需要关注一些实现就好了。再生活中也有比如公司分成了各个部门,人类分成了男人女人,由各个职业。每个人只需要关注自己的事情,简单明了。这就是单一职责的好处。

如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个变化会引起其他意想不到的变化,使得整个程序脆弱不堪。我们经常说要写高内聚,低耦合的代码,如果违反了单一职责那么就只能写出高耦合的代码了。

如何使用单一职责

如果你能想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责。如果类的职责多了,我们就需要把他们分离开来,当然了,这个原则比较具有主观性。有的人觉得这个类不是单一职责,有的人觉得是单一职责。

判断是否单一职责要结合具体的业务场景和使用,如果你有多个引起这个类变化的地方,那么就需要重新考虑一下这个类是否符合单一职责了。

在我们平常开发中,没必要一开始纠结是否符合单一职责,随着开发的演进,程序越来越庞大,我们需要随着业务的发展将类分的越来越细才好,要持续重构。一定不要等到已经往一个类里面塞了很多代码才考虑划分成单一职责。那样的话拆分起来很费劲而且耗时耗力。在添加新功能的时候要想一想这个功能应该添加到哪里,是否符合单一职责

开发中可以根据下面几点来判断是否满足单一职责:

  • 类中的代码过多,影响了代码的可读性和维护性。
  • 和这个类有依赖关系的类比较多,耦合过高。
  • 比较难给类命名,不清楚这个类是干啥的了。
  • 类中大量的方法都是集中操作类的某几个属性。

类的职责是否越单一越好

显然不是的,如果一个类过于职责单一,比如加密类,有一个加密方法和解密方法。

1
2
3
4
5
6
7
8
9
class hash {
public function encode() {
//...
}

public function decode() {
//...
}
}

如果我们执着于单一职责,要把这个类拆分成两个类。一个加密类一个解密类。

1
2
3
4
5
6
7
8
9
10
11
12
class hashDecode{
public function decode() {

}
}

class hashEncode{
public function encode() {

}
}

如果这时候根据新的需求,我们的加密算法需要改变,那么解密算法也需要改变,我们要同时改变两个类,如果忘记其中一个,那么整个程序就会崩溃。我们的最终目标是写出高内聚低耦合的代码。而不是生搬硬套某一个设计原则或者设计模式。设计原则是总结出来指导我们进行程序设计的方针。合理使用可以使得我们的程序更加健壮

参考资料:

  • 大话设计模式
  • 极客时间设计模式之美

贫血模型

什么是贫血模型

贫血模型就是缺血了,缺东西,也就是只有数据但是没有业务逻辑或者有业务逻辑但是没有数据。

比如你有一个计算类,他有一个加法计算的方法。但是他不持有计算的数据。

和贫血模型对应的就是充血模型。

什么是充血模型

充血模型就是不缺血了,有数据同样有业务逻辑。

比如你的计算类现在不只有加法计算,还有需要的数据。

我们现在进行的开发基本上都是基于贫血模型开发的。

比如一个电商系统,有商品模型,但是一个商品模型只有商品的基本信息,数据。如果需要获取一个商品的总价,那么我们需要调用model里面的一个方法来计算。这就是典型的贫血模型。

如果我们在商品模型里面提供一个计算总价的方法。把数据和业务逻辑放在一起,这就是充血模型。

充血模型

充血模型要将数据和业务高度内聚在一个类中,使得类更加饱满,也是高内聚低耦合的一种实践。不过充血模型需要更好的设计整个类才能实现。因为要考虑到扩展性,测试性,可读性,复用性等。充血模型注定比贫血模型更加耗费时间,更加难以实现。

现在更推崇敏捷开发。快速实现需求,慢慢迭代升级。如果使用充血模型也容易搞得四不像。也因此贫血模型更让人喜欢。贫血模型也更容易设计和实现,日常开发中随便写的都是贫血模型。而充血模型需要进行好好的设计。那怎么设计充血模型呢?可以根据DDD领域驱动设计来实现。

领域驱动设计DDD

DDD的概念很早就有了,但是一直没有火起来。现在又出现了充血模型的概念。

DDD的核心思想其实就是根据不同的领域,功能来设计建模,划分模块。微服务的划分就可以借鉴DDD的思想,根据不同的业务领域来划分。

DDD可以用来指导我们如果做软件设计,但是想要用好DDD,需要对自身的业务足够理解,足够熟练。

领域是一个组织所做的事情以及其包含的一切。领域驱动设计就是从自身的领域出发,分析自身领域内的一切关联关系,根据其设计我们的软件。构造充血模型。也就是充血模型是和业务高度耦合的,完全从自身的领域出发进行设计。可以先将自身的领域细分出多个子领域,然后再每个子领域中设计出各自的领域模型。也就是充血模型。

比如电商领域可以分成用户,商品,订单,物流等子领域。然后设计出各自的实体对象,根据全局领域的上下文来分析需要承担哪些责任也就是哪些数据和业务逻辑。

为什么很多人不推荐mysql连表join查询

join查询是什么?是连表查询,我们需要两个表的数据,就会使用join来进行连表。那么mysql里面是怎么连表的呢?它和我们自己查询出一张表的数据在遍历去查询另外一个表是不是一样呢?

join查询

join查询还可以写成left join,表示的是根据左边的表来查询右边的表。但实际上,优化器会进行优化,选择合适的表来做驱动表,不一定是左边的表。

Index Nested-Loop Join

NLJ算法,这是一种基于索引的算法,如果你的连表用到了索引,则会使用这个算法。

比如下面的查询语句:

b.uid字段有索引,并且假设a是小表,优化器选择了a表作为驱动表,b表作为被驱动表。

1
select * from a left join b on a.id = b.uid;

执行流程大致如下:

  1. 读取表a的第一行数据
  2. 使用表a的id去b表的索引树上查询到对应的b表id
  3. 通过查询的b表id回表查询b表数据
  4. 把a表和b表数据放在一起
  5. 重复上面的步骤直到a表遍历完成
  6. 返回数据

可以看到这和我们自己查询出一张表然后遍历查询过程是一样的。

但是它的优势是省略了多次连接数据库,连接数据库是比较耗费资源和时间的,这样来看,使用join是更加好的选择。当然前提是你的join被驱动表有索引。

这个过程扫描了整个表a和表a的id对应的每一行表b。假设a表的数据量是n,那么扫描了n + n行,当然不算回表。如果算上回表其实相当于n + n + n行。

我们再看一下这个算法的时间复杂度。

b+树中,定位一个记录的时间复杂度大约是log(m)。m是b表的数据行数。为什么这里使用了b表的数据行数而不是上面说的a表的行数呢,因为这里指的是b表索引树的时间复杂度,当然是跟b表索引树大小挂钩也就是b表数据大小挂钩了。如果在算上回表查询,那么时间复杂度大约是2log(m)。

a表要进行全表扫描,那么a表的时间复杂度就是n,再加上每一行要去b表中查询,那么去b表查询的时间就是n * 2log(m)。加起来就是n + (2nlog(m))。

这里面2是常量可以忽略不记,而且2是因为回表造成的,如果我们使用覆盖索引,那么这个2就可以去掉了。

nlog(m) 比 n小,所以显然n的影响是最大的。也就是说n越小,那么我们连表的速度就越快。所以我们连表的时候要使用更小的那张表作为驱动表,然后给被驱动表的连表字段上面加上索引或者覆盖索引。这样我们的连表其实还是很快的。

Block Nested-Loop Join

如果你的被驱动表字段上面没有索引,那么mysql就会使用另外一种算法。这个算法叫BNL算法。

本来没有索引的情况下,按照上面的流程应该是下面这样:

  1. 读取表a的第一行数据
  2. 使用表a的id去b表上进行全表扫描查询到对应的b表数据
  3. 把a表和b表数据放在一起
  4. 重复上面的步骤直到a表遍历完成
  5. 返回数据

这样的话对于每一行表a的数据都要进行b表的全表扫描,也就是如果表a记为n行,表b记为m行,那么需要扫描n * m + n行数据。和上面的n + (2nlog(m))相比,可以看到慢了多少倍。

所以mysql使用了另外一种方法,也就是BNL算法,这个算法做了一下优化,流程变成了下面这样:

  1. 读取表a的所有数据放入join buffer
  2. 对表b进行全表扫描,然后把每一条数据和join buffer中的数据做对比。
  3. 把满足条件的数据返回。

这样的话,也就是扫描了一遍表a和表b,那么需要扫描n + m行数据。可以看到比上面的n + (nm)来说少了n倍的扫描量。当然了,这个算法,还需要在join buffer中进行nm次对比数据,但是这样内存判断也要比上面的方法好很多。

同样的这个算法对于两张表都是全表扫描,也就无所谓上面说的需要小表做驱动表了,反正都一样,都要全表扫描。

join buffer存在内存中,那么他就有大小的限制,参数join_buffer_size就是限制join buffer大小的。默认值是256k。如果表a的数据大于join buffer的大小,那么就会分段,也就是分多次进行。

但是分多次执行就会有一个问题了。比如分成了c次,那么表b就要经历c次的全表扫描。所以内存允许,当然Join buffer大一点速度会快一些。

这样的话就不是上面的n + m了,而是变成了n + (c*m),c是根据join buffer大小和n的大小来决定的。如果n越小或者join buffer size越大,那么c就越小。也就是说,如果我们使用小表作为驱动表,那么在遇到数据量超过join buffer的时候,速度也会比较快。

总结

总的来说,Join的时候注意下面几点:

  • 总是用小表作为驱动表比较好。
  • 能加索引就在被驱动表join字段上面加索引,使用NLJ算法而不是BNL算法。
  • 在没有索引,并且驱动表数据量过大时,可以通过调大join_buffer_size的值来加速连表查询。

mysql是怎么操作order by来进行排序的

mysql的排序用到了sort buffer,sort buffer是一个内存块。

mysql会先取出需要排序的数据,然后把数据放入sort buffer,当所有数据都放入sort buffer或者sort buffer满了就开始排序,然后将排序好的结果返回给客户端。

参数sort_buffer_size显示的就是sort buffer的大小。

如果数据量超过sort buffer,那么就会通过磁盘临时文件辅助进行排序,如果数据量比较小,则可以直接在内存中进行。

在内存中排序会使用快排算法,而通过磁盘临时文件则会使用归并排序算法

排序步骤可以分为以下几步:

  1. 取出select的数据存入sort buffer。
  2. 在sort buffer中进行快排或者归并排序算法。
  3. 如果有limit按照limit取相应的结果集进行返回。

rowid排序算法

这里面select出来的数据量可能会很大,跟你要查询的列多少有关,如果你的列很多,那么mysql可能会使用另外一种排序方法,叫做rowid排序。

rowid算法不管你查询出来的结果集,它只把必要的字段放入sort buffer中,这样sort buffer就可以存入更多的数据来进行排序。

必要的字段也就是你排序需要的字段和主键字段,比如order by time那么他只会放入id 和 time,然后按照time字段排序完成后再通过主键id回表查询一遍数据,然后返回数据。

显然rowid算法还需要再次回表,所以效率上要低一些,所以不是mysql默认使用的排序方法。

只有当你的内存不够用,查询的列太多的时候,mysql才会使用这种算法。

排序步骤可以分为以下几步:

  1. 取出排序的字段和id存入sort buffer。
  2. 在sort buffer中进行快排或者归并排序算法。
  3. 按照排序结果和limit数量回表查询然后返回数据。

不需要排序的方法

既然这样,我们为了更快速,可以避免mysql排序。

innoDB的索引是有序的,也就是说,如果我们要排序的字段本身就是有序的,那么就不用排序了。

所以我们可以在排序字段上建立索引,而如果我们查询的字段不多,甚至可以建立覆盖索引,那么速度会快很多。

mysql到底有没有进行排序,可以通过explain的执行计划来看。如果最后有using filesort,就表示使用了排序算法。

一文带你搞懂为什么大家都推荐count(*)而不是count(id)

在开发中,我们经常用到count这个函数来计算行数,尤其是后台列表展示。

那么究竟应该使用count()还是count(id)呢?很多人,很多书籍都推荐使用count()。

count(*)

在每个引擎里面其实是不一样的,比如myisam引擎,会把总行数存起来,如果你需要总行数,那么直接返回,那速度嗖嗖嗖的。

我们常用的innoDB引擎呢,他就真的是一行行计数了。

那为什么一行行计数还推荐count(*)呢?

innoDB是索引树组织表,不仅有主键索引树还有其他索引树,而count(*)反正是要全表扫描,那么优化器会去遍历最小的索引树,普通索引树的叶子节点只有id,当然要更小一些。

执行这个的时候innoDB遍历全表,但不取值,并且不判断,因为count(*)肯定不是null,直接累加。

count(id)

执行这个的时候innoDB遍历全表,把每一行数据的id值取出来返回给mysql,mysql拿到数据以后判断不为空则+1。累加后返回。

count(1)

执行这个的时候innoDB遍历全表,但不取值,直接返回给mysql,mysql收到返回值,给返回值赋值1,然后判断,累加。

count(字段)

这个和count(id)类似,只不过如果允许为空的话,需要把值取出来做个判断,多个判断。如果不允许为空,那就一样了。

这么一对比,就看出来了,显然count()更快,这也是因为mysql对count()这个语句专门做了优化。所以更推荐count(*)

count(字段) 慢于 count(id) 慢于 count(1) 慢于 count(*)

除了这些我们同样可以自己实现计数。

我们可以使用缓存系统,将计数放入缓存,然后持久化。比如redis。

我们也可以直接创建一个计数的表,把计数字段放入表里。

推荐一些学习网站

coursera

coursera是国际最大的高校慕课网站。上面有很多很火的课程,可以选择免费旁听,只不过免费的没法做作业。但是课件都是一样的,很多都有了中文字幕。也可以选择付费学习或者申请助学金,如果申请助学金成功就不用掏钱了。

coursera

edx

edX是麻省理工和哈佛大学于2012年4月联手创建的大规模开放在线课堂平台。

这上面也有很多免费的优秀课程。

coursera

优达学城

优达学城 Udacity 是来自硅谷的前沿技术平台,由 Google无人车之父 Sebastian Thrun 创立,与 Google、Facebook、Amazon 等全球顶尖技术公司联合开发了一系列的专业认证项目,为前沿技术领域培育了数万名顶尖专业人才,已与百度、腾讯、滴滴出行等中国前沿技术企业开发了一系列的课程项目

中国大学mooc

中国大学mooc是国内高校慕课平台,里面都是高校老师授课,还是免费的。让你在家学习高校课程。

学堂在线

学堂在线是由清华大学研发出的中文MOOC(大规模开放在线课程,简称慕课)平台,于2013年10月10日正式启动,面向全球提供在线课程。任何拥有上网条件的学生均可通过该平台,在网上学习课程视频。

极客时间

极客时间极客邦科技出品的 IT 类知识服务产品,内容包含专栏订阅、极客新闻、热点专题、直播、视频和音频等多种形式的知识服务。 [1] 产品形态包括移动 App,移动端网站、PC 端网站、微信平台等。

不过极客时间是收费的,虽然不贵,学生还有半价优惠,平时也有活动。

mysql索引详解-你的数据库用对索引了嘛

mysql的索引一般分为主键索引,唯一索引,普通索引,联合索引,覆盖索引等。

索引这么多,到底该怎么用,用哪个索引适合,需不需要索引呢?

innoDB的索引

innoDB里面一般采用b+树索引模型,当然还有其它比如哈希索引,全文索引,空间索引。但是常用的还是b+树索引。

B+树是一个N叉平衡树。为什么不用二叉平衡树呢,因为二叉平衡树高度太高了,n叉平衡树可以控制树的高度,大概在3-4的高度,而树的根节点一般存在内存中,这样只需要做2-3次磁盘操作就可以了。大量的减少了磁盘操作。

主键索引

主键索引是一种聚簇索引,什么是聚簇索引呢,就是和数据放在一起的。b+树的叶子节点存放的是页,每个页的默认大小是16kb。主键索引树的叶子节点存放的是主键id和数据。一般我们都会用id做为主键。如果一个表不指定主键,Innodb会使用row id作为主键。

我们使用主键查询数据,来看一下执行计划。

1
explain select * from auth_users where id = 1;

执行计划

可以看到里面key列显示的是primary,表示使用了主键索引。row列是1,表示扫描了一行。通过树搜索的方式快速定位了主键id的位置并且取出数据进行了返回。

为什么主键推荐使用递增id呢,这是因为对索引树的增删改查要更加快速。所以一般使用自增主键。也避免了插入引起的页分裂和删除引起的页合并

如果现在表里有id=4,5,6的数据,你插入了一条id=3的数据。这时候存放id=4,5,6数据的数据页满了,就需要页分裂,变成两个数据页。

页合并就是相邻的两个数据页的数据都挺少的,可以合并到一起,那么就会页合并。

使用自增主键则只需要不断往后写入就可以了,不需要担心中间的插入。而且自增主键占用的空间同样相对较小。

二级索引

二级索引也是非聚簇索引。包括唯一索引普通索引。他们同样是b+树的方式存放,只是他们的叶子节点中存放的并不是真正的数据,而是主键id。那么通过这种索引怎么找到数据呢,其实是先找到对应的主键id,再去主键索引树中通过主键id找到对应的数据,也叫回表

唯一索引和普通索引的区别在于查询的时候,唯一索引查询到条件对应的数据后不会接着查询了,而普通索引会接着查询,直到不满足条件为止。

覆盖索引

覆盖索引是一种优化的手段。覆盖索引也是一种联合索引

比如我们查询用户信息的时候,如果需要根据用户名查询用户名和密码。我们在用户名字段创建了一个索引。那么查询的时候就会走索引,但是查到的是id,还需要再去主键中找到数据,取出用户名和密码两个字段。

如果我们在用户名的索引树中不仅存了id,还存了我们需要的密码字段,不就不需要再去回表了吗?那我们就需要创建一个用户名和密码的联合索引。这样我们就不需要再回表了,这也就是覆盖索引。覆盖了我们要查询的字段。当然,你通过索引查询的时候,执行计划显示的行数还是1.这是因为回表这个操作是在innoDB里面做的,mysql是感觉不到的。

索引下推

索引下推也是一种优化。比如我们要查询用户名是张三密码是123456的用户。

1
select * from users where name = '张三' and pwd = '123456'

在mysql5.6以前,只能查询到张三这个数据以后回表找到数据在判断密码。

而mysqll5.6增加索引下推的优化之后,可以在索引遍历过程中,对name和pwd两个字段同时做判断。

唯一索引和普通索引

这两个索引到底该怎么选择,比如用户名字段,用户名当然是不能重复的了。那么它应该使用唯一索引还是普通索引呢?

一般用户注册的时候我们会判断用户名是否重复。所以用户名字段加唯一索引的价值并不大,我们的业务已经能保证它不重复了。

我们在上面说过,查询的时候,唯一索引查询到数据直接就返回了,显然要比普通索引快一些。

但是真的也就快了那么一点,因为InnoDB引擎在读取的时候,是读取一个数据页的数据。它会先把一个数据页读取到内存中,然后查询,那么在内存中多查一次其实没啥感觉。

在看一下更新的时候。

数据在内存里面

如果数据在内存中,那么唯一索引会判断更新后是否会破坏唯一性,如果不破坏则更新。

普通索引则直接更新。

这里显示普通索引更好,不用判断。但是这个影响页很小,和上面一样,在内存中操作的。

数据不在内存中

如果数据不在内存中,那么唯一索引就需要从磁盘读取数据,然后判断,更新。

普通索引则直接写入change buffer,然后就完成了,而change buffer则是在内存中,内存操作,少了磁盘操作。

整体来看,普通索引貌似比唯一索引更友好,唯一索引为了唯一性牺牲了插入和修改的性能。

change buffer

这个是一块内存中的空间,顾名思义,他就是为了修改而生的,如果你修改了数据,不需要直接更新磁盘,而是放入change buffer.change buffer满了,或者一定时间,或者当出现查询操作的时候,会merge数据,比如你更新了手机号,然后要查询这个用户的手机号。那么这时候内存中没有这个数据,从磁盘查询,磁盘查询到的是以前的手机号,因为数据没有更新到磁盘。这时候change buffer就会把更新数据合并到内存中的数据,使得查询到的是最新数据。

那么为什么唯一索引不适用change buffer,因为他需要判断唯一性。

但是向刚才举得例子,更新完立即查询,其实和唯一索引的更新没有啥区别了,反而因为change buffer还麻烦了。所以,change buffer更适合更新完不立即查询的场景。

mysql事务详解-4种事务隔离级别该怎么选择

事务要保证ACID,其中就有隔离性。

当我们有多个事务在执行的时候,如果保证每个事务都是隔离的,而不是数据混乱的呢?sql标准的四种隔离级别如下:

  • 读未提交 RU(Read Uncommitted),在这个级别下,是所有数据共享的,A事务在执行的任何操作,B事务都能看见,不管有没有提交。
  • 读已提交 RC(Read Committed),在这个级别下,只有A事务提交的数据,B事务才能看见。提交前的数据对其他事务是不可见的。
  • 可重复读 RR(Repeatable Read),在这个级别下,一个事务内读取的数据都是重复的,也就是都是一致的,就算别的事务提交了数据,你读取的还是原来的数据。
  • 串行化 (serializable), 对于同一行记录,写会加写锁,读会加读锁。

事务隔离的原理

这些是怎么做到的呢?因为事务是具备回滚功能的,如果一旦事务出错就会回滚整个事务。

用过github的应该都知道,github上的代码是具备回滚功能的,如果上线之后发现bug,会回滚到上一个版本,github就是基于版本控制的。

事务同样是基于版本控制的。有一个undo log回滚日志来记录这些版本。比如对于一行用户数据:

{id: 1, name: 张三, mobile:13245678976, pwd: 123456}

事务1进行了修改手机号操作,这时候还没有提交。数据库就会有两个版本的数据。这些都记录在undo log里面,这也就是所谓的MVCC的概念。

{id: 1, name: 张三, mobile:13245678976, pwd: 123456} 原始版本
{id: 1, name: 张三, mobile:12322223333, pwd: 123456} 事务1版本

事务2同时启动,但是稍后进行查询操作,获取用户信息,如果你是读未提交级别,那么读取的是事务1版本数据:

{id: 1, name: 张三, mobile:12322223333, pwd: 123456} 事务1版本

如果你是其他级别,那么你读取的是原始版本数据:

{id: 1, name: 张三, mobile:13245678976, pwd: 123456} 原始版本

如果事务1已经提交了修改,那么事务2在读已提交级别下获取的也是事务1版本数据。

这个是怎么判断的呢?

每个事务有自己的事务id,这个id是单调递增的。在每行数据的版本上会记录事务id。在事务启动的时候,会维护一个已经启动但没有提交的事务的有序数组。有序也就是从小到大,排好序了。如果事务id小于这个数组中第一个id,那么这个事务一定是一个已经提交的事务。如果事务id大于这个数组,那么这个事务是在我之后启动的事务。如果事务id在这个数组中,那么他在我启动的时候一定是未提交的。

拿上面的数据举例,原始版本的事务id为100,事务1的事务id为101,事务2的事务id为102。

{id: 1, name: 张三, mobile:13245678976, pwd: 123456} 原始版本 100
{id: 1, name: 张三, mobile:12322223333, pwd: 123456} 事务1版本 101

当事务2启动后,事务数组为[101],假设这时候事务1未提交。

如果是可重复读级别下,那么当查询到最新版本事务id=101,在数组中,那么不可见,接着查询上一个版本,上一个版本事务id = 100,发现小于数组最小值,那么这个值是正确的,取出这个版本的数据。

这样就算后面有事务提交,但是它提交后的版本的事务id不满足要求,那么查看到的数据永远都是同一个版本的数据,这也就是一致性读

但是如果你在事务2中先更新了密码再执行查询,那么就会有三个版本的数据

{id: 1, name: 张三, mobile:13245678976, pwd: 123456} 原始版本 100
{id: 1, name: 张三, mobile:12322223333, pwd: 123456} 事务1版本 101
{id: 1, name: 张三, mobile:12322223333, pwd: 123456789} 事务2版本 102

这时候查询,事务数组为[101,102]。

再可重复读级别下,查询到最新数据事务id=102,发现和自己的一样,那么是可见的。因为是自己修改的,自己修改的不能不让自己看到啊,所以这时候查询到的数据会是最新数据。这也就是当前读

可重复读级别和读已提交级别的区别如下:

  • 可重复读是再事务开始时候创建一致性视图,数组,这些东西,它只认事务启动前提交的数据。
  • 读已提交级别则是每次执行sql语句的时候创建一致性视图,数组,这些东西,它认的是语句启动前提交的数据。

这四个隔离级别,常用的就是读已提交和可重复读两个级别。