开门见山,在选择悲观锁和乐观锁时,需要根据具体的应用场景进行考虑:

  • 悲观锁适用于对数据的修改操作频繁,且并发冲突较为频繁的场景。
  • 乐观锁适用于对数据的读操作频繁,冲突较少发生的场景

锁的概念

首先了解两种锁的区别是什么

悲观锁 

悲观锁是一种保守的锁策略,它假设在整个数据访问过程中会发生冲突,因此在访问数据之前会先获取锁。悲观锁主要用于对数据的修改操作,它确保在一个事务中只有一个线程能够访问和修改数据,其他线程必须等待锁的释放。悲观锁的特点是阻塞其他线程的访问,从而保证数据的一致性。

乐观锁

乐观锁是一种乐观的锁策略,它假设在整个数据访问过程中不会发生冲突,因此不会立即获取锁。乐观锁主要基于版本号或时间戳来实现,每个数据记录都会有一个版本号或时间戳,当要更新数据时,会比较当前版本号或时间戳与更新前获取的版本号或时间戳是否一致,如果一致则可以更新,否则表示数据已被其他线程修改,需要进行相应的处理(如回滚或重试)。乐观锁的特点是不会阻塞其他线程的访问,通过检测冲突并处理冲突来保证数据的一致性。

参考

对于锁机制的评论以及这两种锁选型在DBMS中的运用,我们可以看一个stackoverflow中的高赞回答

翻译并提炼

1. 乐观锁是一种策略,读取记录时记录版本号(也可以使用日期、时间戳或校验和/哈希等方法),在写回记录之前检查版本号是否变化。在写回记录时,通过版本号进行过滤更新,确保原子性(即在检查版本号和将记录写入磁盘之间没有被更新),并一次性更新版本号。如果记录已变更(即版本号与当前不同),则终止事务,用户可以重新开始。

2. 乐观锁适用于高并发系统和三层架构,其中可能没有保持与数据库的连接。在这种情况下,客户端无法维护数据库锁定,因为连接是从连接池获取的,可能在不同访问之间使用的是不同的连接。

3. 悲观锁是在使用期间独占地锁定记录,具有比乐观锁更好的完整性,但需要在应用程序设计中小心避免死锁。要使用悲观锁,需要直接与数据库建立连接(如在两层客户端服务器应用程序中通常是这样),或者有一个可外部访问的事务ID,可以独立于连接使用。后一种情况下,可以使用TxID打开事务,然后使用该ID重新连接。数据库管理系统维护锁,并允许通过TxID恢复会话。这就是使用两阶段提交协议(如XA或COM+事务)的分布式事务的工作原理。

开发中的应用

Java

乐观锁

Java中的乐观锁可以通过使用版本号或时间戳实现。常见的实现方式包括:

  • 版本号实现:在数据记录中添加一个版本号字段,每次更新时将版本号加1,更新时比较版本号是否一致。
  • 时间戳实现:在数据记录中添加一个时间戳字段,每次更新时更新时间戳,更新时比较时间戳是否一致。

悲观锁

Java中的悲观锁可以使用synchronized关键字或Lock接口的实现类(如ReentrantLock)来实现。这些锁机制会在访问数据前先获取锁,并在操作完成后释放锁,确保同一时刻只有一个线程能够访问数据。

代码实战

下面以热门文章排行榜点赞为技术背景来给出Java的乐观锁和悲观锁的实现代码和分析。

乐观锁实现
public class Article {
    private int id;
    private String title;
    private int likes;
    private int version;

    // getters and setters

    public void like() {
        // 获取当前版本号
        int currentVersion = getVersion();

        // 模拟并发冲突,假设有其他线程对文章进行了更新
        updateArticleFromDatabase();

        // 更新前的版本号与当前版本号比较
        if (currentVersion == getVersion()) {
            // 版本号一致,可以进行更新更新点赞数
            setLikes(getLikes() + 1);
            setVersion(getVersion() + 1);
            saveArticleToDatabase();
        } else {
            // 版本号不一致,表示数据已被其他线程修改,需要进行相应处理
            // 可以选择回滚或重试等策略
        }
    }

    private void updateArticleFromDatabase() {
        // 从数据库中获取最新的文章信息,更新当前对象的属性
    }

    private void saveArticleToDatabase() {
        // 将更新后的文章信息保存到数据库
    }
}

每个文章对象都有一个版本号属性(version),在点赞操作时,先获取当前版本号,然后模拟并发冲突,假设有其他线程对文章进行了更新。接着,再次比较获取的版本号与当前版本号是否一致,如果一致,则进行点赞操作,并更新版本号,最后将更新后的文章信息保存到数据库。如果版本号不一致,表示数据已被其他线程修改,可以选择回滚或重试等策略。

悲观锁实现
public class Article {
    private int id;
    private String title;
    private int likes;

    // getters and setters

    public synchronized void like() {
        // 对文章进行点赞操作
        setLikes(getLikes() + 1);
        saveArticleToDatabase();
    }

    private void saveArticleToDatabase() {
        // 将更新后的文章信息保存到数据库
    }
}

通过使用synchronized关键字修饰点赞方法,确保在同一时刻只有一个线程能够访问和修改文章的点赞数。其他线程在访问该方法时会被阻塞,直到锁被释放。

MySQL

乐观锁

乐观锁在MySQL中通常使用行版本号(Row Versioning)或时间戳来实现。MySQL提供了多个实现乐观锁的机制,如MVCC(Multi-Version Concurrency Control)和CAS(Compare and Set)等。乐观是即使在较弱的隔离级别(读已提交)或者在后续数据库事务中继续执行读取和写入操作时也能正常工作。但我们不能忽视其缺点,如果数据库访问框架捕获到乐观锁异常(OptimisticLockException),会触发回滚操作,因此会丢失当前执行事务之前所做的所有工作。

乐观锁适用于以下场景:

  • 当需要对数据进行更新操作时,可以在更新语句中使用版本号或时间戳的判断,如果更新前后的版本号或时间戳一致,表示数据未被其他事务修改,可以进行更新操作。
  • 当需要读取数据时,乐观锁不会阻塞其他事务的修改操作,因此可以在读取数据时使用乐观锁,通过比较版本号或时间戳来判断数据是否发生了修改。

悲观锁

悲观锁在MySQL中通常使用SELECT ... FOR UPDATE语句来实现。当执行SELECT ... FOR UPDATE语句时,MySQL会为选定的行添加锁,确保其他事务无法修改或读取这些行,从而实现悲观锁的效果。悲观锁适用于以下场景:

  • 当需要对数据进行更新操作时,可以在事务中使用SELECT ... FOR UPDATE语句获取悲观锁,确保在事务提交之前其他事务无法修改数据。
  • 当需要读取数据时,如果对数据的一致性要求较高,也可以使用SELECT ... FOR UPDATE语句获取悲观锁,阻塞其他事务对数据的修改操作。

注意,我们在使用MySQL时,可选择的锁是很多的,如表锁、行级锁、间隙锁等,可以根据具体的需求选择合适的锁机制来实现并发控制。

11-14 14:39