第一节:
大体来说,Mysql可以分为Server层和存储引擎层两部分。
Server 层包括连接器、查询缓存、分析器、优化器、执行器等,连接器作用:管理连接,权限验证;分析器:词法分析,语法分析;优化器:执行计划生成,索引选择;执行器:操作引擎,返回结果。
支持InnoDB、MyISAM、Memory等多个存储引擎。InnoDB在5.5.5版本开始成为了默认存储引擎。我们在创建表的时候可以加engine=memory来指定存储引擎。
第二节:
更新一条数据的时候会涉及到两个重要的日志模块,redo log(重做日志) 和 binlog(归档日志)。
这两种日志有以下三点不同:
1、redo log是InnoDB引擎特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。
2、redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如给ID=1这一行的K字段加1.
3、redo log 是循环写的,空间固定会用完;binlog是可以追加写入的。‘追加写’是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
第三节:
1、务的特性:原子性、一致性、隔离性、持久性
2、多事务同时执行的时候,可能会出现的问题:脏读、不可重复读、幻读
3、事务隔离级别:读未提交、读提交、可重复读、串行化
4、不同事务隔离级别的区别:
读未提交:一个事务还未提交,它所做的变更就可以被别的事务看到
读提交:一个事务提交之后,它所做的变更才可以被别的事务看到
可重复读:一个事务执行过程中看到的数据是一致的。未提交的更改对其他事务是不可见的
串行化:对应一个记录会加读写锁,出现冲突的时候,后访问的事务必须等前一个事务执行完成才能继续执行
5、配置方法:启动参数transaction-isolation
6、事务隔离的实现:每条记录在更新的时候都会同时记录一条回滚操作。同一条记录在系统中可以存在多个版本,这就是数 据库的多版本并发控制(MVCC)。
7、回滚日志什么时候删除?系统会判断当没有事务需要用到这些回滚日志的时候,回滚日志会被删除。
8、什么时候不需要了?当系统里么有比这个回滚日志更早的read-view的时候。
9、为什么尽量不要使用长事务。长事务意味着系统里面会存在很老的事务视图,在这个事务提交之前,回滚记录都要保留, 这会导致大量占用存储空间。除此之外,长事务还占用锁资源,可能会拖垮库。
10、事务启动方式:一、显式启动事务语句,begin或者start transaction,提交commit,回滚rollback;二、set autocommit=0, 该命令会把这个线程的自动提交关掉。这样只要执行一个select语句,事务就启动,并不会自动提交,直到主动执行 commit或rollback或断开连接。
11、建议使用方法一,如果考虑多一次交互问题,可以使用commit work and chain语法。在autocommit=1的情况下用begin显 式启动事务,如果执行commit则提交事务。如果执行commit work and chain则提交事务并自动启动下一个事务。
第四节:
1.索引的作用:提高数据查询效率
2.常见索引模型:哈希表、有序数组、搜索树
3.哈希表:键 - 值(key - value)。
4.哈希思路:把值放在数组里,用一个哈希函数把key换算成一个确定的位置,然后把value放在数组的这个位置
5.哈希冲突的处理办法:链表
6.哈希表适用场景:只有等值查询的场景
7.有序数组:按顺序存储。查询用二分法就可以快速查询,时间复杂度是:O(log(N))
8.有序数组查询效率高,更新效率低
9.有序数组的适用场景:静态存储引擎。
10.二叉搜索树:每个节点的左儿子小于父节点,父节点又小于右儿子
11.二叉搜索树:查询时间复杂度O(log(N)),更新时间复杂度O(log(N))
12.数据库存储大多不适用二叉树,因为树高过高,会适用N叉树
13.InnoDB中的索引模型:B+Tree
14.索引类型:主键索引、非主键索引
主键索引的叶子节点存的是整行的数据(聚簇索引),非主键索引的叶子节点内容是主键的值(二级索引)
15.主键索引和普通索引的区别:主键索引只要搜索ID这个B+Tree即可拿到数据。普通索引先搜索索引拿到主键值,再到主键索引树搜索一次(回表)
16.一个数据页满了,按照B+Tree算法,新增加一个数据页,叫做页分裂,会导致性能下降。空间利用率降低大概50%。当相邻的两个数据页利用率很低的时候会做数据页合并,合并的过程是分裂过程的逆过程。
17.从性能和存储空间方面考量,自增主键往往是更合理的选择。
第五节:
回表:回到主键索引树搜索的过程,称为回表
覆盖索引:某索引已经覆盖了查询需求,称为覆盖索引,例如:select ID from T where k between 3 and 5
在引擎内部使用覆盖索引在索引K上其实读了三个记录,R3~R5(对应的索引k上的记录项),但对于MySQL的Server层来说,它 就是找引擎拿到了两条记录,因此MySQL认为扫描行数是2
最左前缀原则:B+Tree这种索引结构,可以利用索引的"最左前缀"来定位记录
只要满足最左前缀,就可以利用索引来加速检索。
最左前缀可以是联合索引的最左N个字段,也可以是字符串索引的最左M个字符
第一原则是:如果通过调整顺序,可以少维护一个索引,那么这个顺序往往就是需要优先考虑采用的。
索引下推:在MySQL5.6之前,只能从根据最左前缀查询到ID开始一个个回表。到主键索引上找出数据行,再对比字段值。
MySQL5.6引入的索引下推优化,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减 少回表次数。
联合索引的技巧:
1、覆盖索引:如果查询条件使用的是普通索引(或是联合索引的最左原则字段),查询结果是联合索引的字段或是主键,不 用回表操作,直接返回结果,减少IO磁盘读写读取正行数据
2、最左前缀:联合索引的最左 N 个字段,也可以是字符串索引的最左 M 个字符
3、联合索引:根据创建联合索引的顺序,以最左原则进行where检索,比如(age,name)以age=1 或 age= 1 and name=‘张三’可以使用索引,单以name=‘张三’ 不会使用索引,考虑到存储空间的问题,还请根据业务需求,将查找频繁的 数据进行靠左创建索引。
4、索引下推:like 'hello%’and age >10 检索,MySQL5.6版本之前,会对匹配的数据进行回表查询。5.6版本后,会先过滤掉 age<10的数据,再进行回表查询,减少回表率,提升检索速度
第六节:
根据加锁范围:MySQL里面的锁可以分为:全局锁、表级锁、行级锁
一、全局锁:
对整个数据库实例加锁。
MySQL提供加全局读锁的方法:Flush tables with read lock(FTWRL) 解锁 unlock tables
这个命令可以使整个库处于只读状态。使用该命令之后,数据更新语句、数据定义语句和更新类事务的提交语句等操作都会被阻塞。
使用场景:全库逻辑备份。
风险:
1.如果在主库备份,在备份期间不能更新,业务停摆
2.如果在从库备份,备份期间不能执行主库同步的binlog,导致主从延迟
官方自带的逻辑备份工具mysqldump,当mysqldump使用参数--single-transaction的时候,会启动一个事务,确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。
一致性读是好,但是前提是引擎要支持这个隔离级别。
如果要全库只读,为什么不使用set global readonly=true的方式?
1.在有些系统中,readonly的值会被用来做其他逻辑,比如判断主备库。所以修改global变量的方式影响太大。
2.在异常处理机制上有差异。如果执行FTWRL命令之后由于客户端发生异常断开,那么MySQL会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为readonly之后,如果客户端发生异常,则数据库就会一直保持readonly状态,这样会导致整个库长时间处于不可写状态,风险较高。
二、表级锁
MySQL里面表级锁有两种,一种是表锁,一种是元数据锁(meta data lock,MDL)
表锁的语法是:lock tables ... read/write
可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。lock tables语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。
对于InnoDB这种支持行锁的引擎,一般不使用lock tables命令来控制并发,毕竟锁住整个表的影响面还是太大。
MDL:不需要显式使用,在访问一个表的时候会被自动加上。
MDL的作用:保证读写的正确性。
在对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。
读锁之间不互斥。读写锁之间,写锁之间是互斥的,用来保证变更表结构操作的安全性。
MDL 会直到事务提交才会释放,在做表结构变更的时候,一定要小心不要导致锁住线上查询和更新。
第七节:
两阶段锁:在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放, 而是要等到事务结束时才释放。
建议:如果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。
死锁:当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态。
解决方案:
1、通过参数 innodb_lock_wait_timeout 根据实际业务场景来设置超时时间,InnoDB引擎默认值是50s。
2、发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑(默认是开启状态)。
如何解决热点行更新导致的性能问题?
1、如果你能确保这个业务一定不会出现死锁,可以临时把死锁检测关闭掉。一般不建议采用
2、控制并发度,对应相同行的更新,在进入引擎之前排队。这样在InnoDB内部就不会有大量的死锁检测工作了。
3、将热更新的行数据拆分成逻辑上的多行来减少锁冲突,但是业务复杂度可能会大大提高。
innodb行级锁是通过锁索引记录实现的,如果更新的列没建索引是会锁住整个表的。
第八节:
1.innodb支持RC和RR隔离级别实现是用的一致性视图(consistent read view)
2.事务在启动时会拍一个快照,这个快照是基于整个库的.
基于整个库的意思就是说一个事务内,整个库的修改对于该事务都是不可见的(对于快照读的情况)
如果在事务内select t表,另外的事务执行了DDL t表,根据发生时间,要嘛锁住要嘛报错(参考第六章)
3.事务是如何实现的MVCC呢?
(1)每个事务都有一个事务ID,叫做transaction id(严格递增)
(2)事务在启动时,找到已提交的最大事务ID记为up_limit_id。
(3)事务在更新一条语句时,比如id=1改为了id=2.会把id=1和该行之前的row trx_id写到undo log里,
并且在数据页上把id的值改为2,并且把修改这条语句的transaction id记在该行行头
(4)再定一个规矩,一个事务要查看一条数据时,必须先用该事务的up_limit_id与该行的transaction id做比对,
如果up_limit_id>=transaction id,那么可以看.如果up_limit_id<transaction id,则只能去undo log里去取。去undo log查找数 据的时候,也需要做比对,必须up_limit_id>transaction id,才返回数据
4.什么是当前读,由于当前读都是先读后写,只能读当前的值,所以为当前读.会更新事务内的up_limit_id为该事务的transaction id
5.为什么rr能实现可重复读而rc不能,分两种情况
(1)快照读的情况下,rr不能更新事务内的up_limit_id,
而rc每次会把up_limit_id更新为快照读之前最新已提交事务的transaction id,则rc不能可重复读
(2)当前读的情况下,rr是利用record lock+gap lock来实现的,而rc没有gap,所以rc不能可重复读。
第九节:
选择普通索引还是唯一索引?
对于查询过程来说:
a、普通索引,查到满足条件的第一个记录后,继续查找下一个记录,知道第一个不满足条件的记录
b、唯一索引,由于索引唯一性,查到第一个满足条件的记录后,停止检索
但是,两者的性能差距微乎其微。因为InnoDB根据数据页来读写的。
对于更新过程来说:
概念:change buffer
当需要更新一个数据页,如果数据页在内存中就直接更新,如果不在内存中,在不影响数据一致性的前提下,InnoDB会将这些更新操作缓存在change buffer中。下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行change buffer中的与这个页有关的操作。
change buffer是可以持久化的数据。在内存中有拷贝,也会被写入到磁盘上
purge:将change buffer中的操作应用到原数据页上,得到最新结果的过程,成为purge
访问这个数据页会触发purge,系统有后台线程定期purge,在数据库正常关闭的过程中,也会执行purge
唯一索引的更新不能使用change buffer
change buffer用的是buffer pool里的内存,change buffer的大小,可以通过参数innodb_change_buffer_max_size来动态设置。这个参数设置为50的时候,表示change buffer的大小最多只能占用buffer pool的50%。
将数据从磁盘读入内存涉及随机IO的访问,是数据库里面成本最高的操作之一。
change buffer 因为减少了随机磁盘访问,所以对更新性能的提升很明显。
change buffer使用场景
在一个数据页做purge之前,change buffer记录的变更越多,收益就越大。
对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时change buffer的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。
反过来,假设一个业务的更新模式是写入之后马上会做查询,那么即使满足了条件,将更新先记录在change buffer,但之后由于马上要访问这个数据页,会立即触发purge过程。
这样随机访问IO的次数不会减少,反而增加了change buffer的维护代价。所以,对于这种业务模式来说,change buffer反而起到了副作用。
索引的选择和实践:
尽可能使用普通索引。
redo log主要节省的是随机写磁盘的IO消耗(转成顺序写),而change buffer主要节省的则是随机读磁盘的IO消耗。
第十六节:
1.MySQL会为每个线程分配一个内存(sort_buffer)用于排序该内存大小为sort_buffer_size
1>如果排序的数据量小于sort_buffer_size,排序将会在内存中完成
2>如果排序数据量很大,内存中无法存下这么多数据,则会使用磁盘临时文件来辅助排序,也称外部排序
3>在使用外部排序时,MySQL会分成好几份单独的临时文件用来存放排序后的数据,然后在将这些文件合并成一个大文件
2.mysql会通过遍历索引将满足条件的数据读取到sort_buffer,并且按照排序字段进行快速排序
1>如果查询的字段不包含在辅助索引中,需要按照辅助索引记录的主键返回聚集索引取出所需字段
2>该方式会造成随机IO,在MySQL5.6提供了MRR的机制,会将辅助索引匹配记录的主键取出来在内存中进行排序,然后在回表
3>按照情况建立联合索引来避免排序所带来的性能损耗,允许的情况下也可以建立覆盖索引来避免回表
全字段排序
1.通过索引将所需的字段全部读取到sort_buffer中
2.按照排序字段进行排序
3.将结果集返回给客户端
缺点:
1.造成sort_buffer中存放不下很多数据,因为除了排序字段还存放其他字段,对sort_buffer的利用效率不高
2.当所需排序数据量很大时,会有很多的临时文件,排序性能也会很差
优点:MySQL认为内存足够大时会优先选择全字段排序,因为这种方式比rowid 排序避免了一次回表操作
rowid排序
1.通过控制排序的行数据的长度来让sort_buffer中尽可能多的存放数据,max_length_for_sort_data
2.只将需要排序的字段和主键读取到sort_buffer中,并按照排序字段进行排序
3.按照排序后的顺序,取id进行回表取出想要获取的数据
4.将结果集返回给客户端
优点:更好的利用内存的sort_buffer进行排序操作,尽量减少对磁盘的访问
缺点:回表的操作是随机IO,会造成大量的随机读,不一定就比全字段排序减少对磁盘的访问
3.按照排序的结果返回客户所取行数
show processlist 命令,看看当前语句处于什么
第十九节:
rc模式下:
session 1:
begin;
select * from t where c=5 for update;
session 2:
delete from t where c=10 --等待
session 3:
insert into t values(100001,8) --成功
session 1:
commit
session 2:事务执行成功
rr模式下:
begin;
select * from t where c=5 for update;
session 2:
delete from t where c=10 --等待
session 3:
insert into t values(100001,8) --等待
session 1:
commit
session 2:事务执行成功
session 3:事务执行成功
从上面这两个简单的例子,可以大概看出上锁的流程.
不管是rr模式还是rc模式,这条语句都会先在server层对表加上MDL S锁,然后进入到引擎层。
rc模式下,由于数据量不大只有10W。通过实验可以证明session 1上来就把该表的所有行都锁住了。
导致其他事务要对该表的所有现有记录做更新,是阻塞状态。为什么insert又能成功?
说明rc模式下for update语句没有上gap锁,所以不阻塞insert对范围加插入意向锁,所以更新成功。
session 1commit后,session 2执行成功。表明所有行的x锁是在事务提交完成以后才释放。
rr模式下,session 1和session 2与rc模式下都一样,说明rr模式下也对所有行上了X锁。
唯一的区别是insert也等待了,是因为rr模式下对没有索引的更新,聚簇索引上的所有记录,都被加上了X锁。其次,聚簇索引每条记录间的间隙(GAP),也同时被加上了GAP锁。由于gap锁阻塞了insert要加的插入意向锁,导致insert也处于等待状态。只有当session 1 commit完成以后。session 1上的所有锁才会释放,S2,S3执行成功
由于例子中的数据量还比较小,如果数据量达到千万级别,就比较直观的能看出,上锁是逐行上锁的一个过程.扫描一条上一条,直到所有行扫描完,rc模式下对所有行上x锁。rr模式下不仅对所有行上X锁,还对所有区间上gap锁.直到事务提交或者回滚完成后,上的锁才会被释放。
第二十一节:
我总结的加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”。
原则 1:加锁的基本单位是 next-key lock。希望你还记得,next-key lock 是前开后闭区间。
原则 2:查找过程中访问到的对象才会加锁。
优化 1:索引上的等值查询,给唯一索引加锁的时候,next-key lock 退化为行锁。
优化 2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock 退化为间隙锁。
一个 bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止。
https://time.geekbang.org/column/article/75659