事务
可以理解为数据库执行的一个最基础的单位,其包含有限的操作命令(crud)。
事务的属性(ACID):事务必须满足四个属性,原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)
原子性:要么执行完全结束,要么全部不执行。避免数据执行不完全带来的错误数据,所以事务必须具有原子性(commit、rollback)。即在事务A提交之前,如果发生错误,则需要回退到事务执行前的状态。
一致性:指的是事务在执行的前后,数据库的数据一定会保持一致性的状态。可以理解为系统从一种状态转变为另一种状态。事务A在提交之后,对系统的改变,事务B一定会感知到相同的变化。
隔离性:指的是事务之间的执行相互独立。在并发多个事务执行的时候,每个事务内部的操作不会影响到其他的事务。事务的执行可以抽象为串行执行(这里是修改上的串行)。针对不同情况,事务的隔离会有不同的隔离级别。
持久性:事务一旦提交成功就会被更新到到数据库,不会再被回退。
=================================================================
事务的隔离性级别:
针对并发执行的事务,会出现以下问题:(脏读、不可重复读、幻读)
1.脏读:事务A读取事务B更新的数据,数据B进行了回滚操作,那么A读取到的数据是回滚前的数据是脏数据。
2.不可重复读:在一次事务中,前后两次查询的数据不一致。主要是在事务未提交前,有其他的事务进行了更新提交操作。
3.幻读:事务A在对系统进行更新以后,事务B对系统进行了插入或者删除操作,导致事务A发现仍有数据未更新,如同幻觉。
四种事务隔离级别:(处理并发问题)
隔离级别 脏读 不可重复读 幻读
读未提交 是 是 是
读已提交 否 是 是
可重复读 否 否 是
可串行化 否 否 否
读未提交(Read Uncommitted):在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。事务A可以读取事务B未提交的数据,如果事务B回滚,此时A读取的数据即为脏数据。
读已提交(Read Committed):事务A只能读取事务B,或者其他事务已经提交的数据。所以不会出现脏读的数据,但是会出现前后两次不一致的不可重复读,或者幻读。
可重复读 (Repeatable Read):这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。这是因为MVCC机制,select读取不会更改版本号,是快照读,而insert、update和delete会更新版本号,是当前读(当前版本)(注意,这里不会破坏数据的一致性)。无法解决幻读的现象。
可串行化(Serializable):当事务A开启此隔离的级别时候,当其他事务插入一条记录报错,表会被锁了插入失败,mysql中事务隔离级别为serializable时会锁整个表,防止幻读的出现,但是完全的串行换操作会导致效率极低,一般不会使用此隔离级别。
=================================================================
设置全局事务隔离级别:
>SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; >SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED; >SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; >SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
设置当前会话事务隔离级别:
>set session transaction isolation level READ UNCOMMITTED; >set session transaction isolation level READ COMMITTED; >set session transaction isolation level REPEATABLE READ; >set session transaction isolation level SERIALIZABLE;
查询事务隔离级别分析工具:
>show variables like '%iso%'; >select @@global.transaction_isolation;
事务隔离级别加锁分析
锁的类型:
锁模式
共享锁(share(S) Lock):
SELECT ... LOCK IN SHARE MODE
排他锁(exclusive(X) Lock):
SELECT * FROM table_name WHERE … FOR UPDATE
意向锁(Intention Locks):表级别的锁,自动施加,自动释放。
锁在细力度上又可以分为 表锁和行锁
共享锁(share(S) Lock):共享锁,顾名思义此对象锁是可以共享的,即事务A对数据加上共享锁以后,其它事务也可以加上共享锁,但是不能加排它锁。共享锁只能读,不能修改数据。(行锁)。
排他锁(exclusive(X) Lock):当且只有一个事务可以对数据加锁,而其他事务不能再加任何类型的锁,排他锁可以读,也可以写。(行锁)。
意向锁(Intention Locks):表级别的锁,是在数据库的某行加锁(s或者x)的获取时候,自动加上意向锁(IS或者IX),记录有此类(s或者x)锁。(表级别锁不会和行级别锁冲突)
为什么要使用意向锁:当事务要加入一个表级锁的时候,我们需要遍历每一行看一看是否有其他锁防止冲突,如果数据量较大那么性能将会极低,所以加上意向锁可以直接判断是否有意向锁即可。
获得锁
1.事务在获得某个数据的S锁,必须先获得一个IS或者更强的锁
2.事务在获得某个数据的X锁,必须先获得表的IX锁
加锁
事务在加入表级锁时,先判断是否有X、IX、S、IS锁,有则失败
X IX S IS
X Conflict Conflict Conflict Conflict
IX Conflict Compatible Conflict Compatible
S Conflict Conflict Compatible Compatible
IS Conflict Compatible Compatible Compatible
可以看出,共享锁S是兼容S和IS的。IS与IX意向锁是对表加锁相互兼容的。
=================================================================
锁的类型
记录锁(Record Locks):innoDB的行锁是对索引的加锁,innoDB是一个聚簇索引使用的是B+树来进行实现的,所以innoDB的锁只有在使用索引的条件时候,才会加锁,即便是不同行但是相同索引也会有冲突。当不使用索引时,则会使用表锁。
间隙锁(Gap Locks):对索引项之间的"间隙"加锁,不包括索引项本身(左右开区间)。e.g. SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; GAP锁会阻止其他事务进行的插入操作,不会阻止其他事务获取相同间隙的gap锁并且gap S-lock和gap X-lock不冲突。
Next-Key Locks : Next-Key 锁 = 记录锁+间隙锁。锁定一个范围并且锁定记录本身索引。InnoDB的默认加锁方式。
插入意向锁(Insert Intention Locks):是间隙锁的一种,在插入一条记录之前,需要先拿到插入间隙的插入意向锁。当其他事务持有目标间隙的Gap锁时会阻塞。
=================================================================
MVCC(Multiversion Concurrency Control)多版本并发控制
作用:实现了读不加锁不会导致阻塞,写加锁,读写不冲突,在读多写少的场景下极大的提高了其效率,增加了其并发性。RC和RR级别使用。
实现:在每一行记录的后面增加两个隐藏列,记录创建版本号和删除版本号。每个事务都有唯一的递增版本号,那么每一操作的创建版本号或者删除版本号都会使用事务的版本号。
=================================================================
INSERT:InnoDB为每个新增行记录当前事务编号作为创建ID
UPDATE:记录修改当前行的值,写事务编号,回滚指针指向undo log中的修改前的行
DELETE:将删除位置为删除
SELECT:查询出数据行的版本小于等于当前事务的版本号的数据,如删除位为删除则表示已删除
1.创建一个name=sql的数据,事务号为1
>insert into test (name) values(1);
id name create version delete version
1 sql 1
2.接着更新 这个数据,使得name=innodb,其事务版本号为2。
具体操作 先把以前这条数据删除,在插入新的数据,使用版本号来标记
>update test set name= 'innodb' where id=1;
id name create version delete version
1 sql 1 2 (代表 这条数据已经被删除)
1 innodb 2
3.删除操作,事务版本号为3
>delete test where id = 1;
id name create version delete version
1 sql 1 2 (代表 这条数据已经被删除)
1 innodb 2 3 (代表 这条数据已经被删除)
4.查询条件: 创建版本号 < 当前版本号 < 删除版本号
RR:在RR的时候,读的是当前的快照表,读取的是固定版本(第一次select的版本)的数据,所以一个事务内的读是一致的。但是 insert update delete操作的是当前读,是最新版本的数据。(快照读)
RC:读事务每次都读取最近的版本,因此两次对同一字段的读可能读到不同的数据(幻读),但能保证每次都读到最新的数据。(当前读)
快照读:读到的数据可能是历史版本数据。
当前读:读到的数据是最近版本数据,特殊的读操作,插入/更新/删除操作,属于当前读需要加锁。
=================================================================
针对隔离级别加锁分析
RU: Select不加锁,写加X锁
RC: Select快照读不加锁,写加X锁
RR: Select快照读不加锁,写加X Next-key锁
Serializable: Select加S Next-key锁,写加X Next-key锁
=================================================================
RU与RC:区别在于快照读与无快照读
RC与RR:一致性读肯定是读取在某个时间点已经提交了的数据,RC的时间点为当前时间点,RR的时间点是第一次select的时间点
RR与Serializable:RR通过快照读,读取的都是过去某个时间点的快照,而Serializable级别下都是加了S锁的读,督导的都是当前时间点的,这意味着在提交前,其他事务无法进行提交。
=================================================================
死锁
从图中,可以看到一个Update操作的具体流程:当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁(current read).待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录.一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止.因此,Update操作内部,就包含了一个当前读.
注:根据上图的交互,针对一条当前读的SQL语句,InnoDB与MySQL Server的交互,是一条一条进行的,因此,加锁也是一条一条进行的.先对一条满足条件的记录加锁,返回给MySQL Server,做一些DML操作;然后在读取下一条加锁,直至读取完毕.
单个SQL组成的事务,从宏观上来看,锁是在这个语句上一次获得的,但从底层实现上来看,是逐个记录行查询,得到符合条件的记录即对该行记录的索引加锁.,而加锁的过程是边查边加、逐行获得。
这个一个数据库的设计原则,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。在一个事务中先加锁执行完操作,再统一在commit前释放所有的锁。
RC与RR的区别,RC级别下不会加GAP锁,RR级别下会加GAP锁
两个表、两行记录,交叉获得和申请互斥锁,相同表记录行锁冲突:事务A按照一定顺序加锁,事务B按照相反的顺序加锁就会出现死锁
如何避免死锁
在程序中添加对死锁的重试
完成相关变动后尽早提交事务
修改多表或同表的多行数据时,按照一定的顺序
添加合适的索引
一切都没用则可以使用表锁
热门评论
写的真不错,总结的很到位,值得收藏