架构与思维:漫谈高并发业务的CAS及ABA

1 高并发场景下的难题

1.1 典型支付场景

这是最经典的场景。支付过程,要先查询买家的账户余额,然后计算商品价格,最后对买家进行进行扣款,像这类的分布式操作,如果是并发量低的情况下完全没有问题的,但如果是并发扣款,那可能就有一致性问题。在高并发的分布式业务场景中,类似这种 "查询+修改" 的操作很可能导致数据的不一致性。

1.2 在线下单场景

同理,买家在电商平台下单,往往会涉及到两个动作,一个是扣库存,第二个是更新订单状态,库存和订单一般属于不同的数据库,需要使用分布式事务保证数据一致性。

1.3 跨行转账场景

跨行转账问题也是一个典型的分布式事务,用户A同学向B同学的账户转账500,要先进行A同学的账户-500,然后B同学的账户+500,既然是 不同的银行,涉及不同的业务平台,为了保证这两个操作步骤的一致,数据一致性方案必然要被引入。

2 CAS方案

分布式CAS(Compare-and-Swap)模式就是一种无锁化思想的应用,它通过无锁算法实现线程间对共享资源的无冲突访问,既保证性能高效,有保证数据的强一致性,避免了上面集中问题的产生。CAS模式包含三个基本操作数:内存地址V、旧的预期值A和要修改的新值B。在更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

我们以 1.1节典型支付场景 作为例子分析(参考下图):

  • 初始余额为 800

  • 业务1和业务2同时查询余额为800

  • 业务1执行购买操作,扣减去100,结果是700,这是新的余额。理论上只有在原余额为800时,扣减的Action才能执行成功。

  • 业务2执行生活缴费操作(比如自动交电费),原余额800,扣减去200,结果是600,这是新的余额。理论上只有在原余额为800时,扣减的Action才能执行成功。可实际上,这个时候数据库中的金额已经变为600了,所以业务2的并发扣减不应该成功。

根据上面的CAS原理,在Swap更新余额的时候,加上Compare条件,跟初始读取的余额比较,只有初始余额不变时,才允许Swap成功,这是一种常见的降低读写锁冲突,保证数据一致性的方法。

3 引出ABA问题

在CAS(Compare-and-Swap)操作中,ABA问题是一个常见的挑战。这边假设三个操作数------内存位置(V)、预期原值(A)和新值(B)。ABA问题是指当某个线程读取一个共享变量V的值为A,之后准备将其更新为B时,另一个线程可能已经将其从A改为了B,然后又改回了A。此时,当前线程仍认为V的值是原始的A,因此CAS操作会将V的值更新为B,但实际上V的值已经被其他线程改变过。

它有如下危害:

1. 数据一致性受损,并导致业务逻辑错误在复杂的业务逻辑中,共享变量的值往往代表了某种业务状态或条件。ABA问题可能导致这些状态或条件被意外地改变,从而引发业务逻辑错误,如库存超卖、资金重复发放等。★ 以下的图详细描述了ABA是怎么导致库存逻辑出错的:

2. 难以调试与定位ABA问题通常发生在多线程环境下,且其触发条件较为隐蔽。因此,当系统出现由ABA问题导致的异常时,往往难以快速定位问题原因,增加了调试的复杂性和时间成本。

4 不同维度的处理方式

ABA出现的原因,是CAS的过程中,只关注Value值的校验。但是忽略了这个值还是不是之前的那个值,可以参考上面的库存图例。所以某些情况下,Value虽然相同,却已经不是原来的数据了。

解决方案:CAS不能只比对 Value,还必须确保的是原来的数据,才能修改成功。一般的做法是,给 Value 设置一个Version(版本号),用来比对,一个数据一个版本,每次数据变化的时候版本跟随变化,这样的话就不会随随便便修改成功。

4.1 应用程序层

Java中的java.util.concurrent.atomic包提供了解决ABA问题的工具类。在Go语言中,通常使用sync/atomic包提供的原子操作来处理并发问题,并引入版本号或时间戳的概念。示例代码如下:

复制代码
type ValueWithVersion struct {  
	Value     int32  
	Version   int32  
}  
  
var sharedValue atomic.Value // 使用atomic.Value来存储ValueWithVersion的指针  
  
func updateValue(newValue, newVersion int32) bool {  
	current := sharedValue.Load().(*ValueWithVersion)  
	if current.Value == newValue && current.Version == newVersion {  
		// CAS操作:只有当前值和版本号都匹配时,才更新值  
		newValueWithVersion := &ValueWithVersion{Value: newValue, Version: newVersion + 1}  
		sharedValue.Store(newValueWithVersion)  
		return true  
	}  
	return false  
}  

4.2 数据层

1、CAS策略

复制代码
update stock set num_val=$num_new_val where sid=$sid and num_val=$num_old_val

2、CAS策略+Version,避免ABA问题

复制代码
# 这边注意,有了version,就没必要再比较old_val了
update stock set num=$num_new_val, version=$version_new where sid=$sid and version=$version_old

5 总结

  1. 高并发下的难题:支付、下单、跨行转账

  2. CAS方案以及引发的ABA问题

  3. 不同维度的处理方式:应用层、数据层

文章转载自: ++Hello-Brand++

原文链接: https://www.cnblogs.com/wzh2010/p/18031157

体验地址: 引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

相关推荐
虫小宝2 小时前
返利软件的分布式缓存架构:Redis集群在高并发场景下的优化策略
分布式·缓存·架构
一水鉴天2 小时前
整体设计 之 绪 思维导图引擎 之 引 认知系统 之 引 认知系统 之 序 认知元架构 之6 拼句 之1 (豆包助手 之8)
架构·认知科学
纪元A梦2 小时前
Redis最佳实践——安全与稳定性保障之高可用架构详解
redis·安全·架构
Dontla2 小时前
流行的前端架构与后端架构介绍(Architecture)
前端·架构
熊文豪3 小时前
KingbaseES读写分离集群架构解析
数据库·架构·kingbasees·金仓数据库·电科金仓
往事随风去3 小时前
别再纠结了!IM场景下WebSocket和MQTT的正确选择姿势,一文讲透!
后端·websocket·架构
爱读源码的大都督4 小时前
为什么Spring 6中要把synchronized替换为ReentrantLock?
java·后端·架构
一水鉴天4 小时前
整体设计 之 绪 思维导图引擎 之 引 认知系统 之 引 认知系统 之 序 认知元架构 之 元宇宙:三种“即是”逻辑与数据安全措施的适配(豆包助手 之10)
架构·认知科学
虫小宝5 小时前
淘宝客app的API网关设计:认证授权与流量控制策略
java·分布式·架构
码农小伙6 小时前
单体到微服务拆分方案
微服务·架构