在高并发压力测试期间,库存扣减操作偶发性失败,错误率约2.3%。日志中出现以下异常:
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException:
Deadlock found when trying to get lock; try restarting transaction
应用层捕获到死锁异常后进行了事务回滚,但部分用户反馈"库存充足却提示下单失败"。
首先开启MySQL死锁日志记录(需在my.cnf中配置):
SET GLOBAL innodb_print_all_deadlocks = ON;
从MySQL错误日志中提取到以下关键信息:
*** (1) TRANSACTION:
TRANSACTION 123456789, ACTIVE 0 sec updating rows
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 11264, 3 row lock(s), undo log entries 1
MySQL thread id 111, OS thread handle 0x7f8b1c0b1700, query id 2222 10.0.0.1 user updating
UPDATE product_stock SET quantity = quantity - 1 WHERE product_id = 10086 AND quantity > 0
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 333 page no 4 n bits 80 index `idx_product_id` of table `db`.`product_stock` trx id 123456789 lock_mode X locks rec but not gap
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 333 page no 4 n bits 80 index `idx_product_id` of table `db`.`product_stock` trx id 123456789 lock_mode X locks rec but not gap waiting
*** (2) TRANSACTION:
TRANSACTION 123456790, ACTIVE 0 sec updating rows
mysql tables in use 1, locked 1
5 lock struct(s), heap size 11264, 3 row lock(s), undo log entries 1
MySQL thread id 112, OS thread handle 0x7f8b1c0b2700, query id 2223 10.0.0.2 user updating
UPDATE product_stock SET quantity = quantity - 1 WHERE product_id = 10086 AND quantity > 0
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 333 page no 4 n bits 80 index `idx_product_id` of table `db`.`product_stock` trx id 123456790 lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 333 page no 4 n bits 80 index `idx_product_id` of table `db`.`product_stock` trx id 123456790 lock_mode X locks rec but not gap waiting
检查相关表结构:
CREATE TABLE `product_stock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`product_id` bigint(20) NOT NULL,
`quantity` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
原始扣减库存的代码逻辑:
@Transactional(rollbackFor = Exception.class)
public boolean reduceStock(Long productId, Integer quantity) {
// 1. 检查库存是否充足
Integer currentStock = stockMapper.getStockByProductId(productId);
if (currentStock < quantity) {
throw new RuntimeException("库存不足");
}
// 2. 更新库存
int rows = stockMapper.reduceStock(productId, quantity);
return rows > 0;
}
对应的Mapper SQL:
<update id="reduceStock">
UPDATE product_stock
SET quantity = quantity - #{quantity}
WHERE product_id = #{productId}
AND quantity >= #{quantity}
</update>
修改UPDATE语句,将库存检查合并到UPDATE中:
<update id="reduceStock">
UPDATE product_stock
SET quantity = quantity - #{quantity}
WHERE product_id = #{productId}
AND quantity >= #{quantity}
</update>
业务代码调整为:
@Transactional(rollbackFor = Exception.class)
public boolean reduceStock(Long productId, Integer quantity) {
int rows = stockMapper.reduceStock(productId, quantity);
if (rows == 0) {
// 更新失败说明库存不足
throw new RuntimeException("库存不足");
}
return true;
}
@Transactional(rollbackFor = Exception.class)
public boolean reduceStockWithLock(Long productId, Integer quantity) {
// 1. 加锁查询
ProductStock stock = stockMapper.selectForUpdate(productId);
if (stock.getQuantity() < quantity) {
throw new RuntimeException("库存不足");
}
// 2. 更新库存
stockMapper.updateStock(stock.getId(), stock.getQuantity() - quantity);
return true;
}
对应的SQL:
<select id="selectForUpdate" resultType="ProductStock">
SELECT * FROM product_stock
WHERE product_id = #{productId}
FOR UPDATE
</select>
增加版本号字段:
ALTER TABLE product_stock ADD COLUMN version int NOT NULL DEFAULT 0;
public boolean reduceStockWithVersion(Long productId, Integer quantity) {
int retryTimes = 3;
for (int i = 0; i < retryTimes; i++) {
ProductStock stock = stockMapper.getStock(productId);
if (stock.getQuantity() < quantity) {
throw new RuntimeException("库存不足");
}
int rows = stockMapper.updateStockWithVersion(
productId, quantity, stock.getVersion());
if (rows > 0) {
return true;
}
}
throw new RuntimeException("并发更新失败,请重试");
}
实施解决方案后,我们进一步采取了以下措施:
经过以上优化,死锁问题完全解决,系统在同等压力下运行稳定,库存扣减错误率降至0.001%以下。
启示:MySQL死锁问题往往源于并发控制不当,需要从数据库设计、SQL编写、业务逻辑等多个层面进行综合考量才能有效解决。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。