原子性保证事务是一个不可分割的工作单元,其中的操作要么全部完成,要么全部不执行。如果事务中的任何部分失败,则整个事务将被撤销。
假设有一个事务包含三个操作:INSERT INTO users (name, age) VALUES (‘Alice’, 25)、UPDATE accounts SET balance = balance + 100 WHERE user_id = 1 和 DELETE FROM logs WHERE user_id = 1。如果在执行第三个操作时发生了错误,那么数据库会根据日志将前两个操作撤销,使数据库状态回到事务开始之前,就像这个事务从未执行过一样。
一致性确保事务必须使数据库从一个一致状态转换到另一个一致状态。事务执行前后,数据库的完整性约束没有被破坏。
在一个学生成绩管理系统中,规定学生的成绩范围为0 - 100分。如果一个事务试图将某个学生的成绩更新为120分,那么该事务将违反域完整性约束,DBMS会拒绝执行此操作,以确保数据的一致性。
多个事务并发执行时,一个事务的执行不应影响其他事务的执行。每个事务都应在独立的环境中运行,就像它是唯一正在运行的事务一样。
持久性确保一旦事务提交,它对数据库所做的更改将是永久性的,即使系统发生故障也不会丢失。
在一个在线支付系统中,当用户完成一笔支付后,涉及到更新用户的余额信息等操作。假设在事务提交后,服务器突然断电,由于采用了持久性机制,数据库可以根据日志恢复支付操作,确保用户的余额信息不会丢失,支付业务能够正常完成。
在数据库管理系统中,事务可以分为显式事务(Explicit Transactions)和隐式事务(Implicit Transactions)。
1. 定义:显式事务是由用户显式地通过SQL语句开始和结束的事务。用户明确地使用 BEGIN TRANSACTION、COMMIT 和 ROLLBACK 等语句来控制事务的边界。
2. 特点:
3. SQL语句:
4. 示例:
-- 开始事务
START TRANSACTION;
-- 执行SQL操作
INSERT INTO account (id, balance) VALUES (1, 1000);
UPDATE account SET balance = balance - 100 WHERE id = 1;
INSERT INTO transaction_log (account_id, amount) VALUES (1, -100);
-- 提交事务
COMMIT;
-- 或者回滚事务
-- ROLLBACK;
1. 定义:隐式事务是由数据库自动管理的事务。每个单独的SQL语句被视为一个独立的事务,自动提交或回滚。
2. 特点:
3. SQL语句:
4. 示例:
-- 插入操作自动提交
INSERT INTO account (id, balance) VALUES (1, 1000);
-- 更新操作自动提交
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 插入操作自动提交
INSERT INTO transaction_log (account_id, amount) VALUES (1, -100);
特性 | 显式事务 (Explicit Transactions) | 隐式事务 (Implicit Transactions) |
---|---|---|
控制方式 | 用户显式控制事务的开始和结束 | 数据库自动管理事务的开始和结束 |
事务边界 | 开发者定义事务的开始和结束点 | 每个SQL语句被视为一个独立的事务 |
灵活性 | 可以包含多个操作,根据需要进行回滚 | 每个操作都是独立的,无法包含多个操作 |
性能 | 可以更好地控制事务的粒度,减少锁的持有时间,提高并发性能 | 每个操作自动提交,可能增加锁的持有时间,影响并发性能 |
适用场景 | 复杂的事务操作,需要确保多个操作的原子性 | 简单的操作,每个操作独立,不需要复杂的事务控制 |
1. 定义:Savepoint 是事务中的一个标记点,允许用户在事务中设置一个保存点,并在需要时回滚到该保存点,而不影响事务中其他部分的操作。这为事务提供了更灵活的回滚机制。
2. 特点:
3. SQL语句:
4. 示例:
-- 开始事务
START TRANSACTION;
-- 执行一些操作
INSERT INTO account (id, balance) VALUES (1, 1000);
-- 设置保存点
SAVEPOINT savepoint1;
-- 执行更多操作
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 设置另一个保存点
SAVEPOINT savepoint2;
-- 执行更多操作
INSERT INTO transaction_log (account_id, amount) VALUES (1, -100);
-- 回滚到 savepoint2
ROLLBACK TO SAVEPOINT savepoint2;
-- 释放 savepoint1
RELEASE SAVEPOINT savepoint1;
-- 提交事务
COMMIT;
详细步骤:
1)开始事务:START TRANSACTION;
2)插入操作:INSERT INTO account (id, balance) VALUES (1, 1000);
3)设置保存点 savepoint1:SAVEPOINT savepoint1;
4)更新操作:UPDATE account SET balance = balance - 100 WHERE id = 1;
5)设置保存点 savepoint2:SAVEPOINT savepoint2;
6)插入操作:INSERT INTO transaction_log (account_id, amount) VALUES (1, -100);
7)回滚到 savepoint2:ROLLBACK TO SAVEPOINT savepoint2;(注意:这将撤销插入到 transaction_log 的操作,但保留 savepoint1 之前的更新操作。)
8)释放 savepoint1:RELEASE SAVEPOINT savepoint1;
9)提交事务:COMMIT;
代码示例(Java + JDBC):
import java.sql.*;
public class SavepointExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "root";
try (Connection conn = DriverManager.getConnection(url, user, password)) {
// 关闭自动提交模式
conn.setAutoCommit(false);
try {
// 开始事务
conn.setAutoCommit(false);
// 执行一些操作
Statement stmt = conn.createStatement();
stmt.executeUpdate("INSERT INTO account (id, balance) VALUES (1, 1000)");
// 设置保存点
Savepoint savepoint1 = conn.setSavepoint("savepoint1");
// 执行更多操作
stmt.executeUpdate("UPDATE account SET balance = balance - 100 WHERE id = 1");
// 设置另一个保存点
Savepoint savepoint2 = conn.setSavepoint("savepoint2");
// 执行更多操作
stmt.executeUpdate("INSERT INTO transaction_log (account_id, amount) VALUES (1, -100)");
// 回滚到 savepoint2
conn.rollback(savepoint2);
// 释放 savepoint1
conn.releaseSavepoint(savepoint1);
// 提交事务
conn.commit();
System.out.println("事务提交成功");
} catch (SQLException e) {
// 回滚事务
conn.rollback();
System.out.println("事务回滚");
e.printStackTrace();
} finally {
// 恢复自动提交模式
conn.setAutoCommit(true);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
1. 定义:只读事务是指在事务期间不允许对数据库进行任何修改操作。只读事务可以提高并发性能,因为数据库管理系统可以对只读操作进行优化,减少锁的使用。
2. 特点:
3. SQL语句:
4. 示例:
-- 开始只读事务
SET TRANSACTION READ ONLY;
START TRANSACTION;
-- 执行只读操作
SELECT * FROM account WHERE id = 1;
-- 提交事务
COMMIT;
详细步骤:
1)设置只读事务:SET TRANSACTION READ ONLY;
2)开始事务:START TRANSACTION;
3)执行只读操作:SELECT * FROM account WHERE id = 1;
4)提交事务:COMMIT;
代码示例(Java + JDBC):
import java.sql.*;
public class ReadOnlyTransactionExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "root";
try (Connection conn = DriverManager.getConnection(url, user, password)) {
// 关闭自动提交模式
conn.setAutoCommit(false);
try {
// 设置只读事务
conn.setReadOnly(true);
// 开始事务
conn.setAutoCommit(false);
// 执行只读操作
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM account WHERE id = 1");
while (rs.next()) {
int id = rs.getInt("id");
int balance = rs.getInt("balance");
System.out.println("id = " + id + ", balance = " + balance);
}
// 提交事务
conn.commit();
System.out.println("事务提交成功");
} catch (SQLException e) {
// 回滚事务
conn.rollback();
System.out.println("事务回滚");
e.printStackTrace();
} finally {
// 恢复自动提交模式
conn.setAutoCommit(true);
// 恢复可写模式
conn.setReadOnly(false);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
数据库隔离级别定义了事务之间如何隔离,以确保数据的一致性和完整性。SQL标准定义了四个隔离级别,每个级别都提供了不同程度的隔离性,同时也带来了不同的并发性能和数据一致性问题。数据库隔离级别从低到高依次为:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)、串行化(Serializable)。以下是详细的隔离级别及其产生的问题。
事务可以读取其他事务未提交的数据。
脏读(Dirty Read):事务读取了其他事务未提交的数据,如果其他事务回滚,读取的数据将不一致。
脏读示例:
1. 数据库初始状态
假设有一个表 account,包含以下数据:
id balance 1 1000.00 2. 流程图
事务A 事务B BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;BEGIN;
SELECT balance FROM account WHERE id = 1;SELECT balance FROM account WHERE id = 1; ROLLBACK; 3. 详细步骤
1)事务A开始并更新数据
事务A 开始执行,并更新 account 表中 id = 1 的 balance 为 500。
-- 事务A BEGIN; UPDATE account SET balance = 500 WHERE id = 1;
2)事务B开始并读取数据
事务B 开始执行,并读取 account 表中 id = 1 的 balance。
-- 事务B BEGIN; SELECT balance FROM account WHERE id = 1;
事务B 读取到的 balance 是 500,这是事务A未提交的数据。
3)事务A回滚
事务A 回滚,撤销之前的操作,将 balance 恢复为 1000。
-- 事务A ROLLBACK;
4)事务B再次读取数据
事务B 再次读取 account 表中 id = 1 的 balance。
-- 事务B SELECT balance FROM account WHERE id = 1;
事务B 读取到的 balance 是 1000,与之前读取到的 500 不一致。
4. 脏读问题分析
- 初始状态:balance 为 1000。
- 事务A更新数据:将 balance 更新为 500,但未提交。
- 事务B读取数据:读取到事务A未提交的 balance 值 500。
- 事务A回滚:balance 恢复为 1000。
- 事务B再次读取数据:读取到 balance 为 1000,与之前读取到的 500 不一致。
5. 代码示例
以下是使用Java和JDBC的代码示例,展示脏读的问题:
import java.sql.*; public class DirtyReadExample { public static void main(String[] args) { String url = "jdbc:mysql://localhost:3306/test"; String user = "root"; String password = "root"; try (Connection connA = DriverManager.getConnection(url, user, password); Connection connB = DriverManager.getConnection(url, user, password)) { // 设置事务隔离级别为读未提交 connA.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); connB.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED); // 事务A开始并更新数据 connA.setAutoCommit(false); Statement stmtA = connA.createStatement(); stmtA.executeUpdate("UPDATE account SET balance = 500 WHERE id = 1"); System.out.println("事务A更新数据,balance = 500"); // 事务B开始并读取数据 connB.setAutoCommit(false); Statement stmtB = connB.createStatement(); ResultSet rsB = stmtB.executeQuery("SELECT balance FROM account WHERE id = 1"); if (rsB.next()) { int balance = rsB.getInt("balance"); System.out.println("事务B读取数据,balance = " + balance); } // 事务A回滚 connA.rollback(); System.out.println("事务A回滚"); // 事务B再次读取数据 rsB = stmtB.executeQuery("SELECT balance FROM account WHERE id = 1"); if (rsB.next()) { int balance = rsB.getInt("balance"); System.out.println("事务B再次读取数据,balance = " + balance); } // 提交事务B connB.commit(); } catch (SQLException e) { e.printStackTrace(); } } }
输出结果:
事务A更新数据,balance = 500 事务B读取数据,balance = 500 事务A回滚 事务B再次读取数据,balance = 1000
6. 总结
通过上述流程和代码示例,可以看出脏读的问题在于事务B读取了事务A未提交的数据,而事务A最终回滚,导致事务B读取的数据不一致。选择合适的隔离级别(如读已提交或更高)可以避免脏读问题。
事务只能读取其他事务已经提交的数据。
不可重复读(Non-repeatable Read):事务在同一个查询中多次读取同一数据时,数据可能被其他事务修改,导致结果不一致。
不可重复读示例:
1. 数据库初始状态
假设有一个表 account,包含以下数据:
id balance 1 1000.00 2. 流程图
事务A 事务B BEGIN;
SELECT balance FROM account WHERE id = 1;BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
COMMIT;SELECT balance FROM account WHERE id = 1; 3. 详细步骤
1)事务A开始并读取数据
事务A 开始执行,并读取 account 表中 id = 1 的 balance。
-- 事务A BEGIN; SELECT balance FROM account WHERE id = 1;
事务A 读取到的 balance 是 1000。
2)事务B开始并更新数据
事务B 开始执行,并更新 account 表中 id = 1 的 balance 为 500。
-- 事务B BEGIN; UPDATE account SET balance = 500 WHERE id = 1; COMMIT;
3)事务A再次读取数据
事务A 再次读取 account 表中 id = 1 的 balance。
-- 事务A SELECT balance FROM account WHERE id = 1;
事务A 读取到的 balance 是 500,与之前读取到的 1000 不一致。
4. 不可重复读问题分析
- 初始状态:balance 为 1000。
- 事务A读取数据:读取到 balance 为 1000。
- 事务B更新数据:将 balance 更新为 500 并提交。
- 事务A再次读取数据:读取到 balance 为 500,与之前读取到的 1000 不一致。
5. 代码示例
import java.sql.*; public class NonRepeatableReadExample { public static void main(String[] args) { String url = "jdbc:mysql://localhost:3306/test"; String user = "root"; String password = "root"; try (Connection connA = DriverManager.getConnection(url, user, password); Connection connB = DriverManager.getConnection(url, user, password)) { // 设置事务隔离级别为读已提交 connA.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); connB.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // 事务A开始并读取数据 connA.setAutoCommit(false); Statement stmtA = connA.createStatement(); ResultSet rsA = stmtA.executeQuery("SELECT balance FROM account WHERE id = 1"); if (rsA.next()) { int balance = rsA.getInt("balance"); System.out.println("事务A第一次读取数据,balance = " + balance); } // 事务B开始并更新数据 connB.setAutoCommit(false); Statement stmtB = connB.createStatement(); stmtB.executeUpdate("UPDATE account SET balance = 500 WHERE id = 1"); connB.commit(); System.out.println("事务B更新数据,balance = 500"); // 事务A再次读取数据 rsA = stmtA.executeQuery("SELECT balance FROM account WHERE id = 1"); if (rsA.next()) { int balance = rsA.getInt("balance"); System.out.println("事务A第二次读取数据,balance = " + balance); } // 提交事务A connA.commit(); } catch (SQLException e) { e.printStackTrace(); } } }
输出结果:
事务A第一次读取数据,balance = 1000 事务B更新数据,balance = 500 事务A第二次读取数据,balance = 500
6. 总结
通过上述流程和代码示例,可以看出不可重复读的问题在于事务A在同一个查询中多次读取同一数据时,数据被事务B修改,导致结果不一致。选择合适的隔离级别(如可重复读或更高)可以避免不可重复读问题。
事务在同一个查询中多次读取同一数据时,数据保持一致,即使其他事务修改了这些数据。
幻读(Phantom Read):事务在同一个查询中多次读取同一范围的数据时,数据集可能被其他事务插入或删除,导致结果不一致。
幻读示例:
1. 数据库初始状态
假设有一个表 account,包含以下数据:
id balance 1 1000.00 2 2000.00 2. 流程图
事务A 事务B BEGIN;
SELECT * FROM account WHERE balance > 1500;BEGIN;
INSERT INTO account (id, balance) VALUES (3, 3000);
COMMIT;SELECT * FROM account WHERE balance > 1500; 3. 详细步骤
1)事务A开始并读取数据
事务A 开始执行,并读取 account 表中 balance > 1500 的所有记录。
-- 事务A BEGIN; SELECT * FROM account WHERE balance > 1500;
事务A 读取到的记录是:
id balance 2 2000.00 2)事务B开始并插入数据
事务B 开始执行,并插入一条新的记录 id = 3,balance = 3000。
-- 事务B BEGIN; INSERT INTO account (id, balance) VALUES (3, 3000); COMMIT;
3)事务A再次读取数据
事务A 再次读取 account 表中 balance > 1500 的所有记录。
-- 事务A SELECT * FROM account WHERE balance > 1500;
事务A 读取到的记录是:
id balance 2 2000.00 3 3000.00 4. 幻读问题分析
- 初始状态:表中有两条记录,id = 1 和 id = 2,balance 分别为 1000 和 2000。
- 事务A读取数据:读取到 balance > 1500 的记录,即 id = 2,balance = 2000。
- 事务B插入数据:插入一条新的记录 id = 3,balance = 3000。
- 事务A再次读取数据:读取到 balance > 1500 的记录,包括 id = 2 和 id = 3,出现了新的记录 id = 3,即幻读。
5. 代码示例
import java.sql.*; public class PhantomReadExample { public static void main(String[] args) { String url = "jdbc:mysql://localhost:3306/test"; String user = "root"; String password = "root"; try (Connection connA = DriverManager.getConnection(url, user, password); Connection connB = DriverManager.getConnection(url, user, password)) { // 设置事务隔离级别为可重复读 connA.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); connB.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ); // 事务A开始并读取数据 connA.setAutoCommit(false); Statement stmtA = connA.createStatement(); ResultSet rsA = stmtA.executeQuery("SELECT * FROM account WHERE balance > 1500"); System.out.println("事务A第一次读取数据:"); while (rsA.next()) { int id = rsA.getInt("id"); int balance = rsA.getInt("balance"); System.out.println("id = " + id + ", balance = " + balance); } // 事务B开始并插入数据 connB.setAutoCommit(false); Statement stmtB = connB.createStatement(); stmtB.executeUpdate("INSERT INTO account (id, balance) VALUES (3, 3000)"); connB.commit(); System.out.println("事务B插入数据,id = 3, balance = 3000"); // 事务A再次读取数据 rsA = stmtA.executeQuery("SELECT * FROM account WHERE balance > 1500"); System.out.println("事务A第二次读取数据:"); while (rsA.next()) { int id = rsA.getInt("id"); int balance = rsA.getInt("balance"); System.out.println("id = " + id + ", balance = " + balance); } // 提交事务A connA.commit(); } catch (SQLException e) { e.printStackTrace(); } } }
输出结果:
事务A第一次读取数据: id = 2, balance = 2000 事务B插入数据,id = 3, balance = 3000 事务A第二次读取数据: id = 2, balance = 2000 id = 3, balance = 3000
6. 总结
通过上述流程和代码示例,可以看出幻读的问题在于事务A在同一个查询中多次读取同一范围的数据时,数据集被事务B插入了新的记录,导致结果不一致。选择合适的隔离级别(如串行化)可以避免幻读问题。
事务完全串行化执行,即一个事务在另一个事务提交之前必须等待。
性能下降:由于事务之间完全隔离,系统并发性能较低。
-- 事务A
BEGIN;
SELECT * FROM table_name WHERE column1 > 10; -- 第一次读取
-- 事务B
BEGIN;
INSERT INTO table_name (column1) VALUES (15);
-- 事务B必须等待事务A提交或回滚
-- 事务A
SELECT * FROM table_name WHERE column1 > 10; -- 第二次读取,结果一致
COMMIT;
为了更好地理解不同隔离级别如何解决这些问题,以下是各个隔离级别的比较:
隔离级别 | 脏读(Dirty Read) | 不可重复读(Non-repeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
读未提交 | 允许 | 允许 | 允许 |
读已提交 | 禁止 | 允许 | 允许 |
可重复读 | 禁止 | 禁止 | 允许 |
串行化 | 禁止 | 禁止 | 禁止 |
选择合适的隔离级别可以根据具体的应用需求来平衡并发性能和数据一致性。
事务传播行为定义了在多个事务性方法之间调用时,如何管理和关联这些事务。以下是常见的几种事务传播行为:
-- 加共享锁
SELECT * FROM table_name WHERE id = 1 LOCK IN SHARE MODE;
-- 加排他锁
SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
请求锁 \ 当前锁 | 无锁 | S锁 | X锁 |
---|---|---|---|
无锁 | 是 | 是 | 是 |
S锁 | 是 | 是 | 否 |
X锁 | 是 | 否 | 否 |
**死锁(Deadlock)**是指两个或多个事务互相等待对方持有的资源(如锁),从而导致所有这些事务都无法继续执行的状态。在数据库系统中,死锁通常发生在多个事务竞争共享资源时,每个事务都持有某些资源并等待其他事务释放它们所需的资源。
根据Coffman条件,死锁的发生需要满足以下四个必要条件:
假设有一个简单的银行转账系统,包含两个账户A和B。有两个事务T1和T2分别进行转账操作:
事务 | 操作 |
---|---|
T1 | BEGIN; LOCK TABLE account A IN EXCLUSIVE MODE; – 获取账户A的排他锁。UPDATE account SET balance = balance - 100 WHERE id = A; |
T2 | BEGIN; LOCK TABLE account B IN EXCLUSIVE MODE; – 获取账户B的排他锁。UPDATE account SET balance = balance + 100 WHERE id = B; |
T1 | LOCK TABLE account B IN EXCLUSIVE MODE; |
T2 | LOCK TABLE account A IN EXCLUSIVE MODE; |
在这个例子中:
结果是T1等待T2释放账户B的锁,而T2等待T1释放账户A的锁,形成了死锁。
死锁检测是指通过定期检查系统状态来发现是否存在死锁。常见的检测方法包括:
死锁预防是指通过限制事务的行为来避免死锁的发生。常见的预防策略包括:
一旦检测到死锁,系统需要采取措施解除死锁。常见的解除方法包括:
在数据库系统中,死锁是一个常见的问题,特别是在高并发环境下。为了应对死锁,数据库管理系统通常会采用以下几种方式:
两段锁协议(2PL)是一种并发控制协议,用于确保数据库事务的可串行化调度。根据该协议,每个事务的执行过程被划分为两个阶段:扩展阶段(Growing Phase) 和 收缩阶段(Shrinking Phase)。
通过将事务的操作严格划分为两个阶段,2PL能够确保事务的调度是可串行化的。即,多个事务并发执行的结果等价于某个顺序执行这些事务的结果。这有助于维护数据库的一致性和隔离性。
可串行化 vs 串行化:
假设我们有一个包含两个事务 ( T1 ) 和 ( T2 ) 的银行转账系统,涉及三个账户 ( A )、( B ) 和 ( C )。我们将逐步展示这两个事务如何遵循2PL执行。
1. 初始状态
事务 ( T1 ) :将账户 ( A ) 中的 50 元转到账户 ( B )。
事务 ( T2 ):将账户 ( B ) 中的 100 元转到账户 ( C )。
2. 执行过程
事务 ( T1 ):
1)进入扩展阶段:
2)进入收缩阶段:
事务 ( T2 ):
1)进入扩展阶段:
2)进入收缩阶段:
3. 关键点分析
1)扩展阶段和收缩阶段的划分:
2)防止冲突:
优点:
缺点:
死锁示例:
1. 场景:
假设有一个简单的数据库,包含两个数据项 A 和 B,并且有两个事务 T1 和 T2 需要访问这些数据项。
事务 T1 的操作:
1)获取 A 的 S 锁。
2)获取 B 的 X 锁。
3)执行一些操作。
4)释放 A 的锁。
5)释放 B 的锁。事务 T2 的操作:
1)获取 B 的 S 锁。
2)获取 A 的 X 锁。
3)执行一些操作。
4)释放 B 的锁。
5)释放 A 的锁。2. 时间线示例:
1)初始状态:A 和 B 均未被锁定。
2)时间点 t1:
- T1 开始执行,并获取 A 的 S 锁。
- 状态:A (S, T1),B (无锁)
3)时间点 t2:
- T2 开始执行,并获取 B 的 S 锁。
- 状态:A (S, T1), B (S, T2)
4)时间点 t3:
- T1 尝试获取 B 的 X 锁,但由于 B 已经被 T2 锁定,T1 阻塞。
- 状态:A (S, T1), B (S, T2)
5)时间点 t4:
- T2 尝试获取 A 的 X 锁,但由于 A 已经被 T1 锁定,T2 阻塞。
- 状态:A (S, T1), B (S, T2)
此时,T1 和 T2 都在等待对方释放锁,从而形成死锁。
事务T1 事务T2 A B 初始状态:A 和 B 均未被锁定 获取 A 的 S 锁 状态:A (S, T1),B (无锁) 获取 B 的 S 锁 状态:A (S, T1), B (S, T2) 尝试获取 B 的 X 锁 T1 阻塞,等待 T2 释放 B 的锁 尝试获取 A 的 X 锁 T2 阻塞,等待 T1 释放 A 的锁 形成死锁 事务T1 事务T2 A B3. 示例代码
为了更清晰地展示上述死锁情况,以下是伪代码示例:
public class TwoPhaseLockingExample { public static void main(String[] args) { final Object A = new Object(); final Object B = new Object(); Thread t1 = new Thread(() -> { synchronized (A) { System.out.println("T1: Locked A"); try { Thread.sleep(100); // 模拟其他操作 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (B) { System.out.println("T1: Locked B"); // 执行操作 System.out.println("T1: Releasing B"); } System.out.println("T1: Releasing A"); } }); Thread t2 = new Thread(() -> { synchronized (B) { System.out.println("T2: Locked B"); try { Thread.sleep(100); // 模拟其他操作 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (A) { System.out.println("T2: Locked A"); // 执行操作 System.out.println("T2: Releasing A"); } System.out.println("T2: Releasing B"); } }); t1.start(); t2.start(); } }
运行上述代码,您会看到 T1 和 T2 进入死锁状态。通过日志输出可以看到它们分别持有不同的锁并等待对方释放锁。
三级封锁协议是数据库系统中用于确保事务隔离性的三种不同级别的封锁规则。每级封锁协议都对事务加锁提出了不同的要求,以保证不同程度的数据一致性和并发性。
一级封锁协议(One-Phase Locking Protocol, 1PL)是数据库管理系统中最基本的并发控制协议之一。它要求事务在修改数据项之前必须获得排他锁(X锁),并在事务结束时释放所有锁。该协议主要用于防止丢失更新问题。
假设我们有两个事务 ( T1 ) 和 ( T2 ),它们分别对账户 ( A ) 进行操作:
关键点分析:
1)加X锁:
2)更新操作:
3)提交与解锁:
4)等待过程:
优点:
缺点:
一级封锁协议适用于对一致性要求不高、但对性能要求较高的场景。例如,在一些简单的应用程序中,可能只需要确保更新操作的一致性,而对读取操作的隔离性要求不高。具体应用场景包括:
二级封锁协议(Two-Phase Locking Protocol, 2PL)是一种用于数据库管理系统中控制并发事务的协议。它确保事务在读取或修改数据项之前获得适当的锁,并且在事务结束前不释放任何锁,以保证数据的一致性和隔离性。
假设我们有两个事务 ( T1 ) 和 ( T2 ),它们分别对账户 ( A ) 进行操作:
关键点分析:
1)加S锁:
2)加X锁:
3)等待过程:
优点:
缺点:
二级封锁协议适用于对一致性要求较高且读多写少的场景。具体应用场景包括:
三级封锁协议(Three-Phase Locking Protocol, 3PL)是数据库管理系统中用于控制并发事务的一种高级机制。它在二级封锁协议的基础上进一步加强了锁的管理,确保事务在读取数据项时只能加共享锁(S锁),并且在事务结束前不能释放任何锁。这有效地解决了不可重复读和幻读问题。
假设我们有两个事务 ( T1 ) 和 ( T2 ),它们分别对账户 ( A ) 进行操作:
关键点分析:
1)加S锁:
2)加X锁:
3)等待过程:
优点:
缺点:
三级封锁协议适用于对数据一致性要求极高的场景。具体应用场景包括: