0

0

mysql如何使用乐观锁处理并发

P粉602998670

P粉602998670

发布时间:2025-09-24 09:59:01

|

481人浏览过

|

来源于php中文网

原创

答案:乐观锁通过版本号或时间戳在更新时检查数据一致性,适用于读多写少场景。1. 在表中添加version字段,读取数据时获取版本号;2. 更新时用WHERE条件验证版本号未变,成功则自增版本号;3. 若UPDATE影响行数为0,说明版本已变,需重试或报错;4. 与悲观锁相比,乐观锁并发性能好但冲突高时重试成本大;5. 实现时需避免漏加版本条件、INT溢出、无退避重试等问题;6. 可选替代方案包括时间戳和业务字段CAS,但版本号最通用。

mysql如何使用乐观锁处理并发

在MySQL里,要处理并发问题,特别是避免“丢失更新”这样的情况,乐观锁是一个非常实用的策略。它不像悲观锁那样在操作前就锁定资源,而是假设冲突不常发生,只在提交更新时检查数据是否被其他事务修改过。简单来说,就是通过一个版本号或时间戳来判断,如果你要修改的数据在我读取后已经被别人动过了,那这次更新就作废,需要重新来过。

解决方案

实现MySQL乐观锁的核心思路,是在数据表中增加一个版本字段(比如 versionupdate_time)。

当你从数据库读取一条记录时,会同时获取它的当前版本号。

SELECT data_field, version FROM your_table WHERE id = 123;

假设你读到 version = 1

然后,在你的业务逻辑处理完,准备更新这条记录时,你需要带上之前读取到的版本号作为条件。

UPDATE your_table
SET data_field = 'new_value', version = version + 1
WHERE id = 123 AND version = 1;

这条SQL语句的精妙之处在于:

  1. version = version + 1:成功更新后,版本号会自增,表明数据已经被修改。
  2. WHERE id = 123 AND version = 1:只有当这条记录的 id 匹配,并且它的 version 仍然是你当初读取到的那个值时,更新才会成功。

如果这条 UPDATE 语句执行后,影响的行数(ROWS AFFECTED)是0,那就说明在你处理数据期间,其他事务已经修改了这条记录,导致 version 字段不再是 1 了。这时,你的更新尝试就失败了,需要根据业务需求进行重试(重新读取数据、重新处理、再次尝试更新)或者报错。

乐观锁与悲观锁,我该如何选择?

这确实是数据库并发控制中一个绕不开的哲学问题。我的看法是,这没有绝对的优劣,只有适不适合你当前场景的考量。

乐观锁就像它的名字一样,对并发冲突持乐观态度。它觉得“冲突没那么频繁,我们没必要一开始就大动干戈地锁住资源,浪费性能”。所以,它把冲突检测放到了更新阶段。这种方式在读多写少的场景下表现出色。比如,一个电商网站的商品详情页,大部分用户都在浏览,只有少数用户会购买或修改商品信息。如果用悲观锁,每次读取商品信息都加锁,那网站性能肯定会雪崩。乐观锁在这里就游刃有余,用户读数据几乎没有开销,只有在更新时才可能遇到冲突,即便冲突了,重试几次通常也能成功。它的缺点是,如果并发写操作非常频繁,导致冲突不断,那么大量的重试会消耗更多资源,反而不如悲观锁高效。而且,冲突解决的逻辑需要应用层去实现,增加了开发复杂度。

悲观锁则恰恰相反,它觉得“冲突随时可能发生,为了数据安全,我必须提前把资源锁住”。它在读取数据时就对数据加上排他锁,直到事务结束才释放。这确保了在整个事务期间,其他任何事务都不能修改或读取被锁定的数据(取决于锁的类型)。这种模式适用于写多读少、冲突频繁的场景,或者对数据一致性要求极高,绝不允许任何形式的冲突(比如银行转账)。它的优点是简单粗暴,数据一致性保障强。但代价是,锁的开销大,会降低并发性能,容易产生死锁。

