分布式锁和mysql事物扣库存_浅谈库存扣减和锁

先说场景:

物品W现在库存剩余1个,  用户P1,P2同时购买.则只有1人能购买成功.(前提是不允许超卖)

秒杀也是类似的情况, 只有1件商品,N个用户同时抢购,只有1人能抢到..

这里不谈秒杀设计,不谈使用队列等使请求串行化,就谈下怎么用锁来保证数据正确.

常见的实现方案有以下几种:

1.代码同步, 例如使用 synchronized ,lock 等同步方法

2.不查询,直接更新  update table set surplus = (surplus - buyQuantity) where id = xx and (surplus - buyQuantity) > 0

3.使用CAS, update table set surplus = aa where id = xx and version = y

4.使用数据库锁, select xx for update

5.使用分布式锁(zookeeper,redis等)

下面就针对这几种方案来分析下;

1.代码同步, 例如使用 synchronized ,lock 等同步方法

面试的时候,我经常会问这个问题,很大一部分人都会回答用这个方案来实现.

伪代码如下:

public synchronized void buy(String productName, Integer buyQuantity) {

// 其他校验...

// 校验剩余数量

Product product = 从数据库查询出记录;

if (product.getSurplus < buyQuantity) {

return "库存不足";

}

// set新的剩余数量

product.setSurplus(product.getSurplus() - quantity);

// 更新数据库

update(product);

// 记录日志...

// 其他业务...

}

在方法声明加上synchronized关键字,实现同步,这样2个用户同时购买,到buy方法时候同步执行,第2个用户执行的时候,会库存不足.

嗯.. 看着挺合理的,以前我也是这么干的. 所以现在碰到别人这样回答,我就会在心里默默的想.小伙子你是没踩过这坑啊.

先说下这个方案的前提配置:

1).使用spring 声明式事务管理

2).事务传播机制使用默认的(PROPAGATION_REQUIRED)

3).项目分层为controller-service-dao 3层, 事务管理在service层

这个方案不可行,主要是因为以下几点:

1).synchronized 作用范围是单个jvm实例, 如果做了集群,分布式等,就没用了

2).synchronized是作用在对象实例上的,如果不是单例,则多个实例间不会同步(这个一般用spring管理bean,默认就是单例)

3).单个jvm时,synchronized也不能保证多个数据库事务的隔离性. 这与代码中的事务传播级别,数据库的事务隔离级别,加锁时机等相关.

3-1).先说隔离级别,常用的是 Read Committed 和 Repeatable Read ,另外2种不常用就不说了

3-1-1)RR(Repeatable Read)级别.mysql默认的是RR,事务开启后,不会读取到其他事务提交的数据

根据前面的前提,我们知道在buy方法时会开启事务.

假设现在有线程T1,T2同时执行buy方法.假设T1先执行,T2等待.

spring的事务开启和提交等是通过aop(代理)实现的,所以执行buy方法前,就会开启事务.

这时候T1,T2是两个事务,当T1执行完后,T2执行,读取不到T1提交的数据,所以会出问题.

3-1-2).RC(Read Committed)级别.事务开启后,可以读取到其他事务提交的数据

看起来这个级别可以解决上面的问题.T2执行时,可以读取到T1提交的结果.

但是问题是,T2执行的时候, T1的事务提交了吗?

事务和锁的流程如下

1.开启事务(aop)

2.加锁(进入synchronized方法)

3.释放锁(退出synchronized方法)

4.提交事务(aop)

可以看出是先释放锁,再提交事务.所以T2执行查询,可能还是未读到T1提交的数据,还会出问题

3-2).根据3-1中的问题,发现主要矛盾是事务开启和提交的时机与加锁解锁时机不一致.有小伙伴们可能就想到了解决方案.

3-2-1).在事务开启前加锁,事务提交后解锁.

确实是可以,这相当于事务串行化.抛开性能不谈,来谈谈怎么实现.

如果使用默认的事务传播机制,那么要保证事务开启前加锁,事务提交后解锁,就需要把加锁,解锁放在controller层.

这样就有个潜在问题,所有操作库存的方法,都要加锁,而且要是同一把锁,写起来挺累的.

而且这样还是不能跨jvm.

3-2-2).将查询库存,扣减库存这2步操作,单独提取个方法,单独使用事务,并且事务隔离级别设置为RC.

这个其实和上面的3-2-1异曲同工,最终都是讲加解锁放在了事务开启提交外层.

比较而言优点是入口少了. controller不用处理.

