【专题】事务与并发控制
1. 事务的概述
事务通常包含一系列INSERT、DELETE、UPDATE等更新操作语句,这些更新操作是一个不可分割的逻辑工作单元。
每个事务的处理必须要满足ACID的4个特性:
-
即原子性(A)、一致性(C)、隔离性(I)、持久性(D)。
2. 事务的ACID特性
2.1 原子性(Atomicity)
原子性是指事务中包含的所有操作要么全做,要么全不做。
破坏原子性的情况:
-
出现意外而被DBMS夭折,例如系统发生死锁,一个事务被选中作为牺牲者。
-
电源中断、硬件故障或者软件错误而使系统垮台。
-
遇到了意料之外的情况,如不能从磁盘读取或读取了异常数据等。
确保事务原子性的方法:
使用DBMS的事务日志文件,把那些未成功执行的事务中已执行的操作对数据产生的影响“抹掉”。
事务日志文件记录了每个事务对数据库所作变更的“旧值”和“新值”,当一个事务不能完成时,将这些变更了的“新值”恢复到它的“旧值”。
2.2 一致性(Consistency)
一致性是指数据库在事务操作前和事务处理后,其中的数据必须都满足业务规则约束。
转账事务必须保证两个账户的总钱数不变(这就是一种一致性的限制),转账前总数是多少,转账后的总数还是多少。
2.3 隔离性(Isolation)
隔离性是数据库允许多个并发事务同时对数据进行读写和修改的能力。
-
隔离性可以防止多个事务并发执行时,由于它们的操作命令交叉执行而导致的数据不一致状态。
隔离性要求一个事务的执行不能被其他事务干扰。
2.4 持久性(Durability)
当事务处理结束后,它对数据的修改应该是永久的,即使是系统在遇到故障的情况下也不会丢失。
3. MySQL事务控制语句
3.1 修改提交方式
SET AUTOCOMMIT = 0|1;
-
SET AUTOCOMMIT=1
是默认的,为自动提交事务模式。
-
SET AUTOCOMMIT=0
,设置之后的所有事务都需要通过明确的命令进行提交和回滚。
3.2 开始事务
START TRANSACTION;
或BEGIN WORK;
-
在存储过程中只能使用
START TRANSACTION
语句来开启一个事务,因为MySQL数据库分析器会自动将BEGIN
识别为BEGIN…END
语句。
3.3 提交事务
COMMIT [WORK] [AND [NO] CHAIN] [[NO] RELEASE];
-
把事务中对数据库的修改进行永久保存。
3.4 回滚事务
ROLLBACK [WORK] [AND [NO] CHAIN] [[NO] RELEASE];
-
取消对数据库所做的任何修改。
3.5 设置保存点
设置存储点:
SAVEPOINT 保存点名;
回滚到指定存储点:
ROLLBACK [WORK] TO SAVEPOINT 保存点名;
4. 并发控制
事务串行执行:DBMS按顺序一次执行一个事务,执行完一个事务后才开始另一事务的执行。
事务并发执行:DBMS同时执行多个事务对同一数据的操作。
-
在单处理机系统中,事务的并发执行实际上是这些并发事务轮流交叉进行的,这种并发方式称为交叉并发方式。
-
在多处理机系统中,每个处理机可以运行一个事务,多个处理机可以同时运行多个事务,实现事务真正的并发运行,这种并发执行方式称为同时并发方式。
并发执行的事务,可能会对数据库的一致性会造成破坏。
-
DBMS要对事务并发执行进行控制,确保并发事务间数据访问上的互不干扰,保证事务的隔离性。
4.1 并发执行可能引起的问题
事务对数据库中数据可以进行读和写操作。
-
读不会破坏数据,但写可能导致数据不正确。
事务并发执行可能引发的问题:
-
读-读:保持数据一致性。
-
读-写:不可重复读。
-
写-读:读脏数据。
-
写-写:丢失更新。
丢失更新:
-
又称为覆盖未提交的数据 。
-
原因:两个(或多个)事务对同一数据并发地写入引起,称为写—写冲突。
-
结果:与串行地执行两个(或多个)事务的结果不一致。
不可重复读:
-
又称为读值不可复现。
-
原因:该问题因读—写冲突引起。
-
结果:第二次读的值与前次读的值不同。
读脏数据:
-
又称为读未提交的数据 。
-
原因:由于后一事务读了前一个事务写了但尚未提交的数据引起,称为写—读冲突。
-
结果:读到有可能要回退的更新数据。
4.2 事务隔离级别
事务并发操作引发问题的解决方法:
-
设置事务隔离级别;
-
封锁。
事务隔离级别的定义:
-
隔离级别定义了一个事务与其他事务的隔离程度。
并发事务可能发生的异常情况:
-
丢失更新;
-
读脏数据;
-
不可重复读;
-
幻影读。
属于不可重复读的问题。与不可重复的区别是:不可重复读的操作对象是数据,而幻影读的操作对象是表中的记录。
事务隔离级别的分类:
read uncommitted(未提交读):
-
用户可以对数据执行未提交读;在事务结束前可以更改数据集内的数值,行也可以出现在数据集中或从数据集消失。它是4个级别中限制最小的级别。
read committed(提交读):
-
此隔离级别不允许用户读一些未提交的数据,因此不会出现读脏数据的情况,但数据可以在事务结束前被修改,从而产生不可重复读或幻影数据。
repeatable read(重复读):
-
此隔离级别保证在一个事务中重复读到的数据会保持同样的值,而不会出现读脏数据、不可重复读的问题。但允许其他用户将新的幻影行插入数据集,且幻影行包括在当前事务的后续读取中。
serializable(串行读):
-
此隔离级别是4种隔离级别中限制最大的级别,称为可串行读,不允许其它用户在事务完成之前更新数据集或将行插入数据集内。
4.3 MySQL事务隔离级别设置
隔离级别的设置:
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE |REPEATABLE READ |READ COMMITED |READ UNCOMMITED;
示例:查看当前会话事务隔离级别
SELECT @@transaction_isolation;
READ UNCOMMITED隔离级别:
-
所有事务都可以看到其他未提交事务的执行结果。该隔离级别很少用于实际应用。
示例:脏读现象
# 打开MySQL客户机A USE test; SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT @@transaction_isolation; START TRANSACTION; SELECT * FROM account; # 打开MySQL客户机B USE test; SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; UPDATE account SET balance=balance+1000 WHERE account_no=1; #未提交事务 # 打开MySQL客户机A SELECT * FROM account; # 关闭MySQL客户机A与MySQL客户机B,由于两个客户机的事务都没有提交,所以,account表中的数据没有变化,'李三'账户的余额仍然是200。
READ COMMITED隔离级别:
-
一个事务只能看见已提交事务所做的改变。避免脏读现象,但可能出现不可重复读和幻影读。
示例:不可重复读现象
# 打开MySQL客户机A USE test; SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; SELECT @@transaction_isolation; START TRANSACTION; SELECT * FROM account; # 打开MySQL客户机B SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION; UPDATE account SET balance=balance+1000 WHERE account_no=1; COMMIT; # 打开MySQL客户机A SELECT * FROM account; # MySQL客户机A在同一个事务中两次执行“SELECT * FROM account;”的结果不相同,造成不可重复读现象。
REPEATABLE READ隔离级别:
-
是MySQL的默认事务隔离级别,它确保在同一事务内相同的查询语句的执行结果一致。避免脏读及不可重复读的现象,但可能出现幻影读现象。
示例:幻影读现象
# 打开MySQL客户机A USE test; SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT @@transaction_isolation; START TRANSACTION; SELECT * FROM account; # 打开MySQL客户机B SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION; INSERT INTO account VALUES(10,'赵六',3000); COMMIT; SELECT * FROM account; # 打开MySQL客户机A SELECT * FROM account; # 查询结果显示account表中不存在account_no=10的账户信息。 # 由于MySQL客户机A检测到account表中不存在account_no=10的账户信息,在MySQL客户机A继续执行下面INSERT语句。 INSERT INTO account VALUES(10,'赵六',3000); # 运行结果显示account表中确实存在account_no=10的账户信息,但由于REPEATABLE READ(可重复读)隔离级别使用了“障眼法”,使得MySQL客户机A无法查询到account_no=10的账户信息,这种现象称为幻影读现象。
SERIALIZABLE隔离级别:
-
是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突。
示例:避免幻影读现象
# 打开MySQL客户机A USE test; SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; SELECT @@transaction_isolation; START TRANSACTION; SELECT * FROM account; # 打开MySQL客户机B SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; START TRANSACTION; INSERT INTO account VALUES(20,'马七',5000); SELECT * FROM account; # 由于发生了锁等待超时引发的错误异常,事务被回滚,所以account_no=20的账户信息并没有添加到account表中。
-
对于大部分应用来说,
READ COMMITTED
是最合适的隔离级别。 -
如果所处的数据库中具有大量的并发事务,并且对事务的处理和响应速度要求较高,则使用
READ COMMITTED
隔离级别比较合适。 -
如果所连接的数据库用户比较少,多个事务并发地访问同一资源的概率比较小,并且用户的事务可能会执行很长一段时间,在这种情况下使用
REPEATABLE READ
或SERIALIZABLE
隔离级别较合适。
5. 封锁
5.1 锁
一个锁实质上就是允许(或阻止)一个事务对一个数据对象的存取特权。
-
一个事务对一个对象加锁的结果是将其它事务“封锁”在该对象之外,特别是防止了其他事务对该对象的更改,而加锁的事务则可以执行它所希望的处理并维持该对象的正确状态。
基本锁:
排它锁(X锁、写锁)。
-
事务更新数据前必须先加上X锁;
-
数据对象加上X锁,其它事务对该对象即不能加S锁也不能加X锁
-
事务对数据加X锁后,对锁定数据即能读取也能修改。
共享锁(S锁、读锁)。
-
事务读取数据前必须先加上S锁;
-
数据对象加上S锁后,其它事务只能对该对象加S锁不能加X锁
-
事务对数据加S锁后,对锁定数据只能读取。
基本锁的相容矩阵:
锁的粒度:
-
封锁对象的大小称为封锁粒度。
-
封锁的对象可以是字段、记录、表等逻辑单元;也可以是页(数据页或索引页)、块等物理单元。
-
封锁粒度越小,系统中能够被封锁的对象就越多,但封锁机构复杂,系统开销也就越大。
-
封锁粒度越大,系统中能够被封锁的对象就越少,并发度越小,封锁机构简单,相应系统开销也就越小。
-
实际应用中,选择封锁粒度应同时考虑封锁开销和并发度两个因素,对系统开销与并发度进行权衡,以求得最优的效果。
-
需要处理大量元组的用户事务可以以关系为封锁单元;对于一个处理少量元组的用户事务,可以以元组为封锁单位。
5.2 封锁协议
在运用X锁和S锁对数据对象加锁时,还需要约定一些规则,比如何时申请X锁或S锁、持锁时间、何时释放等,这些规则称为封锁协议。
一级封锁协议:
-
写-写操作导致“丢失更新”问题,在写操作前加X锁。
二级封锁协议:
-
写-读操作导致“读脏数据”问题,在写操作前加X锁和读操作前加S锁。
-
数据对象加了X锁,不能再加S锁
三级封锁协议:
-
读-写操作导致“不可重复读”问题,在写操作前加X锁和读操作前加S锁。
-
数据对象加了S锁,不能再加X锁。
5.3 封锁带来的问题
“饿死”问题:
-
存在一个事务序列,其中每个事务都申请对某数据项加S锁,且每个事务在授权加锁后一小段时间内释放封锁。
-
有一个事务T2欲在该数据项上加X锁,则将永远轮不上封锁的机会。这种现象称为“饿死”。
-
解决方法:
当下一个事务T3请求对数据R加S锁时,授权加锁的条件是:
-
不存在数据R上持有X锁的其他事务;
-
不存在等待对数据R加锁且先于T3申请加锁的事务。
-
活锁”问题:
-
系统可能使某个事务永远处于等待状态,得不到封锁的机会,这种现象称为“活锁”。
-
解决方法:
-
采用“先来先服务”的策略。
-
提高事务的“优先”级别。
-
“死锁”问题:
-
系统中两个或两个以上的事务都处于等待状态,并且每个事务都在等待其中另一个事务解除封锁,它才能继续执行下去,结果造成任何一个事务都无法继续执行,这种现象称系统进入“死锁”状态。
-
解决方法:
-
一次加锁法。
-
顺序加锁法。
-
一次加锁法:
每个事务必须将所有要使用的数据对象全部依次加锁,并要求加锁成功,只要一个加锁不成功,表示本次加锁失败,则应该立即释放所有已加锁成功的数据对象,然后重新开始从头加锁。
缺点:
对某一事务所要使用的全部数据一次性加锁,扩大了封锁的范围,从而降低了系统的并发度。
数据库中的数据是不断变化的,原来不需要封锁的数据,在执行过程中可能会变成封锁对象,所以在开始时,需要扩大封锁范围,将可能要封锁的数据全部加锁,降低了并发度,影响系统运行效率。
顺序加锁法:
顺序加锁法是预先对所有可加锁的数据对象强加一个封锁顺序,同时要求所有事务都只能按此顺序封锁数据对象。
缺点:
事务的封锁请求可能随着事务的执行而动态地决定,随着数据操作的不断变化,维护这些数据的封锁顺序需要很大的系统开销。
“死锁”检测:
-
利用事务等待图测试系统中是否存在死锁。
-
在事务等待图中沿着箭头方向存在一个循环,那么死锁的条件就形成了,系统进入死锁状态。
“死锁”解除:
-
DBA从依赖相同资源的事务中抽出某个事务作为牺牲品,将它撤消,释放其占用的所有数据资源。
抽取牺牲事务的标准:
选择一个处理死锁代价最小的事务,将其撤销。
取消等级低的用户事务。
6. 两段封锁协议
并发事务不同的调度(即事务的执行次序)可能会产生不同的结果,调度应当按如下方法进行:
-
串行调度。
-
执行结果等价于串行调度的调度也是正确的。这样的调度叫做可串行化调度。
-
采用两段封锁协议(2PL协议)封锁并发事务的调度是可串行化的。
两段封锁协议规定所有的事务应遵守下面两条规则:
-
在对任何一个数据进行读写操作之前,事务必须获得对数据的封锁。
-
在释放一个封锁之后,事务不再申请和获得任何其他封锁。
两段封锁协议不是一个具体的协议,但其思想可融入到具体的加锁协议之中。
2PL协议仍有可能导致死锁的发生,而且可能会增多。
7. MySQL的并发控制
7.1 表级锁
加表级锁:
LOCK TABLES 表名 READ|WRITE [,表名 READ|WRITE,…] ;
-
READ施加表级读锁;
-
WRITE施加表级写锁。
解锁:
UNLOCK TABLES;
7.2 行级锁
在查询语句中,为符合查询条件的记录施加共享锁:
SELECT * FROM 表名 WHERE 条件 LOCK IN SHARE MODE;
在查询语句中,为符合查询条件的记录施加排他锁:
SELECT * FROM 表名 WHERE 条件 FOR UPDATE;
在更新(INSERT、UPDATE、DELETE)语句中,MySQL将会对符合条件的记录自动施加隐式排他锁。
7.3 表的意向锁
-
意向共享锁(IS),事务在给一个数据行加共享锁之前必须先取得该表的IS锁。
-
意向排他锁(IX),事务在给一个数据行加排他锁之前必须先取得该表的IX锁。
-
意向锁是InnoDB自动加的,不需要用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁;对于普通SELECT语句,InnoDB不会加任何锁。
原文地址:https://blog.csdn.net/Pqf18064375973/article/details/143694869
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!