所以,我的选择标准是:

  • 低冲突率、读多写少、对响应时间敏感的场景,优先考虑乐观锁
  • 高冲突率、写多读少、对数据一致性要求极高、宁愿牺牲部分并发性的场景,考虑悲观锁
  • 有时候,甚至可以混合使用,对核心关键数据用悲观锁,其他用乐观锁。

实现乐观锁时,有哪些“坑”要避免?

尽管乐观锁看起来很优雅,但在实际落地中,还是有些“坑”是需要我们特别留心的,不然可能事与愿违。

阳光订餐系统
阳光订餐系统

欢迎使用阳光订餐系统,本系统使用PHP5+MYSQL开发而成,距离上一个版本1.2.8发布已经有一年了。本系统集成了留言本,财务管理,菜单管理,员工管理,安全管理,WAP手机端等功能,并继续继承1.X老版本简单、实用、美观的特点,在老版本上的基础上做了如下更新:1.更简洁的前台与后台,菜单及功能布局更合理。2.更合理的文件结构,合理适度的模板机制以及OO运用,更易于理解的代码,更适于二次开发;3.

下载

一个最常见的,也是最致命的“坑”,就是忘记在 UPDATE 语句中加入版本号的 WHERE 条件。如果你只写了 SET version = version + 1 而没有 WHERE version = old_version,那所有并发的更新都会成功,只是版本号不断递增,但数据依然会被覆盖,这就完全失去了乐观锁的意义,成了彻头彻尾的“丢失更新”。这就像你精心设计了一道防火墙,结果把大门敞开了。

另一个需要注意的,是版本号的数据类型。如果你的系统并发量非常大,或者某个业务场景下对同一条记录的更新极其频繁,那么 INT 类型的 version 字段可能会面临溢出的风险。虽然这在大多数业务中不常见,但一旦发生,会导致版本号回绕,引发严重的数据一致性问题。这时候,使用 BIGINT 这样的更大整数类型会更安全。或者,如果使用时间戳作为版本号,要确保时间戳的精度足够,并且在比较时能够精确匹配,避免因为毫秒级的差异导致误判。

还有就是重试机制的设计。乐观锁的冲突解决策略,很大程度上依赖于应用层的重试。如果发生冲突,你的应用应该怎么做?是立即重试,还是等待一段时间再重试?重试多少次?有没有最大重试次数?如果没有一个合理的重试策略,可能会导致:

  1. 无限重试:在极端高并发下,如果每次重试都失败,系统会陷入死循环,耗尽资源。
  2. 重试风暴:大量失败的事务同时进行重试,反而加剧了数据库的压力,形成恶性循环。 所以,设计一个带有指数退避(Exponential Backoff)和最大重试次数的重试机制至关重要,这能有效缓解系统压力。

最后,别忘了事务的边界。乐观锁的版本检查和更新操作,通常需要在同一个数据库事务中完成。虽然乐观锁本身不依赖数据库的锁定机制,但为了保证 SELECTUPDATE 之间的操作原子性,将它们包裹在一个事务中是良好的实践。

除了版本号,还有其他实现乐观锁的姿势吗?

当然有,版本号(version number)只是最常见、最直观的一种实现方式,但不是唯一。从本质上讲,乐观锁就是一种“先检查后更新”的模式,只要能确保更新时数据没有被别人动过,都可以算作乐观锁的范畴。

一种常见的替代方案是使用时间戳(timestamp)。你可以在表中增加一个 updated_at 或者 last_modified_time 字段,类型可以是 DATETIMETIMESTAMP。每次更新记录时,都自动更新这个字段为当前时间。

-- 读取
SELECT data_field, updated_at FROM your_table WHERE id = 123;
-- 更新
UPDATE your_table
SET data_field = 'new_value', updated_at = NOW()
WHERE id = 123 AND updated_at = '2023-10-27 10:00:00'; -- 假设这是之前读取到的时间戳

这种方式和版本号类似,但它利用了时间的自然递增性。需要注意的是,时间戳的精度问题,如果并发非常高,在同一毫秒内发生多次更新,可能会导致时间戳相同,从而无法正确检测冲突。MySQL的 TIMESTAMP 字段通常能精确到微秒,这在大多数情况下是足够的,但仍需留意。

