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

2024-11-19 09:36:53 +0800 CST views 637

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

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

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. 总结

悲观锁和乐观锁是两种不同的并发控制机制。悲观锁通过加锁避免并发冲突,适合高冲突、高一致性要求的场景;乐观锁假设冲突较少,通过版本号或时间戳检测冲突,适合读多写少的场景。实际应用中,应根据业务场景选择合适的锁机制,以确保性能和数据一致性。

复制全文 生成海报 并发控制 数据库 多线程编程

推荐文章

API 管理系统售卖系统
2024-11-19 08:54:18 +0800 CST
FcDesigner:低代码表单设计平台
2024-11-19 03:50:18 +0800 CST
Redis函数在PHP中的使用方法
2024-11-19 04:42:21 +0800 CST
Web 端 Office 文件预览工具库
2024-11-18 22:19:16 +0800 CST
mysql 优化指南
2024-11-18 21:01:24 +0800 CST
css模拟了MacBook的外观
2024-11-18 14:07:40 +0800 CST
filecmp,一个Python中非常有用的库
2024-11-19 03:23:11 +0800 CST
使用xshell上传和下载文件
2024-11-18 12:55:11 +0800 CST
Go语言中的mysql数据库操作指南
2024-11-19 03:00:22 +0800 CST
Golang 中你应该知道的 Range 知识
2024-11-19 04:01:21 +0800 CST
nginx反向代理
2024-11-18 20:44:14 +0800 CST
Go配置镜像源代理
2024-11-19 09:10:35 +0800 CST
Rust 高性能 XML 读写库
2024-11-19 07:50:32 +0800 CST
Requests库详细介绍
2024-11-18 05:53:37 +0800 CST
微信小程序热更新
2024-11-18 15:08:49 +0800 CST
PHP 允许跨域的终极解决办法
2024-11-19 08:12:52 +0800 CST
Vue3中如何实现响应式数据?
2024-11-18 10:15:48 +0800 CST
Vue3中如何处理组件间的动画?
2024-11-17 04:54:49 +0800 CST
HTML5的 input:file上传类型控制
2024-11-19 07:29:28 +0800 CST
File 和 Blob 的区别
2024-11-18 23:11:46 +0800 CST
Golang - 使用 GoFakeIt 生成 Mock 数据
2024-11-18 15:51:22 +0800 CST
四舍五入五成双
2024-11-17 05:01:29 +0800 CST
程序员茄子在线接单