缺点除了上面的不能跨jvm,还有就是 单独的这个方法,需要放到另外的service类中.

因为使用spring,同一个bean的内部方法调用,是不会被再次代理的,所以配置的单独事务等需要放到另外的service bean 中

2.不查询,直接更新

看完第一种方案,有小伙伴就说了. 你说的那么复杂,那么多问题,不就是因为查询的数据不是最新的吗?

我们不查询,直接更新不就行啦.

伪代码如下:

public synchronized void buy(String productName, Integer buyQuantity) {

// 其他校验...

int 影响行数 = update table set surplus = (surplus - buyQuantity) where id = 1 ;

if (result < 0) {

return "库存不足";

}

// 记录日志...

// 其他业务...

}

测试后发现库存变成-1了, 继续完善下

public synchronized void buy(String productName, Integer buyQuantity) {

// 其他校验...

int 影响行数 = update table set surplus = (surplus - buyQuantity) where id = 1 and (surplus - buyQuantity) > 0 ;

if (result < 0) {

return "库存不足";

}

// 记录日志...

// 其他业务...

}

测试后,功能OK;

这样确实可以实现,不过有一些其他问题:

1). 不具备通用性,例如add操作

2). 库存操作一般要记录操作前后的数量等,这样没法记录

3). 其他...

但是根据这个方案,可以引出方案3.

3.使用CAS, update table set surplus = aa where id = xx and yy = y

CAS是指compare/check and swap/set 意思都差不多,不必太纠结是哪个单词

我们将上面的sql修改一下:

int 影响行数 = update table set surplus = newQuantity where id = 1 and surplus = oldQuantity ;

这样,线程T1执行完后,线程T2去更新,影响行数=0,则说明数据被更新, 重新查询判断执行.伪代码如下:

public void buy(String productName, Integer buyQuantity) {

// 其他校验...

Product product = getByDB(productName);

int 影响行数 = update table set surplus = (surplus - buyQuantity) where id = 1 and surplus = 查询的剩余数量 ;

while (result == 0) {

product = getByDB(productName);

if (查询的剩余数量 > buyQuantity) {

影响行数 = update table set surplus = (surplus - buyQuantity) where id = 1 and surplus = 查询的剩余数量 ;

} else {

return "库存不足";

}

}

// 记录日志...

// 其他业务...

}

看到重新查询几个字,小伙伴们应该就又想到事务隔离级别问题了.

没错,所以上面代码中的getByDB方法,必须单独事务(注意同一个bean内单独事务不生效哦),而且数据库的事务隔离级别必须是RC,

否则上面的代码就会是死循环了.

上面的方案,可能会出现一个CAS中经典问题. ABA的问题.

ABA是指:

线程T1 查询,库存剩余  100

线程T2 查询,库存剩余  100

线程T1 执行subupdate t set surplus = 90 where id = x and surplus = 100;

线程T3 查询, 库存剩余 90

线程T3 执行add  update t set surplus = 100 where id = x and surplus = 90;

线程T2 执行subupdate t set surplus = 90 where id = x and surplus = 100;

这里线程T2执行的时候,库存的100已经不是查询到的100了,但是对于这个业务是不影响的.

一般的设计中CAS会使用version来控制.

update t set surplus = 90 ,version = version+1 where id = x and version = oldVersion ;

这样,每次更新version在原基础上+1,就可以了.

使用CAS要注意几点,

1)失败重试次数,是否需要限制

2)失败重试对用户是透明的

4.使用数据库锁, select xx for update

方案3种的cas,是乐观锁的实现, 而select for udpate 则是悲观锁. 在查询数据的时候,就将数据锁住.

伪代码如下:

public void buy(String productName, Integer buyQuantity) {

// 其他校验...

Product product = select * from table where name = productName for update;

if (查询的剩余数量 > buyQuantity) {

影响行数 = update table set surplus = (surplus - buyQuantity) where name = productName ;

} else {

return "库存不足";

}

// 记录日志...

// 其他业务...

}

线程T1 进行sub , 查询库存剩余 100

线程T2 进行sub , 这时候,线程T1事务还未提交,线程T2阻塞,直到线程T1事务提交或回滚才能查询出结果.

所以线程T2查询出的一定是最新的数据.相当于事务串行化了,就解决了数据一致性问题.

对于select for update,需要注意的有2点.

1) 统一入口:所有库存操作都需要统一使用 select for update ,这样才会阻塞, 如果另外一个方法还是普通的select, 是不会被阻塞的

