【MySQL之MySQL底层分析篇】系统学习MySQL,从应用SQL语法到底层知识讲解,这将是你见过最完成的知识体系

MySQL体系结构

Client Connectors:接入方,程序员通过Client Connectors(JDBC、ODBC)与MySQL打交道
Connection Pool:连接池,管理缓冲用户连接、用户名、密码、权限校验等需要缓存的需求
SQL Interface:SQL接口,接受用户的SQL命令,并且返回用户需要查询的结果
Parser:解析器,验证和解析SQL命令
Optimizer:查询优化器,在查询之前会对SQL语句进行优化
Cache和Buffer:缓存区,如果查询缓存命中,查询语句就可以直接去查询缓存中取数据
pluggable storage Engines:插件式存储引擎,MySQL与文件系统打交道
File System:文件系统,数据、日志(redo,undo)、索引、错误日志、查询记录、慢查询等

在这里插入图片描述

MySQL执行流程

一条SQL查询语句是如何执行的

在这里插入图片描述
连接器跟客户端连接mysql -h$IP -P$port -u$username -p
查询缓存因为缓存失效太频繁,所以8.0开始彻底撤销了这个功能
分析器进行语法分析和词法分析
优化器对SQL操作进行优化,例如决定使用哪个索引,或者多表关联时决定连接顺序
执行器判断是否有操作权限,如果有操作权限则打开表,调用指定的存储引擎操作表
存储引擎执行SQL语句

如何执行SQL语句的呢?
在这里插入图片描述
在这里插入图片描述
交叉连接、左连接、右连接、内连接、外连接相关示例
交叉连接就是笛卡尔积
左连接左连接(left join)即为两张表进行连接时,是以处于left join语句左侧的表为基准去匹配left join语句右边的表,如果左表中的一条数据在右表中能找到与之对应的一条数据,那么就会出现在以虚表形式存在的结果表中,如果没有找到,那么会以null来代替右表中的数据去匹配左表。
右连接本质上是相当于将上述的左连接的这个过程反过来,以连接语句right join右侧的表为基准去匹配左边的表,剩下的道理是一样的,不再赘述。
内连接用两张表进行匹配的时候,如果表中任意一条数据在另一张表中都是找不到对应数据的话,那么在结果表中是不会有这一条数据的。
外连接如果某张表中的数据在另一张中找不到对应的条目并不影响它依然出现在查询的结果中,这对于两张表都是满足的,两边都有出现null的可能

一条SQL更新语句是如何执行的

简述Undo log、Redo log、bin log
Undo log记录某数据被修改前的值,可以用来在事务失败时进行rollback(保持事务一致性)
Redo log记录某数据被修改后的值,可以用来恢复未写入磁盘的数据(实现数据的持久性)
bin log记录数据库执行更改的所有SQL语句(数据备份)

我们再来看执行器和InnoDB引擎在执行这个简单的update语句时的内部流程
在这里插入图片描述

  1. 执行器先找存储引擎ID=2这一行。ID是主键,存储引擎直接使用树搜索找到这一行。如果ID=2这一行的数据页本来就在存储索引的内存Buffer Pool中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  2. 执行器拿到存储引擎给的数据行,进行更新操作,再调用引擎接口写入这行新数据。
  3. 存储引擎将这行数据更新到Buffer Pool中,同时将旧数据记录到Undo log中,将更新数据记录到Redo log中。此时Redo log处于Prepare状态,然后告知执行器执行完成了,随时可以提交事务
  4. 执行器生成这个操作的bin log,并把bin log写入磁盘。
  5. 执行器调用存储引擎的提交事务接口,引擎把刚刚写入的redo log改成commit状态,更新完成。

MySQL存储结构

段、区、页的分配策略
起初向表中插入数据的时候,是从某个碎片区以单个页面来分配存储空间的。当某个段已经占用了32个碎片区页面之后,就会申请以完整的为单位来分配存储空间。
在这里插入图片描述

数据页
在这里插入图片描述

1. 页头File Header
在这里插入图片描述

2. 页尾File Trailer
页头和页尾的校验和相对应,由于我们的操作系统传输单元数据块通常是4KB,一个页是4个数据块。如果碰到我断电的情况,那么一些不走运的页,可能只传输了一两个数据块,并没有完整传输,这种不完整就需要用页头和页尾的校验和通过一些验证算法进行验证。默认使用crc32.
在这里插入图片描述

