自学内容网 自学内容网

MySQL 乐观锁与悲观锁

一、引言

在数据库管理系统中,并发控制是确保多个事务能够正确地执行而不破坏数据完整性的关键技术。MySQL 作为广泛使用的关系型数据库,提供了多种并发控制机制,其中乐观锁和悲观锁是两种常见的方法。本文将深入探讨 MySQL 中的乐观锁和悲观锁的概念、原理、实现方式以及在实际应用中的优缺点,帮助读者更好地理解和运用这两种并发控制技术。

二、悲观锁的概念与原理

(一)定义

悲观锁(Pessimistic Locking)是一种保守的并发控制方法,它假设在事务执行过程中,可能会有其他事务对同一数据进行修改,因此在读取数据时就对数据进行加锁,以防止其他事务的干扰。

(二)原理

  1. 加锁机制
    • 当一个事务要对某行数据进行操作时,它会先向数据库申请对该数据行的锁。如果此时没有其他事务持有该锁,数据库就会为这个事务分配锁,并且其他事务在尝试获取该锁时会被阻塞,直到持有锁的事务释放锁为止。
    • 例如,事务 A 要对表中的一行数据进行修改,它会向数据库申请对该行数据的排他锁(Exclusive Lock)。在事务 A 持有锁期间,其他事务无法对该行数据进行修改或读取(如果是排他锁的话,共享锁也会被阻塞)。
  2. 锁的类型
    • 悲观锁主要有两种类型:共享锁(Shared Lock)和排他锁。
      • 共享锁:多个事务可以同时持有共享锁,用于读取数据。当一个事务持有共享锁时,其他事务可以继续申请共享锁来读取数据,但不能申请排他锁来修改数据。
      • 排他锁:只有一个事务可以持有排他锁,用于修改数据。当一个事务持有排他锁时,其他事务既不能申请共享锁也不能申请排他锁。
  3. 锁的范围
    • 悲观锁可以在不同的级别上进行加锁,包括行级锁、表级锁和页级锁。
      • 行级锁:只对某一行数据进行加锁,锁的粒度最小,并发度最高,但开销也最大。
      • 表级锁:对整个表进行加锁,锁的粒度最大,并发度最低,但开销也最小。
      • 页级锁:对数据库中的一页数据进行加锁,锁的粒度和并发度介于行级锁和表级锁之间。

(三)实现方式

  1. 使用 SQL 语句实现悲观锁
    • 在 MySQL 中,可以使用 SELECT... FOR UPDATE 语句来实现悲观锁。这个语句会在读取数据的同时对数据行加排他锁,其他事务在尝试读取或修改被锁定的数据行时会被阻塞。
    • 例如:
BEGIN TRANSACTION;
-- 对 id 为 1 的数据行加排他锁
SELECT * FROM table_name WHERE id = 1 FOR UPDATE;
-- 对数据行进行修改
UPDATE table_name SET column_name = new_value WHERE id = 1;
COMMIT;

  1. 使用数据库事务实现悲观锁
    • 可以在数据库事务中使用 BEGIN TRANSACTION 和 COMMIT 语句来包裹对数据的操作,并且在事务中使用 SELECT... FOR UPDATE 语句来加锁。这样可以确保在事务执行过程中,数据不会被其他事务修改。
    • 例如:
BEGIN TRANSACTION;
-- 查询数据并加锁
SELECT * FROM table_name WHERE condition FOR UPDATE;
-- 对数据进行操作
UPDATE table_name SET column_name = new_value WHERE condition;
COMMIT;

(四)示例

以下是一个使用悲观锁的示例,假设有一个商品表,包含商品 ID、商品名称和库存数量三个字段。现在有两个事务同时要对库存数量进行减操作,使用悲观锁来确保数据的一致性。

  1. 事务 A
BEGIN TRANSACTION;
-- 对商品 ID 为 1 的商品加排他锁
SELECT * FROM product WHERE id = 1 FOR UPDATE;
-- 查询当前库存数量
SELECT stock FROM product WHERE id = 1;
-- 假设当前库存数量为 10,减去 2
UPDATE product SET stock = stock - 2 WHERE id = 1;
COMMIT;

  1. 事务 B
BEGIN TRANSACTION;
-- 尝试对商品 ID 为 1 的商品加排他锁,但由于事务 A 已经持有了该锁,所以事务 B 会被阻塞
SELECT * FROM product WHERE id = 1 FOR UPDATE;
-- 等待事务 A 释放锁后,查询当前库存数量
SELECT stock FROM product WHERE id = 1;
-- 假设当前库存数量为 8(事务 A 减去了 2),再减去 3
UPDATE product SET stock = stock - 3 WHERE id = 1;
COMMIT;

