MySQL高级部分
MySQL5 和MySQL8 区别
- MySQL8之前 默认字符集为 latin1,utf8字符集指向的是 utf8mb3 不设置utf8会导致插入中文数据报错
- MySQL 8.0以后默认字符集utf8,指向的是 utf8mb4
MySQL字符集
服务器级别 service
数据库级别 database
表级别 创建表的时候设置的
CREATE TABLE 表名 (列的信息)
[[DEFAULT] CHARACTER SET 字符集名称]
[COLLATE 比较规则名称]]
ALTER TABLE 表名
[[DEFAULT] CHARACTER SET 字符集名称]
[COLLATE 比较规则名称]
列级别 创建列的时候设置的
CREATE TABLE 表名(
列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称],
其他列...
);
ALTER TABLE 表名 MODIFY 列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称];
MySQL目录
- MySQL数据库文件的存放路径:/var/lib/mysql/
- 相关命令目录:/usr/bin(mysqladmin、mysqlbinlog、mysqldump等命令)和/usr/sbin
- 配置文件目录:/usr/share/mysql-8.0(命令及配置文件),/etc/mysql(如my.cnf)
MySQL逻辑架构及SQL执行流程
总的来说,一条SQL的执行过程就是:客户端通过连接器建立连接,这个操作进行权限验证,通过连接器之后会先前往缓存,根据sql作为key去查询,查到直接返回,否则前往解析器,经过分析器对sql语句的分析、解析,得到一个mysql可以理解的语法,随后进入优化器,mysql会根据你查询的条件进行适当的优化,之后在经过执行器,这就真正的开始前往存储引擎查询数据,最后将查询到的数据返回给客户端,顺便写入缓存(不一定,MySQL8.0以后没有缓存)。
我们拆开来看,一共有下面这几步
第一步:连接器
建立连接,同时校验账号密码及IP。权限(库权限,表权限,列权限)
第二步:查询缓存(过于鸡肋,MySQL8以后删掉了)
连接器得工作完成后,客户端就可以向 MySQL 服务发送 SQL 语句了,MySQL 服务收到 SQL 语句后,就会解析出 SQL 语句的第一个字段,看看是什么类型的语句。
如果 SQL 是查询语句(select 语句),MySQL 就会先去查询缓存里查找缓存数据,看看之前有没有执行过这一条命令,这个查询缓存是以 key-value 形式保存在内存中的,key 为 SQL 查询语句,value 为 SQL 语句查询的结果。
如果查询的语句命中查询缓存,那么就会直接返回 value 给客户端。如果查询的语句没有命中查询缓存中,那么就要往下继续执行,等执行完后,查询的结果就会被存入查询缓存中。
对于更新比较频繁的表,查询缓存的命中率很低的,因为只要一个表有更新(增删改)操作,那么这个表的查询缓存就会被清空
第三步:解析器
在正式执行 SQL 查询语句之前, MySQL 会先对 SQL 语句做解析,这个工作交由由解析器来完成,解析器会做两件事情。
1,词法分析 MySQL 会根据你输入的字符串识别出关键字出来,构建出 SQL 语法树,这样方面后面语法分析获取 SQL 类型、表名、字段名、 where 条件等等。
2,语法分析 根据上面分析的结果,语法解析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。(如果我们输入的 SQL 语句语法不对,或者数据表或者字段不存在,都会在解析器这个阶段报错。)
第四步:优化器
此阶段主要是进行SQL语句的优化会根据执行计划进行最优的选择,匹配合适的索引,选择最佳的执行方案。
截止到现在,还没有真正的读表,而是产出了一个执行计划。于是进入到执行器阶段
第五步:执行器
根据表的引擎定义,调用存储引擎API对表进行读写操作
存储引擎
索引
索引数据结构(B+树)
为了方便理解,把数据页的上层叫做目录页
最下层数据页也就是叶子节点,存放数据(只在聚簇索引中存放)
每一页数据页存放16KB大小的数据,里面的数据按照顺序排列,上层目录页的黄色表示数据页最小数据,粉色表示数据页的页码(地址指向),而再上一层其实就是为了防止数据量很大的时候,只能在第二层去循环查找到属于哪一个目录页(循环的目录页次数过多),在用二分法去找到具体的数据页
常见的索引
- 聚簇索引(主键索引) 包含数据
就是上面B+树的图~
隐士的聚簇索引有什么用。针对的是哪一列呢???
-
二级索引(辅助索引、非聚簇索引)
这种索引的方式去查找数据,只能通过索引列的值找到对应的主键ID,而查询不到其他列的相关数据,所以需要通过ID再去聚簇索引查找一遍。这个过程叫做 回表 也就是通过非聚簇索引查找完整的数据需要查找2次B+树(实际上不一定非得回表,还是要看查询语句查询的列是否被全部被索引命中) -
联合索引
实际上就是在二级索引的数据列里在增加一列, 并且图中的列应该包含主键ID进行唯一性的区分。
聚簇索引与非聚簇索引的区别是什么?
InnoDB的B+树索引的注意事项
1.根页面位置万年不动
学习的时候为了方便理解是按照从下往上的方式创建数据页,但实际过程是按照从上往下的方式创建数据页,根节点永远不变
每当为某个表创建一个B+树索引(聚集索引不是人为创建的,默认就有)的时候,都会为这个索引创建一个根节点页面。最开始表中没有数据的时候,每个B+树索引对应的根节点中既没有数据,也没有目录项记录
随后向表中插入数据时,先把用户记录存储到这个根节点中
当根节点中的可用空间用完时继续插入,此时会将根节点中的所有记录复制到一个新分配的页,比如页a中,然后对这个页a进行页分裂的操作,得到另一个新页,如页b。这时新插入的记录根据键值(也就是聚簇索引中的主键值,二级索引中对应的索引列的值)的大小就会被分配到页a或者页b中,而根节点便升级为存储目录项记录的页)
2.内节点中目录项记录的唯一性: 在非聚簇索引中,可能某个数据页里某列有相同的值,这时需要带上一个主键ID做区分
3.一个数据页最少存储2条记录:
- 页的大小:INNODB使用固定大小的数据页来存储数据,通常为16KB。为了充分利用每个数据页的空间,最少存储两条记录可以避免过多的碎片化。
- 页的读取和写入:当数据库需要读取或写入数据时,通常是以数据页为单位进行操作。最少存储两条记录可以减少I/O操作的次数,提高数据的读取和写入效率。
- 防止数据页频繁分裂。数据页分裂是指当一个数据页已经满了,需要插入新的记录时,需要将该数据页分裂成两个数据页,这个操作会带来一定的性能和存储开销。通过在一个数据页中至少存储2条记录,可以减少数据页分裂的频率,因为在插入新记录时,已经有足够的空间可用。这有助于提高数据库的性能和效率。
B-Tree和B+Tree区别
- B+树内节点不存储数据,所有 data 存储在叶节点导致查询时间复杂度固定为 log n。而B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)。
- B+树叶节点两两通过链表方式相连大大增加区间(范围)访问性,可使用在范围查询等,而B-树每个节点 key 和 data 在一起,则无法区间查找(主要是部分数据可能还存在上层节点 ,增加了IO次数)。
- B+树更适合外部存储。由于内节点无 data 域,每个节点能索引的范围更大更精确(B+数的非叶子节点不存放data,目录页(数据页)可以存放更多元素,更矮胖,减少io次数)
怎么解释数据页是双向链表方式连接?
其实数据页的头部信息就存放了上一页和下一页的相关信息
创建和删除索引
- 创建
- 创建表的时候创建索引
CREATE TABLE table_name [col_name data_type]
[UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY] [index_name] (col_name [length]) [ASC |
DESC]
CREATE TABLE test1(
id INT NOT NULL,
name varchar(30) NOT NULL,
XXX(索引类型 如唯一索引UNIQUE等,也可以不加,表示普通索引)INDEX uk_idx_id(索引名)(id)(字段名)
);
- 在已经存在的表上创建索引
ALTER TABLE table_name ADD [UNIQUE | FULLTEXT | SPATIAL] [INDEX | KEY]
[index_name] (col_name[length],...) [ASC | DESC]
- 删除
ALTER TABLE table_name DROP INDEX index_name;
适合创建索引场景
- 字段的数值有唯一性的限制
- 频繁作为 WHERE 查询条件的字段
- 经常 GROUP BY 和 ORDER BY 的列
同时创建GROUP BY 和 ORDER BY 的列索引时,要 将GROUP BY的列放前面 - UPDATE、DELETE 的 WHERE 条件列
- DISTINCT 字段需要创建索引
- 多表 JOIN 连接操作时,创建索引注意事项
首先, 连接表的数量尽量不要超过 3 张 ,因为每增加一张表就相当于增加了一次嵌套的循环,数量级增长会非常快,严重影响查询的效率。
其次, 对 WHERE 条件创建索引 ,因为 WHERE 才是对数据条件的过滤。如果在数据量非常大的情况下,没有 WHERE 条件过滤是非常可怕的。
最后, 对用于连接的字段创建索引 ,并且该字段在多张表中的类型必须一致 ,不能一个为 int 另一个为 varchar 类型。 类型不一致会涉及到函数转换,导致索引失效 - 使用列的类型小的创建索引
- 使用字符串前缀创建索引
- 区分度高(散列性高)的列适合作为索引
- 使用最频繁的列放到联合索引的左侧
这样也可以较少的建立一些索引。同时,由于"最左前缀原则",可以增加联合索引的使用率。 - 在多个字段都要创建索引的情况下,联合索引优于单值索引
不适合创建索引场景
- 在where中使用不到的字段,不要设置索引
- 数据量小的表最好不要使用索引
- 有大量重复数据的列上不要建立索引(其实就是散列性不高的列)
- 避免对经常更新的表创建过多的索引
- 不建议用无序的值作为索引
例如身份证、UUID(在索引比较时需要转为ASCII,并且插入时可能造成页分裂)、MD5、HASH、无序长字
符串等。 - 删除不再使用或者很少使用的索引
- 不要定义冗余或重复的索引
慢查询日志使用
怎么查看慢sql呢?
-
开启慢查询日志参数
mysql > set global slow_query_log='ON'
;
可以看到慢查询分析已经开启,同时文件保存在 /var/lib/mysql/atguigu02-slow.log 文件中 -
修改long_query_time阈值(多少秒算慢sql,记录到日志中)
查看mysql中默认是多少秒mysql > show variables like '%long_query_time%';
测试发现:设置global的方式对当前session的long_query_time失效。对新连接的客户端有效。所以可以一并
执行下述语句
mysql > set global long_query_time = 1;
mysql> set long_query_time=1;
-
慢查询日志分析工具:mysqldumpslow
查看mysqldumpslow的帮助信息
mysqldumpslow --help
命令的具体参数如下:
-a: 不将数字抽象成N,字符串抽象成S (显示正常的入参)
-s: 是表示按照何种方式排序:
c: 访问次数
l: 锁定时间
r: 返回记录
t: 查询时间
al:平均锁定时间
ar:平均返回记录数
at:平均查询时间 (默认方式)
ac:平均查询次数
-t: 即为返回前面多少条的数据; -
关闭慢查询日志
开启后记得要关闭,一般生产环境如果不开启,毕竟开启慢查询日志对性能有影响
EXPLAIN 分析
列名 | 描述 |
---|---|
id | 在一个大的查询语句中每个SELECT关键字都对应一个 唯一的id |
select_type | SELECT查询类型 |
table | 表名 |
partitions | 匹配的分区信息 |
type | 针对单表的访问方法(system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL ,一般来说,好的sql查询至少达到range级别 ,最好能达到ref) |
possible_keys | 可能使用的索引 |
key | 实际使用的索引(如果没有使用索引,则为null) |
key_len | 实际使用的索引的长度 (主要对于联合索引,看命中的索引个数,当然是命中越多越好??? ) |
ref | 显示哪些列或常量被用于查找索引列上的值 ,有可能是一个常数,函数等 |
rows | 预估的需要读取的记录条数(越小越好) |
filtered | 某个表经过搜索条件过滤后剩余记录条数的百分比(相对越大越好) |
Extra | 额外信息 |
key_len
计算方法:
key_len 列表示当优化器决定使用某个索引执行查询时,该索引记录的最大长度: 对于使用固定长度类型的索引列来说,它实际占用的存储空间的最大长度就 是该固定值,对于指定字符集的变长类型的索引列来说,比如VARCHAR(100) 100位字符,使用的字符集是 utf8,那么该列实际占用的最大存储空间就 是 100 x 3 = 300 个字节。
如果该索引列可以存储 NULL 值,则 key_len 比不可以存储 NULL 值时多 1 个 字节。
对于可变长度(varchar)字段来说,都会有 2 个字节的空间来存储该变长列的实际长度
type
- system:(非InnoDB)MyISAM引擎
当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的,比如 MyISAM、Memory,那么对该表的访问方法就是 system - const:唯一查询,主键或者唯一二级索引列与常数进行等值匹配
- eq_ref(关联查询,被驱动表条件为唯一索引)
EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id
- ref(普通索引查询)
普通索引列允许为null的情况下,数量必然是不受限制的,所以我们采用 key IS NULL 这种形式的搜索条件最多只能使用 ref 的访问方法,而不是 const 的访问方法
EXPLAIN SELECT * FROM s1 WHERE order_no = 'a';
- ref_or_null(普通二级索引+null值)
explain SELECT * FROM order_exp_cut WHERE order_no= 'abc' OR order_no IS NULL;
- index_merge(使用多个索引)
EXPLAIN SELECT * FROM s1 WHERE order_no = 'a' OR insert_time = '2021-03-22 18:36:47';
- unique_subquery(主键列子查询)
EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2 where s1.insert_time = s2.insert_time) OR order_no = 'a';
- index_subquery(普通列子查询)
EXPLAIN SELECT * FROM s1 WHERE order_no IN (SELECT order_no FROM s2 where s1.insert_time = s2.insert_time) OR order_no = 'a';
- range(范围)
EXPLAIN SELECT * FROM s1 WHERE order_no IN ('a', 'b', 'c');
EXPLAIN SELECT * FROM s1 WHERE order_no > 'a' AND order_no < 'b';
- index(扫描整个索引并使用索引覆盖)
EXPLAIN SELECT key_part2 FROM s1 WHERE key_part3 = 'a';
- all(全表扫描)
EXPLAIN SELECT * FROM s1;
索引优化和查询优化
首先都有哪些维度可以进行数据库优化呢?
1.索引失效,没有充分利用索引 — 创建索引
2.关联查询join太多表 — SQL优化
3.服务器调优及参数调优(缓冲。线程池,连接数等)—调整my.cnf
4.分库分表,缓解数据库压力
索引失效
这种失效场景只是规则,具体还是得看优化器的成本计算(后续覆盖索引有例子)
1. 最左前缀法则(针对联合索引。最左边的列 必须出现在where条件中)
使用复合索引,需要遵循最佳左前缀法则,即如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列
2. 计算、函数、类型转换(自动或手动)导致索引失效
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.name LIKE 'abc%'; 索引生效,是根据右边进行模糊搜索,但是左边还能走索引
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE LEFT(student.name,3) = 'abc'; 索引失效,函数作用在了索引列上
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name=123; 索引失效,实际上做了隐士转换。将int类型转换成varchar类型
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE name='123'; 索引生效
3. 范围条件右边的列索引失效(范围列不失效,所有一般将范围列放在联合索引最右边)
EXPLAIN SELECT SQL_NO_CACHE * FROM student WHERE student.age=30 AND student.classId>20 AND student.name = 'abc';
索引只作用了age和classId. int类型4字节,非空1字节。 key_len: 4+1+4+1=10
4. 不等于(!= 或者<>)索引失效
5. null可以使用索引,is not null无法使用索引
6. like以通配符%开头索引失效
7. OR 前后存在非索引的列,索引失效(or两边的字段必须都有索引,否则都失效)
8. 数据库和表的字符集统一使用utf8mb4
统一使用utf8mb4( 5.5.3版本以上支持)兼容性更好,统一字符集可以避免由于字符集转换产生的乱码。不
同的 字符集 进行比较前需要进行 转换 会造成索引失效。
关联查询优化
join 的语法(左联,右联,内联,外联)这里就不说了。主要说下join的执行过程:
EXPLAIN SELECT * FROM t1 JOIN t2 ON (t1.a=t2.a)
首先说一下驱动表和被驱动表,用EXPLAIN 查看,驱动表在上面,被驱动表在下面
- 从表t1中读入一行数据 R;
- 从数据行R中,取出a字段到表t2里去查找;
- 取出表t2中满足条件的行,跟R组成一行,作为结果集的一部分;
- 重复执行步骤1到3,直到表t1的末尾循环结束。
总结:这个过程是先遍历表t1,然后根据从表t1中取出的每行数据中的a值,去表t2中查找满足条件的记录。MySQL5.5版本之前,这就是Nested Loop Join(嵌套循环),在MySQL5.5以后的版本中,MySQL通过引入BNLJ(缓存块嵌套循环连接) 算法来优化嵌套执行,当被驱动表的连接条件字段没有建立索引时,mysql每次读取被驱动表数据之前会将驱动表的多条数据一次性存入缓冲区中,然后再全表扫描被驱动表。
通过这种方式,我们可以发现,其实可以在被驱动表中的join连接字段上加入索引,这样就不需要全表扫码t2表了,减少内层表数据的匹配次数,这是INLJ(索引嵌套循环连接)
从MySQL的8.0.20版本开始将废弃BNLJ,因为从MySQL8.0.18版本开始就加入了hash join,默认都会使用hash join 。
对于被连接的数据子集较小的情况,Nested Loop是个较好的选择。
Hash Join 是做大数据集连接时的常用方式,优化器使用两个表中较小(相对较小)的表利用 join key在内存中建立散列表,然后扫描较大的表并探测散列表,找出与hash表匹配的行。
优化
- 整体效率比较:INLJ (索引方式)> BNLJ(缓存方式) > SNLJ(嵌套循环方式)
- 保证被驱动表的JOIN字段已经创建了索引(减少内层表的循环匹配次数)
- 需要JOIN 的字段,数据类型保持绝对一致。
- LEFT JOIN 时,选择小表作为驱动表, 大表作为被驱动表 。减少外层循环的次数。
- INNER JOIN 时,MySQL会自动将 小结果集的表选为驱动表 。选择相信MySQL优化策略。
- 能够直接多表关联的尽量直接关联,不用子查询。(减少查询的趟数)
- 增大join_buffer_size的大小(一次缓存的数据越多,那么内存表的扫描次数就越少)。
- 减少驱动表不必要的字段查询(字段越少,join buffer所缓存的数据就越多)。
排序优化
优化建议:
- SQL 中,可以在 WHERE 子句和 ORDER BY 子句中使用索引,目的是在 WHERE 子句中 避免全表扫描 ,在 ORDER BY 子句 避免使用 FileSort 排序 。当然,某些情况下全表扫描,或者 FileSort 排序不一定比索引慢。但总的来说,我们还是要避免,以提高查询效率。
- 尽量使用 Index 完成 ORDER BY 排序。如果 WHERE 和 ORDER BY 后面是相同的列就使用单索引列,如果不同就使用联合索引。
- 无法使用 Index 时,需要对 FileSort 方式进行调优
具体是否使用到索引还得根据实际数据量等情况才得知,下面主要是规则:
- order by时不用limit,可能会索引失效,主要看是否需要回表,如果需要回表,那可能不如直接将数据取出到内存中,再进行排序;
- order by的顺序错误,也会索引失效— 这里指的是order by的多个字段与联合索引顺序不一致,导致失效
- order by规则不一致,也会失效。这里的规则指的是字段对应的升序降序(这里字段应该是指多个字段,一个字段的话就不存在相反不相反了)。但是如果都相反的话,又会使用上索引。是因为,虽然每个数据之间是单向连接,但是数据页是双向链表连接的,可以反过来读取数据页
覆盖索引
select的数据列从索引中就能够取得,不必根据索引里的id再次回表读取数据文件获取其他数据列,有效减少IO次数。
判断是否使用了覆盖索引:当一个查询所查询的列都在索引字段上,且在查询分析器EXPLAIN的Extra列可以看到“Using index” ,则可判定为覆盖索引。
按照之前所知道的,出现<> ,!= 这种情况的时候,是不会使用索引的。但是,在select的列在索引列里都包含时,这种情况下优化器就会计算成本,发现走索引的成本更低,就出现了索引没有失效的场景
索引下推
索引下推 Index Condition Pushdown(ICP) 是MySQL 5.6中新特性,是一种在存储引擎层使用索引过滤数据的一种优化方式。ICP可以减少存储引擎访问基表的次数以及MySQL服务器访问存储引擎的次数。
ICP的使用条件:
① 只能用于二级索引(secondary index)
②explain显示的执行计划中type值(join 类型)为 range 、 ref 、 eq_ref 或者 ref_or_null 。
③ 并非全部where条件都可以用ICP筛选,如果where条件的字段不在索引列中,还是要读取整表的记录
到server端做where过滤。
④ ICP可以用于MyISAM和InnnoDB存储引擎
⑤ MySQL 5.6版本的不支持分区表的ICP功能,5.7版本的开始支持。
⑥ 当SQL使用覆盖索引时,不支持ICP优化方法。
简单来说,就是通过索引找到对应数据后,再把where条件后,联合索引有的列继续判断过滤一部分数据后,再去回表查询,减少IO次数
数据库设计
三大范式
第一范式:有主键,具有原子性,字段不可分割
这user表就不满足第一范式,因为地址还可以再拆分成省市区
CREATE TABLE `user` (
`user_id` varchar(50) NOT NULL COMMIT '人员编号',
`name` varchar(50) DEFAULT NULL COMMENT '姓名',
`mobile` varchar(11) DEFAULT NULL COMMENT '电话',
`email` varchar(50) DEFAULT NULL COMMENT '邮箱',
`address` varchar(50) DEFAULT NULL COMMENT '地址(如湖南省长沙市岳麓区XXX街道XXX)'
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
第二范式:确保每列都和主键完全依赖
第三范式:确保每列都和主键列直接相关,而不是间接相关
部门信息表 :每个部门有部门编号(dept_id)、部门名称、部门简介等信息。
员工信息表 :每个员工有员工编号、姓名、部门编号。列出部门编号后就不能再将部门名称、部门简介
等与部门有关的信息再加入员工信息表中。
调优
第1步:选择适合的 数据库管理系统 DBMS(数据量很大可以考虑选择收费的SQL service, pgSQL等)
第2步:优化表设计(三大范式等)
- 增加冗余字段
- 优化数据类型
- 对整数类型数据进行优化
- 对于非负型的数据(如自增ID、整型IP)来说,要优先使用无符号整UNSIGNED来存储。因为无符号存储的数值范围更大。
- 既可以使用文本类型也可以使用整数类型的字段,要选择使用整数类型
- 避免使用TEXT、BLOB数据类型
- 避免使用ENUM类型
- 使用TIMESTAMP存储时间
- 用DECIMAL代替FLOAT和DOUBLE存储精确浮点数
第3步:优化逻辑查询(对SQL语句的优化,子查询等)
第4步:优化物理查询(对索引的优化,使用上索引)
第5步:使用 Redis 或 Memcached 作为缓存
第6步:库级优化
- 分库
- 经典的数据库拆分方案,主库负责写,从库负责读。
事务
事务四大特性
- 原子性(Atomicity):事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。
- 一致性(Consistency):事务必须使数据库从一个一致状态变换到另外一个一致状态,常见例子,银行转账场景。
- 隔离性(Isolation):一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性(Durability):一个事务一旦提交成功,它对数据库中数据的改变将是永久性的,接下来的其他操作或故障不应对其有任何影响。
事务分类
个人理解:
显式事务:关闭自动提交事务属性,使用手动开启事务(START TRANSACTION 或者 BEGIN
),提交事务方式(COMMIT
)
隐式事务:通过自动提交事务的方式执行增删改操作
事务并发问题
- 脏读
对于两个事务 Session A、Session B,Session A 读取了已经被 Session B 更新但还没有被提交的字段。
之后若 Session B 回滚 ,Session A 读取的内容就是错误的数据。 - 不可重复读
对于两个事务Session A、Session B,Session A 读取了一个字段,然后 Session B 更新了该字段。 之后
Session A 再次读取同一个字段, 值就不同了。那就意味着发生了不可重复读。 - 幻读( Phantom )
对于两个事务Session A、Session B, Session A 从一个表中读取了一个字段, 然后 Session B 在该表中插
入了一些新的行。 之后, 如果 Session A 再次读取同一个表, 就会多出几行。那就意味着发生了幻读。
MySQL隔离级别
- READ UNCOMMITTED :读未提交
在该隔离级别,所有事务都可以看到其他未提交事务的执行结
果。不能避免脏读、不可重复读、幻读。 - READ COMMITTED :读已提交,
它满足了隔离的简单定义:一个事务只能看见已经提交事务所做
的变化。可以避免脏读,但不可重复读、幻读问题仍然存在。这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。 - REPEATABLE READ :可重复读 MySQL默认隔离级别,(MySQL在此隔离级别下就可以解决幻读)
事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。因为针对的是某条数据,所以可以避免脏读、不可重复读,但幻读问题仍然存在。这是MySQL的默认隔离级别。 - SERIALIZABLE :可串行化
确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止
其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。
实现方式:
事务隔离级别的实现基于 锁机制 和 MVCC,不同的隔离级别对锁的使用是不同的
select语句均是快照读(MVCC解决)
delete / update / select …for update 等语句是加锁实现的
读已提交: 把释放锁调整到事务提交之后,在事务提交前,其他事务是无法对该行数据进行任何操作(包括读),效率极低,于是MySQL使用了一个并发版本控制机制,叫做MVCC。
可重复读: MySQL采取MVCC解决这个问题,在一个事务中第一次执行查询会创建一个Read View,之后的查询会复用之前的Read View。这样就保证了同一个事务多次读取数据时数据的一致性。
MySQL在可重复读隔离级别下解决幻读
MySQL的InnoDB 默认隔离级别是可重复读,它默认开启了间隙锁(只有RR隔离级别下才有间隙锁),使用临键锁(间隙锁 + 行锁)的方式来加锁,解决幻读。
MVCC对于 读已提交 和 可重复读 两种隔离级别差别就是:读已提交级别下,同一个事务下,每次查询都会创建一个 Read View,而可重复读只会复用第一次创建的Read View。
MySQL事务日志
redo log
redo log是事务持久性的保证
下面为一个update事务中,redo日志的整体流程
① 第 1 步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝;
② 第 2 步:生成一条重做日志并写入 redo log buffer,记录的是数据被修改后的值;
③ 第 3 步:当事务 commit 时,将 redo log buffer 中的内容刷新到 redo log file,对 redo log file采用追加写的方式;
④ 第 4 步:定期将内存中修改的数据刷新到磁盘中
redo log的刷盘策略
针对这种情况,InnoDB给出 innodb_flush_log_at_trx_commit 参数,该参数控制commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:
设置为0 :表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日志的同步)
设置为1 :表示每次事务提交时都将进行同步,刷盘操作( 默认值 )
设置为2 :表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由os操作系统自己决定什么时候同步到磁盘文件。
undo log
undo log是事务原子性的保证。在事务中更新数据 的前置操作其实是要先写入一个 undo log 。
下面是一个update事务中,redo 和 undo log的流程图
undo log的删除
- 针对于insert undo log:
因为insert操作的记录,只对事务本身可见,对其他事务不可见。故该undo log可以在事务提交后直接删除,不需要进行purge操作。 - 针对于update undo log:
该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等
待purge线程进行最后的删除。
锁
表锁
上锁方式:
- 隐式上锁(默认,自动加锁自动释放)
select //上读锁
insert、update、delete //上写锁 - 显式上锁(手动)
lock table tableName read
;//读锁
lock table tableName write
;//写锁 - 解锁(手动)
unlock tables
;//所有锁表
行锁
上锁方式:
- 隐式上锁(默认,自动加锁自动释放)
select //不会上锁
insert、update、delete //上写锁 - 显式上锁(手动)
select * from tableName lock in share mode
;//读锁
select * from tableName for update
;//写锁
行锁类型:
- 记录锁
记录锁是封锁记录,记录锁也叫行锁,例如:select * from user where id = 1 for update;
它会在 id=1 的记录上加上记录锁,以阻止其他事务插入,更新,删除 id=1 这一行。 - 间隙锁
间隙锁基于非唯一索引,它锁定一段范围内的索引记录。使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据select * from user where id < 10 for update;
即所有在 [1,10)区间内的记录行都会被锁住,所有id 为 1、2、3、4、5、6、7、8、9 的数据行的插入会被阻塞
select …for update正常情况会是记录锁,但是这条记录不存在,导致直接锁了上下区间,也就是8-12 开区间
3. 临键锁
select * from student where id <=8 and id > 3 for update
感觉就是带了一个闭区间··
悲观锁
顾名思义,就是对于数据的处理持悲观态度,总认为会发生并发冲突,获取和修改数据时,别人会修改数据。所以在整个数据处理过程中,需要将数据锁定。
悲观锁的实现,通常依靠数据库提供的锁机制实现,比如mysql的排他锁,select … for update
来实现悲观锁。
如:select * from student where id =8
需注意,MySQL会将扫描到的所有行全部加锁,而不是id为8的行,所以最好是加索引
乐观锁
顾名思义,就是对数据的处理持乐观态度,乐观的认为数据一般情况下不会发生冲突,只有提交数据更新时,才会对数据是否冲突进行检测。
乐观锁的实现不依靠数据库提供的锁机制,需要我们自已实现,实现方式一般是记录数据版本,一种是通过版本号,一种是通过时间戳。给表加一个版本号或时间戳的字段,读取数据时,将版本号一同读出,数据更新时,将版本号加1。
MVCC
写的比较好理解,先参考这个吧~~~
https://blog.csdn.net/qq_36389060/article/details/124242769?ops_request_misc=&request_id=&biz_id=102&utm_term=MVCC&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-3-124242769.nonecase&spm=1018.2226.3001.4187