3. 数据行
在这里插入图片描述

MySQL说:数据行有4种格式:DYNAMIC,REDUNDANT,COMPACT,COMPRESSED,默认为DYNAMIC,可以通过innodb_default_row_format进行查询和变更。

真实数据区

MySQL说:第一个就是主键位置,如果是复合主键,那也会依序排在这里。如果没有主键,就会优先用一个NOT NULL、UNIQUE的列作为主键,如果这个都没有的话,我的innoDB就会构建一个6B的DB_ROW_ID字段存储在这里。
MySQL说:紧接着是6B的事务ID字段DB_TX_ID,7B的回滚指针字段DB_ROLL_PTR
MySQL说:继续向右就记录除了主键和值为null的列之外的真实数据了,因为值为null的列会用其他方法表示,目的就是为了节省空间。

额外信息区

下一行:下一个数据行真实数据的地址。
行类型:0代表普通数据行,1代表索引目录行,2是最小行,3是最大行。
位置:用13bit的heap_no来标记该行在整个页的位置
组行数:如果这个行是分组的最后一行,则在这里标记该组的行数
每层最小:不懂,但是在后面视频有详细讲解
删除标记:在删除数据行时并不会直接移除,而是修改这个标记。同时将这行的next_record指向一个称为垃圾链表的地方,这个链表会用于事务回滚。
NULL值列表:用来记录值为NULL的列,根据列的个数来开辟合适的长度。
可变字段长度列表:记录了数据行里所有变成字段的实际长度。

4. 最大行和最小行
每当我体内创建一个新页,都会自动分配两个行。一个行类型为2的最小行Infimum,固定在0号heap_no位置。一个行类型为3的最大行Supremum,固定在1号heap_no位置。这两个行并不存储真实的信息,而是作为数据行链表的头和尾。
在这里插入图片描述
5. 页目录
当程序猿大哥想要查询数据时,需要沿着链表顺序一个个比对查找,这样显然是不行的。所以我进化出了一个页目录的器官,它会将页内包括头行、尾行在内的所有行进行分组,约定头行为单独为一组,其他每个组最多有8条数据。同时把每个组最后一个行在页中的地址,按照主键从小到大的顺序记录到页中,这个区域叫做页目录,页目录的每一个位置称为一个槽,每个槽对应一个分组。
在页目录中根据二分法查找到组,再在组里面遍历八个数据行即可找到。
在这里插入图片描述

MySQL存储引擎

我们可以使用命令:show engines;来查看 MySQL 支持哪些存储引擎,如下图所示。我们可以看到 MySQL 默认使用的存储引擎是 InnoDB。
在这里插入图片描述

MySQL索引机制

索引数据结构的选择
简单来说,索引的出现其实就是为了提高数据查询的效率。索引常用类型有:
哈希表只适合等值查询,无法范围查询
有序数组可以等值查询和范围查询,插入删除太麻烦了
二叉树适合查询,也适合删除
二叉排序树容易退化成链表
二叉平衡树还是太深了,并且每个结点都太小了
B树虽然已经降低了树的高度,但是仍然有下降空间,并且不便于范围查询
B+树基本已经完美了,所以MySQL使用的索引结构就是B+树

索引类型
普通索引:加速查询
唯一索引:加速查询+列值唯一
主键索引:加速查询+列值唯一+不能为null
全文索引:加速查询

创建索引
在创建表的时候创建索引

CREATE TABLE t1 (
id INT NOT NULL,
name CHAR(30) NOT NULL,
UNIQUE INDEX UniqIdx(id)
);

在已存在的表上创建索引

ALTER TABLE book ADD UNIQUE INDEX UniqidIdx (bookId);

判断要不要加索引
1. 适合使用索引的场景
查询多、更新少、低重复的字段
常常用来排序(order by)分组(group by)的字段

2. 不适合使用索引的场景
查询少、更新多、高重复的字段

索引判断是否生效
EXPLAIN语句将为我们输出详细的SQL执行信息,其中:

  • possible_keys行给出了MySQL在搜索数据记录时可选用的各个索引。
  • key行是MySQL实际选用的索引。
EXPLAIN SELECT * FROM book WHERE year_publication=1990;

如果possible_keys行和key行都包含year_publication字段,则说明在查询时使用了该索引。

索引失效场景
1. 最佳左前缀法则
指的是查询从索引的最左前列开始并且不跳过索引中的列

