悲观锁和乐观锁是两种常见的并发控制机制,用于处理多线程或多进程环境中的数据访问冲突问题。它们在数据库系统、分布式系统和多线程编程中都有广泛应用。这篇文章我们来分析下他们的原理以及使用场景。
悲观锁(Pessimistic Lock)是一种假设冲突会频繁发生的锁机制。每次数据访问时,都会先加锁,直到操作完成后才释放锁,这样可以确保在锁持有期间,其他线程无法访问这段数据,从而避免了并发冲突。
悲观锁的实现通常有以下两种方式:
SELECT ... FOR UPDATE
。适用于对数据并发冲突非常敏感的场景,例如银行转账操作、库存扣减等需要严格数据一致性的操作。
下面我们用 Java + MySQL 展示了一个悲观锁的具体实现。
假设有一个银行账户表(Account
),包含账户 ID和余额两个字段,我们希望在更新账户余额时使用悲观锁,以确保数据的一致性。
整个运行流程分为以下4个步骤:
SELECT ... FOR UPDATE
)。@Transactional
注解,整个方法执行在一个事务中,确保在事务提交之前,锁定的记录不会被其他事务修改。 代码解读复制代码CREATE TABLE Account (
id INT PRIMARY KEY,
balance DECIMAL(10, 2) NOT NULL
);
代码解读复制代码public class Account {
private int id;
private BigDecimal balance;
// Getters and Setters
}
代码解读复制代码public interface AccountMapper {
Account getAccountByIdForUpdate(int id);
void updateAccount(Account account);
}
代码解读复制代码<mapper namespace="com.example.AccountMapper">
<select id="getAccountByIdForUpdate" resultType="com.example.Account">
SELECT id, balance FROM Account WHERE id = #{id} FOR UPDATE
</select>
<update id="updateAccount">
UPDATE Account
SET balance = #{balance}
WHERE id = #{id}
</update>
</mapper>
代码解读复制代码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
来锁定记录。这种机制确保了在操作完成之前,其他线程无法修改锁定的记录,从而实现了悲观锁的并发控制。
通过了解悲观锁的具体实现,可以在需要严格数据一致性的场景中有效地避免并发冲突。
乐观锁(Optimistic Lock)是一种假设冲突不会频繁发生的锁机制。每次数据访问时,不会加锁,而是在更新数据时检查是否有其他线程修改过数据。如果检测到冲突(数据被其他线程修改过),则重试操作或报错。
乐观锁通常实现方式有以下两种:
适用于读多写少的场景,例如用户评论系统、社交媒体点赞等,这些场景下并发冲突概率较低。
乐观锁的实现通常涉及到版本号(或时间戳)机制,以便在更新数据时检测是否发生了并发修改。 我们还是用上面的示例,展示了如何在 Java中使用乐观锁进行并发控制。
假设有一个银行账户表(Account
),包含账户ID、余额和版本号三个字段,现在希望在更新账户余额时使用乐观锁,以确保数据的一致性。
整个运行流程总结为下面 3个步骤:
代码解读复制代码CREATE TABLE Account (
id INT PRIMARY KEY,
balance DECIMAL(10, 2) NOT NULL,
version INT NOT NULL
);
1. Account类java
代码解读复制代码public class Account {
private int id;
private BigDecimal balance;
private int version;
// Getters and Setters
}
2. AccountMapper接口java
代码解读复制代码public interface AccountMapper {
Account getAccountById(int id);
int updateAccount(Account account);
}
3. AccountMapper的SQL实现xml
代码解读复制代码<mapper namespace="com.example.AccountMapper">
<select id="getAccountById" resultType="com.example.Account">
SELECT id, balance, version FROM Account WHERE id = #{id}
</select>
<update id="updateAccount">
UPDATE Account
SET balance = #{balance}, version = #{version}
WHERE id = #{id} AND version = #{oldVersion}
</update>
</mapper>
4. AccountService类java
代码解读复制代码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");
}
}
}
示例说明:
OptimisticLockException
。1. 假设前提:
2.性能:
3.应用场景:
本文我们详细分析了悲观锁和乐观锁的原理、区别、实现方式和应用场景,实际工作中,可以根据具体需求选择合适的并发控制机制,以保证系统的性能和数据一致性。