【MySQL实战45讲】

第一讲:

MySQL的基本架构大体可以分为Server层和存储引擎层两部分。Server层包括连接器、查询缓存、分析器、优化器、执行器等。存储引擎默认是InnoDB,还有MyISAM和Memory等。

当输入一条SQL语句,例如select * from T where ID = 10;首先你要先连接到这个数据库上,连接器负责跟客户端建立连接、获取权限、维持和管理连接。连接建立完成后,就会先去查询缓存看看之前是否执行过这条语句,如果找到直接返回客户端,如果不在,继续执行后面的部分。但是查询缓存的失效非常频繁,只要对一个表有更行,这个表的所有查询缓存就被清空,所以几乎只有静态表才适合用。因此可以按需使用,甚至MySQL8.0版本直接把这个功能去掉了。接下来进入分析器,分析器先做词法分析(看你写的语句里面的字符串分别是什么意思),再做语法分析(看你写的是不是正确语法),如果不对会报错。然后MySQL就知道你要做什么了,接下来进入优化器,你输入的语句可能有不同的执行方案(比如多表关联可以有不同的连接顺序),优化器选择它认为最优的方案。最后进入执行阶段,先判断是否有权限,如果有就打开表执行,执行器会根据表的引擎定义,去使用这个引擎提供的接口。最后就会查到数据返回客户端。

第二讲:

MySQL的更新流程涉及到两个重要的日志模块。首先是redo log(重做日志),是InnoDB引擎特有的日志。如果每次更新都要写入磁盘,磁盘就要找到对应的那条记录更新,整个过程IO成本、查找成本都很高。MySQL就采用了一种WAL的技术,也就是Write Ahead Logging,关键点就是先写日志,再写磁盘。具体来说就是,当有一条记录需要更新时,InnoDB引擎就先把记录写到redo log里,并更新内存,这个更新就算完成了。当系统比较空闲的时候,InnoDB会在合适的时间将这个记录更新到磁盘中(类似先在小黑板上记账,有空再腾到记帐本上,然后把小黑板上的记录擦一下留地方给接下来的记录)。redo log是固定大小的,从头开始写,写到尾部又回到开头循环写。后面有一个当前记录的位置,前面有一个当前要擦除的位置,如果记录位置已经追上要擦除的位置了(就是从后面绕道头部重新开始写了),表示日志满了要先擦一下,也就是把擦除位置推进一下。有了redo log,InnoDB就可以保证即使数据库异常重启,之前提交的记录也不会丢失,称之为crash-safe。

另外一个是binlog(归档日志),这个是Server层自己的日志(上面那个redo log是InnoDB引擎的日志)。最开始MySQL里并没有InnoDB引擎,自带的是MyISAM,然后它没有crash-safe功能,只能用binlog归档。

这两种日志的不同:①redo log是InnoDB特有的,而binlog是MySQL的Server层实现的,所有引擎都可以使用。②redo log是物理日志,记录的是“在某个数据页上做了什么修改”,而binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1”。③redo log是循环写的,空间固定,会用完,而binlog是可以追加写入的,写到一定大小会切换到下一个,不会覆盖。

update语句执行流程:例如update T set c = c + 1 where ID = 2; 先找到ID=2这一行,拿到引擎给的行数据,给它的值加一,再调用引擎接口把数据写进去,引擎把这行数据更新到内存中,同时把这个更新操作记录在redo log中,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。然后执行器生成这个操作的binlog并写入磁盘,然后执行器调用引擎的提交事务接口,把刚刚写的redo log改成commit状态,更新完成。

可以注意到redo log有两阶段提交,prepare和commit。这是为了让两个日志的逻辑一致。如果不使用两阶段提交,数据库的状态就有可能和用日志恢复出来的状态不一致。(其实我的理解就是,redo log写了之后这个事务才能提交,但是我们既不能:让事务提交之后,发生异常重启,但是binlog没写,这样到时候恢复状态的时候就少了一个事务,也就是少了一次更新;也不能:让binlog写了,但redo log没写,也就是事务没提交,那样的话异常重启之和发现binlog里面多了一个事务。所以我们先写redo log存在那,但是不提交事务,等binlog写了之后再提交。)简单来说就是要让这两个日志保持逻辑上的一致。

第三讲:

事务就是要保证一组数据库操作,要么全都成功,要么全都失败。MySQL中,事务的支持是在引擎中实现的,MyISAM不支持事务,InnoDB支持事务。

事务的特性:原子性、一致性、隔离性、持久性。

当数据库上有多个事务同时执行的时候,可能出现脏读、不可重复读、幻读等问题。为了解决这些问题,就有了隔离级别的概念。(隔离的越严实,效率就越低。)

SQL的事务隔离级别包括:读未提交(一个事务还没提交时,它做的变更就能被别的事务看到)、读提交(一个事务提交后,它做的变更才能被别的事务看到)、可重复读(一个事务执行过程中看到的数据,总是跟这个事务启动时看到的数据一致,当然在这个级别下,未提交的事务变更对其他事务也是不可见的)、串行化(对于同一行记录,写会加“写锁”,读会加“读锁”,当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成才能继续执行)。

【刷贪心题目】

1.K次取反后最大化的数组和(1005题):没通过。(忘记按绝对值排了。)

排序的时候要按绝对值从大到小排!然后从头遍历,遇到负数先反转,也就是先把绝对值大的负数反转了,然后如果k还没用完,如果k是偶数就可以偶数次反转同一个数,相当于没反转。如果k是奇数就把绝对值最小那个数(也就是排序后的最后一个数)多次反转,最终相当于反转一次。

2.加油站(134题):没通过。

如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明各个加油站的剩余量rest[i]相加一定是大于等于零的。每个加油站的剩余量rest[i]为gas[i] - cost[i]。i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,起始位置从i+1算起,再从0计算curSum。

具体操作就是用curSum记录已走过的站点的剩余油量之和,totalSum记录总的剩余油量之和。然后start记录应该从哪个站点走可以走完一圈,初始为0,即从0出发。然后从0开始遍历数组,每次记录curSum += gas[i] - cost[i],totalSum += gas[i] - cost[i],如果curSum小于0,start = i + 1,curSum = 0,意思就是从0开始到i会遇到这个油量不够的情况,所以从i+1开始,然后重新开始计算curSum。最后for遍历完成,对totalSum进行判断,如果totalSum都小于0说明无论如何走不了一圈,return -1(所以相当于排除了走不完的情况,我们最后得到的start一定是可以走完的)。