另一种思路是基于业务字段的比较(Compare-And-Swap,CAS)。这种方式不额外增加版本字段,而是直接利用业务字段的旧值来判断。比如,你要更新一个商品的库存 stock 字段:

-- 读取(这里可能不需要读取旧值,直接在更新时判断)
-- 更新
UPDATE products
SET stock = stock - 1
WHERE id = 456 AND stock > 0 AND stock = 10; -- 假设你期望的旧库存是10

这里,AND stock = 10 就是一个乐观锁的判断条件。它确保只有当 stock 字段的值仍然是你期望的 10 时,更新操作才会执行。如果 stock 已经被其他事务修改成了 9,那么你的更新就不会成功。这种方式特别适用于只更新特定字段,并且该字段的值具有明确的“旧值”预期的情况。它的优点是侵入性小,不需要额外字段,但缺点是如果业务逻辑复杂,涉及多个字段的更新,这种方式会变得非常笨重,难以维护。

总的来说,版本号是实现乐观锁最通用、最健壮的方式,而时间戳和基于业务字段的CAS则是在特定场景下的灵活变通。选择哪种,取决于你的具体需求、数据特性以及对复杂度的接受程度。

相关专题

更多
数据分析工具有哪些
数据分析工具有哪些

数据分析工具有Excel、SQL、Python、R、Tableau、Power BI、SAS、SPSS和MATLAB等。详细介绍:1、Excel,具有强大的计算和数据处理功能;2、SQL,可以进行数据查询、过滤、排序、聚合等操作;3、Python,拥有丰富的数据分析库;4、R,拥有丰富的统计分析库和图形库;5、Tableau,提供了直观易用的用户界面等等。

675

2023.10.12

SQL中distinct的用法
SQL中distinct的用法

SQL中distinct的语法是“SELECT DISTINCT column1, column2,...,FROM table_name;”。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

319

2023.10.27

SQL中months_between使用方法
SQL中months_between使用方法

在SQL中,MONTHS_BETWEEN 是一个常见的函数,用于计算两个日期之间的月份差。想了解更多SQL的相关内容,可以阅读本专题下面的文章。

346

2024.02.23

SQL出现5120错误解决方法
SQL出现5120错误解决方法

SQL Server错误5120是由于没有足够的权限来访问或操作指定的数据库或文件引起的。想了解更多sql错误的相关内容,可以阅读本专题下面的文章。

1084

2024.03.06

sql procedure语法错误解决方法
sql procedure语法错误解决方法

sql procedure语法错误解决办法:1、仔细检查错误消息;2、检查语法规则;3、检查括号和引号;4、检查变量和参数;5、检查关键字和函数;6、逐步调试;7、参考文档和示例。想了解更多语法错误的相关内容,可以阅读本专题下面的文章。

356

2024.03.06

oracle数据库运行sql方法
oracle数据库运行sql方法

运行sql步骤包括:打开sql plus工具并连接到数据库。在提示符下输入sql语句。按enter键运行该语句。查看结果,错误消息或退出sql plus。想了解更多oracle数据库的相关内容,可以阅读本专题下面的文章。

674

2024.04.07

sql中where的含义
sql中where的含义

sql中where子句用于从表中过滤数据,它基于指定条件选择特定的行。想了解更多where的相关内容,可以阅读本专题下面的文章。

567

2024.04.29

sql中删除表的语句是什么
sql中删除表的语句是什么

sql中用于删除表的语句是drop table。语法为drop table table_name;该语句将永久删除指定表的表和数据。想了解更多sql的相关内容,可以阅读本专题下面的文章。

410

2024.04.29

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

74

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

相关下载

更多

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
MySQL 教程
MySQL 教程

共48课时 | 1.6万人学习

MySQL 初学入门(mosh老师)
MySQL 初学入门(mosh老师)

共3课时 | 0.3万人学习

简单聊聊mysql8与网络通信
简单聊聊mysql8与网络通信

共1课时 | 779人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号