分布式项目线程安全问题(电商扣减库存的安全问题1)

article/2025/9/14 6:56:39

电商减库存存在的安全问题

@Override
public void deductStock(Map<Long, Integer> skuMap) {for (Map.Entry<Long, Integer> entry : skuMap.entrySet()) {Long skuId = entry.getKey();Integer num = entry.getValue();// 查询skuSku sku = getById(skuId);//  判断库存是否充足if (sku.getStock() < num) {// 如果不足,抛出异常throw new LyException(400, "库存不足!");}// 如果充足,扣减库存 update tb_sku set stock = stock - 1, sold = sold + 1  where id = 1Map<String,Object> param = new HashMap<>();param.put("id", skuId);param.put("num", num);getBaseMapper().deductStock(param);}
}

上面这样的操作存在安全风险,因为我们的代码是允许多线程的环境,当多个用户并发访问时,先判断库存是否充足,会出现一种情况:

  • 判断的时候,库存是充足的,但是在减库存之前,有其它线程抢先一步,扣减库存,导致库存不足了,此时就会出现超卖现象!

思路1,同步锁
按照以往的思路,我们应该怎么做?

  • 我们一般需要加同步锁,synchronized,目的是让多线程执行,从而保证线程安全,但是加Synchronized只能保证当前jvm内的线程安全。
  • 如果搭建一个微服务集群,同步锁synchronized就失效了,原因是因为线程所,在进程时会失效,因为每个进程都有自己的锁。
  • 解决多进行安全的问题,必须使用进程锁(分布式锁):
  • 在这里插入图片描述

标题:分布式锁

分布式锁其实可以这样理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥保持一致性
举个不恰当的例子:假设共享资源就是一个房子,里面有各种各样的书,分布式系统就是要进屋子里看书的那个人,分布式锁就是保证这房子里面,只有一个门,并且一次只能一个人进去,而且门只有一把钥匙,然后许多人进去看书,可以,排队,第二个人没有钥匙,那就等着,等第一个人出来,然后你在拿这钥匙进去,就这样以此类推。

二:实现原理

  • 互斥性
  • 保证同一时间只要一个客户端可以拿到锁,也就是可以对共享资源进行操作
  • 安全性
  • 只有加锁的服务才能有解锁的权限,也就是不能让a加的锁,bcd都可以解锁,如果都能解锁,那分布式就没有意义了
  • 可能出现的情况就是a去查询发现持有锁,就在准备解锁,这时候突然a持有的锁过期了,然后b就去获取锁,因为a锁过期,b拿到锁,这时候a继续执行第二部进行解锁如果不加校验,就将b持有的锁就给删除了
    避免死锁
  • 出现死锁就会导致后续的任何服务都拿不到锁,不能在对共享资源进行任何操作了
  • 保证加锁与解锁操作是原子性操作
  • 这个其实属于是实现分布式锁的问题,假设a用redis实现分布式锁
  • 假设枷锁操作,操作步骤分为两步
  • 设置key set(key ,value) 2:给key设置过期时间
  • 假设现在a刚实现set后,程序崩了就导致了没给key设置过期,时间就导致key一直存在就发生了死锁

三.使用redis实现分布式锁

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足一下四个条件

  • 互斥性,在任意时刻,只要一个客户端能持有锁

  • 不会发生死锁,即使有一个客户端持有锁的时候崩溃而没有主动解锁,也能保证后续其他客户端能加锁

  • 具有容错性,只要大部分的redis节点正常允许,客户段就可以加锁和解锁

  • 解铃还须系铃人,枷锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

    可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time),这个set()方法一共有五个形参:

  • 第一个为key,我们使用key来当锁,因为key是唯一的。

  • 第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用UUID.randomUUID().toString()方法生成。

  • 第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;

  • 第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

  • 第五个为time,与第四个参数相呼应,代表key的过期时间。

总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。

心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。

思路2,数据库排它锁

数据库锁简单来说有两种:

  • 共享锁:读操作时会开启共享锁,此时大家都可以查询
  • 排他锁(互斥锁):一般是写操作会开启排它锁,此时其他事务无法获取共享锁或排它锁,会阻塞

要保证安全,必须排它锁
但是我们之前的业务是先查询sku(读),然后判断是否充足,然后减库存(写),这样就会导致多个请求同时查询到一样的库存,减库存还是有安全问题
我们必须在查询时加排他锁,怎么办

  • 可以通过select …for update语法来开启,但时我们要加锁的商品不止一个,此时加锁就是范围锁,甚至时表锁,性能会有较大的影响

思路3:乐观锁

