优化MySQL并发事务:如何避免更新丢失问题?
背景描述
现在有两个事务,事务A和事务B,他们都需要修改同一行数据,这行数据原始值为100,事务A的操作是数据增加100,事务B的操作也是增加100,预期的最终结果是300,现在如何保证最终的数据是300的?什么时候会出现200的情况?
执行结果
最终数据是300的情况(可重复读) :
在可重复读(REPEATABLE READ)隔离级别下,这种情况较为容易实现。此隔离级别是MySQL默认级别,它可以有效避免脏读、不可重复读和幻读问题。
-
执行流程:
- 事务A开始,执行查询操作(
SELECT
),获取值100。 - 事务A给这行数据加上读锁(共享锁)或排他锁(根据是否使用
SELECT ... FOR UPDATE
或SELECT ... LOCK IN SHARE MODE
)。 - 事务A在查询结果的基础上增加100,更新这行数据,然后提交事务,释放任何持有的锁。
- 接着,事务B开始,执行查询操作,获取到更新后的值200(因为事务A已经提交)。
- 事务B给这行数据加上读锁或排他锁。
- 事务B在查询结果的基础上增加100,更新这行数据,然后提交事务,释放任何持有的锁。
- 事务A开始,执行查询操作(
-
使用的锁:
- 根据是否声明
FOR UPDATE
或LOCK IN SHARE MODE
,可能会使用记录锁(行锁)或next-key锁(next-key锁是行锁和gap锁的组合,防止幻读)。 - 如果没有使用上述子句,那么InnoDB存储引擎默认会在更新操作时自动给涉及的行加上排他锁(X锁) 。
- 根据是否声明
最终数据是300的情况(读未提交) :
在读未提交(READ UNCOMMITTED)隔离级别下,事务可以读取到其他事务未提交的更改,也就是这个隔离级别允许发生“脏读”(Dirty Reads)
-
执行流程:
- 事务A在读未提交的隔离级别下启动。
- 事务A执行 SELECT 查询操作,读取数据值为100。
- 然后事务A增加这个值100,执行 UPDATE 操作,更新该数据行为200。此时,事务A还没有提交,所以更改是未提交的。
- 在读未提交隔离级别下,事务B也启动。
- 事务B执行 SELECT 操作,在这个隔离级别下,它可以读取到事务A未提交的更改,也就是它可能会读取到数据值为200。
- 事务A提交事务后,事务A对数据行的更改变成持久的,这时候数据是200。
- 基于事务B读到的200的数据值,事务B决定将这个数据增加100,更新操作将这个值更改为300。
- 然后事务B提交,这时数据行的更新已经持久化为300。
出现200的情况(低级别的隔离级别) :
在较低级别的隔离级别,如读未提交(READ UNCOMMITTED)或读提交(READ COMMITTED),可能会出现最终结果是200的情况。
-
执行流程:
- 事务A开始,执行查询操作,获取值100。
- 事务B几乎同时间开始,并执行查询操作,也读取到值100(尤其在READ UNCOMMITTED级别,或事务B在READ COMMITTED级别下且在事务A提交之前读取数据)。
- 事务A增加100,更新数据库行为200,然后提交。
- 事务B没有得到事务A的更新(因为它已经读取了数据),它也增加100,在其本地的值基础上(100),并更新数据库行为200,然后提交。
-
使用的锁:
- 在READ UNCOMMITTED级别下,不使用行级锁定,事务可以读取未提交的数据。
- 在READ COMMITTED级别下,每次查询都会读取最新的已提交数据,但如果事务B在事务A提交之前读取数据,它不会再次查询数据库。
- 在REPEATABLE READ级别下,第一次读取后,事务内的读操作会在提交前看到同样的数据,即事务B第一次查询后再次查询依然会是100,直到事务提交才会读到新的值。
出现200的情况(并发导致更新丢失) :
-
执行流程
- 事务A在可重复读的隔离级别下启动。
- 事务B也在可重复读的隔离级别下启动。
- 事务A执行SELECT查询,并且读取到这行数据的初始值100。
- 事务A在查询结果的基础上增加100,并执行UPDATE操作,将该行的值变为200。
- 在UPDATE操作过程中,事务A为这行数据加上排他锁。
- 事务A提交事务,行数据变为200,并释放排他锁。
- 因为事务B也在可重复读隔离级别下,所以即便事务A已经提交更新,事务B的SELECT操作依然会看到事务B自己快照里的数据,即历史值100。这意味着事务B在这个阶段读取到的是100,而非事务A更新后的200。
- 此时,事务B尝试基于它自己读取到的那个100的值增加100并且执行UPDATE。
- 由于事务A已经提交,并释放了排他锁,事务B现在确实能对这行数据进行更新操作,但是基于自己快照里的原始值100,它更新行数为200,而非基于事务A的更新结果。
- 事务B提交事务,数据的最新状态是200。
更新丢失问题
当两个或多个事务读取同一数据,并基于此数据进行更新时,其中一个事务的更新可能会由于另一个事务的更新而被覆盖,导致第一个事务的更改丢失。什么场景下会出现这种问题呢?
假设一个场景,在事务中涉及到先查询后更新的操作,那么使用排他锁时,是在事务开始时加锁,还是说在更新数据时加锁?
这有两种常见的策略,各有不同的问题:
1、 查询时不加锁,更新时加锁:
在这种策略中,事务在查询数据时不会对数据行加锁。只有当事务执行更新操作时,才会对相关数据行加上排他锁。这样做的好处是可以减少锁的持有时间,从而提高并发性能。但是,这种策略可能会导致“丢失更新”(Lost Update)问题,即两个事务可能同时读取相同的数据,然后几乎同时更新,导致一个事务的更新覆盖另一个事务的更新。
2、查询时就加锁,保持到事务结束:
在这种策略中,事务在查询数据时就会对数据行加上排他锁,并且保持锁直到事务结束(提交或回滚)。这样可以确保在事务执行期间,其他事务无法修改被锁定的数据行,从而避免了“丢失更新”问题。然而,这种策略会增加锁的持有时间,可能会降低系统的并发性能。
在MySQL的InnoDB存储引擎中,默认的隔离级别是可重复读(REPEATABLE READ)。在这个隔离级别下,InnoDB会使用以下策略,查询时不加锁,更新时加锁:
- 当执行普通的SELECT查询时,InnoDB不会加排他锁。
- 当进行写操作,如UPDATE、DELETE、INSERT时,会对涉及的数据行加上排他锁。
这里是两种不同SELECT语句的例子:
-
普通查询(不加锁) :
SELECT * FROM your_table WHERE id = 1;
-
查询并锁定(加排他锁) :
如果你希望在读取数据时立即加锁以确保数据不会被其他事务修改,你可以使用SELECT … FOR UPDATE语句。这会对选中的行加上排他锁,直到当前事务结束。
SELECT * FROM your_table WHERE id = 1 FOR UPDATE;
那么MySQL是不是存在“更新丢失”问题呢?它是怎么解决的呢?
如何解决“更新丢失”问题呢?
我们先来看看解决“更新丢失”有哪些办法:
- 使用合适的隔离级别:可重复读或串行化(SERIALIZABLE)隔离级别可以防止这种问题。
- 悲观锁定:通过在读取数据时立即加上
SELECT ... FOR UPDATE
,以获取对相关数据行的排他锁,防止其他事务并发修改。 - 乐观锁定:通过版本号或时间戳来检查在读取数据后和更新数据前是否有其他事务对数据作了修改,如果检测到了变化,可以拒绝更新或重新读取最新数据再尝试更新。乐观锁定一般需要在数据表中使用一个额外的字段来控制版本(version)或者保存时间戳(timestamp)。
下面是如何在MySQL中使用乐观锁定的方法和步骤:
- 修改表结构: 在你的数据表中增加一个版本号字段或时间戳字段。例如:
ALTER TABLE your_table ADD COLUMN version INT DEFAULT 0;
- 读取数据: 在进行数据操作时,你需要读取数据和它当前的版本号。例如:
SELECT value, version FROM your_table WHERE id = 1;
- 更新数据: 更新数据时,除了更改数据之外,还需要增加版本号,并检查更新时的版本号是否跟读取时的一致。如果不一致,说明数据在读取之后有过其他更新,当前更新应该被拒绝。
UPDATE your_table
SET value = value + 100, version = version + 1
WHERE id = 1 AND version = read_version;
- 操作结果处理: 执行更新后,需要检查操作是否成功。你可以根据更新影响行数来判断,如果影响行数为0,则说明在读取数据和尝试更新之间,数据已经被其他地方更新过,此时可以选择重新读取再尝试更新或返回错误提示。
int affectedRows = preparedStatement.executeUpdate();
if (affectedRows == 0) {// Handle the optimistic lock failure (e.g., throw an exception or retry)
}
在Java代码中,可以通过检查数据库操作返回的受影响行数来实现乐观锁定逻辑。如果受影响行数为0,则说明更新并没有成功,很可能是因为版本号不一致导致的更新冲突。应用程序可以根据业务场景,选择是重新尝试、放弃操作,还是抛出异常来通知用户。
乐观锁定在并发不是特别高,冲突概率较低的系统中是非常有用的,并且相对于悲观锁,它提供了更好的性能和用户体验。但在高冲突环境下,乐观锁可能会导致大量的冲突和重试,从而可能影响系统的整体性能。
MySQL怎么解决“更新丢失”问题?
在MySQL默认的事务隔离级别(REPEATABLE READ)下,解决并发事务中的“更新丢失”的关键流程主要包括:
- Next-Key锁: 当事务对数据进行修改时,InnoDB会使用Next-Key锁锁定这些数据,并且锁定它们的索引记录以及索引记录之间的间隙,防止并发事务插入新的记录。
- 检查当前版本: 在更新数据前,事务会检查此行数据的当前版本,确保没有其他事务已经修改过这个数据。如果数据已经被修改,事务会等待或得到更新冲突的通知。
- 行锁定: 当执行UPDATE或DELETE操作时, InnoDB会对涉及的每行数据加上排它锁(X锁),这使得其他事务不能同时对这些行进行写操作。
- 使用Undo日志保持一致性读: 即便有事务正在对数据行进行修改,其他事务因为Undo日志的存在,仍然可以读取到修改前的数据快照,保证了在更新过程中可以进行一致性非锁定读取。
- 版本控制: InnoDB通过为每个事务维护一个版本链来实现MVCC。事务在开始时记录下系统版本号,只查询版本早于当前事务版本的数据记录,避免了读取到其他事务修改后的数据,从而防止了更新丢失。
- COMMIT前确认: 在事务提交前,系统会检查所有修改是否与其他事务有冲突。如果发现冲突,当前事务需要重新执行或等待其他事务完成。
- REDO日志: 为了保证即便数据库发生故障,已经提交的修改也不会丢失,所有的更改操作都会记录在REDO日志中。
- 隔离和序列化操作: 当需要修改数据时,InnoDB通过锁定机制对相关的数据行进行隔离,确保操作的序列化,防止更新丢失。
排他锁
排他锁(Exclusive Lock),通常简称为X锁,是数据库中用于控制并发访问的一种锁机制。当一个事务对数据行加上排他锁时,它可以确保在这个事务完成并释放锁之前,其他事务不能对该数据行进行任何读取或修改操作。
排他锁的主要特点如下:
- 写操作保护:排他锁主要用于写操作,确保在事务执行更新、删除等操作时,数据不会被其他并发事务干扰。
- 互斥性:当一个事务持有排他锁时,其他事务必须等待直到锁被释放。这种互斥性保证了数据的一致性和完整性。
- 防止死锁:通过合理的锁策略,排他锁可以帮助防止死锁的发生。例如,事务可以按照一致的顺序获取锁,避免循环等待。
- 锁的释放:一旦事务完成了对数据的修改并提交,排他锁就会被释放,其他事务可以开始对数据行进行操作。
在MySQL中,当执行INSERT、UPDATE或DELETE操作时,InnoDB存储引擎会自动为涉及的数据行加上排他锁。这些锁在事务提交或回滚后自动释放。排他锁是实现事务隔离的关键机制之一,它有助于维护事务的原子性和一致性。
需要注意的是,排他锁可能会导致性能问题,特别是在高并发的场景下。如果事务长时间持有锁,可能会导致其他事务等待,从而降低系统的并发性能。因此,在设计数据库操作时,应该尽量减少锁的持有时间,合理地使用锁,以及考虑使用其他并发控制机制,如乐观锁等,来平衡性能和数据一致性的需求。
相关文章:

优化MySQL并发事务:如何避免更新丢失问题?
背景描述 现在有两个事务,事务A和事务B,他们都需要修改同一行数据,这行数据原始值为100,事务A的操作是数据增加100,事务B的操作也是增加100,预期的最终结果是300,现在如何保证最终的数据是300的…...

物联网设备管理系统设计
一、引言 物联网设备管理系统设计旨在通过物联网技术实现对设备的全面监控、控制和管理,以提高设备的运行效率、降低运维成本,并确保数据的安全性和完整性。本设计将结合当前物联网技术的发展趋势和实际应用需求,提出一个清晰、可扩展的物联网…...

python之Bible快速检索器
内容将会持续更新,有错误的地方欢迎指正,谢谢! python之Bible快速检索器 TechX 坚持将创新的科技带给世界! 拥有更好的学习体验 —— 不断努力,不断进步,不断探索 TechX —— 心探索、心进取! 助力快…...

微服务-网关
网关:就是网络的关口,负责请求的路由、转发、身份校验 在SpringCloud中网关的实现包括两种: 快速入门 引入依赖 路由属性 网关路由对应的Java类型是RouteDefinition,其中常见的属性有: id:路由唯一标示ur…...

OpenAI项目爆改GLM——以基于llama_index的pdf阅读助手
最近在做大模型agent构建,看了许多不错的开源项目,但是clone下来就是一整个不能用,因为github上开源的项目基本都是基于openai做的。而如果想要转成国内大模型backbone,需要修改的地方挺多的。 现在以一个简单的pdf reader agent…...

如何在Java中处理ParseException异常?
如何在Java中处理ParseException异常? 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿! 在Java编程中,ParseException异常是开发者在处理…...

Java中如何解决BadPaddingException异常?
Java中如何解决BadPaddingException异常? 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿! 在Java编程中,BadPaddingException异常是一个…...

数电大作业-四输入表决器
(PCB和multisim仿真画的有很大问题,没有VCC输入和GND,没学过直接裸画的,之后会好好看视频学习) 应用背景: 四个评委,三个及以上评委同时按下通过按钮时,选手才能通过。否则不通过。…...

ONLYOFFICE 桌面编辑器 8.1重磅来袭:全新功能提升您的办公效率
文章目录 前言ONLYOFFICE 桌面编辑器8.1一、PDF编辑:告别“头痛”时刻二、幻灯片版式:秒变“设计大师”三、无缝切换:办公界的“快速通道”四、语言支持:全球通吃的“翻译官”五、 隐藏“连接到云”板块:摆脱“云”的束…...

网络协议安全:TCP/IP协议栈的安全问题和解决方案
「作者简介」:北京冬奥会网络安全中国代表队,CSDN Top100,就职奇安信多年,以实战工作为基础对安全知识体系进行总结与归纳,著作适用于快速入门的 《网络安全自学教程》,内容涵盖Web安全、系统安全等12个知识域的一百多个知识点,持续更新。 这一章节我们需要知道TCP/IP每…...

VERYCLOUD睿鸿股份亮相亚马逊云科技中国峰会2024
5月30日,为期两天的亚马逊云科技中国峰会在上海世博中心圆满落幕。 多位大咖现场分享,生成式AI时代的数据战略,企业级AI应用,最新技术、产品重磅发布,创新行业解决方案 …… 作为亚马逊云科技的生态合作伙伴&#x…...

2-15 基于matlab的蚁群,模拟退火,遗传,神经网络,禁忌搜索等智能优化算法对TSP问题
基于matlab的蚁群,模拟退火,遗传,神经网络,禁忌搜索等智能优化算法对TSP问题。五种优化算法对多个城市路径进行规划,通过优化速度、距离可比较五种方法的优劣。程序已调通,可直接运行。 2-15 蚁群优化算法 …...

kylinos 国产操作系统离线安装firefox 麒麟操作系统安装新版本firefox
1. 火狐地址: 下载 Firefox 浏览器,这里有简体中文及其他 90 多种语言版本供您选择 2. 选择: 3. 下载完之后,上传到离线机器 4. 解压缩: tar -xvjf firefox-127.0.1.tar.bz2 5. 去点击解压后的文件夹,找…...

Python 类对象
Python 类对象 经典迭代器 可迭代对象的定义: 使用内置的iter可以获取迭代器的对象。如果对象实现了能返回迭代器的__iter__方法,那么对象就是可迭代的。序列都可以迭代。实现了__getitem__方法,而且接受从0开始的索引,这种对象也…...

pytest unittest temp path单元测试创建临时文件
参考了这个:Test Files Creating a Temporal Directory in Python Unittests | Simple IT 🤘 Rocks 并使用pathlib做了优化: import tempfile import unittest from pathlib import Pathclass TestExample(unittest.TestCase):def test_exa…...

在线样机生成器,制作精美的电脑手机壁纸图片展示
在线样机生成器,可以制作精美的电脑手机壁纸图片展示。在线样机生成器支持不同的模型如浏览器、手机、笔记本电脑、手表等结合使用,帮助用户快速生成样机展示图片。下面小编就来和大家分享一款免费的在线样机生成器-壁纸样机生成器。 壁纸样机生成器是一…...

FreeRTOS实时操作系统
1.认识实施操作系统 1.1 裸机和实时操作系统 裸机: 早期嵌入式开发没有嵌入式操作系统的概念,直接操作裸机,在裸机上写程序,比如用51单片机基本就没有操作系统的概念。 通常把程序设计为前后台系统,主要分为两部分&a…...

C/S、B/S架构(详解)
一、CS、BS架构定义 CS架构(Client-Server Architecture)是一种分布式计算模型,其中客户端和服务器之间通过网络进行通信。在这种架构中,客户端负责向服务器发送请求,并接收服务器返回的响应。服务器则负责处理客户端的…...

代码随想录算法训练营第六十五天|KM99. 岛屿数量——深搜、KM99. 岛屿数量——广搜、KM100. 岛屿的最大面积
代码随想录算法训练营第六十五天 KM99. 岛屿数量——深搜 题目链接:KM99. 岛屿数量 使用递归深度搜索,将每次遇到的岛屿上下左右记录为已经到过,如果遇到没到过的说明它上下左右不是之间遍历过的岛屿,结果计数1。最后统计计数即…...

Lua 面向对象编程
Lua 面向对象编程 Lua 是一种轻量级的编程语言,通常用于嵌入应用程序中,提供灵活的扩展和定制功能。尽管 Lua 本身是一种过程式语言,但它提供了强大的元机制,允许开发者实现面向对象的编程范式。本文将探讨 Lua 中的面向对象编程(OOP)概念、实现方式以及最佳实践。 面向…...

AI赋能前端:你的Chrome 控制台需要AI(爱)
像会永生那样去学习,像明天就要死亡那样去生活。——圣雄甘地 大家好,我是柒八九。一个专注于前端开发技术/Rust及AI应用知识分享的Coder 此篇文章所涉及到的技术有 AI(Gemini)ChromeDevTool🪜魔法接码平台因为,行文字数所限,有些概念可能会一带而过亦或者提供对应的学习…...

代码随想录-Day38
509. 斐波那契数 斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) 0,F(1) 1 F(n) F(n - 1) F(n - 2),其中 …...