在这个示例中,事务 A 先对商品 ID 为 1 的商品加排他锁,然后进行库存数量的减操作。事务 B 在尝试对同一商品加排他锁时被阻塞,直到事务 A 释放锁后,事务 B 才能继续执行。这样可以确保在两个事务执行过程中,库存数量不会被错误地修改。

三、乐观锁的概念与原理

(一)定义

乐观锁(Optimistic Locking)是一种相对乐观的并发控制方法,它假设在事务执行过程中,很少会有其他事务对同一数据进行修改,因此在读取数据时不对数据进行加锁,而是在更新数据时检查数据是否被其他事务修改过。如果数据没有被修改,就进行更新;如果数据被修改了,就重新读取数据并再次尝试更新,直到更新成功为止。

(二)原理

  1. 版本号机制
    • 乐观锁通常使用版本号来实现。在数据库表中添加一个版本号字段,每次对数据进行修改时,版本号会自动加 1。当一个事务要更新数据时,它会先读取数据的当前版本号,然后在更新数据时将版本号作为一个条件进行判断。如果版本号没有变化,说明数据没有被其他事务修改过,可以进行更新;如果版本号发生了变化,说明数据被其他事务修改过,需要重新读取数据并再次尝试更新。
    • 例如,表中有一行数据,包含字段 id、name 和 version。初始版本号为 1。事务 A 读取了这行数据,此时版本号为 1。然后事务 B 对这行数据进行了修改,版本号变为 2。当事务 A 要更新数据时,它会检查版本号是否仍然为 1。如果是,就进行更新,并将版本号加 1 变为 2;如果不是,就说明数据被其他事务修改过,需要重新读取数据并再次尝试更新。
  2. 时间戳机制
    • 除了版本号机制,乐观锁还可以使用时间戳来实现。在数据库表中添加一个时间戳字段,每次对数据进行修改时,时间戳会更新为当前时间。当一个事务要更新数据时,它会先读取数据的当前时间戳,然后在更新数据时将时间戳作为一个条件进行判断。如果时间戳没有变化,说明数据没有被其他事务修改过,可以进行更新;如果时间戳发生了变化,说明数据被其他事务修改过,需要重新读取数据并再次尝试更新。
    • 例如,表中有一行数据,包含字段 id、name 和 timestamp。初始时间戳为当前时间。事务 A 读取了这行数据,此时时间戳为某个特定值。然后事务 B 对这行数据进行了修改,时间戳更新为新的时间。当事务 A 要更新数据时,它会检查时间戳是否仍然为之前读取的值。如果是,就进行更新,并将时间戳更新为当前时间;如果不是,就说明数据被其他事务修改过,需要重新读取数据并再次尝试更新。

(三)实现方式

  1. 使用 SQL 语句实现乐观锁
    • 在 MySQL 中,可以使用 WHERE 子句来实现乐观锁。在更新数据时,将版本号或时间戳作为条件进行判断,如果条件满足,就进行更新;如果条件不满足,就说明数据被其他事务修改过,需要重新读取数据并再次尝试更新。
    • 例如,使用版本号实现乐观锁:
-- 读取数据并获取版本号
SELECT id, name, stock, version FROM product WHERE id = 1;
-- 假设当前版本号为 1,库存数量为 10,要减去 2
UPDATE product SET stock = stock - 2, version = version + 1 WHERE id = 1 AND version = 1;

  • 如果更新成功,说明数据没有被其他事务修改过;如果更新失败(影响的行数为 0),说明数据被其他事务修改过,需要重新读取数据并再次尝试更新。

  1. 使用数据库事务实现乐观锁
    • 可以在数据库事务中使用 BEGIN TRANSACTION 和 COMMIT 语句来包裹对数据的操作,并且在事务中使用 SELECT 和 UPDATE 语句来实现乐观锁。这样可以确保在事务执行过程中,数据不会被其他事务修改。
    • 例如:
BEGIN TRANSACTION;
-- 查询数据并获取版本号
SELECT id, name, stock, version FROM product WHERE id = 1;
-- 对数据进行操作
-- 假设当前版本号为 1,库存数量为 10,要减去 2
UPDATE product SET stock = stock - 2, version = version + 1 WHERE id = 1 AND version = 1;
COMMIT;

  • 如果更新成功,说明数据没有被其他事务修改过;如果更新失败(影响的行数为 0),说明数据被其他事务修改过,需要重新读取数据并再次尝试更新。

(四)示例

以下是一个使用乐观锁的示例,假设有一个订单表,包含订单 ID、订单状态和版本号三个字段。现在有两个事务同时要对订单状态进行修改,使用乐观锁来确保数据的一致性。

  1. 事务 A
