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
)和事务管理的情况下,这种情况是可以避免的。让我们进一步详细说明为什么在上述代码中,正确的事务管理可以防止超卖问题。
悲观锁和事务管理的作用
-
FOR UPDATE
锁定机制:- 当一个事务在执行
SELECT ... FOR UPDATE
时,数据库会对查询到的行加锁。 - 这个锁是排他锁(exclusive lock),意味着其他事务在当前事务完成之前无法对这些行进行更新操作。
- 其他事务尝试执行相同的
SELECT ... FOR UPDATE
或更新操作时会被阻塞,直到当前事务提交或回滚。
- 当一个事务在执行
-
事务的隔离性:
- 通过使用
@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乐观锁
使用乐观锁来解决超卖问题是一种常见的做法,尤其是在高并发环境下。乐观锁的核心思想是通过版本号或时间戳来检测并发冲突,而不是像悲观锁那样直接加锁。
实现
实现步骤
-
添加版本号字段:在商品表中添加一个版本号字段(如
version
),用于记录数据的版本。 -
查询商品信息:在查询商品信息时,获取当前的库存和版本号。
-
更新库存时检查版本号:在执行更新操作时,使用条件更新语句,确保版本号未改变。
-
处理更新失败:如果更新失败(受影响的行数为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 实现乐观锁时,关键在于通过条件更新语句确保数据的一致性。
乐观锁是不是可能会导致后进来的请求比先进来的请求先扣减库存?
是的,乐观锁的确可能导致后到的请求比先到的请求先扣减库存。这是因为乐观锁不直接锁定资源,而是依赖于版本号(或时间戳)来检测并发修改。当多个请求几乎同时到达时,谁先完成更新取决于具体的执行顺序和竞争情况。
可能的情形
-
请求 A 和 请求 B 几乎同时到达:
- 请求 A 读取了当前库存和版本号。
- 请求 B 也读取了相同的库存和版本号。
-
请求 B 先完成更新:
- 请求 B 成功更新库存并增加版本号。
-
请求 A 尝试更新:
- 请求 A 尝试更新时发现版本号不匹配,更新失败。
- 请求 A 需要重新读取最新的库存和版本号,可能需要重试。
这种情况在乐观锁中是正常的,因为乐观锁的设计理念是允许一定程度的并发,只有在提交时才检查冲突。因此,乐观锁适合于读多写少的场景,或者对更新的顺序要求不严格的场合。
如何应对这种情况
-
重试机制:当更新失败时,可以实现一个重试机制,重新获取最新的库存和版本号,再次尝试更新。
-
通知用户:如果库存不足或者更新失败,可以通知用户当前操作无法完成,让用户选择其他操作或稍后再试。
-
业务逻辑调整:如果业务逻辑对顺序要求严格,可以考虑在应用层进行排队处理,或者结合其他机制(如分布式锁)来确保顺序。
总结
乐观锁的设计初衷是为了提高并发性能,允许多个事务同时进行操作,但在提交时检查冲突。因此,它可能导致后到的请求先完成更新。在某些应用场景下,这种行为是可以接受的,但如果业务逻辑对顺序有严格要求,需要额外的机制来保证顺序性。
redis分布式锁
使用 Redis 分布式锁来解决超卖问题是一种常见的方案。Redis 提供了一种简单而高效的方式来实现分布式锁。下面是一个使用 Java 和 MyBatis 实现 Redis 分布式锁的基本示例。
环境准备
- 依赖库:确保你的项目中包含 Redis 客户端库,比如 Jedis 或 Redisson。
- 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);}}
}
注意事项
- 锁的超时:合理设置锁的超时时间,以防止死锁。
- 锁的粒度:根据业务需求设置锁的粒度,避免锁的范围过大导致性能问题。
- 错误处理:在实际应用中,需添加更多的错误处理和日志记录,以便排查问题。
这个示例只是一个基本实现,具体的实现细节可能需要根据你的业务需求进行调整。