3)乐观锁与悲观锁
- 乐观锁:假设没有冲突,提交时验证。
- 悲观锁:在操作前锁定资源。
乐观锁和悲观锁是两种常见的锁机制,它们用于解决并发数据访问和修改时可能出现的冲突问题。它们各自的使用场景和实现方式有所不同。
1. 乐观锁(Optimistic Locking)
乐观锁假设在大多数情况下不会发生冲突,因此不在操作开始时立即加锁,而是在提交操作时检查是否存在冲突。如果数据在操作过程中被其他事务修改,则会回滚当前操作,通常会要求重新尝试。
实现方式:
版本号机制:每当对数据进行修改时,会更新版本号。在提交数据时,系统会检查版本号是否发生变化。如果版本号没有变化,说明没有其他事务修改该数据,可以提交;如果版本号变化,说明其他事务已修改该数据,则当前事务会回滚,避免数据不一致。
时间戳机制:类似于版本号机制,通过记录数据的最后更新时间戳,操作时比对时间戳,如果在提交时数据已经被其他事务修改,当前事务会回滚。
示例:
假设我们有一个 users
表,其中有一个 version
字段表示版本号。
sql
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(100),
version INT
);
在更新操作时,使用乐观锁验证版本号是否一致:
javascript
const updateUser = async (userId, newName, version) => {
const result = await db.query(
"UPDATE users SET name = ?, version = version + 1 WHERE id = ? AND version = ?",
[newName, userId, version]
);
if (result.affectedRows === 0) {
throw new Error(
"Optimistic lock failed: The record was updated by another transaction"
);
}
};
2. 悲观锁(Pessimistic Locking)
悲观锁假设并发操作之间会发生冲突,因此在操作之前会锁定资源,防止其他事务对同一资源进行操作。悲观锁通过显式地获取锁,确保其他事务无法访问该资源,直到当前事务完成。
实现方式:
- 行级锁:对于数据库的每一行数据,只有当前事务可以进行操作,其他事务必须等待当前事务释放锁。
- 表级锁:锁住整个表,防止其他事务进行任何操作。
- 悲观读锁:读取数据时加锁,防止其他事务修改该数据。
- 悲观写锁:修改数据时加锁,防止其他事务读取或修改该数据。
示例:
假设我们使用 SQL 查询实现悲观锁,通过 SELECT ... FOR UPDATE
语句来锁定某行数据:
javascript
const updateUserPessimistic = async (userId, newName) => {
const connection = await db.getConnection();
await connection.beginTransaction();
try {
// 获取行级锁,锁定数据
const [rows] = await connection.query(
"SELECT * FROM users WHERE id = ? FOR UPDATE",
[userId]
);
if (rows.length === 0) {
throw new Error("User not found");
}
// 执行更新操作
await connection.query("UPDATE users SET name = ? WHERE id = ?", [
newName,
userId,
]);
await connection.commit();
} catch (error) {
await connection.rollback();
throw error;
} finally {
connection.release();
}
};
在这个例子中,我们通过 FOR UPDATE
锁住了查询的行,确保其他事务无法修改这条数据直到当前事务提交。
3. 乐观锁与悲观锁的对比
特性 | 乐观锁 | 悲观锁 |
---|---|---|
假设 | 假设没有冲突 | 假设会发生冲突 |
加锁时机 | 不加锁,提交时验证数据是否冲突 | 操作前加锁,锁住数据或表 |
性能 | 高并发时性能好,避免了锁的开销 | 会引入锁竞争,降低并发性能 |
适用场景 | 并发冲突少,适用于读多写少的场景 | 并发冲突多,适用于读写竞争较多的场景 |
事务阻塞 | 只有在提交时验证,失败时需要回滚 | 操作期间会阻塞其他事务 |
事务失败概率 | 较低,除非提交时发生冲突 | 较高,因为可能出现死锁或锁等待 |
总结
- 乐观锁适合于并发冲突较少的场景,因为它避免了不必要的加锁,能提高系统的并发性,但可能需要处理提交时的冲突。
- 悲观锁适合于并发冲突较多的场景,尤其是对数据的修改频繁,需要确保数据一致性的场景,但由于加锁机制,它会带来性能上的损耗,尤其是在高并发环境中。