2) 加锁顺序:如果有多个锁,那么加锁顺序要一致,否则会出现死锁.

5.使用分布式锁(zookeeper,redis等)

使用分布式锁,原理和方案1种的synchronized是一样的.只不过synchronized的flag只有jvm进程内可见,而分布式锁的flag则是全局可见.方案4种的select for update 的flag 也是全局可见.

分布式锁的实现方案有很多:基于redis,基于zookeeper,基于数据库等等.前面一篇博客写了基于redis的简易实现

基于redis setnx的简易分布式锁

需要注意,使用分布式锁和synchronized锁有同样的问题,就是锁和事务的顺序,这个在方案1里面已经讲过.不再重复.

做个简单总结:

方案1: synchronized等jvm内部锁不适合用来保证数据库数据一致性,不能跨jvm

方案2: 不具备通用性,不能记录操作前后日志

方案3: 推荐使用.但是如果数据竞争激烈,则自动重试次数会急剧上升,需要注意.

方案4: 推荐使用.最简单的方案,但是如果事务过大,会有性能问题.操作不当,会有死锁问题

方案5: 和方案1类似,只是能跨jvm

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

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

相关文章

【POJ - 2631 】Roads in the North(树的直径)

题干&#xff1a; Building and maintaining roads among communities in the far North is an expensive business. With this in mind, the roads are build such that there is only one route from a village to a village that does not pass through some other village…

齐博php百度编辑器上传图片_php版百度编辑器ueditor怎样给上传图片自动添加水印?...

百度ueditor是广泛使用的所见即所得图文排版编辑插件&#xff0c;功能比较完善&#xff0c;美中不足就是不支持自动加水印。万维景盛工程师搜集到php版ueditor自动加水印的教程&#xff0c;希望对大家有帮助。1.打开ueditor目录下的php目录下的config.json 文件在上传配置项添加…

【HDU - 1285】确定比赛名次 (拓扑排序)

题干&#xff1a; 有N个比赛队&#xff08;1<N<500&#xff09;&#xff0c;编号依次为1&#xff0c;2&#xff0c;3&#xff0c;。。。。&#xff0c;N进行比赛&#xff0c;比赛结束后&#xff0c;裁判委员会要将所有参赛队伍从前往后依次排名&#xff0c;但现在裁判委…

mysql dql_Mysql中的DQL查询语句

欢迎进入Linux社区论坛&#xff0c;与200万技术人员互动交流 >>进入 Mysql中的DQL查询语句 1、查询所有列 --查询 学生 表所有记录(行) select *from 学生 --带条件的查询 select *from 学生 where 年龄19 2、查询指定的列 --查询 所有人的姓名和性别 select 姓名,性欢迎…

【POJ - 1028】 Web Navigation( 栈 or 模拟队列 )

题干&#xff1a; Standard web browsers contain features to move backward and forward among the pages recently visited. One way to implement these features is to use two stacks to keep track of the pages that can be reached by moving backward and forward. …

python循环post请求_循环post请求太多

我正在做一个scrapy spider&#xff0c;我必须发送一个post请求循环才能转到下一个页面&#xff0c;问题是它只发送一个post请求。querystring更改每个页面的元素“currentPage”&#xff0c;因此我必须为每个页面更改此键的值并发送post。但是&#xff0c;正如我之前所说&…

【POJ - 2387】 Til the Cows Come Home(单源最短路Dijkstra算法)

题干&#xff1a; Bessie is out in the field and wants to get back to the barn to get as much sleep as possible before Farmer John wakes her for the morning milking. Bessie needs her beauty sleep, so she wants to get back as quickly as possible. Farmer Jo…

递归Java_递归的Java实现

递归是一种应用非常广泛的算法(或者编程技巧)。递归求解问题的分解过程&#xff0c;去的过程叫“递”&#xff0c;回来的过程叫“归”。递归需要满足的三个条件&#xff1a;1. 一个问题的解可以分解为几个子问题的解&#xff1b;2. 这个问题与分解之后的子问题&#xff0c;除了…

【HDU - 2112】 HDU Today(dijkstra单源最短路 + map转换)

题干&#xff1a; HDU Today Time Limit : 15000/5000ms (Java/Other) Memory Limit : 32768/32768K (Java/Other) Total Submission(s) : 11 Accepted Submission(s) : 5 Problem Description 经过锦囊相助&#xff0c;海东集团终于度过了危机&#xff0c;从此&#…

JAVA线程并发数量控制_线程同步工具(二)控制并发访问多个资源

