高并发下丢失更新的解决方案

news2025/2/21 19:50:05

作者:谢益培

1 背景

关键词:并发、丢失更新

预收款账户表上有个累计抵扣金额的字段,该字段的含义是统计商家预收款账户上累计用于抵扣结算成功的金额数。更新时机是,账单结算完成时,更新累计抵扣金额=累计抵扣金额+账单金额。

2 问题及现象

发现当账单结算完成时,偶尔会发生累计抵扣金额字段值更新不准确的现象。
比如,某商家账户上累计抵扣金额原本为0元,当发生两笔分别为10和8的账单结算完成后,理论上累计抵扣金额应该变为18元,但实际为10元。也就是说,第二次更新把前一次更新内容给覆盖掉了。

3 问题分析

该问题为典型的第二类丢失更新问题。

3.1 概念解释

事务在并发情况下,常见如下问题:

  1. 脏读:一个事务读取了已被另个一个事务修改但尚未提交的数据。当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中;这时另外一个事务也访问这个数据,然后使用了这个未提交的数据。
  2. 不可重复读:在一个事务内,多次读同一数据,读到的结果不同。第一个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
  3. 幻读:同一事务中,当同一个查询执行多次的时候,由于其他事务进行了插入操作并提交事务,导致每次返回不同的结果集。幻读是事务非独立执行时发生的一种现象。例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表的全部数据行。同时,第二个事务也修改了这个表中的数据,这种修改是向表中插入了一行新数据。那么,就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好像发生了幻觉一样。
  4. 更新丢失:两个事务同时更新一行数据,一个事务对数据的更新把另一个事务对数据的更新覆盖了。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。

 图1 SQL标准定义了4种数据库事务隔离级别

第一类丢失更新:A事务撤销时,把已经提交的B事务的更新数据覆盖了。SQL标准中未对此做定义,所有数据库都已解决了第一类丢失更新的问题。

图2 第一类丢失更新

 

第二类丢失更新:A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失。第二类丢失更新,和不可重复读本质上是同一类并发问题,通常将它看成不可重复读的特例。当两个或多个事务查询相同的记录,然后各自基于查询的结果更新记录时会造成第二类丢失更新问题。每个事务不知道其它事务的存在,最后一个事务对记录所做的更改将覆盖其它事务之前对该记录所做的更改。

图3 第二类丢失更新

 

3.2 疑惑点

发生问题的代码:

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
public void finishDeductTransaction(String customerCode, String entityCode, String currency, ABTransaction abTransaction) {
    Account account = getAccount(customerCode, entityCode, currency);

    BigDecimal newValue = account.getCumulativeDeductionAmount().add(transaction.getTransactionAmount());
    account.setCumulativeDeductionAmount(newValue);
    //持久化
    Account update = new Account();
    update.setId(account.getId());
    update.setCumulativeDeductionAmount(account.getCumulativeDeductionAmount());
    accountBalanceInfoMapper.update(update);
}

上述代码中可以看到,该方法已设置事务隔离级别为可重复读(isolation = Isolation.REPEATABLE_READ,也是MySQL的默认隔离级别)。按照之前对隔离级别规范的理解,可重复读级别是应该能够避免第二类更新丢失的问题的,但为啥还是发生了呢?!于是上网查阅相关资料,得到的结论是:MySQL数据库,设置事务隔离级别为可重复读无法避免发生“第二类丢失更新”问题。从这个案例中也得到一个教训就是,规范标准和所选的产品(组件)实际实现情况,两者需要同时考虑,对于边缘性或存在争议的规范内容要尽可能避免直接使用,最好通过其他机制来保证。

4 解决方案

以下整理了针对该问题的常见解决方案并按解决思路进行了分类。

4.1 依赖数据库的思路

方法1:

将事务隔离级别改为串行,能解决但并发性能低,还可能导致大量超时和锁竞争。

方法2:

调整SQL语句,将更新赋值逻辑改为“c=c+x”形式,其中c为要更新的字段,x为增量值。这种方式能确保累加值不会被覆盖。但这种方式需要额外编写特殊的SQL,而且严格意义上讲,存在业务逻辑泄露到持久层的不规范问题。

4.2 悲观锁思路

方法3:

方法执行时增加分布式锁,来控制同一账户同一时刻只有一个线程可对其进行操作。效果等同将事务级别改为串行,也是排队执行,并发性能也低,只是锁机制不是由数据库实现了而已。

分布式锁的实现方式有多种,比如该项目中有封装好的基于Redis的分布式锁,其注解的使用方式如下:

@CbbSingle(key = "QF:finishDeductTransaction:customerCode", value = {"#{customerCode}"})
public void finishDeductTransaction(String customerCode, String entityCode, String currency, ABTransaction abTransaction) {

方法4:

通过SQL语句启用数据库排他锁,例如:select * from table where name=’xxx’ for update。通过此sql查询到的数据就会被数据库上排他锁,因此其他事务就无法对该数据及进行修改了。

该方法比上面两种在并发性能方面会好一些,但仍然可能存在锁等待和超时情况发生。

4.3 乐观锁思路

方法5:

乐观锁的思路是假设并发冲突发生概率较低,开启事务时先不加锁,在更新数据时通过版本比对以及判断影响行数来判断是否更新成功。

其中,版本概念,可以是要更新记录的版本号,或者更新时间等。也可以用旧值条件或校验和等方式;

该方法并发性能最好,但一旦发生并发冲突会导致方法执行失败,此时就需要搭配额外的重试或自旋逻辑来闭环。
思路如下:

//先查询出来要更新的数据
select column1,id,version from table where id=1001;
//进行业务逻辑处理
//更新这条数据
update table set column1=xx where id=1001 and version=查询出来当时的version值
//判断影响行数
if (records < 1) 更新失败...

本案例中的问题便是采用这种方法解决的:

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
public void finishDeductTransaction(String customerCode, String entityCode, String currency, ABTransaction abTransaction) {
    Account account = getAccount(customerCode, entityCode, currency);

    BigDecimal newValue = account.getCumulativeDeductionAmount().add(transaction.getTransactionAmount());
    account.setCumulativeDeductionAmount(newValue);

    //持久化
    Account update = new Account();
    update.setId(account.getId());
    update.setVersion(account.getVersion());
    update.setCumulativeDeductionAmount(account.getCumulativeDeductionAmount());
    int records = accountBalanceInfoMapper.updateByIdAndVersion(update);
    if (records < 1) {
        throw new SingleThreadException("更新时数据版本号已发生改变。原因:发生并发事务");
    }
}

5 总结

  1. 对于常见并发事务问题,需要将事务隔离级别和锁机制结合起来一起使用;
  2. 需要对并发问题从业务场景上进行分析和识别,对于并发冲突少的场景,首选乐观锁思路;对于并发冲突高的场景采用悲观锁思路;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/28934.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ImmunoChemistry艾美捷Annexin V-FITC细胞凋亡检测试剂盒方案

膜联蛋白V是具有血管抗凝血活性的钙和磷脂结合蛋白家族的成员。体外实验结果表明&#xff0c;它可能通过与凝血酶原竞争磷脂酰丝氨酸&#xff08;PS&#xff09;结合位点而在抑制凝血中发挥作用。在健康细胞中&#xff0c;PS通常保存在细胞膜的内小叶&#xff08;胞质侧&#x…

网络是怎样连接的--TCP大致控制流程

文章目录5.1 协议栈内部结构5.2 套接字作用5.3 创建套接字5.4 两类控制信息5.5 连接操作的实际过程5.6 收发数据5.6.1将http请求消息发给协议栈5.6.2 注意对较大数据进行拆分5.6.3 序号和ACK5.6.4 等待超时时间5.6.5 窗口机制提升效率5.6.6 ACK与窗口合并5.7 断开连接5.8 删除套…

MCE | 新冠 德尔塔病毒

冠状病毒&#xff0c;其表面有突出的棒状尖峰&#xff0c;在电镜下可以观察到像王冠一样的放射状凸起而得名。冠状病毒的基本结构如图 1 所示&#xff0c;包括刺突糖蛋白 (S)、包膜 (E)、膜 (M) 和核衣壳 (N)。 图 1. 冠状病毒结构2020 年石正丽教授在 Nature 发表的论文 A …

浅识JVM

⭐️前言⭐️ 本篇文章&#xff0c;博主分享的是在面试中JVM常考的考点&#xff0c;希望这篇文章能够对你有用。 &#x1f349;博客主页&#xff1a; &#x1f341;【如风暖阳】&#x1f341; &#x1f349;精品Java专栏【JavaSE】、【备战蓝桥】、【JavaEE初阶】、【MySQL】、…

多维度比对三种软件开发方式后,无代码开发到底赢在哪?

近年来受信息化、数字化和 5G、云计算发展的影响&#xff0c;以及企业管理的不确定性和复杂性增加&#xff0c;国内管理软件的需求及行业市场整体规模也连年高速增长&#xff0c;进而带动了管理软件开发方式的变革。 但是由于传统自主及外包开发方式存在的周期长、成本高、不能…

DataX二次开发——(9)新增s3reader和s3writer模块

1 背景 DataX3.0支持阿里的OSS的读写&#xff0c;但没支持S3的读写&#xff0c;虽然OSS的也是基于S3协议去做二开的&#xff0c;但是一些参数有点区别&#xff0c;所以按照阿里的OSSReader和OSSWriter开发了S3Reader和S3Writer。 2 代码开发 2.1 s3reader 2.1.1 项目结构 2…

pytorch快速上手(8)-----pytorch优化器简介

文章目录一、简介二、optimizer属性方法1. zero_grad()2. step()3. add_param_group()4. state_dict()5. load_state_dict()学习率动量三、常见优化器介绍1. BGD&#xff08;Batch Gradient Descent&#xff09;2. Stochastic Gradient Descent&#xff08;SGD&#xff09;3. M…

记录一次因执行时间过长锁已经释放导致finally块再次unlock引发的异常

一、前言 因为我的一个需求需要请求一个耗时比较长的接口&#xff08;耗时长其实是对接方的锅&#xff09;&#xff0c;该接口交给了Spring事务管理&#xff0c;并且使用了分布式锁&#xff0c;但是在请求的时候&#xff0c;出现error,看日志发现是unlock的时候没有锁可以去解…

牛客网语法篇练习复合类型(二)

1.输入NxM矩阵&#xff0c;矩阵元素均为整数&#xff0c;计算其中大于零的元素之和。 a,b map(int,input().split()) list1 [] sum 0 for i in range(a):list1.extend(list(map(int,input().split()))) for i in list1:if i>0:sumi print(sum) 2.给你一个整数n&#xff…

辨别代码能否引发线程安全问题--避免在平时写代码时引发线程安全问题

前景提要&#xff1a; 本篇文章只是入门&#xff0c;目的在于在脑海中构建一个Java运行的模型&#xff0c;然后可以在平时写代码时对是否引发线程安全问题有感知。 文章目录引入了解辨别线程安全问题之前先来构建一个计算机运行模型了解线程安全问题怎么能不知道线程和进程了解…

搭建repo服务器管理多个git工程

参考自&#xff1a;搭建repo服务器管理多个git工程     repo系列讲解 —— Android系统源码(AOSP)下载 1、repo介绍 Android使用git作为代码管理工具&#xff0c;开发了gerrit进行代码审核&#xff0c;以便更好的对代码进行集中式管理。还开发了repo命令行工具&#xff0…

MySQL8.0 binlog进阶

MySQL8.0经过这几年的操揉磨治&#xff0c;已经上升到海平面了。其中binlog也悄然无声带来了不一样的变化。高可用核心复制基础binlog变化更应该进一步了解。从参数入手&#xff0c;了解带来的变化。 slave回放算法 slave_rows_search_algorithms 当使用基于 row-based复制格…

牛客网语法篇练习循环控制(二)

1.今天牛牛学到了回文串&#xff0c;他想在数字里面找回文&#xff0c;即回文数&#xff0c;回文数是正着读与倒着读都一样的数&#xff0c;比如1221&#xff0c;343是回文数&#xff0c;433不是回文数。请输出不超过n的回文数。 a int(input()) for i in range(1,a1):n str…

CSS-counter 计数器详细教程+使用场景示例

counter一. counter计数器二. 属性和方法1. 计数器命名/重置2. 计数器-值递增规则3. 计数器显示 counter() / counters() 函数三 代码示例1. 重新开始计数2. counters嵌套使用3. 借助CSS计数器呈现CSS var变量值一. counter计数器 计数器是一种特殊的数字跟踪器&#xff0c;通常…

systemd的unit配置文件详解

Systemd 是 Linux 的系统和服务的管理器&#xff0c;兼容 SysV 和 LSB初始化脚本&#xff0c;Systemd有以下特性&#xff1a; 积极的并行化能力使用套接字和 D-Bus 激活来启动服务提供按需启动守护进程&#xff0c;使用 Linux cgroups 跟踪进程支持系统状态的快照和恢复维护挂…

艾美捷Cas9核酸酶应用说明及实例展示

Product Description:Recombinant Streptococcus pyogenes Cas9 (wt) protein expressed in an E. coli . Form:Liquid Preparation Method:E. coli expression system Purity:≥ 95% by SDS-PAGE Activity:20 nM CRISPR/Cas9-C-NLS nuclease incubated for 1 hour at 37℃…

【新知实验室 TRTCIM】实时互动课堂最佳实践

【新知实验室 TRTC&IM】实时互动课堂最佳实践一、新知实验室-TRTC腾讯云音视频产品体验官计划活动简介二、产品简介TRTCIM三、最佳实践3.1 官方快速上手TRTC(快速跑通)3.1.1 注册腾讯云账号3.1.2 使用实时音视频(需先开通)3.1.3 创建应用3.1.4 查看项目(查看密钥和快速上手…

java基础—String

我们都知道 创建一个字符串最简单的方式是 String meaasge "java资讯";当然还可以用构造来创建 &#xff08;不推荐&#xff0c;开发中不要用&#xff09; String str2new String("java资讯");这两种创建最主要的区别在于&#xff0c;一个在公共池中&…

gitlab CI/CD 自动化部署vue项目到阿里云服务器步骤

目录1&#xff0c;gitlab托管vue项目2&#xff0c;本地项目连接到远程仓库3&#xff0c;设置gitlab-runner4&#xff0c;编写yml文件5&#xff0c;部署到阿里云服务器&#xff08;本地设置&#xff09;5.1 安装相关依赖5.2 vue项目中添加deploy.js文件5.3 注册deploy命令5.4 验…

文本生成图像工作简述2--常用数据集分析与汇总

文本到图像的 AI 模型仅根据简单的文字输入就可以生成图像。用户可以输入他们喜欢的任何文字提示——比如&#xff0c;“一只可爱的柯基犬住在一个用寿司做的房子里”——然后&#xff0c;人工智能就像施了魔法一样&#xff0c;会产生相应的图像。 文本生成图像&#xff08;te…