上述思路1和思路2都是枷锁,实现互斥,保证线程的安全,我们称之为悲观锁。

  • 悲观锁:认为线程安全问题一定会发生,因此会加锁保证线程串行执行,从而保证安全,我们为了追求性能,可以使用乐观锁的机制
  • 乐观锁,认为线程安全的问题一定会发生,因为允许许多线程并执行,一般会在执行那一刻进行判断和比较,然后根据是否存在风险来决定是否执行操作
  • 举例说明,可以给库存表加一个字段,叫version
id  stock    version
10	10			1

执行更新前,先查询库存及version
然后判断库存是否充足,如果充足,执行sql

update tb_stock set stock = stock - #{num}, version = version + 1 WHERE id = #{id} AND version = 1

乐观锁就先比较执行的思路,其实就是CAS(compare and set)的思想。
CAS的思想在很多地方都使用,例如

  • JDK的JUC包下的AtomicInteger,AtomicLong等等
  • Redis的watch,也是乐观锁,CAS原理

简化:我们在减库存中,可以用stock来代替version,执行sql时判断stock是否跟自己查询到一样

update tb_stock set stock = stock - 1 WHERE id = 10 AND stock = 10

思路4:继续简化
我们可以不查询库存,直接执行sql,在sql语句中做判断
语句是这样的

update tb_stock set stock = stock - 1 WHERE id = 10 AND stock >= 1

思路5:继续简化
我们最终的目的是,库存不能超卖,不能为负数,因为我们可以设置stock字段为无符号整数,数据库自动会写入数据字段,如果为负,会抛出异常,我们就无需枷锁或任何其他判断了
在这里插入图片描述


http://chatgpt.dhexx.cn/article/VUMhYJrv.shtml

相关文章

分布式项目中 如何保证线程安全问题?-------ZooKeeper

前沿&#xff1a; 上篇文章我们聊到了在解决分布式项目中线程安全问题&#xff0c;提到解决方案还有其他的&#xff0c;那么在此提出 基于 zookeeper 解决分布式项目中的线程安全问题 也是目前市面上比较流行的。做为一个高级开发工程师也是必须要学习的。 ZooKeeper是什么东…

分布式线程安全(redis、zookeeper、数据库)

https://blog.csdn.net/u010963948/article/details/79006572 Q:一个业务服务器&#xff0c;一个数据库&#xff0c;操作&#xff1a;查询用户当前余额&#xff0c;扣除当前余额的3%作为手续费 synchronized lock db lock Q&#xff1a;两个业务服务器&#xff0c;一个数据库&…

分布式集群中如何保证线程安全?

目录 分布式集群中的线程安全问题 解决方法 串行化 分布式锁 Redis如何实现呢&#xff1f; 问题&#xff1a;setnx刚好获取到锁&#xff0c;业务逻辑出现异常&#xff0c;导致锁无法释放 问题&#xff1a;可能会释放其他服务器的锁。 问题&#xff1a;删除操作缺乏原子…

java outlook 发送邮件_基于java使用JavaMail发送邮件

一、邮件的相关概念 邮件协议。主要包括&#xff1a; SMTP协议&#xff1a;Simple Mail Transfer Protocol&#xff0c;即简单邮件传输协议&#xff0c;用于发送电子邮件 POP3协议&#xff1a;Post Office Protocol 3&#xff0c;即邮局协议的第三个版本&#xff0c;用于接收邮…

java 发邮件(有正文,有图片,有附件)

一 需求: 1 java实现邮件发送 2 发送内容: ① 正文: 图片说明和图片 ② 附件一: 图片作为附件发送 ③ 附件二: Excel表格 二 思路: 1首先创建一个 Java 工程&#xff0c;把下载好的 javax.mail.jar 作为类库加入工程 2邮件创建步骤: 配置连接邮件服务器的参数( 邮件服务器SM…

java接收邮件_Java实现邮件收发

一. 准备工作 1. 传输协议 SMTP协议-->发送邮件: 我们通常把处理用户smtp请求(邮件发送请求)的服务器称之为SMTP服务器(邮件发送服务器) POP3协议-->接收邮件: 我们通常把处理用户pop3请求(邮件接收请求)的服务器称之为POP3服务器(邮件接收服务器) 2. 邮件收发原理 闪电…

java发送邮件工具类

1. 普通java实现邮件发送 1.1 创建maven项目&#xff0c;配置pom.xml文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance&qu…

java发送邮件带附件

一、 开启SMTP服务 1.基本都在邮箱设置里&#xff0c;开启后会获得神秘代码&#xff0c;后面有用。 2.记得添加依赖&#xff0c;或者自己添加jar包。 <dependency><groupId>javax.mail</groupId><artifactId>mail</artifactId><version>…