CSS阴影优化气泡框样式
<body> <div class"pop">气泡框</div> </body>body{display: flex;justify-content: center;align-items: center;height: 100% } .pop{display: flex;justify-content: center;align-items: center;background: #409eff;width: 150px;heigh…...

强化安全新篇章:韶关石油化工可燃气体报警器年检解析
韶关,这座位于广东省北部的城市,近年来在石油化工行业取得了显著的发展。 随着一批批大型石化企业的进驻和投产,韶关不仅成为了区域性的石化产业基地,也为地方经济带来了强劲的增长动力。 然而,随着石化产业的快速发…...

Centos7 Docker部署PgSQL
拉取镜像 docker pull postgres:14.7运行容器 docker run --restartalways --nethost --shm-size"2g" --name pgsql -v /home/postgresql/data/pgdata:/var/lib/postgresql/data -v /etc/localtime:/etc/localtime -e POSTGRES_PASSWORDtest2023 -d postgres:14…...

LeetCode:经典题之21、24 题解及延伸
系列目录 88.合并两个有序数组 52.螺旋数组 567.字符串的排列 643.子数组最大平均数 150.逆波兰表达式 61.旋转链表 160.相交链表 83.删除排序链表中的重复元素 389.找不同 1491.去掉最低工资和最高工资后的工资平均值 896.单调序列 206.反转链表 92.反转链表II 141.环形链表 …...

【C++11】initializer_list详解!
一、什么是initializer_list? nitializer_list 是一种C11新的类型特性,它允许我们以统一的方式初始化对象。它是一个代表数组的轻量级包装器,通常用于构造函数和函数参数中,以允许传递一个初始化元素列表。 initializer_list也是一种模板类…...

如何在Java中处理UnsupportedOperationException异常?
如何在Java中处理UnsupportedOperationException异常? 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿! 在Java编程中,我们经常会遇到各…...

WPS没保存关闭了怎么恢复数据?4个方法(更新版)
想象一下,你正在用WPS奋笔疾书,灵感如泉水般涌出,突然间,电脑却跟你开了个玩笑——啪地一下,文档未保存就关闭了!是不是感觉像是被泼了一盆冷水,所有的热情瞬间熄灭?别急,…...

elementplus el-table(行列互换)转置
Element Plus v2.4.0, repl v3.4.0 <template> <div><el-table :data"tableData" style"width: 100%"><el-table-column prop"name" label"名字" width"180" /><el-table-column prop"wei…...