BEGIN TRANSACTION;
-- 读取订单状态和版本号
SELECT status, version FROM order_table WHERE id = 1;
-- 假设当前订单状态为'待处理',版本号为 1
-- 将订单状态修改为'已处理'
UPDATE order_table SET status = '已处理', version = version + 1 WHERE id = 1 AND version = 1;
COMMIT;

  1. 事务 B
BEGIN TRANSACTION;
-- 读取订单状态和版本号
SELECT status, version FROM order_table WHERE id = 1;
-- 假设此时事务 A 已经将订单状态修改为'已处理',版本号变为 2
-- 将订单状态修改为'已发货'
UPDATE order_table SET status = '已发货', version = version + 1 WHERE id = 1 AND version = 2;
COMMIT;

在这个示例中,事务 A 先读取订单状态和版本号,然后将订单状态修改为 ' 已处理 ',并将版本号加 1。事务 B 在读取订单状态和版本号时,发现版本号已经发生了变化,说明数据被其他事务修改过,所以它会重新读取数据并再次尝试更新。这样可以确保在两个事务执行过程中,订单状态不会被错误地修改。

四、悲观锁与乐观锁的比较

(一)适用场景

  1. 悲观锁适用场景
    • 当数据竞争比较激烈,多个事务同时对同一数据进行修改的可能性较大时,使用悲观锁可以有效地避免数据冲突,确保数据的一致性。
    • 例如,在银行系统中,对账户余额的修改操作需要保证数据的准确性,此时可以使用悲观锁来防止多个事务同时修改账户余额导致数据不一致。
    • 另外,对于一些对数据一致性要求非常高的场景,如金融交易、库存管理等,也适合使用悲观锁。
  2. 乐观锁适用场景
    • 当数据竞争不激烈,多个事务同时对同一数据进行修改的可能性较小时,使用乐观锁可以提高系统的并发性能,减少锁的开销。
    • 例如,在一些用户量较大但数据修改频率较低的系统中,如论坛、博客等,使用乐观锁可以提高系统的响应速度和吞吐量。
    • 另外,对于一些需要频繁读取数据但很少修改数据的场景,也适合使用乐观锁。

(二)性能比较

  1. 悲观锁性能影响因素
    • 悲观锁的性能主要受到锁的粒度、锁的持有时间和锁的竞争程度等因素的影响。
    • 锁的粒度越小,并发度越高,但开销也越大;锁的粒度越大,并发度越低,但开销也越小。
    • 锁的持有时间越长,对其他事务的阻塞时间也越长,系统的并发性能越低;锁的持有时间越短,对其他事务的阻塞时间也越短,系统的并发性能越高。
    • 锁的竞争程度越高,事务等待锁的时间也越长,系统的并发性能越低;锁的竞争程度越低,事务等待锁的时间也越短,系统的并发性能越高。
  2. 乐观锁性能影响因素
    • 乐观锁的性能主要受到版本号或时间戳的更新频率、数据冲突的概率和重试次数等因素的影响。
    • 版本号或时间戳的更新频率越高,系统的开销也越大;版本号或时间戳的更新频率越低,系统的开销也越小。
    • 数据冲突的概率越高,事务重试的次数也越多,系统的开销也越大;数据冲突的概率越低,事务重试的次数也越少,系统的开销也越小。
    • 重试次数越多,系统的响应时间也越长;重试次数越少,系统的响应时间也越短。
  3. 性能比较总结
    • 一般来说,在数据竞争不激烈的情况下,乐观锁的性能要优于悲观锁,因为乐观锁不需要加锁,减少了锁的开销和事务等待锁的时间。
    • 但是,在数据竞争激烈的情况下,乐观锁可能会因为频繁的重试导致性能下降,此时悲观锁的性能可能会更好,因为悲观锁可以有效地避免数据冲突,减少事务重试的次数。

(三)优缺点比较

  1. 悲观锁的优缺点
    • 优点:
      • 能够有效地避免数据冲突,确保数据的一致性。
      • 实现简单,容易理解和使用。
    • 缺点:
      • 会降低系统的并发性能,因为事务在等待锁的过程中会被阻塞。
      • 可能会导致死锁问题,当多个事务相互等待对方释放锁时,就会发生死锁。
      • 增加了系统的开销,因为需要维护锁的状态和进行锁的管理。
  2. 乐观锁的优缺点
    • 优点:
      • 不会降低系统的并发性能,因为事务在读取数据时不需要加锁。
      • 不会导致死锁问题,因为事务在更新数据时才进行检查,不会相互等待锁。
      • 减少了系统的开销,因为不需要维护锁的状态和进行锁的管理。
    • 缺点:
      • 不能完全保证数据的一致性,当数据冲突的概率较高时,可能会导致事务重试次数过多,影响系统的性能。
      • 实现相对复杂,需要在数据库表中添加版本号或时间戳字段,并在更新数据时进行检查和处理。

