业务-超卖问题(易理解)

news/2024/12/11 18:56:09/

mysql悲观锁

使用 MySQL 行锁来解决超卖问题可以通过悲观锁机制来实现。悲观锁在操作数据库时会锁定相应的行,确保在事务完成之前其他事务无法修改这些行。以下是使用 Java 和 MyBatis 实现这一方案的步骤。

实现

1. 数据库表设计

假设我们有一个 products 表,其中包含产品的库存信息:

CREATE TABLE products (id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(255),stock INT
);

2. MyBatis Mapper 接口

定义一个 Mapper 接口,用于与数据库交互。

java">public interface ProductMapper {// 查询产品信息并加锁Product selectProductForUpdate(int productId);// 更新库存int updateStock(@Param("productId") int productId);
}

3. MyBatis XML 配置

在 MyBatis 的 XML 配置文件中,定义 SQL 语句。

<mapper namespace="com.example.mapper.ProductMapper"><select id="selectProductForUpdate" resultType="com.example.model.Product">SELECT id, name, stockFROM productsWHERE id = #{productId}FOR UPDATE</select><update id="updateStock">UPDATE productsSET stock = stock - 1WHERE id = #{productId} AND stock > 0</update></mapper>

4. 服务层实现

在服务层中,使用上述 Mapper 接口进行库存扣减操作。

java">@Service
public class ProductService {@Autowiredprivate ProductMapper productMapper;@Transactionalpublic boolean purchaseProduct(int productId) {// 查询当前产品信息并加锁Product product = productMapper.selectProductForUpdate(productId);if (product == null || product.getStock() <= 0) {return false; // 库存不足或产品不存在}// 更新库存int updateCount = productMapper.updateStock(productId);return updateCount > 0;}
}

5. 事务管理

确保在服务层使用事务管理(如 Spring 的 @Transactional 注解),以确保操作的一致性。FOR UPDATE 会在事务中锁定选定的行,因此必须在事务中使用。

6. 悲观锁的注意事项

  • 性能影响:由于悲观锁会锁定数据库行,这可能会影响系统的并发性能,尤其是在高并发环境下。
  • 死锁风险:如果多个事务以不同的顺序锁定资源,可能会导致死锁。因此,需要小心设计锁的顺序和范围。
  • 锁等待:在高并发情况下,锁等待可能导致响应时间变长。

通过使用 MySQL 的行锁机制,您可以确保在高并发环境下的库存扣减操作是安全的。悲观锁通过在查询时锁定行,防止其他事务进行修改,从而防止超卖问题。

上面的代码,查询商品信息之后,还没有更新库存,另一个事务查询商品信息,那不还是有超卖问题?

你提出的担忧是合理的,但在正确使用悲观锁(FOR UPDATE)和事务管理的情况下,这种情况是可以避免的。让我们进一步详细说明为什么在上述代码中,正确的事务管理可以防止超卖问题。

悲观锁和事务管理的作用

  1. FOR UPDATE 锁定机制

    • 当一个事务在执行 SELECT ... FOR UPDATE 时,数据库会对查询到的行加锁。
    • 这个锁是排他锁(exclusive lock),意味着其他事务在当前事务完成之前无法对这些行进行更新操作。
    • 其他事务尝试执行相同的 SELECT ... FOR UPDATE 或更新操作时会被阻塞,直到当前事务提交或回滚。
  2. 事务的隔离性

    • 通过使用 @Transactional 注解,确保整个方法在一个事务中执行。
    • 在事务内,SELECT ... FOR UPDATE 和后续的 UPDATE 操作都是在锁定的行上进行的。
    • 事务的隔离性保证了在事务内的操作不会被其他事务看到,也不会被其他事务干扰。

详细执行流程

假设有两个并发事务尝试购买同一产品:

  • 事务 A 开始,执行 SELECT ... FOR UPDATE,成功锁定行。

    • 事务 A 检查库存,如果库存足够,继续执行。
    • 事务 A 执行 UPDATE 操作,减少库存。
    • 事务 A 提交,释放锁。
  • 事务 B 开始,试图执行 SELECT ... FOR UPDATE

    • 由于事务 A 锁定了行,事务 B 被阻塞。
    • 事务 B 在事务 A 提交后,才能继续执行。
    • 事务 B 重新检查库存,发现库存不足(因为事务 A 已经扣减库存),因此无法完成扣减操作。

为什么不会超卖

  • 在事务 A 完成之前,事务 B 无法获取锁,也无法读取或修改行。
  • 事务 B 在获取到锁后,重新检查库存,确保操作的安全性。
  • 这种机制确保了在高并发的情况下,库存扣减操作是安全的,不会导致超卖。

结论

通过正确地使用悲观锁和事务管理,上述代码可以有效地防止超卖问题。关键在于 FOR UPDATE 锁定机制确保了在一个事务中读取和更新操作的原子性和隔离性。

mysql乐观锁

使用乐观锁来解决超卖问题是一种常见的做法,尤其是在高并发环境下。乐观锁的核心思想是通过版本号或时间戳来检测并发冲突,而不是像悲观锁那样直接加锁。

实现

实现步骤

