原理篇
架构、索引、事务、锁、日志、性能调优
高可用
读写分离、分库分表、分布式ID、高可用、分布式数据库、分布式事务、分布式锁
架构
1 执行一条 SQL 查询语句,期间发生了什么?
(1)连接器:客户端通过连接器与Server层建立连接(TCP连接),连接器还负责管理连接、校验用户身份;
(2)查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0
已删除该模块;
(3)解析器:解析 SQL,对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
(4)执行 SQL:执行 SQL 共有三个阶段:
1)预处理阶段:检查表或字段是否存在;将 select *
中的 *
符号扩展为表上的所有列。
2)优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划;
3)执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端,(三种执行方式:主键索引查询、全表扫描、索引下推);
执行一条 select 语句,期间发生了什么? | 小林coding (xiaolincoding.com)
InnoBD存储引擎
2 为什么InnoDB代替了MyISAM?
经过上述的一系列对比后,对于为何使用InnoDB
替换了MyISAM
引擎的原因,相信各位小伙伴也能感受出来,这里就等价于稍微做个总结:
-
①存储方式:
MyISAM
引擎会将表数据和索引数据分成两个文件存储。 -
②索引支持:因为
MyISAM
引擎的表数据和索引数据是分开的,因此不支持聚簇索引。 -
③事务支持:由于
MyISAM
引擎没有undo-log
日志,所以不支持多条SQL
组成事务并回滚。 -
④故障恢复:
MyISAM
引擎依靠bin-log
日志实现,bin-log
中未写入的数据会永久丢失。 -
⑤锁粒度支持:因为
MyISAM
不支持聚簇索引,因此无法实现行锁,所有并发操作只能加表锁。 -
⑥并发性能:
MyISAM
引擎仅支持表锁,所以多条线程出现读-写并发场景时会阻塞。 -
⑦内存利用度:
MyISAM
引擎过于依赖MySQL Server
,对缓冲池、异步IO
技术开发度不够。
上述这些MyISAM
不支持的,InnoDB
引擎全都支持,也正由于这方方面面的原因,InnoDB
引擎开始崭露锋芒,而作为MySQL
亲生子的MyISAM
自此之后跌落神坛,最终到了MySQL5.6
版本时,MyISAM
彻底让出了MySQL
默认存储引擎的宝座。
作者:竹子爱熊猫 链接:(十三)MySQL引擎篇:半道出家的InnoDB为何能替换官方的MyISAM? - 掘金
3 MySQL 一行记录的存储结构
问题引入:
-
MySQL 一行记录是怎么存储的?
-
MySQL 的 NULL 值会占用空间吗?
-
MySQL 怎么知道 varchar(n) 实际占用数据的大小?
-
varchar(n) 中 n 最大取值为多少?
-
行溢出后,MySQL 是怎么处理的?
MySQL 的数据存放在哪个文件?
可以看到,共有三个文件,这三个文件分别代表着:
-
db.opt,用来存储当前数据库的默认字符集和字符校验规则。
-
t_order.frm ,t_order 的表结构会保存在这个文件。
-
t_order.ibd,t_order 的表数据会保存在这个文件。
表空间的结构
表空间由段(segment)、区(extent)、页(page)、行(row)组成,InnoDB存储引擎的逻辑存储结构大致如下图:
图片来源:小林coding
空间组成:页组成区,区形成段,段组成表空间。
InnoDB 的数据是按「页」为单位来读写的,默认每个页的大小为 16KB,也就是最多能保证 16KB 的连续存储空间。
在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区(extent)为单位分配。每个区的大小为 1MB,对于 16KB 的页来说,连续的 64 个页会被划为一个区,这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了。
段一般分为数据段、索引段和回滚段等。
索引
索引简述
-
索引是加快查询的一种数据结构,其原理是插入时对数据排序,缺点是会影响插入的性能;
-
MySQL 当前支持 B+树索引、全文索引、R 树索引;
-
B+ 树索引的高度通常为 3~4 层,高度为 4 的 B+ 树能存放 50 亿左右的数据;
-
由于 B+ 树的高度不高,查询效率极高,50 亿的数据也只需要插叙 4 次 I/O;
-
MySQL 单表的索引没有个数限制,业务查询有具体需要,创建即可,不要迷信个数限制;
-
可以通过表 sys.schema_unused_indexes 和索引不可见特性,删除无用的索引。
数据结构
B+ 树索引是数据库系统中最为常见的一种索引数据结构,几乎所有的关系型数据库都支持它。
B+树索引的特点是: 基于磁盘的平衡树,但树非常矮,通常为 3~4 层,能存放千万到上亿的排序数据。树矮意味着访问效率高,从千万或上亿数据里查询一条数据,只用 3、4 次 I/O。
B+ 树索引由根节点(root node)、中间节点(non leaf node)、叶子节点(leaf node)组成,其中叶子节点存放所有排序后的数据。
回表
一条查询SQL
经历了两次查询才获取到数据,这个过程则被称之为回表。
例如:用户表中,基于ID
字段先建立了一个主键索引,然后又基于name
字段建立了一个普通索引,此时MySQL
默认会选用主键索引作为聚簇索引,将表数据和主键索引存在同一个文件中,也就是主键索引的每个索引节点,都直接对应着行数据。而基于name
字段建立的索引,其索引节点存放的则是指向聚簇索引的ID
值。
首先会走name
字段的索引,然后找到对应的ID
值,然后再基于查询到的ID
值,再走ID
字段的主键索引,最终得到一整条行数据并返回。
回表动作会导致额外的查询开销,因此尽量可以基于主键做查询,如果实在需要使用非主键字段查询,那么尽量要写明查询的结果字段,而并非使用*
。
当然,实际情况中建立联合索引,利用索引覆盖特性,从而避免使用辅助索引,这样也能够消除回表动作。
建立索引时原则
-
①经常频繁用作查询条件的字段应酌情考虑为其创建索引。
-
②表的主外键或连表字段,必须建立索引,因为能很大程度提升连表查询的性能。
-
③建立索引的字段,一般值的区分性要足够高,这样才能提高索引的检索效率。
-
④建立索引的字段,值不应该过长,如果较长的字段要建立索引,可以选择前缀索引。
-
⑤建立联合索引,应当遵循最左前缀原则(概念:组成联合索引的多个列,越靠左边优先级越高,同时也只有
SQL
查询条件中,包含了最左的字段,才能使用联合索引),将多个字段之间按优先级顺序组合。 -
⑥经常根据范围取值、排序、分组的字段应建立索引,因为索引有序,能加快排序时间。
-
⑦对于唯一索引,如果确认不会利用该字段排序,那可以将结构改为
Hash
结构。 -
⑧尽量使用联合索引代替单值索引,联合索引比多个单值索引查询效率要高。
同时,除开上述一些建立索引的原则外,在建立索引时还需有些注意点:
-
❶值经常会增删改的字段,不合适建立索引,因为每次改变后需维护索引结构。
-
❷一个字段存在大量的重复值时,不适合建立索引,比如之前举例的性别字段。
-
❸索引不能参与计算,因此经常带函数查询的字段,并不适合建立索引。
-
❹一张表中的索引数量并不是越多越好,一般控制在
3
,最多不能超过5
。 -
❺建立联合索引时,一定要考虑优先级,查询频率最高的字段应当放首位。
-
❻当表的数据较少,不应当建立索引,因为数据量不大时,维护索引反而开销更大。
-
❼索引的字段值无序时,不推荐建立索引,因为会造成页分裂,尤其是主键索引。
想要查看一条SQL
是否使用了索引,需要用到一个自带的分析工具ExPlain
type
:当前执行计划查询的类型,有几种情况:
-
all
:表示走了全表查询,未命中索引或索引失效。 -
system
:表示要查询的表中仅有一条数据。 -
const
:表示当前SQL
语句的查询条件中,可以命中索引查询。 -
range
:表示当前查询操作是查某个区间。 -
eq_ref
:表示目前在做多表关联查询。 -
ref
:表示目前使用了普通索引查询。 -
index
:表示目前SQL
使用了辅助索引查询。
索引失效
1、查询中带有OR会导致索引失效
2、模糊查询中like以%开头导致索引失效
3、字符类型查询时不带引号导致索引失效
4、索引字段参与计算导致索引失效
5、索引字段参与计算导致索引失效
6、字段被用于函数计算导致索引失效
7、违背最左前缀原则导致索引失效
8、反向范围操作导致索引失效
9、... ...
避免失效方式(反过来看)
①查询SQL
中尽量不要使用OR
关键字,可以使用多SQL
或子查询代替。
②模糊查询尽量不要以%
开头,如果实在要实现这个功能可以建立全文索引。
③编写SQL
时一定要注意字段的数据类型,否则MySQL
的隐式转换会导致索引失效。
④一定不要在编写SQL
时让索引字段执行计算工作,尽量将计算工作放在客户端中完成。
⑤对于索引字段尽量不要使用计算类函数,一定要使用时请记得将函数计算放在=
后面。
⑥多条件的查询SQL
一定要使用联合索引中的第一个字段,否则会打破最左匹配原则。
⑦对于需要对比多个字段的查询业务时,可以拆分为连表查询,使用临时表代替。
⑧在SQL
中不要使用反范围性的查询条件,大部分反范围性、不等性查询都会让索引失效。
⑨.......
索引覆盖
一句话概述:就是要查询的列,在使用的索引中已经包含,被所使用的索引覆盖,这种情况称之为索引覆盖。
例子:
索引下推
也就是将Server
层筛选数据的工作,下推到引擎层处理。
可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。
索引的分类
按照四个角度来分类索引。
-
按「数据结构」分类:B+tree索引、Hash索引、Full-text索引。
-
按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。
-
按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。使用前缀索引的目的是为了减少索引占用的存储空间,提升查询效率。
-
按「字段个数」分类:单列索引、联合索引。
在创建表时,InnoDB 存储引擎会根据不同的场景选择不同的列作为索引:
-
如果有主键,默认会使用主键作为聚簇索引的索引键(key);
-
如果没有主键,就选择第一个不包含 NULL 值的唯一列作为聚簇索引的索引键(key);
-
在上面两个都没有的情况下,InnoDB 将自动生成一个隐式自增 id 列作为聚簇索引的索引键(key);
其它索引都属于辅助索引(Secondary Index),也被称为二级索引或非聚簇索引。创建的主键索引和二级索引默认使用的是 B+Tree 索引。
联合索引的最左匹配原则,在遇到范围查询(如 >、<)的时候,就会停止匹配,也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。注意,对于 >=、<=、BETWEEN、like 前缀匹配的范围查询,并不会停止匹配。
优化索引的方法?
前缀索引优化
使用前缀索引是为了减小索引字段大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减小索引项的大小。
覆盖索引优化
是指 SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能找得到的那些索引,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,可以避免回表的操作。
主键索引最好是自增的
如果我们使用非自增主键,由于每次插入主键的索引值都是随机的,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为页分裂。页分裂还有可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率。
防止索引失效
总结
事务![](https://i-blog.csdnimg.cn/blog_migrate/51ca9c1732514247c23e9e11024c15a0.png)
事务:对数据的操作要么全部执行,要么全部不执行。如果执行没有成功,则会发生回滚,恢复到事务开始的时刻。
事务特性
事务是由 MySQL 的引擎来实现的,常见的 InnoDB 引擎它是支持事务的。不过并不是所有的引擎都能支持事务,比如 MySQL 原生的 MyISAM 引擎就不支持事务。
实现事务必须要遵守 4 个特性,分别如下:
-
原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
-
一致性(Consistency):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
-
隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。
-
持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?
-
持久性是通过 redo log (重做日志)来保证的;
-
原子性是通过 undo log(回滚日志) 来保证的;
-
隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;
-
一致性则是通过持久性+原子性+隔离性来保证;
并行事务的影响
在同时处理多个事务的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题。
脏读
如果一个事务(A)「读到」了另一个「未提交事务(B)修改过的数据」,就意味着发生了「脏读」现象。
不可重复读
在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。
幻读
在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。
事务的四种隔离级别
-
脏读:读到其他事务未提交的数据;
-
不可重复读:前后读取的数据不一致;
-
幻读:前后读取的记录数量不一致。
SQL 标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:
-
读未提交(*read uncommitted*),指一个事务还没提交时,它做的变更就能被其他事务看到;
-
读提交(*read committed*),指一个事务提交之后,它做的变更才能被其他事务看到;
-
可重复读(*repeatable read*),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;
-
串行化(*serializable* );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
也就是说:
-
在「读未提交」隔离级别下,可能发生脏读、不可重复读和幻读现象;
-
在「读提交」隔离级别下,可能发生不可重复读和幻读现象,但是不可能发生脏读现象;
-
在「可重复读」隔离级别下,可能发生幻读现象,但是不可能脏读和不可重复读现象;
-
在「串行化」隔离级别下,脏读、不可重复读和幻读现象都不可能会发生。
四种隔离级别的实现
-
对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
-
对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
-
对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同。
「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View。
MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象,并不是完全解决了。
幻读两种解决方案
基于可重复读隔离级别(默认隔离级)下:
-
针对快照读(普通 select 语句),可重复读隔离级是由 MVCC(多版本并发控制)实现的,实现的方式是开始事务后(执行 begin 语句后),在执行第一个查询语句后,会创建一个 Read View,后续的查询语句利用这个 Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的,即使中途有其他事务插入了新纪录,是查询不出来这条数据的,所以就很好了避免幻读问题。
-
针对当前读(
select ... for update
等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行select ... for update
语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
幻读可能的场景
第一个例子:对于快照读, MVCC 并不能完全避免幻读现象。因为当事务 A 更新了一条事务 B 插入的记录,那么事务 A 前后两次查询的记录条目就不一样了,所以就发生幻读。
第二个例子:对于当前读,如果事务开启后,并没有执行当前读,而是先快照读,然后这期间如果其他事务插入了一条记录,那么事务后续使用当前读进行查询的时候,就会发现两次查询的记录条目就不一样了,所以就发生幻读。
所以,MySQL 可重复读隔离级别并没有彻底解决幻读,只是很大程度上避免了幻读现象的发生。
MVCC
锁
锁粒度
全局锁
加锁:flush tables with read lock
;执行后,整个数据库就处于只读状态了,这时其他线程执行以下操作,都会被阻塞:增删改操作( insert、delete、update)、表结构的更改操作(alter table、drop table )。
释放锁:unlock tables
应用场景:全库逻辑备份
缺点:加上全局锁,意味着整个数据库都是只读状态。
修正:如果数据库的引擎支持的事务支持可重复读的隔离级别,那么在备份数据库之前先开启事务,会先创建 Read View,然后整个事务执行期间都在用这个 Read View,而且由于 MVCC 的支持,备份期间业务依然可以对数据进行更新操作。
表级锁
分类:表锁、元数据锁(MDL)、意向锁、AUTO-INC 锁;
表锁:表锁的颗粒度太大,会影响并发性能,InnoDB 牛逼的地方在于实现了颗粒度更细的行级锁。
//表级别的共享锁,也就是读锁;
lock tables t_student read;
//表级别的独占锁,也就是写锁;
lock tables t_stuent write;
//释放锁
unlock tables
元数据锁(MDL):对数据库操作时会自动配上该锁。事务执行期间,MDL 是一直持有的。写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。
首先我们要知道,在InnoDB
事务中,锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
然后,在MySQL5.5
版本中引入了MDL(Metadata Lock)
,当对一个表做增删改查操作的时候,加MDL
读锁;当要对表做结构变更操作的时候,加MDL
写锁。
意向锁:在对数据加共享锁和独占锁前会先加意向共享锁和意向独占锁。
普通的 select 是不会加行级锁的,普通的 select 语句是利用 MVCC 实现一致性读,是无锁的。
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables ... read
)和独占表锁(lock tables ... write
)发生冲突。
表锁和行锁是满足读读共享、读写互斥、写写互斥的。
意向锁的目的是为了快速判断表里是否有记录被加锁。
AUTO-INC锁:主键自增就是借助该锁实现的。
行级锁
InnoDB
引擎是支持行级锁的,而 MyISAM
引擎并不支持行级锁。
类型:
-
Record Lock,记录锁,也就是仅仅把一条记录锁上;
-
Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身;只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
-
Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。
-
插入意向锁:它并不是意向锁,它是一种特殊的间隙锁,属于行级别锁。
插入意向锁:一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。
如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。
MDL锁
元数据锁(MDL):对数据库操作时会自动配上该锁。事务执行期间,MDL 是一直持有的。写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。
首先我们要知道,在InnoDB
事务中,锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。
然后,在MySQL5.5
版本中引入了MDL(Metadata Lock)
,当对一个表做增删改查操作的时候,加MDL
读锁;当要对表做结构变更操作的时候,加MDL
写锁。
死锁
死锁的四个必要条件
互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。
解锁策略
死锁发生时,从数据库层面,有两种策略通过「打破循环等待条件」来解除死锁状态:
①设置事务等待锁的超时时间。当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。在 InnoDB 中,参数 innodb_lock_wait_timeout
是用来设置超时时间的,默认值时 50 秒。
②开启主动死锁检测。主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect
设置为 on,表示开启这个逻辑,默认就开启。
回归业务的角度来预防死锁:如订单中的幂等性校验。
案例
字节面试SQL题。
本次案例中,事务 A 和事务 B 在执行完后 update 语句后都持有范围为(20, 30)
的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。
发生死锁怎么处理?
①可以通过SHOW ENGINE INNODB STATUS;
来查看死锁日志;
②阅读死锁日志,那两个事务?各自占有什么锁?各自等待占有什么锁?分析循环等待的场景,然后根据当前各个事务执行的SQL分析出加锁类型以及顺序,逆向推断出如何形成循环等待,这样就能找到死锁产生的原因了。
死锁避免
①对索引加锁顺序的不一致很可能会导致死锁,所以如果可以,尽量以相同的顺序来访问索引记录和表。在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能;
②Gap 锁往往是程序中导致死锁的真凶,由于默认情况下 MySQL 的隔离级别是 RR,所以如果能确定幻读和不可重复读对应用的影响不大,可以考虑将隔离级别改成 RC,可以避免 Gap 锁导致的死锁;
③为表添加合理的索引,如果不走索引将会为表的每一行记录加锁,死锁的概率就会大大增大;
④我们知道 MyISAM 只支持表锁,它采用一次封锁技术来保证事务之间不会发生死锁,所以,我们也可以使用同样的思想,在事务中一次锁定所需要的所有资源,减少死锁概率;
⑤避免大事务,尽量将大事务拆成多个小事务来处理;因为大事务占用资源多,耗时长,与其他事务冲突的概率也会变高;
⑥避免在同一时间点运行多个对同一表进行读写的脚本,特别注意加锁且操作数据量比较大的语句;
悲观锁与乐观锁
悲观锁
悲观锁(Pessimistic Lock): 就是很悲观,每次去拿数据的时候都认为别人会修改。所以每次在拿数据的时候都会上锁。这样别人想拿数据就被挡住,直到悲观锁被释放,悲观锁中的共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
机制(优点):悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。
缺点:效率方面,处理加锁的机制会产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性。
类型:数据库中的行锁,表锁,读锁(共享锁),写锁(排他锁),以及 syncronized 实现的锁均为悲观锁。
乐观锁
乐观锁(Optimistic Lock): 就是很乐观,每次去拿数据的时候都认为别人不会修改。所以不会上锁,但是如果想要更新数据,则会在更新前检查在读取至更新这段时间别人有没有修改过这个数据。如果修改过,则重新读取,再次尝试更新,循环上述步骤直到更新成功(当然也允许更新失败的线程放弃操作)。
场景:乐观锁适用于多读的应用类型,这样可以提高吞吐量。
特点:相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。
实现:一般的实现乐观锁的方式就是记录数据版本(version)或者是时间戳来实现,不过使用版本记录是最常用的。
锁的实现
悲观锁阻塞事务、乐观锁回滚重试:它们各有优缺点,不要认为一种一定好于另一种。像乐观锁适用于写少读多的情况下,即冲突真的很少发生的时候,这样可以省去锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行重试,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。(悲观锁适合写多读少的场景。)
悲观锁加锁 sql 语句: select num from t_goods where id = 2 for update
CAS
说到乐观锁,就必须提到一个概念:CAS 什么是 CAS 呢?Compare-and-Swap,即比较并替换,也有叫做 Compare-and-Set 的,比较并设置。
①比较:读取到了一个值 A,在将其更新为 B 之前,检查原值是否仍为 A(未被其他线程改动)。
②设置:如果是,将 A 更新为 B,结束。如果不是,则什么都不做。
上面的两步操作是原子性的,可以简单地理解为瞬间完成,在 CPU 看来就是一步操作。有了 CAS,就可以实现一个乐观锁,允许多个线程同时读取(因为根本没有加锁操作),但是只有一个线程可以成功更新数据,并导致其他要更新数据的线程回滚重试。 CAS 利用 CPU 指令,从硬件层面保证了操作的原子性,以达到类似于锁的效果。
Java 中真正的 CAS 操作调用的 native 方法因为整个过程中并没有“加锁”和“解锁”操作,因此乐观锁策略也被称为无锁编程。换句话说,乐观锁其实不是“锁”,它仅仅是一个循环重试 CAS 的算法而已,但是 CAS 有一个问题那就是会产生 ABA 问题,什么是 ABA 问题,以及如何解决呢?
ABA 问题:如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 "ABA"问题。
ABA 问题解决:我们需要加上一个版本号(Version),在每次提交的时候将版本号+1 操作,那么下个线程去提交修改的时候,会带上版本号去判断,如果版本修改了,那么线程重试或者提示错误信息~
注意点:
①乐观锁并未真正加锁,所以效率高。一旦锁的粒度掌握不好,更新失败的概率就会比较高,容易发生业务失败。
②悲观锁依赖数据库锁,效率低。更新失败的概率比较低。
日志
执行一条 update 语句,期间发生了什么?
更新语句的流程会涉及到 undo log(回滚日志)、redo log(重做日志) 、binlog (归档日志)这三种日志:
-
undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和 MVCC。
-
redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复;
-
binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制;
undo log
-
实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log(回滚日志) 中的历史数据将数据恢复到事务开始之前的状态。
-
实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。
undo log 是如何刷盘(持久化到磁盘)的?
undo log 和数据页的刷盘策略是一样的,都需要通过 redo log 保证持久化。
buffer pool 中有 undo 页,对 undo 页的修改也都会记录到 redo log。redo log 会每秒刷盘,提交事务时也会刷盘,数据页和 undo 页都是靠这个机制保证持久化的。
为什么需要缓存池(Buffer Pool)?
Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。
Buffer Pool缓存什么?
InnoDB 会把存储的数据划分为若干个「页」,以页作为磁盘和内存交互的基本单位,一个页的默认大小为 16KB。因此,Buffer Pool 同样需要按「页」来划分。
在 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的16KB
的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页。
redo log
redo log 是物理日志,记录了某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新,每当执行一个事务就会产生这样的一条或者多条物理日志。
WAL 技术指的是, MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。
所以有了 redo log,再通过 WAL 技术,InnoDB 就可以保证即使数据库发生异常重启,之前已提交的记录都不会丢失,这个能力称为 crash-safe(崩溃恢复)。可以看出来, redo log 保证了事务四大特性中的持久性。
redo log 和 undo log 区别在哪?
-
redo log 记录了此次事务「完成后」的数据状态,记录的是更新之后的值;
-
undo log 记录了此次事务「开始前」的数据状态,记录的是更新之前的值;
为什么需要redo?
-
实现事务的持久性,让 MySQL 有 crash-safe 的能力,能够保证 MySQL 在任何时间段突然崩溃,重启后之前已提交的记录都不会丢失;
-
将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能。
binlog
MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件。
binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。
redo log 和 binlog的区别
①适用对象不同
-
binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用;
-
redo log 是 Innodb 存储引擎实现的日志;
②文件格式不同
-
binlog 有 3 种格式类型,分别是 STATEMENT(默认格式)、ROW、 MIXED;
-
redo log 是物理日志,记录的是在某个数据页做了什么修改,比如对 XXX 表空间中的 YYY 数据页 ZZZ 偏移量的地方做了AAA 更新;
③写入方式不同
-
binlog 是追加写,写满一个文件,就创建一个新的文件继续写,不会覆盖以前的日志,保存的是全量的日志。
-
redo log 是循环写,日志空间大小是固定,全部写满就从头开始,保存未被刷入磁盘的脏页日志。
④用途不同
-
binlog 用于备份恢复、主从复制;
-
redo log 用于掉电等故障恢复。
主从复制3 个阶段
MySQL 集群的主从复制过程梳理成 3 个阶段:
-
写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。
-
同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
-
回放 Binlog:回放 binlog,并更新存储引擎中的数据。
MySQL主从复制3个模型
-
同步复制:MySQL 主库提交事务的线程要等待所有从库的复制成功响应,才返回客户端结果。这种方式在实际项目中,基本上没法用,原因有两个:一是性能很差,因为要复制到所有节点才返回响应;二是可用性也很差,主库和所有从库任何一个数据库出问题,都会影响业务。
-
异步复制(默认模型):MySQL 主库提交事务的线程并不会等待 binlog 同步到各从库,就返回客户端结果。这种模式一旦主库宕机,数据就会发生丢失。
-
半同步复制:MySQL 5.7 版本之后增加的一种复制方式,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。这种半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险。
三日志小结
-
undo-log
:主要用于实现事务ACID
原则中的原子性和MVCC
机制。 -
redo-log
:主要用于实现事务原则中的持久性,确保事务提交后就不会丢失。 -
bin-log
:主要结合redo-log
实现事务原则中的一致性,确保事务提交前后,数据的一致。