2. 不要再索引列上做任何操作
不要在索引列上左任何操作(计算,函数,自动or手动类型转换),否则会导致索引失效转换为全表扫描。

3. 字符串不加单引号会导致索引失效
因为name=2000也可以查出来是因为MySQL在底层做了一次类型转换,把整形2000转换成了字符串’2000’,所以索引失效。

4. where的范围条件判断后面不一定走索引
如果比值(条件范围 / 总范围)小于某个比值时,就会走索引;
如果比值(条件范围 / 总范围)大于某个比值时,就不会走索引

5. like以通配符(%)开头的索引会失效变成全表扫描
通配符出现最前面说明所有都适配,所有都要扫描,不以%开头则不失效

这种情况下应该怎么使用索引呢?例如,在MySQL中模糊查询 mobile like ‘%8765’ ,这种情况是不能使用 mobile 上的索引的,那么我们可以加入冗余列存储为 mobile 的倒叙文本,如 mobile为17312345678,那么 mobile_reverse存储 87654321371,为 mobile_reverse 列建立索引,查询中使用语句 mobile_reverse likereverse(’%5678’) 即可。这种语句是可以使用索引的。

常用的几个MySQL索引结构
聚簇索引叶子结点包含整个数据行
在这里插入图片描述
非聚簇索引叶子结点包含索引和数据的地址
在这里插入图片描述
辅助索引

省流:因为数据存放在聚簇索引中,所以辅助索引找到key,再根据key聚簇索引中找到数据
在这里插入图片描述
省流:反正非聚簇索引找到key的数据地址也要到其他文件找数据,倒不如辅助索引自己去找数据
在这里插入图片描述

全文索引

  • inverted file idnex:{单词,单词文档所在ID}
  • full inverted index:{单词,(单词文档所在ID,具体文档中的位置)}

例如有如下表 A:
在这里插入图片描述

用 inverted file idnex 方式存储内容如下所示:
在这里插入图片描述

用 full inverted index 方式存储内容如下所示:
在这里插入图片描述

3. 使用
3.1 创建全文索引

# 1.创建表时创建全文索引
create table fulltext_test (
    id int(11) NOT NULL AUTO_INCREMENT,
    content text NOT NULL,
    tag varchar(255),
    PRIMARY KEY (id),
    FULLTEXT KEY content_tag_fulltext(content,tag)  // 创建联合全文索引列
) ENGINE=MyISAM DEFAULT CHARSET=utf8;

# 2.在已存在的表上创建全文索引
create fulltext index content_tag_fulltext
    on fulltext_test(content,tag);

# 3.通过 SQL 语句 ALTER TABLE 创建全文索引
alter table fulltext_test
    add fulltext index content_tag_fulltext(content,tag);

3.2 删除全文索引

# 1. 直接使用 DROP INDEX 删除全文索引
drop index content_tag_fulltext
    on fulltext_test;

# 2. 通过 SQL 语句 ALTER TABLE 删除全文索引
alter table fulltext_test
    drop index content_tag_fulltext;

3.3 使用全文索引
和常用的模糊匹配使用 like + % 不同,全文索引有自己的语法格式,使用 match 和 against 关键字,比如

select * from fulltext_test 
    where match(content,tag) against('xxx xxx');

MySQL事务机制

事务类型
扁平事务:事务中的操作,要么都执行,要么都回滚。
带有保存点的扁平事务:扁平事务的全部回滚导致开销太大,所以设置事务可以回到某个保存点
链事务:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。注意,提交事务操作和开始下一个事务操作将合并为一个原子操作。这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行的。
嵌套事务:是一个层次结构框架。有一个顶层事务(top-level transaction)控制着各个层次的事
务。顶层事务之下嵌套的事务被称为子事务(subtransaction)。
分布式事务::通常是一个在分布式环境下运行的扁平事务,同样是要么都发生、要么都失败。

事务隔离级别
在这里插入图片描述
读未提交RU
修改数据的事务A:事务A直接在记录上修改的数据
读取数据的事务B:事务B直接返回记录上的最新值
读已提交RC
修改数据的事务A:事务A在每一个SQL语句开始时创建视图,并在视图上修改数据,SQL执行完成后,视图才将修改的数据同步到记录中。
读取数据的事务B:事务B在每一个SQL语句开始时创建视图,并在视图上读取数据,SQL执行完成后,在下一个SQL语句执行时创建新的视图。
可重复读RR
修改数据的事务A:事务A在每一次事务开启时创建视图,并在视图上修改数据,事务提交后,视图才将修改的数据同步到记录中。
读取数据的事务B:事务B在每一个事务开启时创建视图,并在视图上读取数据,事务提交后,并在下一个事务开启时创建新的视图。
串行化SE
对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。没有视图概念。