声明&#xff1a;本文是《 Java 7 Concurrency Cookbook》的第三章&#xff0c; 作者&#xff1a; Javier Fernndez Gonzlez 译者&#xff1a;郑玉婷控制并发访问多个资源在并发访问资源的控制中&#xff0c;你学习了信号量(semaphores)的基本知识。在上个指南&#xff0c;你实…

*【51nod - 1459】迷宫游戏(记录双向权值的Dijkstra单源最短路)

题干&#xff1a; 你来到一个迷宫前。该迷宫由若干个房间组成&#xff0c;每个房间都有一个得分&#xff0c;第一次进入这个房间&#xff0c;你就可以得到这个分数。还有若干双向道路连结这些房间&#xff0c;你沿着这些道路从一个房间走到另外一个房间需要一些时间。游戏规定…

算法--背包九讲(详细讲解+代码)

背包九讲 目录 第一讲 01背包问题 第二讲 完全背包问题 第三讲 多重背包问题 第四讲 混合三种背包问题 第五讲 二维费用的背包问题 第六讲 分组的背包问题 第七讲 有依赖的背包问题 第八讲 泛化物品 第九讲 背包问题问法的变化 附&#xff1a;USACO中的背包问题 前…

java中白盒测试用例_基于JAVA开发的中国象棋游戏的开发与研究白盒测试用例.doc...

中国象棋白盒测试用例文件状态当前版本V1.0草稿作 者梁世聪完成日期2012/6/17文档模板SSP-VER-T13-V1.0密 级变更历史版本完成日期变更记录作者批准签字V1.02012/6/17无梁世聪梁世聪目 录目录1 目的12 范围13 被测模块列表14 模块逻辑结构14.1 模块逻辑结构图14.2 模块功能定义…

【POJ - 1502】MPI Maelstrom(Dijkstra单源最短路--求一点到其余个点的最小值的最大值)

题干&#xff1a; BIT has recently taken delivery of their new supercomputer, a 32 processor Apollo Odyssey distributed shared memory machine with a hierarchical communication subsystem. Valentine McKees research advisor, Jack Swigert, has asked her to ben…

java 强制清除缓存_IDEA强制清除Maven缓存的方法示例

重新导入依赖的常见方式下面图中的刷新按钮&#xff0c;在我的机器上&#xff0c;并不能每次都正确导入pom.xml中写的依赖项&#xff0c;而是导入之前pom.xml的依赖(读了缓存中的pom.xml)。当然除了这些&#xff0c;还可以下面这样&#xff1a;存在的问题上面虽然是重新导入Mav…

ACM算法--spfa算法--最短路算法

求单源最短路的SPFA算法的全称是&#xff1a;Shortest Path Faster Algorithm。 SPFA算法是西南交通大学段凡丁于1994年发表的。 从名字我们就可以看出&#xff0c;这种算法在效率上一定有过人之处。 很多时候&#xff0c;给定的图存在负权边&#xff0c;这时类似…

knn算法python理解与预测_理解KNN算法

KNN主要包括训练过程和分类过程。在训练过程上&#xff0c;需要将训练集存储起来。在分类过程中&#xff0c;将测试集和训练集中的每一张图片去比较&#xff0c;选取差别最小的那张图片。如果数据集多&#xff0c;就把训练集分成两部分&#xff0c;一小部分作为验证集(假的测试…

【POJ-3259】 Wormholes(判负环,spfa算法)

题干&#xff1a; While exploring his many farms, Farmer John has discovered a number of amazing wormholes. A wormhole is very peculiar because it is a one-way path that delivers you to its destination at a time that is BEFORE you entered the wormhole! Eac…

【HihoCoder - 1550】顺序三元组(思维)

题干&#xff1a; 给定一个长度为N的数组A[A1, A2, ... AN]&#xff0c;已知其中每个元素Ai的值都只可能是1, 2或者3。 请求出有多少下标三元组(i, j, k)满足1 ≤ i < j < k ≤ N且Ai < Aj < Ak。 Input 第一行包含一个整数N 第二行包含N个整数A1, A2, ... …

joptionpane java_Java JOptionPane

Java JOptionPane1 Java JOptionPane的介绍JOptionPane类用于提供标准对话框&#xff0c;例如消息对话框&#xff0c;确认对话框和输入对话框。这些对话框用于显示信息或从用户那里获取输入。JOptionPane类继承了JComponent类。2 Java JOptionPane的声明public class JOptionPa…