详谈【悲观锁】和【乐观锁】的理解

悲观锁和乐观锁是两种常见的并发控制策略,它们用于解决多线程环境下数据的一致性问题。以下是两者的详细介绍和比较:

悲观锁 (Pessimistic Lock)

概念: 悲观锁假设所有事务都会冲突,因此对数据进行加锁以防止其他事务访问。它在读取或写入数据之前,先获取锁,确保此期间没有其他事务能够修改该数据。
使用场景:

  • 数据竞争激烈的环境。
  • 读多写少的场景。
  • 数据一致性要求极高的场景。

实现方式:

  • 数据库级别:通过数据库的锁机制,如行锁、表锁等。例如,使用 SQL 语句 SELECT … FOR UPDATE 来加锁。
  • 应用程序级别:通过同步机制,如 Java 的 synchronized 关键字或 ReentrantLock 类。

优点:

  • 简单易理解。
  • 能够在高并发环境中确保数据的一致性。

缺点:

  • 可能导致死锁。
  • 性能开销较大,因为锁的获取和释放需要额外的时间。
  • 并发性能差,因为其他事务必须等待锁的释放。

乐观锁 (Optimistic Lock)

概念: 乐观锁假设数据冲突的概率较低,因此不会在操作前加锁,而是在提交数据时检查数据是否被其他事务修改。如果数据未被修改,则提交成功;否则,回滚并重试。
使用场景:

  • 数据竞争不激烈的环境。
  • 读多写少的场景。
  • 对性能要求较高的场景。

实现方式:

  • 版本号机制:在数据表中添加一个版本号字段,每次更新时检查和更新版本号。
  • 时间戳机制:在数据表中添加一个时间戳字段,每次更新时检查和更新时间戳。

典型实现:

  1. 读取数据:读取数据的同时,读取当前的版本号(或时间戳)。
  2. 更新数据:提交更新时,检查数据库中的当前版本号(或时间戳)是否与刚才读取的一致。
    • 如果一致,说明数据没有被其他事务修改,可以提交更新,同时更新版本号(或时间戳)。
    • 如果不一致,说明数据已经被其他事务修改,拒绝本次更新,并可以选择重试或报错。

优点:

  • 无需加锁,性能较高。
  • 避免了死锁的风险。

缺点:

  • 适用于冲突较少的场景,不适合高并发写操作的环境。
  • 需要额外的字段来维护版本号或时间戳。
  • 可能会导致频繁的重试,影响性能。

比较

特性悲观锁乐观锁
假设假设会发生冲突假设不会发生冲突
加锁时间操作前加锁提交时检查
实现复杂度较简单较复杂
并发性能较差较好
死锁风险
适用场景数据竞争激烈,高一致性要求数据竞争不激烈,性能要求高

具体示例

悲观锁示例(SQL)
BEGIN;
SELECT * FROM account WHERE id = 1 FOR UPDATE;
-- 进行更新操作
UPDATE account SET balance = balance - 100 WHERE id = 1;
COMMIT;

在这个示例中,我们使用了悲观锁来确保数据的一致性。具体步骤如下:

  1. 开启事务
BEGIN;

这一行代码开始一个新的事务。在事务提交或回滚之前,所有的操作都将在同一个事务上下文中执行。

  1. 加锁读取数据
SELECT * FROM account WHERE id = 1 FOR UPDATE;

这一行代码从 account 表中读取 id 为 1 的记录,并对这行记录加上排他锁(Exclusive Lock)。这样在当前事务结束之前,其他事务无法读取或修改这条记录。
FOR UPDATE 子句确保在读取数据的同时对其加锁,从而防止其他事务对这一行数据进行任何修改。这种方式可以确保后续的更新操作是安全的,没有并发修改的风险。

  1. 更新操作
UPDATE account SET balance = balance - 100 WHERE id = 1;

这一行代码将 id 为 1 的账户余额减少 100。由于我们已经通过 SELECT … FOR UPDATE 对这行数据加了锁,可以保证这个更新操作在安全的环境下进行,不会与其他并发事务冲突。

  1. 提交事务
COMMIT;

这一行代码提交当前事务,将所有更改永久保存到数据库中。如果在事务执行过程中没有发生错误,提交事务后锁将被释放,其他事务就可以访问和修改这条记录了。

注意事项

  • 锁粒度:FOR UPDATE 语句会对读取的行数据加排他锁,这种锁的粒度较细,只锁定需要更新的行,从而尽量减少对其他行的影响。如果需要锁定更多的数据,可以考虑表锁等更高级别的锁。
  • 死锁风险:悲观锁的使用可能会导致死锁,因此在使用它时需要特别注意事务的顺序和锁的获取顺序,避免多个事务之间相互等待的情况。
  • 性能开销:悲观锁会阻止其他事务访问被锁定的数据,可能会降低系统的并发性能。因此,应该根据实际应用场景权衡性能和数据一致性要求,选择合适的并发控制策略。

示例的实际应用场景

假设在一个银行系统中,用户A要从他们的账户中取出100元。你希望确保在读取账户余额和更新账户余额之间,不会有其他事务修改这个账户的余额。悲观锁可以确保在整个事务过程中,账户余额不会被其他事务修改,从而保证了数据的一致性和正确性。

乐观锁示例(Java)