四种隔离级别怎么实现
对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;

对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;

对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。

MVCC保证隔离性
1. 生成一个readView在readview快照中主要包括以下这些字段:
在这里插入图片描述
2. 在mysql存储的数据中,除了我们显式定义的字段,mysql会隐含的帮我们定义两个字段
trx_d=id:事务id,每进行一次事务操作,就会自增1;
roll_pointer:回滚指针,用于找到上一个版本的数据;
在这里插入图片描述
3. readview如何判断版本链中的哪个版本可用呢?
trx_id表示要读取的事务id
(1)如果要读取的事务id等于进行读操作的事务id,说明是我读取我自己创建的记录,那么为什么不可以呢。
(2)如果要读取的事务id小于最小的活跃事务id,说明要读取的事务已经提交,那么可以读取。
(3)max_trx_id表示生成readview时,分配给下一个事务的id,如果要读取的事务id大于max_trx_id,说明该id已经不在该readview版本链中了,故无法访问。
(4)m_ids中存储的是活跃事务的id,如果要读取的事务id不在活跃列表,那么就可以读取,反之不行。
在这里插入图片描述
4.mvcc如何实现RC和RR的隔离级别
(1)RC的隔离级别下,每个读都会生成并获取最新的readview。
(2)RR的隔离级别下,只有在同一个事务的第一个快照读才会创建readview,之后的每次快照读都使用的同一个readview,所以每次的查询结果都是一样的。

MySQL锁机制

1. 全局锁
使用全局锁:执行后,整个数据库就处于只读状态了。

flush tables with read lock

释放全局锁

unlock tables

全局锁的应用场景:全库逻辑备份。

既然备份数据库数据的时候,使用全局锁会影响业务,那有什么其他方式可以避免?
如果数据库的引擎支持的事务支持可重复读的隔离级别,那么在备份数据库之前先开启事务,会先创建 Read View,然后整个事务执行期间都在用这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。因为在可重复读的隔离级别下,即使其他事务更新了表的数据,也不会影响备份数据库时的 Read View

2. 表级锁

表锁
使用表级锁:表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。

//表级别的共享锁,也就是读锁;
lock tables t_student read;

//表级别的独占锁,也就是写锁;
lock tables t_stuent write;

释放表级锁

unlock tables

元数据锁MDL
我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL:

  • 对一张表进行 CRUD 操作时,加的是 MDL 读锁;
  • 对一张表做结构变更操作的时候,加的是 MDL 写锁;

MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。

当有线程在执行 select 语句( 加 MDL 读锁)的期间,如果有其他线程要更改该表的结构( 申请 MDL 写锁),那么将会被阻塞,直到执行完 select 语句( 释放 MDL 读锁)。

反之,当有线程对表结构进行变更( 加 MDL 写锁)的期间,如果有其他线程执行了 CRUD 操作( 申请 MDL 读锁),那么就会被阻塞,直到表结构变更完成( 释放 MDL 写锁)。

意向锁
1. 提出问题:在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。
2. 解决问题: 意向锁在原来的 X/S 锁之上引入了 IX/IS,IX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定:

  • 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
  • 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
    在这里插入图片描述

AUTO-INC锁
表里的主键通常都会设置成自增的,这是通过对主键字段声明 AUTO_INCREMENT 属性实现的。

AUTO-INC 锁是特殊的表锁机制,锁不是再一个事务提交后才释放,而是再执行完插入语句后就会立即释放。

在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 AUTO_INCREMENT 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。

那么,一个事务在持有 AUTO-INC 锁的过程中,其他事务的如果要向该表插入语句都会被阻塞,从而保证插入数据时,被 AUTO_INCREMENT 修饰的字段的值是连续递增的。

3. 行级锁

行级锁分为S锁和X锁
在这里插入图片描述

行级锁的使用
使用行级锁

//对读取的记录加共享锁
select ... lock in share mode;

//对读取的记录加独占锁
select ... for update;

释放行级锁 :因为当事务提交了,锁就会被释放