  1. 添加版本号字段:在商品表中添加一个版本号字段(如 version),用于记录数据的版本。

  2. 查询商品信息:在查询商品信息时,获取当前的库存和版本号。

  3. 更新库存时检查版本号:在执行更新操作时,使用条件更新语句,确保版本号未改变。

  4. 处理更新失败:如果更新失败(受影响的行数为0),说明在你读取之后,数据已被其他事务修改,需要重新读取并尝试更新。

数据库表设计

假设你的商品表结构如下:

CREATE TABLE product (id INT PRIMARY KEY,name VARCHAR(255),stock INT,version INT
);

MyBatis 配置

假设你已经配置好了 MyBatis,这里只展示与乐观锁相关的部分。

查询商品信息
<select id="selectProductById" parameterType="int" resultType="Product">SELECT id, name, stock, versionFROM productWHERE id = #{id}
</select>
更新商品库存
<update id="updateProductStock" parameterType="Product">UPDATE productSET stock = stock - #{quantity}, version = version + 1WHERE id = #{id} AND version = #{version} AND stock >= #{quantity}
</update>

Java 代码

java">public class ProductService {private ProductMapper productMapper;@Transactionalpublic boolean purchaseProduct(int productId, int quantity) {// 1. 查询商品信息Product product = productMapper.selectProductById(productId);if (product == null || product.getStock() < quantity) {return false; // 库存不足或商品不存在}// 2. 更新库存int updatedRows = productMapper.updateProductStock(productId, quantity, product.getVersion());if (updatedRows == 0) {// 更新失败,说明版本号不匹配,可能是被其他事务修改过return false;}return true; // 购买成功}
}

处理并发冲突

在高并发情况下,可能会出现更新失败的情况,此时可以选择:

  • 重试机制:在更新失败时,重新获取商品信息并再次尝试更新。
  • 通知用户:告知用户当前库存不足或其他信息。

总结

乐观锁通过版本号来检测并发冲突,是一种轻量级的并发控制策略。它适用于读多写少的场景,可以有效减少锁争用,提高系统的并发性能。在使用 MyBatis 实现乐观锁时,关键在于通过条件更新语句确保数据的一致性。

乐观锁是不是可能会导致后进来的请求比先进来的请求先扣减库存?

是的,乐观锁的确可能导致后到的请求比先到的请求先扣减库存。这是因为乐观锁不直接锁定资源,而是依赖于版本号(或时间戳)来检测并发修改。当多个请求几乎同时到达时,谁先完成更新取决于具体的执行顺序和竞争情况。

可能的情形

  1. 请求 A请求 B 几乎同时到达:

    • 请求 A 读取了当前库存和版本号。
    • 请求 B 也读取了相同的库存和版本号。
  2. 请求 B 先完成更新:

    • 请求 B 成功更新库存并增加版本号。
  3. 请求 A 尝试更新:

    • 请求 A 尝试更新时发现版本号不匹配,更新失败。
    • 请求 A 需要重新读取最新的库存和版本号,可能需要重试。

这种情况在乐观锁中是正常的,因为乐观锁的设计理念是允许一定程度的并发,只有在提交时才检查冲突。因此,乐观锁适合于读多写少的场景,或者对更新的顺序要求不严格的场合。

如何应对这种情况

  1. 重试机制:当更新失败时,可以实现一个重试机制,重新获取最新的库存和版本号,再次尝试更新。

  2. 通知用户:如果库存不足或者更新失败,可以通知用户当前操作无法完成,让用户选择其他操作或稍后再试。

  3. 业务逻辑调整:如果业务逻辑对顺序要求严格,可以考虑在应用层进行排队处理,或者结合其他机制(如分布式锁)来确保顺序。

总结

乐观锁的设计初衷是为了提高并发性能,允许多个事务同时进行操作,但在提交时检查冲突。因此,它可能导致后到的请求先完成更新。在某些应用场景下,这种行为是可以接受的,但如果业务逻辑对顺序有严格要求,需要额外的机制来保证顺序性。

redis分布式锁

使用 Redis 分布式锁来解决超卖问题是一种常见的方案。Redis 提供了一种简单而高效的方式来实现分布式锁。下面是一个使用 Java 和 MyBatis 实现 Redis 分布式锁的基本示例。

环境准备

  1. 依赖库:确保你的项目中包含 Redis 客户端库,比如 Jedis 或 Redisson。
  2. Redis 服务器:确保你的环境中有一个可用的 Redis 服务器。

使用 Jedis 实现 Redis 分布式锁

这是一个简单的示例,展示如何使用 Jedis 来实现分布式锁,并结合 MyBatis 执行数据库操作。

