并发减库存,怎么保证不超卖?

简介: 秒杀的场景有很多,比如:抢购、抢票、抢红包等等。总之,就是在极短时间内有大量的请求。我们都知道,这种系统设计的大方向就是限流,即通过层层过滤,最终只让相对较少的请求进入到核心业务处理层。

秒杀的场景有很多,比如:抢购、抢票、抢红包等等。总之,就是在极短时间内有大量的请求。


我们都知道,这种系统设计的大方向就是限流,即通过层层过滤,最终只让相对较少的请求进入到核心业务处理层。


这里不谈秒杀设计,不谈使用队列等使请求串行化,就谈下怎么用锁来保证数据正确,就是已经到减库存那一步了,在这一步中如果保证不超卖。


用队列的话,可以是Java自动的队列,也可以用Redis的LPUSH RPOP


重点是扣减库存


我理解,主要的方式是加锁。加锁有两个层面:一个是程序层面,另一个是数据库层面。


image.png


分布式锁

这种场景下应该很少有人用Java自带的锁(比如:synchronized、Lock)吧,因为它们只在同一个JVM内有效,如果你的应用部署了多台的话,应该用分布式锁。


关于Redis分布式锁,可以阅读这篇文章:Spring Boot Redis 实现分布式锁


其实,这里加分布式锁就是将多线程请求转成单线程请求,因为每次只有一个线程获得锁并执行,其余都被阻塞了。


这里有一点需要注意,就是当你应用了事务的话可能会存在问题,请看下面的代码


image.png


可能有人会这样写,第一眼看起来挺好的,没问题啊,但仔细实践证明是由问题的。


我们知道,mysql默认的事务隔离级别是REPEATABLE-READ


关于事务隔离级别这块儿,可在公众号Java技术栈搜索阅读。


在这种隔离级别下,同一个事务中多次读取,返回的数据是一样的


同时,Spring声明式事务默认的传播特性REQUIRED


image.png


Spring声明式事务是Spring AOP最好的例子,Spring是通过AOP代理的方式来实现事务的,也就是说在调用reduceStock()方法的之前就已经开启了事务。


那么,在并发情况下可能会存在这样的情况,假设线程T1和T2都执行到这里,于是它们都开启了事务S1和S2,T1先执行,T2后执行,


由于T2执行的时候事务已经创建了,根据隔离级别,这个时候事务S2读取不到S1已提交的数据,于是就会出现T1和T2读取到的值是一样的,即T2读取的是T1更新前的库存数据。


关于这一点,大家可以自己写个代码测试一下,下面是一段参考:


image.png


鉴于这种情况呢,可以将库存放到Redis中,我们直接读写Redis,这样可以避免受数据库事务的影响,当然这也会带来新的问题,不再讨论。


数据库乐观锁

CAS(compare and swap)比较并交换

在Java中,一个线程想修改某个变量的值,那么第一步是将变量的值从主内存中读取到自己工作内存中,然后修改,最后写回主内存。这个过程可以归结为:读取——修改——写入,在写回内存的时候可能当前内存中那个值已经发生了变化,这个时候如果继续写则会覆盖别人的数据,只有当内存中的那个值和它修改之前读到的那个值一样,才可以写入。这个跟数据库是一样的。Java中通过Unsafe中compareAndSwapObject这样的方法类实现的,它直接调用CPU指令。

image.png



数据库中也有CAS,乐观锁就是一种CAS


经典的乐观锁实现:

数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。


更新的时候带上版本号,只有当前版本号与更新之前查询时的版本一致,才会更新


image.png



ABA问题

这里顺便多提一句,CAS中的ABA问题


假设,原先的值是A,线程-1读取到的值是A,想把它改成D,但是在此期间,有可能其他线程已经多次修改过这个值,只不过最后当线程-1准备将A改成D的时候,它发现恰好还是A,以为没有人改过,其实这时候的A已经不是原来的A了。


也就是说,尽管修改之前做了比较,当然,仍然会出现如下情况:


image.png


产生原因

ABA问题导致的原因,是CAS过程中只简单进行了“值”的校验,有些情况下,“值”虽然相同,却已经不是原来的数据了。


优化方向

CAS不能只比对“值”,还必须确保的是原来的数据,才能修改成功。


常见实践

“版本号”的比对,一个数据一个版本,版本变化,即使值相同,也不应该修改成功。


不仅要关注值,还要关注是不是原来的对象


image.png


基于“值”的CAS乐观锁,可能导致ABA问题。CAS乐观锁,必须保证修改时的“此数据”就是“彼数据”,应该由“值”比对,优化为“版本号”比对。

image.png


image.pngimage.png

相关文章
|
网络协议 Linux C++
Linux C/C++ 开发(学习笔记十二 ):TCP服务器(并发网络编程io多路复用epoll)
Linux C/C++ 开发(学习笔记十二 ):TCP服务器(并发网络编程io多路复用epoll)
367 0
|
存储 并行计算 算法
C++进程间通信之共享内存
C++进程间通信之共享内存
1357 0
|
前端开发 IDE 编译器
HBuilderX 最新安装使用教程,附详细图解,持续更新
HBuilderX 最新安装使用教程,附详细图解,持续更新
|
负载均衡 监控 Go
使用Golang框架构建分布式系统
本文探讨了使用Golang构建分布式系统的方法。Golang因其高效、简洁的语法和并发支持成为理想的开发语言。文中列举了几个常用的Golang框架,如Echo、Gin、gRPC和NATS等,并强调了服务拆分、通信机制、负载均衡等构建分布式系统的关键要素。通过选择合适的框架,遵循需求分析、技术选型、服务设计等步骤,开发者可以构建出高性能、高可用和可扩展的系统。此外,文中还提供了一个使用gRPC和etcd的简单代码案例来说明实现过程。
914 4
|
12月前
|
存储 NoSQL Cloud Native
MongoDB云原生化:为企业开发注入高效动力
MongoDB云原生化为企业开发注入高效动力,分为三部分:1. 介绍阿里云和MongoDB的服务;2. 阿里云MongoDB解决自建模型痛点的功能,包括隔离性、海量数据处理、弹性能力及运维操作优化;3. 客户案例展示。通过云原生架构,MongoDB实现了灵活的扩展、高效的备份恢复和快速的回档能力,显著提升了企业的业务迭代速度和数据管理效率。典型客户如吉比特、莉莉丝、掌阅等受益于这些功能,实现了更稳定和高效的数据库服务。
|
开发工具 iOS开发 git
ios 开发环境常见问题合集
ios 开发环境常见问题合集
527 1
|
JSON 编解码 Rust
Rust 模块化:深入了解 Rust 中的代码组织
关键字`mod、pub、crate、self、super、use`都表示什么含义,如何使用?
322 1
|
存储
Leetcode循环队列(数组实现及链表实现)
Leetcode循环队列(数组实现及链表实现)
202 0
|
XML 监控 Java
使用IDEA社区版如何创建SpringBoot项目?
使用IDEA社区版如何创建SpringBoot项目?
|
设计模式 算法 Java
Go 语言实现模版方法设计模式
在Go语言中,可以使用接口和组合来实现模板方法模式。模板方法模式是一种行为设计模式,它定义了一个算法的骨架,将一些步骤的实现延迟到子类中。下面是一个示例代码,演示了如何在Go中实现模板方法模式
411 0