乐观锁和悲观锁,如何区分?

目录

悲观锁

应用场景

优缺点

示例

数据库表结构

Java实现示例

1. Account类

2. AccountMapper接口

3. AccountMapper的SQL实现

4. AccountService类

注意事项

乐观锁

定义

应用场景

优缺点

示例

数据库表结构

Java实现示例

1. Account类

2. AccountMapper接口

3. AccountMapper的SQL实现

4. AccountService类

区别总结


悲观锁乐观锁是工作中两种常见的并发控制机制,它们主要用于处理多线程或多进程环境中的数据访问冲突问题,在数据库系统、分布式系统和多线程编程中都有广泛应用。这篇文章,我们来分析悲观锁和乐观锁的原理以及使用场景。

悲观锁


悲观锁(Pessimistic Lock),顾名思义,就是持有悲观状态,假设冲突会频繁发生的锁机制。每次数据访问时,都会先加锁,直到操作完成后才释放锁,这样可以确保在锁持有期间,其他线程无法访问这段数据,从而避免了并发冲突。

悲观锁的实现通常有以下两种方式:

  • 数据库:在数据库中,悲观锁通常通过SQL语句实现,例如SELECT ... FOR UPDATE

  • 编程语言:在编程语言中,悲观锁可以使用互斥锁(Mutex)或同步块(Synchronized Block)来实现。

应用场景

适用于对数据并发冲突非常敏感的场景,例如银行转账操作、库存扣减等需要严格数据一致性的操作。

优缺点

  • 优点:可以完全避免并发冲突,保证数据的一致性和完整性。

  • 缺点:由于每次访问数据都需要加锁和解锁,会导致性能开销较大,特别是在并发量高的情况下,容易造成锁竞争和死锁问题。

示例

下面我们用 Java + MySQL 展示了一个悲观锁的具体实现。

假设有一个银行账户表(Account),包含账户 ID和余额两个字段,我们希望在更新账户余额时使用悲观锁,以确保数据的一致性。

整个运行流程分为以下4个步骤:

  1. 获取账户信息并锁定记录(SELECT ... FOR UPDATE)。

  2. 计算新的余额。

  3. 更新账户信息。

  4. 由于使用了@Transactional注解,整个方法执行在一个事务中,确保在事务提交之前,锁定的记录不会被其他事务修改。

数据库表结构
CREATE TABLE Account (
    id INT PRIMARY KEY,
    balance DECIMAL(10, 2) NOT NULL
);

Java实现示例

1. Account类
public class Account {
    private int id;
    private BigDecimal balance;

    // Getters and Setters
}
2. AccountMapper接口
public interface AccountMapper {
    Account getAccountByIdForUpdate(int id);
    void updateAccount(Account account);
}
3. AccountMapper的SQL实现

    
        SELECT id, balance FROM Account WHERE id = #{id} FOR UPDATE
    

    
        UPDATE Account
        SET balance = #{balance}
        WHERE id = #{id}
    

4. AccountService类
import org.springframework.transaction.annotation.Transactional;

publicclass 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) {
            thrownew RuntimeException("Account not found");
        }

        // 更新余额
        account.setBalance(account.getBalance().add(amount));

        // 更新账户信息
        accountMapper.updateAccount(account);
    }
}

示例说明:

  1. Account类:包含账户ID和余额的Java类。

  2. AccountMapper接口:定义了获取账户信息(带锁定)和更新账户信息的方法。

  3. AccountMapper的SQL实现:使用MyBatis或其他ORM框架,定义了SQL查询和更新语句。注意在查询语句中使用FOR UPDATE来锁定记录。

  4. AccountService类:业务逻辑类,在更新账户余额时,先获取当前账户信息并锁定记录,然后更新余额并提交更新。

这种机制确保了在操作完成之前,其他线程无法修改锁定的记录,从而实现了悲观锁的并发控制。

注意事项

  1. 事务管理:使用悲观锁时,需要确保在事务提交之前锁不会被释放,因此必须在事务中使用。

  2. 死锁风险:悲观锁可能会导致死锁,需要特别注意死锁检测和处理。

  3. 性能影响:由于每次操作都需要加锁和解锁,性能可能会受到影响,特别是在高并发情况下。