<!-- Maven 依赖 -->
<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>4.0.1</version>
</dependency>
<dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.7</version>
</dependency>
java">import redis.clients.jedis.Jedis;
import java.util.Collections;public class RedisLockUtil {private static final String LOCK_SUCCESS = "OK";private static final String SET_IF_NOT_EXIST = "NX";private static final String SET_WITH_EXPIRE_TIME = "PX";/*** 尝试获取分布式锁** @param jedis      Redis客户端* @param lockKey    锁* @param requestId  请求标识* @param expireTime 超期时间* @return 是否获取成功*/public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);return LOCK_SUCCESS.equals(result);}/*** 释放分布式锁** @param jedis     Redis客户端* @param lockKey   锁* @param requestId 请求标识* @return 是否释放成功*/public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {String script ="if redis.call('get', KEYS[1]) == ARGV[1] then " +"return redis.call('del', KEYS[1]) " +"else " +"return 0 " +"end";Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));return 1L == (Long) result;}
}

结合 MyBatis 进行库存扣减

java">import org.apache.ibatis.session.SqlSession;
import redis.clients.jedis.Jedis;public class InventoryService {private Jedis jedis;private SqlSession sqlSession;public InventoryService(Jedis jedis, SqlSession sqlSession) {this.jedis = jedis;this.sqlSession = sqlSession;}public boolean reduceStock(int productId, int quantity) {String lockKey = "lock:product:" + productId;String requestId = String.valueOf(Thread.currentThread().getId());int expireTime = 10000; // 10秒try {// 获取锁if (RedisLockUtil.tryGetDistributedLock(jedis, lockKey, requestId, expireTime)) {// 查询当前库存int stock = sqlSession.selectOne("selectStock", productId);if (stock >= quantity) {// 执行库存扣减sqlSession.update("reduceStock", new Object[]{productId, quantity});sqlSession.commit();return true;}}return false;} finally {// 释放锁RedisLockUtil.releaseDistributedLock(jedis, lockKey, requestId);}}
}

注意事项

  1. 锁的超时:合理设置锁的超时时间,以防止死锁。
  2. 锁的粒度:根据业务需求设置锁的粒度,避免锁的范围过大导致性能问题。
  3. 错误处理:在实际应用中,需添加更多的错误处理和日志记录,以便排查问题。

这个示例只是一个基本实现,具体的实现细节可能需要根据你的业务需求进行调整。


http://www.ppmy.cn/news/1554297.html

相关文章

C++ - map,set

set和map介绍 map 和 set 是C STL 中提供的容器, map 和 set 的底层是基于红黑树来实现的. set 是一个包含唯一元素 (key) 的集合&#xff0c;不允许有重复的元素. map 是一个键值对 (key - value) 的集合, 每一个键 (key) 都是唯一的. map 的key - value键值对是通过 pair 来…

互联网、物联网的相关标准

互联网的相关标准 网络通信协议&#xff1a; HTTP&#xff08;Hypertext Transfer Protocol&#xff09;&#xff1a;用于在网络中传输文本、图像、音频和视频等数据的协议。它基于请求-响应模型&#xff0c;客户端发送请求给服务器&#xff0c;服务器返回响应。HTTPS&a…

zookeeper 搭建集群

基础的java 环境先安好&#xff0c;选择3台虚拟机 ip 不一样 机器应为奇数个 zookeeper 奇数个节点实际上是(2*n-1) 比偶数台机器少一台解决成本,并且能够满足 zookeeper 集群过半选举leader 的规则 # 3台虚拟机 将zookeeper 解压到服务器上 #在 conf/ 目录下 找到zoo_s…

Spring Boot 中 WebClient 的实践详解

在现代微服务架构中&#xff0c;服务之间的通信至关重要。Spring Boot 提供了 WebClient&#xff0c;作为 RestTemplate 的替代方案&#xff0c;用于执行非阻塞式的 HTTP 请求。本文将详细讲解 WebClient 的实践&#xff0c;包括配置、使用场景以及常见的优化策略&#xff0c;帮…

巧用缓存:高效实现基于 read4 的文件读取方法

文章目录 摘要描述题目描述要求read4 方法定义read 方法定义 题解答案题解代码题解代码分析示例测试及结果示例测试代码示例运行结果 时间复杂度空间复杂度总结关于我们 摘要 本篇文章将探讨一道经典的编程题&#xff1a;通过 read4 方法实现读取 n 个字符的功能。我们将详细介…

node(multer)上传文件

node(multer)上传文件 from表单上传文件 前端代码 import React from react; import { Form, Button, Upload, message } from antd; import { UploadOutlined } from ant-design/icons; import axios from axios;const FileUploadForm () > {const onFinish async (va…

Three.js曲线篇 8.管道漫游

目录 创建样条曲线 创建管道 透视相机漫游 完整代码 大家不要被这个“管道漫游”这几个字所蒙骗了&#xff0c;学完后大家就知道这个知识点有多脏了。我也是误入歧途&#xff0c;好奇了一下“管道漫游”。好了&#xff0c;现在就给大家展示一下为啥这个只是点脏了。 我也废话…

用友U8+ API接口使用教程

前言 U8和其他的公开的开放API接口有一些差异&#xff0c;他是需要先对接的到代理服务器&#xff0c;通过代理服务器进行对接&#xff0c;所以只要保证U8能上网就能对接&#xff0c;和畅捷通T的模式有点类似 流程&#xff1a; 注册成为开发者&#xff08;用于创建用友U8 API应…