假设 Account 类有一个 version 字段:

public void updateAccountBalance(Long accountId, BigDecimal amount) {
    Account account = accountMapper.selectById(accountId);
    BigDecimal newBalance = account.getBalance().subtract(amount);
    int updatedRows = accountMapper.updateBalance(accountId, newBalance, account.getVersion());
    
    if (updatedRows == 0) {
        throw new RuntimeException("Update failed due to concurrent modification");
    }
}

对应的 SQL:

UPDATE account 
SET balance = ?, version = version + 1 
WHERE id = ? AND version = ?

这条 SQL 语句展示了乐观锁的一种实现方式,具体来说是通过版本号机制来控制并发。乐观锁假设数据冲突的概率较低,因此在提交数据时检查是否发生冲突,如果有冲突则回滚并重试。让我们详细解析这条 SQL 语句:

UPDATE account 
SET balance = ?, version = version + 1 
WHERE id = ? AND version = ?

语句解析

  1. 更新语句
UPDATE account 
SET balance = ?, version = version + 1 
WHERE id = ? AND version = ?
  1. SET 子句
SET balance = ?, version = version + 1
  • balance = ?:表示要更新的余额值。在实际执行时,这个问号会被具体的数值替换。
  • version = version + 1:表示版本号加一。每次更新数据时,版本号都会增加,以此来记录数据的变化次数。
  1. WHERE 子句
WHERE id = ? AND version = ?
  • id = ?:表示要更新的账户ID。在实际执行时,这个问号会被具体的账户ID替换。
  • version = ?:表示当前的数据版本号。在实际执行时,这个问号会被读取时的版本号替换。

工作原理

  1. 读取数据:首先,从数据库中读取目标记录,包括其 balance 和 version 字段。例如,假设我们要读取的账户ID为1,初始余额为1000,版本号为1。
SELECT balance, version FROM account WHERE id = 1;
-- 返回 1000 和 1
  1. 进行业务逻辑处理:在应用程序中进行相应的业务处理,例如将余额减少100。
  2. 提交更新:在更新数据时,使用读取时的版本号进行条件检查。如果记录的版本号没有变化(即没有其他事务修改过),则更新成功;如果版本号已经变化,则说明有其他事务修改过数据,这时更新操作会失败。
UPDATE account 
SET balance = 900, version = version + 1 
WHERE id = 1 AND version = 1;
  1. 检查更新结果
    • 如果更新成功,SQL 语句会返回受影响的行数为1,表示更新成功,数据一致性得到保证。
    • 如果更新失败(即返回受影响的行数为0),说明在读取数据和提交更新之间,数据已经被其他事务修改过,此时需要重新读取数据并重试更新操作。

优点和缺点

优点:

  • 高并发性能:没有实际加锁,避免了悲观锁带来的阻塞问题,提高并发性能。
  • 无死锁风险:由于不涉及实际的锁机制,避免了死锁的问题。

缺点:

  • 重试开销:在数据竞争较为激烈的场景下,可能会导致频繁的重试,影响性能。
  • 实现复杂:需要在表结构中加入版本号字段,并且在应用程序逻辑中处理更新失败的情况。

实际应用场景

乐观锁特别适合于以下场景:

  • 读多写少:例如,用户查询远多于更新的系统,如电商网站的商品浏览。
  • 数据冲突少:例如,分布式系统中不同节点处理各自的数据分片,冲突概率较低。
  • 性能要求高:例如,金融系统中需要快速响应的交易处理。

通过乐观锁,可以在保持数据一致性的同时,最大限度地提高系统的并发性能。

  • 18
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
静态库和动态库在编译和运行时的行为有所不同。静态库在程序编译时会被连接到目标代码中,而动态库则是在程序运行时才被载入。 静态库对应的lib文件叫做静态库,而动态库对应的lib文件叫做导入库。静态库本身包含了实际执行代码、符号表等信息,而导入库只包含了地址符号表等,用于确保程序能够找到对应函数的基本地址信息。\[1\] 静态库的大小通常比较大,因为它包含了实际执行代码和其他必要的信息。而动态库的大小相对较小,因为它只包含了地址符号表等基本信息。\[1\] 使用静态库的程序在编译时会将静态库的代码复制到最终的可执行文件中,因此可执行文件会比较大。而使用动态库的程序在编译时只会包含对动态库的引用,而不会将动态库的代码复制到可执行文件中。这样可以减小可执行文件的大小,并且多个应用程序可以共享同一个动态库的实例,避免了空间浪费。\[3\] 另外,动态库的使用还解决了静态库对程序的更新、部署和发布带来的麻烦。当需要更新动态库时,只需要替换动态库文件即可,而不需要重新编译整个程序。这样可以实现增量更新,方便程序的维护和升级。\[3\] 综上所述,静态库和动态库的主要区别在于编译时和运行时的行为不同,静态库在编译时被连接到目标代码中,而动态库在程序运行时才被载入。此外,静态库会增加可执行文件的大小,而动态库可以实现共享和增量更新。 #### 引用[.reference_title] - *1* *2* [静态库和动态库的区别](https://blog.csdn.net/sinat_16643223/article/details/114027857)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [详谈静态库和动态库的区别](https://blog.csdn.net/weixin_71478434/article/details/126588174)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值