通过了解悲观锁的具体实现,可以在需要严格数据一致性的场景中有效地避免并发冲突。

乐观锁


定义

乐观锁(Optimistic Lock)是一种假设冲突不会频繁发生的锁机制。每次数据访问时,不会加锁,而是在更新数据时检查是否有其他线程修改过数据。如果检测到冲突(数据被其他线程修改过),则重试操作或报错。

乐观锁通常实现方式有以下两种:

  • 版本号机制:每次读取数据时,读取一个版本号,更新数据时,检查版本号是否变化,如果没有变化,则更新成功,否则重试。

  • 时间戳机制:类似版本号机制,通过时间戳来检测数据是否被修改。

应用场景

适用于读多写少的场景,例如用户评论系统、社交媒体点赞等,这些场景下并发冲突概率较低。

优缺点

  • 优点:避免了频繁的锁操作,性能较好,适合读多写少的场景。

  • 缺点:在高并发写操作的场景下,重试可能会频繁发生,导致性能下降

示例

乐观锁的实现通常涉及到版本号(或时间戳)机制,以便在更新数据时检测是否发生了并发修改。我们还是用上面的示例,展示了如何在 Java中使用乐观锁进行并发控制。

假设有一个银行账户表(Account),包含账户ID、余额和版本号三个字段,现在希望在更新账户余额时使用乐观锁,以确保数据的一致性。

整个运行流程总结为下面 3个步骤:

  1. 获取账户信息,包括当前的版本号。

  2. 计算新的余额,并增加版本号。

  3. 尝试更新账户信息,如果版本号匹配则更新成功,否则更新失败并抛出异常。

数据库表结构
CREATE TABLE Account (
    id INT PRIMARY KEY,
    balance DECIMAL(10, 2) NOT NULL,
    version INT NOT NULL
);

Java实现示例

1. Account类
public class Account {
    private int id;
    private BigDecimal balance;
    private int version;

    // Getters and Setters
}
2. AccountMapper接口
public interface AccountMapper {
    Account getAccountById(int id);
    int updateAccount(Account account);
}
3. AccountMapper的SQL实现

    
        SELECT id, balance, version FROM Account WHERE id = #{id}
    

    
        UPDATE Account
        SET balance = #{balance}, version = #{version}
        WHERE id = #{id} AND version = #{oldVersion}
    

4. 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) {
            thrownew 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) {
            // 更新失败,可能是由于并发修改导致的版本号不匹配
            thrownew OptimisticLockException("Update failed due to concurrent modification");
        }
    }
}

示例说明:

  1. Account类:包含账户ID、余额和版本号的Java类。

  2. AccountMapper接口:定义了获取账户信息和更新账户信息的方法。

  3. AccountMapper的SQL实现:使用MyBatis或其他ORM框架,定义了SQL查询和更新语句。注意在更新语句中使用了旧版本号来检测并发修改。

  4. AccountService类:业务逻辑类,在更新账户余额时,先获取当前账户信息及其版本号,然后尝试更新余额和版本号。如果更新失败,抛出一个OptimisticLockException

区别总结


1. 假设前提

  • 悲观锁假设冲突会频繁发生,需要加锁保护。

  • 乐观锁假设冲突不会频繁发生,通过版本号或时间戳来检测冲突。

2.性能

  • 悲观锁性能较低,因为每次操作都需要加锁和解锁。

  • 乐观锁性能较高,但在高并发写操作下可能会频繁重试,影响性能。

3.应用场景

  • 悲观锁适用于并发冲突高、数据一致性要求严格的场景。

  • 乐观锁适用于并发冲突低、读多写少的场景。

本文我们详细分析了悲观锁和乐观锁的原理、区别、实现方式和应用场景,实际工作中,可以根据具体需求选择合适的并发控制机制,以保证系统的性能和数据一致性。

乐观锁和悲观锁,如何区分?_第1张图片

你可能感兴趣的:(备战25秋招,java,数据库,开发语言,安全,后端)