五、实际应用中的考虑因素

(一)业务需求

  1. 数据一致性要求
    • 如果业务对数据一致性要求非常高,不容许任何数据冲突的情况发生,那么悲观锁可能是更好的选择。
    • 例如,在金融交易系统中,对账户余额的修改必须保证绝对的准确性,不能出现任何错误,此时可以使用悲观锁来确保数据的一致性。
  2. 系统并发性能要求
    • 如果业务对系统并发性能要求较高,希望能够尽量减少锁的开销和事务等待锁的时间,那么乐观锁可能是更好的选择。
    • 例如,在一些用户量较大的社交网络系统中,对用户状态的修改频率较低,但同时有大量的用户在读取用户状态,此时可以使用乐观锁来提高系统的并发性能。

(二)数据竞争程度

  1. 数据修改频率
    • 如果数据的修改频率较高,多个事务同时对同一数据进行修改的可能性较大,那么悲观锁可能更适合。
    • 例如,在库存管理系统中,商品的库存数量可能会频繁地被修改,此时使用悲观锁可以有效地避免数据冲突。
  2. 数据读取频率
    • 如果数据的读取频率较高,而修改频率较低,那么乐观锁可能更适合。
    • 例如,在一些新闻网站中,文章的浏览量可能会被频繁地读取,但文章的内容很少被修改,此时使用乐观锁可以提高系统的并发性能。

(三)数据库类型和版本

  1. 数据库支持的锁类型
    • 不同的数据库对悲观锁和乐观锁的支持程度可能不同。在选择锁类型时,需要考虑数据库所支持的锁类型和实现方式。
    • 例如,一些数据库可能只支持行级锁,而不支持表级锁或页级锁;一些数据库可能对乐观锁的实现方式有限制,需要使用特定的语法或函数。
  2. 数据库版本的影响
    • 数据库的版本也可能会影响锁的性能和行为。在选择锁类型时,需要考虑数据库的版本是否稳定,以及是否存在已知的问题或限制。
    • 例如,一些数据库的新版本可能会对锁的实现进行优化,提高锁的性能和并发度;一些数据库的旧版本可能存在一些锁的问题,需要进行升级或修复。

六、总结

MySQL 中的乐观锁和悲观锁是两种不同的并发控制机制,它们在不同的场景下有着各自的优势和适用范围。

悲观锁通过在事务执行过程中对数据进行加锁,以防止其他事务的干扰,确保数据的一致性。它适用于数据竞争激烈、对数据一致性要求高的场景,但会降低系统的并发性能,可能导致死锁问题,并且增加了系统的开销。

乐观锁则假设在事务执行过程中很少会有其他事务对同一数据进行修改,在更新数据时检查数据是否被其他事务修改过。如果数据没有被修改,就进行更新;如果数据被修改了,就重新读取数据并再次尝试更新。它适用于数据竞争不激烈、对系统并发性能要求高的场景,不会降低系统的并发性能,也不会导致死锁问题,并且减少了系统的开销。但它不能完全保证数据的一致性,当数据冲突的概率较高时,可能会导致事务重试次数过多,影响系统的性能。

在实际应用中,需要根据业务需求、数据竞争程度以及数据库类型和版本等因素来选择合适的锁类型。如果业务对数据一致性要求非常高,不容许任何数据冲突的情况发生,并且数据的修改频率较高,那么悲观锁可能是更好的选择。如果业务对系统并发性能要求较高,希望能够尽量减少锁的开销和事务等待锁的时间,并且数据的读取频率较高,而修改频率较低,那么乐观锁可能是更好的选择。

同时,在使用乐观锁和悲观锁时,还需要注意一些问题。对于悲观锁,需要注意锁的粒度、锁的持有时间和锁的竞争程度等因素,以避免降低系统的并发性能和导致死锁问题。对于乐观锁,需要注意版本号或时间戳的更新频率、数据冲突的概率和重试次数等因素,以避免事务重试次数过多,影响系统的性能。

总之,MySQL 中的乐观锁和悲观锁是两种重要的并发控制机制,它们在不同的场景下有着不同的应用价值。通过合理地选择和使用这两种锁类型,可以有效地提高数据库系统的并发性能和数据一致性,满足不同业务场景的需求。


原文地址:https://blog.csdn.net/jam_yin/article/details/143086530

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!