前言
MySQL本身不直接提供悲观锁(Pessimistic Locking)和乐观锁(Optimistic Locking)的实现机制,因为这些锁的概念通常是在应用层面通过不同的策略和工具来实现的。然而,我们可以利用MySQL的一些特性来模拟或支持这两种锁的行为。
一、什么是乐观锁和悲观锁?
乐观锁(Optimistic Locking)和悲观锁(Pessimistic Locking)是两种在数据库管理和并发控制中常用的策略,它们以不同的方式处理数据访问和修改时可能发生的冲突。下面将详细描述这两种锁的工作机制。
1. 悲观锁(Pessimistic Locking)
悲观锁基于一种悲观的态度来处理并发问题,它假设数据冲突将会频繁发生,因此在数据被读取的同时就会被锁定,以防止其他事务对数据进行修改,直到当前事务完成。
工作机制:
-
加锁:当事务需要读取或修改某个数据时,它首先会对该数据加锁。在MySQL中,这通常是通过
SELECT ... FOR UPDATE
语句实现的,它会锁定读取的行直到事务结束(提交或回滚)。 -
数据操作:事务在持有锁的情况下对数据进行读取或修改。
-
解锁:当事务提交时,锁会被释放,其他事务才能访问这些数据。如果事务回滚,锁也会被释放,但所做的修改不会保存到数据库中。
-
等待与冲突解决:如果有多个事务试图对同一数据进行加锁,那么后来的事务将会等待,直到前面的事务释放锁。这可能会导致事务的延迟或死锁(如果两个事务相互等待对方释放锁)。
2. 乐观锁(Optimistic Locking)
乐观锁则基于一种乐观的态度来处理并发问题,它假设数据冲突将不会频繁发生,因此在数据读取时不会加锁,而是在更新数据时检查数据是否被其他事务修改过。
工作机制:
-
数据读取:事务首先读取需要修改的数据及其版本号(或时间戳)。这个版本号是在数据库中额外维护的一个字段,用于跟踪数据的更新情况。
-
数据操作:事务在本地对读取的数据进行修改,但不立即更新到数据库中。
-
提交前的检查:在提交事务之前,事务会再次查询数据库中对应数据的版本号,并将其与第一步读取的版本号进行比较。
-
数据更新:
- 如果版本号相同,说明数据在读取和提交之间没有被其他事务修改过,事务可以安全地更新数据,并将版本号增加(或更新时间戳)。
- 如果版本号不同,说明数据在读取和提交之间已经被其他事务修改过,此时当前事务可以选择重试(重新读取数据并尝试更新)、回滚或向应用层报告错误。
-
提交事务:如果数据更新成功,事务将提交,所做的修改将被保存到数据库中。
优点与缺点:
- 悲观锁的优点是能够在很大程度上避免数据冲突,但缺点是可能会导致性能问题,因为事务在持有锁期间会阻塞其他事务对数据的访问。
- 乐观锁的优点是避免了锁的开销,提高了系统的并发性能,但缺点是当冲突发生时,可能需要事务重试,这可能会增加事务的延迟和复杂性。
在选择使用哪种锁时,需要根据具体的应用场景和性能需求进行权衡。
二、应用实践
悲观锁(Pessimistic Locking)
前面我们说到它的工作机制,悲观锁是假定冲突将频繁发生,因此在操作数据之前先锁定数据。在MySQL中,悲观锁可以通过行级锁(InnoDB存储引擎提供)或表级锁(MyISAM或InnoDB在某些情况下)来实现。
应用实践
-
使用SELECT … FOR UPDATE
这是最常用的悲观锁实现方式。当事务使用
SELECT ... FOR UPDATE
语句读取记录时,MySQL会对这些记录加锁,其他事务必须等待锁释放才能修改这些记录。START TRANSACTION; SELECT * FROM accounts WHERE id = 100 FOR UPDATE; -- 在这里进行更新操作 UPDATE accounts SET balance = balance - 100 WHERE id = 100; COMMIT;
注意:
FOR UPDATE
必须在一个事务中使用,否则会立即释放锁。 -
使用表锁
虽然表锁不是悲观锁的最佳实践(因为它会锁定整个表),但在某些情况下仍然可以使用。通过
LOCK TABLES
和UNLOCK TABLES
命令可以显式地锁定和解锁表。LOCK TABLES accounts WRITE; -- 在这里进行更新操作 UPDATE accounts SET balance = balance - 100 WHERE id = 100; UNLOCK TABLES;
乐观锁(Optimistic Locking)
乐观锁假定冲突不会经常发生,因此只在提交更新时检查数据是否被其他事务修改过。这通常通过版本号(version number)或时间戳(timestamp)来实现。
应用实践
-
使用版本号
在数据库表中添加一个
version
字段,每次更新数据时增加版本号。在更新时,检查版本号是否匹配,如果不匹配则拒绝更新。-- 假设version是表中的一个字段 UPDATE accounts SET balance = balance - 100, version = version + 1 WHERE id = 100 AND version = ?;
在应用中,你需要先查询当前记录的版本号,然后在更新时提交这个版本号。如果更新影响的行数为0,则表示数据在查询和更新之间被其他事务修改了。
-
使用时间戳
类似于版本号,但使用时间戳字段来跟踪记录的最后一次更新时间。
UPDATE accounts SET balance = balance - 100, last_updated = NOW() WHERE id = 100 AND last_updated = ?;
同样,你需要先查询记录的
last_updated
时间,然后在更新时提交这个时间戳。
应用场景
乐观锁和悲观锁在数据库管理和并发控制中各有其适用的应用场景。以下是两者之间的应用场景的详细列举:
乐观锁的应用场景
-
读多写少的场景:
- 在这种场景下,多个事务主要进行数据的读取操作,而写操作相对较少。使用乐观锁可以减少锁的开销,提高系统的并发性能。例如,新闻网站中用户同时浏览新闻的场景,新闻内容被修改的概率较低,适合使用乐观锁。
-
偶尔冲突且回滚成本较低的场景:
- 当数据冲突不频繁,且回滚事务的成本低于读取数据时锁定数据的成本时,使用乐观锁可以获得更高的吞吐量。例如,在电商网站的购物车操作中,虽然多个用户可能同时操作购物车,但购物车内容被同时修改的概率较低,即使发生冲突,回滚购物车操作的成本也相对较低。
-
数据版本控制:
- 在需要保证数据版本一致性的场景下,可以使用乐观锁来控制数据的更新操作。通过版本号或时间戳来跟踪数据的更新情况,确保在更新数据时数据的一致性。
悲观锁的应用场景
-
写多读少的场景:
- 在这种场景下,多个事务主要进行数据的修改操作,而读取操作相对较少。使用悲观锁可以确保数据在修改过程中不会被其他事务干扰,保证数据的一致性。例如,在电商网站的库存扣减操作中,由于库存数据需要频繁更新,且更新操作对数据的准确性要求极高,因此适合使用悲观锁。
-
并发冲突较高的场景:
- 当多个事务同时对同一数据进行读写操作时,使用悲观锁可以避免数据冲突和更新丢失的问题。通过锁定数据,确保在事务完成之前其他事务无法修改该数据。例如,在银行系统的账户转账操作中,由于涉及到多个账户的金额变动,且这些变动必须同时成功或同时失败,因此需要使用悲观锁来确保数据的一致性。
-
数据一致性要求极高的场景:
- 在对数据一致性要求极高的场景下,如金融交易、医疗记录等,使用悲观锁可以确保数据在处理过程中不会被其他事务干扰,从而避免数据不一致的问题。
综上所述,乐观锁和悲观锁各有其适用的应用场景。在选择使用哪种锁时,需要根据具体的应用场景、性能需求和数据一致性要求来进行权衡和选择。
总结
选择悲观锁还是乐观锁取决于你的应用场景。悲观锁适合写操作频繁的场景,因为它可以减少数据冲突,但可能会增加等待时间和锁的竞争。乐观锁适合读多写少的场景,它减少了锁的开销,但在高并发写的情况下可能需要处理更多的冲突。
在实际应用中,还需要考虑事务的隔离级别、锁的粒度(行级锁、表级锁)以及应用的具体需求来选择合适的锁策略。