乐观锁和悲观锁,如何区分?
悲观锁和乐观锁是两种常见的并发控制机制,用于处理多线程或多进程环境中的数据访问冲突问题。它们在数据库系统、分布式系统和多线程编程中都有广泛应用。本文将分析它们的原理、实现以及适用场景。
1. 悲观锁
1.1 定义
悲观锁(Pessimistic Lock)假设数据的访问会经常发生冲突,因此每次操作数据时,都会先对数据加锁,直到操作完成后才释放锁。在锁持有期间,其他线程无法访问这段数据,保证操作的独占性。
1.2 实现方式
- 数据库中:悲观锁通常通过SQL语句实现,如
SELECT ... FOR UPDATE
。 - 编程语言中:可以通过互斥锁(Mutex)或同步块(Synchronized Block)来实现悲观锁。
1.3 应用场景
悲观锁适用于并发冲突较多且需要严格保证数据一致性的场景,例如银行转账、库存扣减等操作。
1.4 优缺点
- 优点:完全避免并发冲突,保证数据一致性和完整性。
- 缺点:由于需要频繁加锁和解锁,性能开销较大,容易产生锁竞争和死锁问题。
1.5 示例
以下是使用Java和MySQL的悲观锁示例。假设有一个银行账户表,使用悲观锁确保在更新余额时不会发生并发修改。
1.5.1 数据库表结构
CREATE TABLE Account (
id INT PRIMARY KEY,
balance DECIMAL(10, 2) NOT NULL
);
1.5.2 Java实现
// Account类
public class Account {
private int id;
private BigDecimal balance;
// Getters and Setters
}
// AccountMapper接口
public interface AccountMapper {
Account getAccountByIdForUpdate(int id);
void updateAccount(Account account);
}
// AccountService类
import org.springframework.transaction.annotation.Transactional;
public class AccountService {
private AccountMapper accountMapper;
public AccountService(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
@Transactional
public void updateAccountBalance(int accountId, BigDecimal amount) {
// 获取账户信息并锁定记录
Account account = accountMapper.getAccountByIdForUpdate(accountId);
if (account == null) {
throw new RuntimeException("Account not found");
}
// 更新余额
account.setBalance(account.getBalance().add(amount));
// 更新账户信息
accountMapper.updateAccount(account);
}
}
该示例中,FOR UPDATE
用于锁定查询到的记录,确保操作的排他性。
1.6 注意事项
- 事务管理:悲观锁需要与事务结合使用,锁在事务提交之前不会被释放。
- 死锁风险:需要注意死锁的检测和处理。
- 性能影响:每次加锁、解锁都会带来性能开销,特别是在高并发情况下。
2. 乐观锁
2.1 定义
乐观锁(Optimistic Lock)假设数据的并发冲突较少,不会主动加锁。在更新数据时,乐观锁会检测数据是否被其他线程修改过,如果发生冲突,则会重试操作或报错。
2.2 实现方式
- 版本号机制:每次读取数据时获取版本号,更新数据时检查版本号是否变化,变化则表示数据已被修改。
- 时间戳机制:使用时间戳检测数据是否被其他线程修改,原理类似于版本号机制。
2.3 应用场景
乐观锁适用于读多写少、并发冲突较少的场景,如用户评论系统、社交媒体点赞等。
2.4 优缺点
- 优点:避免了加锁操作,性能较高,适合读多写少的场景。
- 缺点:在高并发写操作场景下,频繁的重试可能影响性能。
2.5 示例
以下是使用乐观锁的Java示例。假设有一个银行账户表,包含账户ID、余额和版本号。
2.5.1 数据库表结构
CREATE TABLE Account (
id INT PRIMARY KEY,
balance DECIMAL(10, 2) NOT NULL,
version INT NOT NULL
);
2.5.2 Java实现
// Account类
public class Account {
private int id;
private BigDecimal balance;
private int version;
// Getters and Setters
}
// AccountMapper接口
public interface AccountMapper {
Account getAccountById(int id);
int updateAccount(Account account);
}
// AccountService类
public class AccountService {
private AccountMapper accountMapper;
public AccountService(AccountMapper accountMapper) {
this.accountMapper = accountMapper;
}
public void updateAccountBalance(int accountId, BigDecimal amount) {
// 获取账户信息
Account account = accountMapper.getAccountById(accountId);
if (account == null) {
throw new RuntimeException("Account not found");
}
// 记录当前版本号
int currentVersion = account.getVersion();
// 更新余额
account.setBalance(account.getBalance().add(amount));
// 更新版本号
account.setVersion(currentVersion + 1);
// 尝试更新账户信息
int updatedRows = accountMapper.updateAccount(account);
if (updatedRows == 0) {
// 更新失败,可能是由于并发修改导致版本号不匹配
throw new OptimisticLockException("Update failed due to concurrent modification");
}
}
}
在此示例中,更新时会检查数据库中的版本号,如果版本号与读取时一致,则更新成功,否则表示有其他线程修改了数据。
3. 区别总结
特性 | 悲观锁 | 乐观锁 |
---|---|---|
假设前提 | 假设冲突频繁发生,需加锁保护 | 假设冲突不频繁,通过版本号或时间戳检测冲突 |
性能 | 性能较低,需频繁加锁解锁 | 性能较高,但高并发写操作下频繁重试影响性能 |
应用场景 | 适用于并发冲突高、数据一致性要求严格的场景 | 适用于并发冲突低、读多写少的场景 |
4. 总结
悲观锁和乐观锁是两种不同的并发控制机制。悲观锁通过加锁避免并发冲突,适合高冲突、高一致性要求的场景;乐观锁假设冲突较少,通过版本号或时间戳检测冲突,适合读多写少的场景。实际应用中,应根据业务场景选择合适的锁机制,以确保性能和数据一致性。