行级锁的三种实现方式
1. Record Lock锁住的是一条记录

mysql > begin;
mysql > select * from t_test where id = 1 for update;

就是对 t_test 表中主键 id 为 1 的这条记录加上 X 型的记录锁,这样其他事务就无法对这条记录进行修改了。

在这里插入图片描述

当事务执行 commit 后,事务过程中生成的锁都会被释放。

2. Gap Lock锁住的是一个间隙

假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了。
在这里插入图片描述

3. Next-Key Lock是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

假设,表中有一个范围 id 为(3,5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。
在这里插入图片描述
4. 乐观锁与悲观锁
参考文章:大神讲解乐观锁和悲观锁
概念
悲观锁:悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能变动,一个事务拿到悲观锁后,其他任何事务都不能对该数据进行修改,只能等待锁被释放才可以执行。

乐观锁:它认为数据的变动不会太频繁。因此,它允许多个事务同时对数据进行变动。 但是,乐观不代表不负责,那么怎么去负责多个事务顺序对数据进行修改呢?乐观锁通常是通过在表中增加一个版本(version)或时间戳(timestamp)来实现,其中,版本最为常用。事务在从数据库中取数据时,会将该数据的版本也取出来(v1),当事务对数据变动完毕想要将其更新到表中时,会将之前取出的版本v1与数据中最新的版本v2相对比,如果v1=v2,那么说明在数据变动期间,没有其他事务对数据进行修改,此时,就允许事务对表中的数据进行修改,并且修改时version会加1,以此来表明数据已被变动。如果,v1不等于v2,那么说明数据变动期间,数据被其他事务改动了,此时不允许数据更新到表中,一般的处理办法是通知用户让其重新操作。

应用场景
悲观锁:因为悲观锁会影响系统吞吐的性能,所以适合应用在写为居多的场景下。
乐观锁:因为乐观锁就是为了避免悲观锁的弊端出现的,所以适合应用在读为居多的场景下

实例
设置场景:从表中可以看到猪肉脯目前的数量只有1个了。在不加锁的情况下,如果A,B同时下单,就会报错。下面分别从乐观锁和悲观锁来解决这个问题。
在这里插入图片描述

1. 悲观锁解决
利用悲观锁的解决思路是,A下单前先给猪肉脯这行数据(id=1)加上悲观锁(行锁)。此时这行数据只能A来操作,也就是只有A能买。B想买就必须一直等待。当A买好后,B再想去买的时候会发现数量已经为0,那么B看到后就会放弃购买。

那么如何给猪肉脯也就是id=1这条数据加上悲观锁锁呢?我们可以通过以下语句给id=1的这行数据加上悲观锁

select num from goods where id = 1 for update;

下面是悲观锁的加锁图解
在这里插入图片描述


我们通过开启mysql的两个会话,也就是两个命令行来演示。

事务A执行命令给id=1的数据上悲观锁准备更新数据
在这里插入图片描述
这里之所以要以begin开始,是因为mysql是自提交的,所以要以begin开启事务,否则所有修改将被mysql自动提交。

事务B也去给id=1的数据上悲观锁准备更新数据
在这里插入图片描述
我们可以看到此时事务B再一直等待A释放锁。如果A长期不释放锁,那么最终事务B将会报错,这有兴趣的可以去尝试一下。

接着我们让事务A执行命令去修改数据,让猪肉脯的数量减一,然后查看修改后的数据,最后commit,结束事务。
在这里插入图片描述
我们可以看到,此时最后一个猪肉脯被A买走,只剩0个了。

当事务A执行完第3步后,我们看事务B中出现了什么
在这里插入图片描述

我们看到由于事务A释放了锁,事务B就结束了等待,拿到了锁,但是数据此时变成了0,那么B看到后就知道被买走了,就会放弃购买。
通过悲观锁,我们解决了猪肉脯购买的问题。

2. 乐观锁解决
下面,我们利用乐观锁来解决该问题。上面乐观锁的介绍中,我们提到了,乐观锁是通过版本号version来实现的。 所以,我们需要给goods表加上version字段,表变动后的结构如下
在这里插入图片描述
具体解决思路:

  • A和B同时将猪肉脯的数据查出来(id=1,name=猪肉脯,num=1,version=0)
  • 然后A先买,A将id=1和version=0作为条件进行数据更新,即将num-1,并且将version+1。此时猪肉脯的数据为(id=1,name=猪肉脯,num=0,version=1)
  • 最后B开始买,B也将id=1和version=0作为条件进行数据更新,但是更新完后,发现更新的数据行数为0(即查询不到数据,因为version已经不是0了),此时就说明已经有人改动过数据,此时就应该提示用户重新查看最新数据购买。

在这里插入图片描述


我们还是通过开启mysql的两个会话,也就是两个命令行来演示。

事务A执行查询命令,事务B执行查询命令,因为两者查询的结果相同,所以下面我只列出一个截图。
在这里插入图片描述
事务A进行购买更新数据,然后再查询更新后的数据。
在这里插入图片描述
我们可以看到事务A成功更新了数据和版本号。
事务B再进行购买更新数据,然后我们看影响行数和更新后的数据
在这里插入图片描述
可以看到最终修改行数为0,数据没有改变。此时就需要我们告知用户重新处理。

5. 两段锁协议
a. 概念
事务的第一阶段是获得封锁,也称为扩展阶段。
事务的第二阶段是释放封锁,也称为收缩阶段。
b. 例子
T1:Slock A…Slock B…Xlock C…Unlock B…Unlock A…Unlock C
T2:Slock A…Unlock A…Slock B…Xlock C…Unlock C…Unlock B

T1符合两段锁协议,T2不符合。因为整个加锁阶段不能有解锁,解锁阶段不能有加锁。

c. 两段锁是可串行化的充分条件

  • 多个事务的并发执行是正确的,当且仅当其结果与按某一次序串行地执行这些事务时结果相同时,这种调度策略是可串行化(Serializable)调度。
  • 遵循两段锁协议,一定是可串行化的;不遵循两段锁协议,可能是可串行化的,也可能不是

d. 两段锁与死锁
采用两段锁协议也有可能产生死锁,这是因为每个事务都不能及时解除被封锁的数据,可能会导致多个事务都要求对方已经封锁的数据而不能继续运行。
在这里插入图片描述

6. 分段锁
ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
如下图,ConcurrentHashMap使用Segment数据结构,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。所以说,ConcurrentHashMap在并发情况下,不仅保证了线程安全,而且提高了性能。
在这里插入图片描述

7. 锁的升级
synchronized 还是一个重量级锁,是一个效率比较低下的锁。但是在JDK 1.6后,JVM为
了提高锁的获取与释放效率对synchronized 进行了优化,引入了偏向锁和轻量级锁 ,从此以后锁的状
态就有了四种:无锁、偏向锁、轻量级锁、重量级锁。并且四种状态会随着竞争的情况逐渐升级,而且
是不可逆的过程,即不可降级,这四种锁的级别由低到高依次是:无锁、偏向锁,轻量级锁,重量级
锁。如下图所示:
在这里插入图片描述
无锁(还没执行到同步块)
无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源。

偏向锁(只有一个线程执行到同步块)
初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁。执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会判断此时持有锁的线程是否就是自己(持有锁的线程ID也在对象头里),如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

轻量级锁(少量线程执行到同步块)
轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。

重量级锁(大量线程执行到同步块)
如果锁竞争情况严重,某个达到最大自旋次数的线程,会将轻量级锁升级为重量级锁(依然是CAS修改锁标志位,但不修改持有锁的线程ID)。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。

MySQL数据库优化

1. 该如何优化MySQL查询
使用索引
使用索引查询可以提高查询速度,但也要注意索引失效场景。

优化子查询
执行子查询时,MySQL需要为内层查询语句的查询结果建立一个临时表。然后外层查询语句从临时表中查询记录。查询完毕后,再撤销这些临时表,所以可以使用连接查询来替代子查询。

2. 该如何优化MySQL插入
禁用索引
因为每次插入数据都要更新索引,所以对于空表批量导入数据,则不需要进行此操作,因为MyISAM引擎的表是在导入数据之后才建立索引的。

禁用唯一性检查
插入数据时,MySQL会对插入的记录进行唯一性校验。这种唯一性校验也会降低插入记录的速

禁用外键检查
插入数据之前执行禁止对外键的检查,数据插入完成之后再恢复对外键的检查

使用批量插入
插入多条记录时,可以使用一条INSERT语句插入一条记录,也可以使用一条INSERT语句插入多条记录。使用一条INSERT语句插入多条记录的情形如下,而这种方式的插入速度更快。

INSERT INTO fruits VALUES
('x1', '101', 'mongo2', '5.7'),
('x2', '101', 'mongo3', '5.7'),
('x3', '101', 'mongo4', '5.7');

3. 该如何处理慢查询
开启慢查询日志
在my.ini或者my.cnf文件中配置long_query_time选项指定记录阈值,如果查询时间超过阈值,这个查询过程将被记录到慢查询日志文件中。

分析慢查询日志
利用explain关键字可以模拟优化器执行SQL查询语句,来分析sql慢查询语句

常见慢查询优化
让失效的索引起作用、优化数据库结构、分解关联查询、优化LIMIT分页

4. LIMIT分页查询
出现问题
limit 90000,10的意思扫描满足条件的90010行,扔掉前面的90000行,返回最后的10行。

覆盖索引 + 直接定位
建立id索引;查询索引id
select * from t_topic where id>=(select id from t_topic order by id limit 90000,1) limit 10;

MySQL六大范式

第一范式:数据项不可分;
第二范式:消除了部分函数依赖,非主属性完全依赖主属性
第三范式:消除了传递依赖,任何非主属性不依赖于其它非主属性
BC范式:消除了主属性对候选键的部分依赖
第四范式:消除了多值依赖

MySQL主从同步

1. 主服务器(master)把数据更改记录到二进制日志(binlog)中。
2. 从服务器(slave)把主服务器的二进制日志复制到自己的中继日志(relay log)中。
3. 从服务器重做中继日志中的日志,把更改应用到自己的数据库上,以达到数据的最终一致性。

静态SQL动态SQL

区分动态SQL与静态SQL:看下面三种情况

SELECT * FROM student WHERE name = 'John'
<select id = "selectByName" resultType = "student" parameterType = "String">
	SELECT * FROM student WHERE name = #{name}
</select>
DECLARE @SQL AS VARCHAR(100);
DECLARE @CusID1 AS VARCHAR(2);
DECLARE @Column AS VARCHAR(20);
SET @Column = '姓名,省份';
SET @CusID1 = '1';
SET @SQL = 'SELECT' + @Column = 'FROM Customers where 客户ID = ' + @CusID1;
EXEC(@SQL);

分析上述三种情况

  • 第一种情况,不需要传入任何参数,SQL语句完全不变,纯纯的静态SQL;
  • 第二种情况,需要传入name参数,但是SQL语句整体框架不变,也属于静态SQL;
  • 第三种情况,需要传入表名和参数值,改变了SQL语句整体框架,属于动态SQL;

SQL注入防御方法

1. 使用PreparedStatement(首选)
SQL 注入是在解析的过程中生效的,用户的输入会影响 SQL 解析的结果。因此,我们可以通过使用 PreparedStatement,将 SQL 语句的解析和实际执行过程分开,只在执行的过程中代入用户的操作。这样一来,无论黑客提交的参数怎么变化,数据库都不会去执行额外的逻辑,也就避免了 SQL 注入的发生。

String sql = "SELECT * FROM Users WHERE UserId = ?";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setInt(1, userId); 
ResultSet results = statement.executeQuery();

2. 使用存储过程
原理和使用 PreparedStatement 类似,都是通过将 SQL 语句的解析和执行过程分开,来实现防护。区别在于,存储过程防注入是将解析 SQL 的过程,由数据库驱动转移到了数据库本身。

delimiter $$  #将语句的结束符号从分号;临时改为两个$$(可以是自定义)
CREATE PROCEDURE select_user(IN p_id INTEGER)
BEGIN
  SELECT * FROM Users WHERE UserId = p_id;
END$$ 
delimiter;  #将语句的结束符号恢复为分号

call select_user(1);

3. 验证输入
对所有输入进行验证或者过滤操作,能够很大程度上避免 SQL 注入的出现。比如,在通过 userId 获取 Users 相关信息的示例中,我们可以确认 userId 必然是一个整数。因此,我们只需要对 userId 参数,进行一个整型转化(比如,Java 中的 Integer.parseInt,PHP 的 intval),就可以实现防护了。

当然,部分场景下,用户输入的参数会比较复杂。我们以用户发出的评论为例,其内容完全由用户定义,应用无法预判它的格式。这种情况下,应用只能通过对部分关键字符进行过滤,来避免 SQL 注入的发生。比如,在 MySQL 中,需要注意的关键词有" % ’ \ _。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值