乐观锁与悲观锁是什么?

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云原生数据库 PolarDB MySQL 版,通用型 2核8GB 50GB
简介: 本文详细分析了悲观锁和乐观锁的原理、区别、实现方式及应用场景。悲观锁假设冲突频繁,通过加锁保护数据一致性,适用于高并发冲突场景;乐观锁假设冲突较少,通过版本号或时间戳检测冲突,适用于读多写少场景。文章通过具体示例展示了两种锁机制的实现过程,并总结了其优缺点和适用场景,帮助读者根据实际需求选择合适的并发控制机制。

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

悲观锁

定义

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

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

  • 数据库:在数据库中,悲观锁通常通过SQL语句实现,例如SELECT ... FOR UPDATE
  • 编程语言:在编程语言中,悲观锁可以使用互斥锁(Mutex)或同步块(Synchronized Block)来实现。

应用场景

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

优缺点

  • 优点:可以完全避免并发冲突,保证数据的一致性和完整性。
  • 缺点:由于每次访问数据都需要加锁和解锁,会导致性能开销较大,特别是在并发量高的情况下,容易造成锁竞争和死锁问题。

示例

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

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

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

  1. 获取账户信息并锁定记录(SELECT ... FOR UPDATE)。
  2. 计算新的余额。
  3. 更新账户信息。
  4. 由于使用了@Transactional注解,整个方法执行在一个事务中,确保在事务提交之前,锁定的记录不会被其他事务修改。

数据库表结构

sql

代码解读

复制代码

CREATE TABLE Account (
    id INT PRIMARY KEY,
    balance DECIMAL(10, 2) NOT NULL
);

Java实现示例

1. Account类

java

代码解读

复制代码

public class Account {
    private int id;
    private BigDecimal balance;

    // Getters and Setters
}

2. AccountMapper接口

java

代码解读

复制代码

public interface AccountMapper {
    Account getAccountByIdForUpdate(int id);
    void updateAccount(Account account);
}

3. AccountMapper的SQL实现

xml

代码解读

复制代码

<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>

4. AccountService类

java

代码解读

复制代码

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);
    }
}

示例说明:

  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. 尝试更新账户信息,如果版本号匹配则更新成功,否则更新失败并抛出异常。

数据库表结构

sql

代码解读

复制代码

CREATE TABLE Account (
    id INT PRIMARY KEY,
    balance DECIMAL(10, 2) NOT NULL,
    version INT NOT NULL
);

Java实现示例

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");
        }
    }
}

示例说明:

  1. Account类:包含账户ID、余额和版本号的Java类。
  2. AccountMapper接口:定义了获取账户信息和更新账户信息的方法。
  3. AccountMapper的SQL实现:使用MyBatis或其他ORM框架,定义了SQL查询和更新语句。注意在更新语句中使用了旧版本号来检测并发修改。
  4. AccountService类:业务逻辑类,在更新账户余额时,先获取当前账户信息及其版本号,然后尝试更新余额和版本号。如果更新失败,抛出一个OptimisticLockException

区别总结

1. 假设前提

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

2.性能

  • 悲观锁性能较低,因为每次操作都需要加锁和解锁。
  • 乐观锁性能较高,但在高并发写操作下可能会频繁重试,影响性能。

3.应用场景

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

总结

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


转载来源:https://juejin.cn/post/7413950416072310796


相关文章
|
消息中间件 安全 Java
什么是乐观锁、在哪用过乐观锁
什么是乐观锁、在哪用过乐观锁
449 0
|
存储 算法 NoSQL
还分不清 Cookie、Session、Token、JWT?看这一篇就够了
Cookie、Session、Token 和 JWT(JSON Web Token)都是用于在网络应用中进行身份验证和状态管理的机制。虽然它们有一些相似之处,但在实际应用中有着不同的作用和特点,接下来就让我们一起看看吧,本文转载至http://juejin.im/post/5e055d9ef265da33997a42cc
48440 13
|
存储 缓存 NoSQL
MySQL索引详解(一文搞懂)
索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。
49794 17
MySQL索引详解(一文搞懂)
|
监控 负载均衡 Java
5 大 SpringCloud 核心组件详解,8 张图彻底弄懂
本文图文详解 Spring Cloud 的五大核心组件,帮助深入理解和掌握微服务架构。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
5 大 SpringCloud 核心组件详解,8 张图彻底弄懂
|
XML Java 开发者
深入解析 Spring 和 Spring Boot 的区别
深入解析 Spring 和 Spring Boot 的区别
|
存储 缓存 安全
ConcurrentHashMap的实现原理,非常详细,一文吃透!
本文详细解析了ConcurrentHashMap的实现原理,深入探讨了分段锁、CAS操作和红黑树等关键技术,帮助全面理解ConcurrentHashMap的并发机制。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
ConcurrentHashMap的实现原理,非常详细,一文吃透!
|
11月前
|
缓存 JSON NoSQL
为什么是删除缓存,而不是更新缓存?
本文介绍了数据库与缓存一致性的常见方案——Cache-Aside Pattern(旁路缓存模式),并分析了其工作流程及优势。该模式通过应用程序显式管理缓存,确保数据一致性。文章详细探讨了删除缓存而非更新缓存的原因,包括避免数据不一致、简化操作、减少并发问题及提高性能。删除缓存能有效保证下次请求获取最新数据,尤其在高并发场景下,确保系统的简单性和可靠性。
549 0
|
Java 编译器 Spring
面试突击78:@Autowired 和 @Resource 有什么区别?
面试突击78:@Autowired 和 @Resource 有什么区别?
16284 6
|
关系型数据库 MySQL 数据处理
一文彻底理解乐观锁与悲观锁
一文彻底理解乐观锁与悲观锁
1359 0
|
SQL 存储 缓存
老司机总结的12条 SQL 优化方案(非常实用)(一)
老司机总结的12条 SQL 优化方案(非常实用)
老司机总结的12条 SQL 优化方案(非常实用)(一)