MySQL技术——事务和锁机制
- 一、事务
- (1)概述
- (2)ACID特性
- (3)事务并发存在的问题
- (4)事务的隔离级别
- 二、锁机制
- (1)锁的力度
- (2)表的分类: 排他锁、共享锁
- (3)串行化解决幻读(虚读)问题 :间隙锁
- (4)意向共享锁和意向排他锁
- (5)死锁
- (6)锁的优化建议
- 三、多版本并发控制(MVCC)
- 四、MySQL优化问题
- 五、总结
一、事务
(1)概述
事务是数据库区别于文件系统的重要特征之一。在文件系统中,如果正在写文件,但是操作系统奔溃了,这个文件就很有可能被破坏。当然,有一些机制可以把文件回复到某个时间点。不过,如果需要保证两个文件同步,这些文件系统可能就显得无能为力了,例如,当需要更新两个文件时,更新完一个文件后,在更新完第二个文件之前系统重启了,就会产生两个不同步的文件,因此这也就是数据库系统引入事务的主要目的
一个事务是由一条或者多条数据库操作SQL语句所组成的一个不可分割的单元,只有当事务中的所有操作都正常执行完毕后,整个事务才会被提交给数据库;如果由部分事务处理失败,那么事务就要回退到最初的状态。因此事务要么全部执行成功,要么全部失败
(2)ACID特性
每一个事务都必须满足下面的4个特性:
- 事务的原子性(Atomic): 事务是一个不可分割的整体,事务必须具有原子特性,要么全执行,要么全不执行
- 事务的一致性(Consistency): 一个事务执行之前和执行之后,数据库数据必须保持一致性状态。数据库的一致性状态必须由用户来负责,由并发控制机制实现
- 事务的隔离性(Isolation): 当两个或者多个事务并发执行时,为了保证数据的安全性,将一个事务内部的操作与其他事务的存在隔离起来,不被其他正在执行的事务看到,使得并发执行的各个事务之间不能相互影响 | 锁 + MVCC
- 事务的持久性(Durability): 事务完成(commit)后,DBMS保证它对数据库中的数据是永久性的,即使数据库因为故障出错,也应该能够快速恢复数据
(3)事务并发存在的问题
如果事务处理不经过隔离,并发执行事务是通常会发生以下问题:
- 脏读(Dirty Read): 一个事务读取了另一个事务为提交的数据,例如当事务A和事务B并发执行时,当事务A更新后,事务B查询读取到事务A尚未提交的数据,此时事务A回滚,则事务B读到的数据就是无效的脏读数据(事务B读取到来事务A未提交的数据)
- 不可重复读(NonRepeatable Read): 一个事务的操作导致另一个事务前后两处读取到不同的数据。即当事务A和事务B并发执行时,当事务B查询读取到数据后,事务A更新操作更改事务B查询到的数据,此时事务B再次去读取该数据,发现前后两处读的数据不一样(事务B读取了事务A已经提交的数据)
- 虚读 / 幻读(Phantom Read): 一个事务的操作导致另一个事务前后两次查询的结果 数据量不同 ,即当事务A和事务B并发执行时,当事务B查询读取数据后,事务A新增或者删除了一条满足事务B查询条件的记录,此时事务B再去查询,发现查询到前一次不存在的记录,或者前一次查询的一些记录不见了(事务B读取了事务A新增的数据,或者读不到事务A删除的数据)
(4)事务的隔离级别
MySQL默认支持四种隔离级别,如下图:
隔离级别 | 脏读 | 不可重复读 | 虚读 / 幻读 |
---|---|---|---|
未提交读:TRANSACTION_READ_UNCOMMITTED | 可以 | 可以 | 可以 |
已提交读:TRANSACTION_READ_COMMITTED | 不可以 | 可以 | 可以 |
可重复读:TRANSACTION_REPEATABLE_READ | 不可以 | 不可以 | update 可以 |
串行化: TRANSACTION_SERIALIZABLE | 不可以 | 不可以 | 不可以 |
- 串行化是最高的事务级别,由于事务隔离级别越高,为避免冲特所花费的性能也越多
- 在“可重复读”级别中,实际上是可以解决部分的虚读问题,但是不能防止update更新产生的虚读问题,还是要串行化隔离级别
select @@transation_isolation;
mysql默认的隔离级别:可重复读
我们现在一一列举各种隔离级别会出现的结果
未提交读
READ_UNCOMMITTED
已提交读
READ_COMMITTED
可重复读
REPEATABLE-READ
,这里对于幻读就不演示了,可以自行验证
串行化
SERIALIZABLE
二、锁机制
(1)锁的力度
锁的力度分为两种:
- 表级锁:对整张表加锁,开销小,加锁块,不会出现死锁;锁的力度大,发送锁冲突的概率高,并发度低
- 行级锁:对某行记录加锁。开销大,加锁慢,会出现死锁;锁定力度最小,发生锁冲突的概率最低,并发度高
- InnoDB行锁是通过给索引上的索引项加锁来实现的,而不是给表的行记录加锁实现的,这就意味着只有通过索引条件检索数据,InnoDB才使用行级锁,否则InnoDB将使用表锁。
- 由于InnoDB的行锁实现是针对索引字段添加的锁,不是针对行记录加的锁,因此虽然访问的是InnoDB引擎下表的不同行,但是如果使用相同的索引字段作为过滤条件,依然会发生锁冲突,只能串行进行,不能并发进行。
- 即使SQL中使用了索引,但是经过MySQL的优化器后,如果认为全表扫描比使用索引效率更高,此时会放弃使用索引,因此也不会使用行锁,而是使用表锁,比如对一些很小的表,MySQL就不会去使用索引。
(2)表的分类: 排他锁、共享锁
- 共享锁(Shared)
- 排他锁(Exclusive):又称X锁,写锁
- 共享锁(Shared):又称S锁,读锁
X和S锁之间由以下关系:SS可以兼容,SX、XX、XS之间是互斥的
- 一个事务对数据对象O加了S锁,可以对O进行读取操作但不能进行更新操作。加锁期间其他事务能对O加S锁但不能加X锁
- 一个事务对数据对象O加了X锁,就可以对O进行读取和更新。加锁期间其他事务不能对O加任何锁
显示加锁语句
select ... lock in share mode; #强制获取共享锁
select ... for update; #获取排他锁
下面我们来演示一下,两种锁的效果
假设我们开启了两个事务,默认工作在隔离级别为可重复读,两个事务先后执行以下语句
select * from stu where id = 2 for update
,根据上述推导,XX是不可兼容的,所以前一个执行sql语句的事务会对当前查询的记录加一把排他锁,后执行sql语句的事务肯定会被阻塞;若后执行sql语句的事务换成执行... lock in share mode
,同样也是阻塞,因为XS也是不兼容的;若后执行的事务获取其他记录的锁,如id = 3 for update
,该记录没有加X锁,因此可以执行
我们再来看看另外一条sql语句select .. name = 'aaa' for update
select 'bbb'
竟然也会阻塞!!!
这里注意:InnoDB的行锁是加在索引项上面的,并不是单纯的给行记录加锁;如果过滤条件没有索引的话,name只是普通字段,使用的就是表锁,而不是行锁!!!
这里如果我们给name字段添加索引的话,就是加行锁,使得’bbb’能够执行
我们来看看隔离级别为SERIALIZABLE
的结果
(3)串行化解决幻读(虚读)问题 :间隙锁
当我们用范围条件而不是等值条件去查询数据时,并请求共享或者排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但是并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
目的:防止幻读,以满足川=串行化隔离级别的要求
锁的三张算法:
- record lock:行锁
- gap lock:间隙锁
- next-key lock:record lock + gap lock
示例1 范围查询:按过滤条件分为:使用主键索引 或者 辅助索引
示例二 等值查询
(4)意向共享锁和意向排他锁
在使用表锁的时候,涉及到的效率问题
要获取一张表的S或者X锁,需要检查这张表没有被其他事务获取过X锁,这表里的数据没有被其他事务获取过行锁X锁,那么如果这张表有非常多的数据,是一行一行的检查???
意向共享锁和意向排他锁就是用来解决这个问题
- 意向共享锁(IS锁): 事务计划给记录加行共享锁,事务在给一行记录加共享锁前,必须先取得该表的IS锁
- 意向排他锁(IX锁): 事务计划给记录加行排他锁,事务在给一行记录加排他锁前,必须先取得该表的IX锁
当我们需要获取表的X锁时,不需要再检查表中的哪些行被(X或者S)锁占用,只需要检查IX和IS锁即可!
如下图的 X和S都是表锁
X | IX | S | IS | |
---|---|---|---|---|
X | Conflict | Conflict | Conflict | Conflict |
IX | Conflict | 兼容 | Conflict | 兼容 |
S | Conflict | Conflict | 兼容 | 兼容 |
IS | Conflict | 兼容 | 兼容 | 兼容 |
1、意向锁是由InnoDB存储引擎获取行锁之前自己获取的
2、意向锁之间都是兼容的,不会产生冲突
3、意向锁存在的意义是为了更高效的获取表锁
4、意向锁是表级锁,协调表锁和行锁的共存关系,主要目的是显示事务正在锁定某行或者试图锁定某行。
(5)死锁
MyISAM表锁是deadlock free的,这是因为MyISAM总是一次获得所需的所有锁,要么全部满足,要么等待,因此不会发生死锁。但在InnoDB中,出来的单个SQL组成的事务外,锁是逐步获得的,即锁的力度比较小,发生死锁也是可能的,如下:
死锁问题一般都是我们自己的应用造成的,和多线程编程的死锁情况相似,大部分都是由于我们多个线程在获取多个锁资源的时候,获取的顺序不同而导致的死锁问题。因此我们应用在对数据库的多个表做更新的时候,不同的代码段,应对这些表按相同的顺序进行更新操作,以防止锁冲突导致死锁问题。
(6)锁的优化建议
- 尽量使用较低的隔离级别
- 设计合理的所有并尽量使用索引访问数据,使锁更加准确,减少锁冲突的机会提高并发能力
- 选择合理的事务大小,小事务发生锁冲突概率小
- 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对于一个表而言,尽可能以固定的顺序存取表中的行,这样也能减少死锁的发生
- 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响
- 不要申请超过实际需要的锁级别
- 除非必须,查询时不要显示加锁
三、多版本并发控制(MVCC)
MVCC(Multi-Version Concurrency Control,简称MVCC),是MySQL中基于乐观锁理论实现隔离级别的方式,用于已提交读和可重复读隔离级别的实现,也经常被称为多版本并发控制。MVCC机制会生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别的一致性读取。从用户的角度来看,就是数据库提供同一组数据的多个版本(系统版本号和事务版本号)。
MVCC多版本控制的读操作分为两类:
- 快照读:读的是记录的可见版本,不用加锁,如select
- 当前读:读的是记录的最新版本,并且返回当前的记录,如insert,delete,select,update,select … lock in share mode / for update
MVCC: 每一行记录实际上有多个版本,每个版本的记录除了数据本身之外,增加了其它字段
- DB_TRX_ID:记录当前事务ID
- DB_ROLL_PTR:指向undo log日志上数据的指针
已提交读: 每次执行语句的时候都重新生成一次快照(Read View),每次select查询时。
可重复读: 同一个事务开始的时候生成一个当前事务全局性的快照(Read View),第一次select查询时。
快照读取原则:
- 版本未提交无法生成快照
- 版本已提交,但是在快照创建后提交,无法读取
- 版本已提交,但是在快照创建前提交,可以读取
- 当前事务内自己更新,可以读到
InnoDB提供了两个读取操作:
- 锁定读: S、X锁
- 非锁定读: 其依赖于 undo log 回滚日志
undo log
:回滚日志,保存了事务发生之前的数据的一个版本,用于执行时的回滚操作,同时也是实现多版本并发控制(MVCC)下的关键技术
redo log
:重做日志,用于记录事务操作的变化,确保事务的持久性。redo log是在事务开始后就开始记录,不管事务是否提交都会记录下来,在异常发生时(如数据持久化过程中掉电),InnoDB会使用redo log恢复到掉电前的时刻,保证数据的完整性。InnoDB修改操作数据时,不是直接修改磁盘上的数据,实际只是修改Buffer Pool中的数据。InnoDB总是先把Buffer Pool中的数据改变记录到redo log中,用来进行崩溃后的数据恢复。 优先记录redo log,然后再由专门的线程将Buffer Pool中的脏数据刷新到磁盘上。
四、MySQL优化问题
对于MySQL优化问题分为3种:
- SQL和索引的优化: 慢查询日志 -》 根据表的数据量等设置合理的慢查询时间 -》 记录慢查询sql -》 explain 分析sql执行计划 -》 优化措施
- 应用上的优化:
- 连接数据库:访问频繁,引入连接池中间件
- 引入缓存(存储热点数据) redis(重点!!!)
- 缓存数据一致性问题?
- 缓存穿透
- 缓存雪崩
- redis还有哪些功能?
- mysql server上的优化: 各种参数配置!
- MySQL的查询缓存
query_cache_type
、query_cache_size
- 索引和数据缓存,
innodb_buffer_pool_size
越高,磁盘IO越少 - MySQL线程缓存
thread_cache_size
- 并发连接数量和超时时间
max_connections
、wait_timeout
- MySQL的查询缓存
五、总结
- 事务的特征 ACID—— 原子性、一致性、隔离性、持久性
- 事务并发的问题——脏读、不可重复度、虚读/幻读
- 事务的隔离级别——未提交读、已提交读、可重复读、串行话
- 锁的力度——行级锁、表级锁
- 锁的分类——排他锁、共享锁(意向共享锁、意向排他锁)
- 使用非索引字段获取排他锁时,实际上是直接加了表锁
- 串行化解决的幻读问题——间隙锁(范围查询、等值查询)
- 锁的优化建议——低隔离级别、设计合理索引、使用相同顺序、尽可能等值查询
- 可重复读和已提交读隔离级别 通过MVCC机制中的快照读(非锁定读)保证,依赖于undo log回滚日志
- MYSQL优化——sql和索引、应用层、mysql server
🌻🌻🌻以上就是有关于MySQL事务和锁机制的内容,如果聪明的你浏览到这篇文章并觉得文章内容对你有帮助,请不吝动动手指,给博主一个小小的赞和收藏 🌻🌻🌻