事务(transaction)其实就是一序列数据库命令,他们可以组合在一起当做一个单一逻辑单元
在一个事务被标记为完成之前(commited),任何改变可以被回滚(rolled back)
例子:转账,它可以包括(减少发送钱账户的金额量,增加收款人的金额量,给发送方和接收方都增加交易记录)
如果连接被打断,或者说一些操作失败了,所有的操作都需要被回滚
→ Total failure is better than partial failure
每一个事务的终止信号都是通过commit或者roll back来传达的,没有commit,我们做出的改变不会被保存
但其实Sesssions都是有自动提交功能(auto-commit)的,也就是任何命令都能被马上提交,不需要roll back和commit,这种auto-commit方式都是被默认的(set sutocommit = 0 关闭autocommit)
→ START TRANSACTION or BEGIN
BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE acc_nr = 254231426;
UPDATE accounts SET balance = balance + 100.00 WHERE acc_nr = 676535301;
INSERT INTO transfers (acc_nr, amount, reference) VALUES (254231426, -100.00, ’Dentist’);
INSERT INTO transfers (acc_nr, amount, reference) VALUES (676535301, 100.00, ’John Smith’);
COMMIT;
同时事务:
复杂情况:多种同时事务
情况1:按顺序执行事务
很简单去实行,在一些情况下工作的很好
但是有时过慢了:
CPU可以保持空闲当I/O转换过程中,反之亦然
慢的语句可能会阻碍快的那个
事务可能需要去等待用户输入
情况2:事务的交错执行
等待时间一半都减少特别明显了
在单命令事务中也能进行工作(分裂成多种更小的操作)
但是难以正确实施进行
这就是DBMS所做的事
ACID规则:
ACID规则是摆正多个并行事务能被可靠处理的一组方法策略
Atomicity:原子性 Consistency:一致性 Isolation:交易之间不被相互作用 Durability:持久性
复杂的是隔离或可串行化的:确保并行执行相当于对某个顺序的顺序执行。
这就是并发控制的全部内容。
注意:为了保持DB编程的可管理性,DBMS确保串行化是至关重要的,想象一下担心其他查询会妨碍运行的任何查询!
查询被分成许多较小的操作。
允许对这些操作进行交错。
启用并发性
但如果使用相同的数据,则会出现问题。
不能更新(Lost Update)
不好识别(Dirty Reads)
不能一致识别(Inconsistent Reads)
并发控制的目的:
允许通用的并发,但禁止危险的相互作用。
Lost update:
假设有人花了50,并得到了2000,两个事务是同时发生的
语句则被分成很多带顺序的低级操作:
下面是交错操作:
这种方式下在账户里增加2000的效率丢失了
Dirty Reads
当事务读取由另一事务修改的值时,会发生Dirty Reads,但修改不是最终的。
交错操作如下:
返回余额0,即便已经被回滚了
Inconsistent Reads
不一致(不可重复)的读取 发生在在另一事务之间改变时读值多次
交错操作如下:
最大余额的账户寻找不到
读/写锁
目标:
防止对同一数据的并发访问
但要尽量减少对性能的影响
只在必要时限制以防止问题
区分读写操作/访问
交错读取操作是无害的。
交错写入操作会引起问题。
交错读写操作也会引起问题。
两种锁:
读锁(又名共享锁)
写锁(又名专用锁)
可以有多个读锁或单个写锁
在事务访问数据之前,它必须获取锁,如果不可能,稍后再尝试
锁定可以在不同级别下发生:数据库级,表级,页面级别(最常见),行级
数据库级别:只需要单个锁,但不并发
行级:大并发,但需要的锁太多
表和页级锁提供了一个折衷方案
多查询事务随时间获取锁,在需要时获得很容易。
但是什么时候释放它们呢?
越快越好并发,但有问题
那么,如果我们释放一个锁,然后又需要它会发生什么?
那么,如果我们释放一个锁,然后需要一个不同的锁会发生什么?
二相锁定协议
成长阶段:获取锁(无锁释放) 收缩阶段:释放锁(没有获得的锁)也就是在事务结束时释放所有锁
防止有问题的操作交错
两阶段锁定(带表级锁):
死锁(Deadlocks)
两个事务可能互相等待释放锁。
因为两者都在等待,没有一个释放他们的锁。
也可以发生两个以上的事务,这种僵局被称为僵局。
两相锁定不能防止死锁
死锁可以用不同的方式处理:
死锁预防:
如果发生僵局,就采取行动
可能不必要中止交易
死锁检测:
如果发生死锁,只能采取行动
用于检测死锁的开销
额外的问题:饥饿
为了解决死锁,事务需要中止(如果可能的话重新开始)
相同的事务可能会一再中止(它“饿死”了)。
解决方案:总是中止上次提交的事务
所有的事务都是时间戳的。
当中止并重新启动时,保留原始时间戳。
死锁预防:
等待-死亡方案:
如果T1需要一个T2持有的锁:
(等待)如果T1大于T2,等待锁定被释放。
(死亡)如果T1较小,中止T1并稍后重启。
伤口等待方案:如果T1需要一个T2持有的锁:
(受伤)如果T1大于T2,则中止T2并重新启动。
(等待)如果T1较小,等待锁定被释放。
两种方案的共同点:
死锁被解决的了
年轻的过程中止了——没有饥饿
即使没有死锁,事务也可能中止
死锁检测:
目标:只有死锁发生时才会中止
构造和维护等待图
每个事务都是一个节点。
如果Ti在等待Tj持有的锁,则从TI到TJ的有向边。
如果图包含任何循环,则为死锁。如果检测到循环,则中止一个事务以中断循环 例如:最年轻的交易——没有饥饿。
维护等待图和检测周期是昂贵的
最适合于很少并发事务的数据库,或者当重新启动事务是有问题的(例如中间结果已经返回)
死锁解析由DBMS处理,但并非所有的实现都能防止饥饿。,频繁死锁减速处理(DBMS可能需要一些帮助)
谨慎地构造交易可以减少僵局:
提前锁定(在事务开始时),减少锁定操作交错的机会
以固定顺序获取所有事务的锁,防止死锁,但对于大的代码库是困难的
在每种情况下,都必须声明所需的锁。
DBMS提供用于获取/释放锁的工具
DBMSs还允许禁用/减弱锁定,防止/减少死锁,加快查询速度,但能制造出不可复制的讨厌的bugs
在大多数情况下,这是个坏方法!
----------------------------------------------------
频繁类型的死锁:先读取数据,然后更新
许多DBMS提供更新锁来处理这个问题。
只允许读取数据
不能有多个更新锁(或更新+写锁),防止上述类型的死锁
允许单个更新锁+多个读锁,不阻止读写锁定
必须由用户明确声明,例如
数据库恢复
当事务中止时,必须进行更改。
事务被终止可能由于
用户发出的显式回滚
死锁、超时或错误
数据库管理系统崩溃
在DBMS崩溃的情况下,我们需要确保
所有未提交的事务都会回滚。
所有已提交事务的更改被保存。
更新通常只在内存中执行,更快,但在DBMS崩溃期间丢失
事务日志
所有数据修改都记录在事务日志中。
存储“前后”和“后”值
允许改变颠倒
必须在数据更改之前发生日志记录。(日志没有变化是可以的,没有日志的更改是坏的)
元数据也存储在事务日志中。
事务的开始/结束(提交或回滚)
检查点(从内存保存到磁盘的数据),可以识别哪些更新丢失了
当DBMS崩溃时,重新启动它
读取事务日志
标识未提交的事务并将它们滚回。
识别并重新应用已提交事务的丢失更新
dB以一致状态(原子性)开始。
所有承诺的事务持续(持久性)
Summary
事务将数据库命令分组为单元
ACID确保DB相互作用是可靠的
交错操作对性能至关重要。
为了保证隔离,需要进行并发控制
锁定数据会导致死锁
解决僵局的协议(DBMS),
最小化死锁的技术(用户)
使用事务日志的数据库恢