数据库的并发控制与封锁
并发控制
并发控制是确保数据库在多个用户同时访问时保持数据一致性和完整性的机制。主要的并发控制技术包括:
- 锁定(Locking):数据库通过对数据项加锁来限制对它们的访问。锁可以是共享的(读锁)或排他的(写锁)。
- 乐观并发控制:假设冲突很少发生,先允许事务操作,然后在事务提交时检查是否有冲突。
- 悲观并发控制:假设冲突经常发生,因此在事务操作之前就进行控制,例如通过锁定数据。
- 多版本并发控制(MVCC):在不同的时间点创建数据的多个版本,允许读取操作和写入操作并发进行。
事务的一致性
简单理解为一个操作中的全部环节
- 原子性(Atomicity): 原子性确保事务被视为不可分割的单元。事务中的所有操作要么全部成功,要么全部失败。如果在事务执行过程中发生错误,所有的更改都将被回滚,以保持数据的一致性。
- 一致性(Consistency): 一致性确保事务将数据库从一个一致的状态转变为另一个一致的状态。在事务执行期间,数据库的完整性约束得到满足,数据的合法性得到保持。
- 隔离性(Isolation): 隔离性指的是一个事务的执行不受其他事务的影响,每个事务都被隔离开,以防止并发操作导致数据不一致。数据库系统通常采用锁或其他并发控制机制来实现隔离性。
- 持久性(Durability): 持久性确保一旦事务被提交,其结果将永久保存在数据库中,即使发生系统故障或重启,也不会丢失。
事务是一系列的数据库操作,通常包括插入、更新、删除等,这些操作在 BEGIN
之后开始,然后在 COMMIT
之前执行。如果任何一个操作失败,事务将回滚,意味着之前执行的所有操作都将被撤销,数据库将返回到事务开始前的状态,就好像没有执行过这些操作一样
-- 启动事务
BEGIN;
-- 执行一系列数据库操作
INSERT INTO table1 (column1, column2) VALUES (value1, value2);
UPDATE table2 SET column3 = value3 WHERE condition;
-- 其他操作
-- 结束事务并将更改永久保存到数据库
COMMIT;
# 实际操作中应该使用的:
# 启动事务
BEGIN TRANSACTION;
try:
# 执行一系列数据库操作
INSERT INTO table1 (column1, column2) VALUES (value1, value2);
UPDATE table2 SET column3 = value3 WHERE condition;
# 其他操作
# 如果一切正常,提交事务
COMMIT;
except Exception as e:
# 出现错误,回滚事务
ROLLBACK;
# 可以记录错误日志或其他处理
当每个事务中的每个语句都成功执行时,执行 COMMIT
的作用是将这些成功的操作永久保存到数据库中,并结束整个事务。虽然看起来似乎没有必要,但 COMMIT
的目的远不止保存更改。
COMMIT
主要起到以下作用:
- 事务的逻辑分割: 事务可以包含多个操作,这些操作可能涉及多个表或数据集。
COMMIT
用于将这些操作分成逻辑单元,确保它们在数据库中作为一个整体进行保存。这有助于维护数据的一致性和完整性。 - 事务的边界:
COMMIT
定义了一个事务的边界。一旦执行COMMIT
,之前的操作就被认为已经完成,不再受事务的影响。这使得其他事务能够访问已提交的数据,从而支持多个事务之间的并发访问。 - 并发控制: 数据库系统通常使用事务来管理并发访问。
COMMIT
标志着一个事务的完成,允许其他事务进行操作。这有助于避免冲突和确保数据库的数据一致性。 - 保存点:
COMMIT
也可以用于创建保存点,以便在事务后的某个时间点进行回滚。这在某些情况下非常有用,因为它允许你将事务的某个部分保存下来,而不必回滚整个事务。
事务的不一致性
不可重复读
不可重复读是指一个事务对统一数据的读取结果前后不一致,这是由于在两次查询期间该数据被另一个事务修改并提交了。当其中一个事务需要校验或确认数据时,出现读取数据与之前数据不一致。
时间点 | 事务 T1 | 事务 T2 | 并发问题分析 |
---|---|---|---|
t0 | BEGIN; | ||
t1 | SELECT balance FROM accounts; | T1 读取账户余额。 | |
t2 | BEGIN; | ||
t3 | UPDATE accounts SET balance = balance - 100; | ||
t4 | COMMIT; | T2 修改账户余额并提交更改。 | |
t5 | SELECT balance FROM accounts; | T1 再次读取账户余额,发现余额减少了100。 | |
t6 | COMMIT; | T1 的两次读取结果不一致,发生了不可重复读。 |
- 在 t0 时刻:事务 T1 开始。
- 在 t1 时刻:事务 T1 查询账户的余额,假设余额为 1000。
- 在 t2 到 t3 时刻:事务 T2 开始并执行取款操作,将余额更新为 900。
- 在 t4 时刻:事务 T2 提交了更改,余额的更新被持久化到数据库中。
- 在 t5 时刻:事务 T1 再次查询账户的余额,这次查询结果为 900。
- 在 t6 时刻:事务 T1 提交。
在这个例子中,事务 T1 在 t1 和 t5 两个不同的时间点读取了相同的数据(账户余额),但得到了不同的结果。这是因为在两次读取之间,事务 T2 修改了账户余额并提交了更改。这种现象就是所谓的“不可重复读”。
幻读
幻读(Phantom Read)是指在一个事务中,当同一查询被执行多次时,由于其他并发事务的新增或删除操作,返回了一组不同的行。即使单个记录的值没有改变,结果集中的行数却因为另一个并发事务的提交而改变了。
时间点 | 事务 T1 | 事务 T2 | 并发问题分析 |
---|---|---|---|
t0 | BEGIN; | ||
t1 | SELECT COUNT(*) FROM accounts WHERE balance > 1000; | T1 统计余额超过1000的账户总数。 | |
t2 | BEGIN; | ||
t3 | INSERT INTO accounts (balance) VALUES (1500); | T2 创建了一个新账户,余额为1500。 | |
t4 | COMMIT; | T2 提交了新账户的创建。 | |
t5 | SELECT COUNT(*) FROM accounts WHERE balance > 1000; | T1 再次统计余额超过1000的账户总数。 | |
t6 | COMMIT; | T1 提交事务,但它在两次查询中看到了不同的总数。 |
- 在 t0 和 t1 时刻:事务 T1 开始,并执行了一个查询,统计所有余额超过 1000 的账户总数。
- 在 t2 到 t3 时刻:事务 T2 开始,并插入了一个新的账户记录,该账户余额为 1500。
- 在 t4 时刻:事务 T2 提交更改,新账户被正式创建。
- 在 t5 时刻:事务 T1 再次执行同样的查询,这次查询的结果包含了 T2 新创建的账户,因此账户总数增加了。
- 在 t6 时刻:事务 T1 提交事务。
丢失更新
这种并发问题发生在两个事务同时尝试更新同一条记录时,一个事务的更新覆盖了另一个事务的更新,导致其中一个更新丢失
时间点 | 事务 T1 | 事务 T2 | 并发问题分析 |
---|---|---|---|
t0 | BEGIN; | ||
t1 | SELECT content FROM articles; | T1 读取文章内容进行编辑。 | |
t2 | BEGIN; | ||
t3 | SELECT content FROM articles; | T2 同样读取文章内容进行编辑。 | |
t4 | UPDATE articles SET content = ‘X’; | T1 完成编辑,更新文章内容为 ‘X’。 | |
t5 | COMMIT; | T1 提交更改,文章内容现在是 ‘X’。 | |
t6 | UPDATE articles SET content = ‘Y’; | T2 完成编辑,更新文章内容为 ‘Y’。 | |
t7 | COMMIT; | T2 提交更改,文章内容现在是 ‘Y’。 | |
t8 | T1 的更新被 T2 的更新覆盖,发生了丢失更新。 |
- 在 t0 和 t1 时刻:事务 T1 开始,并读取了文章的当前内容进行编辑。
- 在 t2 和 t3 时刻:事务 T2 开始,并读取了文章的相同初始内容进行编辑。
- 在 t4 时刻:事务 T1 完成编辑并更新文章内容为 ‘X’。
- 在 t5 时刻:事务 T1 提交了更改,数据库中文章的内容变为 ‘X’。
- 在 t6 时刻:事务 T2 不知道 T1 的更改,也完成了编辑并试图更新文章内容为 ‘Y’。
- 在 t7 时刻:事务 T2 提交了更改,数据库中文章的内容变为 ‘Y’。
- 在 t8 时刻:结果显示事务 T1 的更新已经丢失,因为它被事务 T2 的更新覆盖。
脏读
“脏读”(Dirty Read)是指在数据库中一个事务读取到另一个事务未提交的数据。如果那个事务回滚,读取到的数据就是“脏”的,因为它读取了实际上从未确认的数据。
时间点 | 事务 T1 | 事务 T2 | 并发问题分析 |
---|---|---|---|
t0 | BEGIN; | ||
t1 | BEGIN; | ||
t2 | UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 100; | T2 将某产品的库存减少了1(未提交)。 | |
t3 | SELECT quantity FROM inventory WHERE product_id = 100; | T1 读取了 T2 更新但未提交的库存数据。 | |
t4 | ROLLBACK; | T2 回滚更改,库存实际未减少。 | |
t5 | COMMIT; | T1 提交事务,但它已经读取了脏数据。 |
- 在 t0 和 t1 时刻:事务 T1 和 T2 同时开始,但尚未执行任何操作。
- 在 t2 时刻:事务 T2 更新了产品 ID 为 100 的库存,减少了 1,但这个更改还没有被提交。
- 在 t3 时刻:事务 T1 查询产品 ID 为 100 的库存数量,读取到了 T2 所做的更改。
- 在 t4 时刻:事务 T2 决定回滚更改,库存数量回到了更新前的状态。
- 在 t5 时刻:事务 T1 提交了更改。然而,它在 t3 时刻读取的库存数据是脏的,因为 T2 的更改被回滚了。
事务的隔离级别
- 读未提交(Read Uncommitted)
- 特点:最低级别的隔离,允许事务读取其他未提交事务的更改。
- 区别:这个级别几乎不提供隔离,可能导致脏读,但并发性能最高。
- 读已提交(Read Committed)
- 特点:只允许事务读取已经提交的更改。
- 区别:相比于读未提交,它防止了脏读,但仍然可能发生不可重复读和幻读。这个级别在很多数据库系统中是默认设置。
- 可重复读(Repeatable Read)
- 特点:保证在同一个事务中多次读取同一数据的结果一致。
- 区别:相比于读已提交,它进一步防止了不可重复读,但在某些数据库系统中仍可能发生幻读(例如在 MySQL 中是防止幻读的)。
- 可串行化(Serializable)
- 特点:最高级别的隔离,事务完全串行执行。
- 区别:它防止了脏读、不可重复读和幻读,但并发性能最低,因为事务完全串行化,可能导致较长的等待时间和锁竞争。
- 可串行化的核心要点
- 隔离性:在这种隔离级别下,事务彼此隔离到了最大程度,以致于一个事务的操作不会影响到其他同时运行的事务。
- 顺序执行的假象:尽管事务可能在物理上并行执行,但其结果与它们被串行执行时的结果相同。
- 防止并发问题:通过这种方式,可串行化隔离级别可以防止脏读、不可重复读和幻读等所有类型的并发问题。