java 邮件模板

邮件发送代码可参照 java 发送邮件 1.情形 邮件发送代码可参照上述&#xff0c;本例只说明如果读取模板文件。公司定义模板较为复杂的情况&#xff0c;可采用此类发送方式 2. 模板 2.1 resource 建立模板 2.2 ftl 模板如下 <p>您好&#xff0c;${name}&#xff0c;您…

使用JAVA实现邮件发送功能

一、准备工作 小编今天以 QQ邮箱 进行演示操作。 想要使用代码操作邮箱发送邮件&#xff0c;需要在邮箱设置中申请开通 POP3/SMTP 服务。 接下来跟着小编的图文一步一步的操作开通吧&#xff01; 1.1 登录网页QQ邮箱&#xff0c;点击页面顶部设置按钮。 1.2 点击后会打开邮箱…

java发送qq邮件

1.登录qq邮箱 1&#xff09;点击设置 2&#xff09;点击账户 3&#xff09;开启第一个服务&#xff0c;我已经开过了 4&#xff09;开启验证&#xff08;让你发送指定内容到某个号码&#xff09;&#xff0c;完成后点击我已发送&#xff0c;就会出现授权码&#xff0c;授权码很…

java实现邮件发送

一.第一步:导入两个jar包。 activation.jar 和 mail.jar, 一定要添加到构建路径(不然找不到包) 两个用于Java发送邮件的jar包-Java文档类资源-CSDN下载 二、创建邮箱工具类:Mail.java import java.util.*; import java.io.*; import javax.mail.*; import javax.m…

Java(81):Java发邮件简单示例

Java Email jar包下载地址&#xff1a;JavaMail API https://www.oracle.com/java/technologies/javamail.html JavaMail 右侧下载&#xff0c;选择jar包下载 API文档参考&#xff1a;JavaMail API documentation https://javaee.github.io/javamail/docs/api/ 或直接引用…

java发送qq邮件_「java发邮件」Java 通过SMTP实现发送QQ邮件 - seo实验室

java发邮件 在Eclipse中创建项目&#xff0c;并把javax.amil.jar和commons-email-1.5,jar复制到项目中 链接&#xff1a;https://pan.baidu.com/s/1sQjA1GEpKi6IJJRGHKxjeA 密码&#xff1a;4ene 添加步骤&#xff1a; 1.首先在项目下创建一个文件夹&#xff0c;保存我们的jar包…

Java发邮件配置-hutool+腾讯企业邮箱

1、技术选型 1.1、hutool工具 1.2、javax.mail 1.3、腾讯企业邮箱2、环境准备 2.1、pom <!--javax.mail--><dependency><groupId>javax.mail</groupId><artifactId>mail</artifactId><version>1.4.7</version></dependen…

Java(83)Java发邮件简单工具类

1、Maven引用 <!-- https://mvnrepository.com/artifact/javax.mail/javax.mail-api --><dependency><groupId>javax.mail</groupId><artifactId>javax.mail-api</artifactId><version>1.6.2</version></dependency><…

java发邮件 动态切换当前发送人

最近项目需要实现一个发送邮件功能&#xff0c;踩了一些坑&#xff0c;最终实现了。 在此写一下心得 开始做的时候一塌糊涂&#xff0c;觉得挺难的&#xff0c;但是做完之后发现其实简单的一批&#xff0c;接下来我就来写一下实现流程。 1、准备好拿来发送邮件的账号&#xf…

Java 发邮件-带附件且正文html格式

入职新公司不久&#xff0c;接到一个给用户发邮件的需求&#xff0c;有两点需要说明的&#xff1a;1&#xff09;正文需要格式化&#xff1b;2&#xff09;需要带附件。 大概了解了一下需求&#xff0c;我马上开始思考&#xff0c;现有项目中是否有类似的接口可以支持&#xf…

java实现发送邮件

本文介绍下java实现邮件的发送&#xff0c;意在网站用户评论时能够及时通知站长和用户评论被回复后能够及时通知用户。 下文介绍下具体实现。 java实现 首先引入springboot的邮箱依赖 <dependency><groupId>org.springframework.boot</groupId><artif…

java邮件发送

一、JavaMail介绍 1、概述 JavaMail是利用现有的邮件账户发送邮件的工具&#xff0c;比如我在网易注册一个邮箱账户&#xff0c;通过JavaMail的操控&#xff0c;我可以不亲自登录网易邮箱&#xff0c;让程序自动的使用网易邮箱发送邮件。这一机制被广泛的用在注册激活和垃圾邮…