Skip to main content

moregeek program

高并发下丢失更新的解决方案_京东云官方的博客-多极客编程

作者:谢益培

1 背景

关键词:并发、丢失更新

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

2 问题及现象

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

3 问题分析

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

3.1 概念解释

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

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

高并发下丢失更新的解决方案_数据库

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

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

高并发下丢失更新的解决方案_脏读_02

图2 第一类丢失更新

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

高并发下丢失更新的解决方案_数据库_03

图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. 需要对并发问题从业务场景上进行分析和识别,对于并发冲突少的场景,首选乐观锁思路;对于并发冲突高的场景采用悲观锁思路;

©著作权归作者所有:来自51CTO博客作者京东云官方的原创作品,如需转载,请与作者联系,否则将追究法律责任

splunk入门体验_马鹏飞的博客-多极客编程

放一段介绍, Splunk是一款功能强大,功能强大且完全集成的软件,用于实时企业日志管理,可收集,存储,搜索,诊断和报告任何日志和机器生成的数据,包括结构化,非结构化和复杂的多行应用程序日志。 ​ 它允许您以可重复的方式快速,可重复地收集,存储,索引,搜索,关联,可视化,分析和报告任何日志数据或机器生成的数据,以识别和解决操作和安全问题。 ​ 此外,splunk还支持各种日志管理用例,例如日志整合

5种典型 api 攻击及预防建议_seal安全的博客-多极客编程

API 帮助跨多个设备互连多个应用程序或软件系统,定义它们可以发出的调用或请求的种类、调用的方式、应使用的数据格式以及应遵守的约定。API 已经发展成为重要的互连,支持不同应用程序架构之间的通信,促进新服务的更快集成和部署。软件开发程序也依赖 API 来提供服务、平台管理和持续部署。涉及移动设备、云数据系统和微服务设计模式的现代应用程序架构需要使用多个 API 作为网关,以促进不同 Web 应用程

6个tips缓解第三方访问风险_seal安全的博客-多极客编程

随着开发和交付的压力越来越大,许多企业选择依赖第三方来帮助运营和发展业务。值得重视的是,第三方软件及服务供应商和合作伙伴也是云环境攻击面的重要组成部分。尽管企业无法完全切断与第三方的关联,但可以在向他们提供进入单一云和多云环境的权限时强制执行最小特权原则。本文将带你了解如何缓解云端业务第三方风险对企业的影响以及缓解相应风险的技巧。   第三方对云环境的影响 第三方,包括供应商、承包商、合作伙伴,甚

与时俱进「风险系统保障质量之路」非同寻常_京东云官方的博客-多极客编程

作者:梁冬冬风险系统复杂且又庞大,质量如何保障需要我们付出一点一滴的努力来浇灌系统之花一、大促备战,求有序,求稳定:大促是每年例行高考,把人和系统的各项能力激发,衡量系统健壮,容错性;凌晨3点的身影就像一束光,夺目耀眼;今年的大促与往年不同,倡导绿色,节能减排,降本增效,把各种资源做到利用最大化,产生更大的价值,让大促备战产生了一丝温度1)压测备战时间表(统筹整体,从4.21-6.23我们把剧本编

openssl 配置自签名证书_虚拟的现实的博客-多极客编程

1.简介CentOS 7 的最小化安装模式,通过 YUM 更新到最新版本。openssl 使用系统自带版本2.自建CA2.1.生成 CA 私钥openssl genrsa -out /etc/pki/CA/private/cakey.pem 40962.2.生成 CA 证书请求openssl req -new -key /etc/pki/CA/private/cakey.pem -out /etc

高效率开发web安全扫描器之路(一)_蜻蜓安全的博客-多极客编程

一、背景 经常看到一些SRC和CNVD上厉害的大佬提交了很多的漏洞,一直好奇它们怎么能挖到这么多漏洞,开始还以为它们不上班除了睡觉就挖漏洞,后来有机会认识了一些大佬,发现它们大部分漏洞其实是通过工具挖掘的,比如说下面是CNVD上面的白帽子大佬 我想成为大佬要怎么做 我一直觉得自己是一个有梦想的人,我也想有一天自己的ID能出现在排行榜中,于是我凭借着自己那一点开发知识,认真研究了一下市面上的安全工

cisco 6800系列交换机硬件架构及日常维护指南_虚拟的现实的博客-多极客编程

1.核心交换机硬件及架构概览核心交换机硬件清单:1机箱C6807-XL=Catalyst 6807-XL 7-slot chassis, 10RU (spare),3年CON-SNT-C6807XLC22交换引擎C6800-SUP6T-XLCatalyst 6800 Sup6T (440G/slot) with 8x10GE, 2x40GE43电源C6800-XL-3KW-ACCatalyst 6

splunk入门体验_马鹏飞的博客-多极客编程

放一段介绍, Splunk是一款功能强大,功能强大且完全集成的软件,用于实时企业日志管理,可收集,存储,搜索,诊断和报告任何日志和机器生成的数据,包括结构化,非结构化和复杂的多行应用程序日志。 ​ 它允许您以可重复的方式快速,可重复地收集,存储,索引,搜索,关联,可视化,分析和报告任何日志数据或机器生成的数据,以识别和解决操作和安全问题。 ​ 此外,splunk还支持各种日志管理用例,例如日志整合

5种典型 api 攻击及预防建议_seal安全的博客-多极客编程

API 帮助跨多个设备互连多个应用程序或软件系统,定义它们可以发出的调用或请求的种类、调用的方式、应使用的数据格式以及应遵守的约定。API 已经发展成为重要的互连,支持不同应用程序架构之间的通信,促进新服务的更快集成和部署。软件开发程序也依赖 API 来提供服务、平台管理和持续部署。涉及移动设备、云数据系统和微服务设计模式的现代应用程序架构需要使用多个 API 作为网关,以促进不同 Web 应用程

与时俱进「风险系统保障质量之路」非同寻常_京东云官方的博客-多极客编程

作者:梁冬冬风险系统复杂且又庞大,质量如何保障需要我们付出一点一滴的努力来浇灌系统之花一、大促备战,求有序,求稳定:大促是每年例行高考,把人和系统的各项能力激发,衡量系统健壮,容错性;凌晨3点的身影就像一束光,夺目耀眼;今年的大促与往年不同,倡导绿色,节能减排,降本增效,把各种资源做到利用最大化,产生更大的价值,让大促备战产生了一丝温度1)压测备战时间表(统筹整体,从4.21-6.23我们把剧本编

openssl 配置自签名证书_虚拟的现实的博客-多极客编程

1.简介CentOS 7 的最小化安装模式,通过 YUM 更新到最新版本。openssl 使用系统自带版本2.自建CA2.1.生成 CA 私钥openssl genrsa -out /etc/pki/CA/private/cakey.pem 40962.2.生成 CA 证书请求openssl req -new -key /etc/pki/CA/private/cakey.pem -out /etc

openflow协议原理及基本配置-网络测试仪实操_日出的博客-多极客编程

一、OpenFlow协议原理1.OpenFlow技术背景●转发和控制分离是SDN网络的本质特点之一。在SDN网络架构中,控制平面与转发平面分离,网络的管理和状态在逻辑上集中到一起,底层的网络基础从应用中独立出来,由此,网络获得前所未有的可编程、可控制和自动化能力。这使用户可以很容易根据业务需求,建立高度可扩展的弹性网络。要实现SDN网络的转控分离架构,就需要在SDN控制器与数据转发层之间建立一个通