悲观锁和乐观锁是两种处理并发访问的不同策略,它们关注的是在多个线程同时访问共享资源时如何保证数据一致性的问题。
1、悲观锁:
- 定义: 悲观锁的基本思想是,在整个数据处理过程中,将共享资源进行加锁,以防止其他线程的干扰。
- 实现:
通常通过数据库的行锁或者Java中的synchronized关键字来实现。在悲观锁的情境下,线程认为在执行期间其他线程可能会修改共享资源,因此在访问共享资源之前,先获取锁,确保自己是独占资源的。例如 Collections.synchronizedMap 多线程Map用到的就是悲观锁。
2、乐观锁:
- 定义:
乐观锁的基本思想是,在整个数据处理过程中,不对共享资源进行加锁,而是在访问时假设其他线程不会修改共享资源,只有在真正更新时才检查是否有冲突。 - 实现: 乐观锁的实现通常依赖于数据版本控制,通常通过版本号(版本控制)或者CAS(Compare and Swap)等机制来实现。在乐观锁的情境下,线程在读取共享资源时不会加锁,而是在更新时检查是否有其他线程修改过,如果有,则进行相应处理。
- 版本号:每条记录可以包含一个版本号字段。读取记录时记住版本号,在更新记录时检查版本号是否发生变化(比如通过WHERE子句比较版本号)。如果版本号相同,则更新数据并增加版本号;如果不同,说明数据在此期间已被其他事务修改。
- 时间戳:使用时间戳也是一种常见的方式,操作类似于版本号。
乐观锁适用于冲突较少的环境,因为它减少了锁的开销,但在高冲突环境中可能导致大量的重试和失败。
悲观锁: 就好比一个人在使用自动取款机(ATM)时,先取号排队,然后在自己的操作过程中,不让其他人插队或者干扰,确保自己独享ATM资源。
乐观锁:
就好比一个人在自动取款机前,直接去尝试取款,但在取款的时候,会检查自己的操作是否被其他人打扰,如果没有被打扰,就顺利完成取款;如果有冲突,就需要重新尝试或者采取其他措施。
总的来说,悲观锁更加悲观地认为会有冲突,因此提前加锁以保护资源;而乐观锁更加乐观地认为冲突不会经常发生,因此先尝试操作,再在需要的时候进行冲突检测和处理。选择乐观锁还是悲观锁,取决于应用的具体需求、数据访问模式和性能考虑。乐观锁在读多写少的场景中表现较好,而悲观锁在写操作频繁的场景中更能保证数据的一致性。在实际应用中,这两种锁常常根据具体情况和业务需求灵活使用。
3、实现
为了更好地理解乐观锁和悲观锁的具体实现,我们可以使用一个简单的银行账户余额更新作为例子。我们假设有一个名为 accounts
的表,其中包括字段 account_id
(账户标识)、balance
(账户余额)和 version
(版本号,用于乐观锁)。
3.1. 乐观锁实现
在使用乐观锁时,我们通常在表中添加一个 version 字段。每次读取记录时,version 字段也会被读取,并在后续的更新中检查这个版本号是否改变。
-
SQL 表结构
CREATE TABLE accounts ( account_id INT AUTO_INCREMENT PRIMARY KEY, balance DECIMAL(10, 2), version INT DEFAULT 1 );
-
更新操作
在进行更新操作时,检查version
字段确保数据未被修改过,然后进行更新,并将version
字段的值增加1。UPDATE accounts SET balance = balance + 100, -- 假设我们要增加100元 version = version + 1 WHERE account_id = 1 AND version = @CurrentVersion;
@CurrentVersion
是从应用程序传入的,基于最初查询得到的版本号
如果version
不匹配,即另一个事务已经更新了记录,这个更新操作将不会改变任何行,应用程序可以据此知道更新失败,可能需要重新尝试或通知用户。
3.2. 悲观锁实现
在使用悲观锁时,可以利用数据库提供的锁机制(如行锁),确保在当前事务完成之前,其他事务不能修改被锁定的数据。
-
SQL 表结构
-- 使用与乐观锁相同的表结构 CREATE TABLE accounts ( account_id INT AUTO_INCREMENT PRIMARY KEY, balance DECIMAL(10, 2) );
-
更新操作
在查询时使用FOR UPDATE
语句来锁定数据行。这会在当前事务完成之前阻止其他事务修改这些行。START TRANSACTION; SELECT balance FROM accounts WHERE account_id = 1 FOR UPDATE; -- 锁定账户1 -- 执行业务逻辑,比如增加余额 UPDATE accounts SET balance = balance + 100 WHERE account_id = 1; COMMIT;
在这个例子中,
SELECT ... FOR UPDATE
语句将阻止其他任何尝试更新或读取(在需要相同锁的情况下)account_id
为 1 的行的事务,直到当前事务提交或回滚。
乐观锁和悲观锁各有用武之地,具体使用哪种锁机制取决于应用场景和并发级别。乐观锁适用于写操作较少的场景,悲观锁则适用于高冲突环境,尤其是在多用户频繁写入同一数据的场合。