feat:热点库存问题
This commit is contained in:
311
docs/高并发库存方案/01-乐观锁方案.md
Normal file
311
docs/高并发库存方案/01-乐观锁方案.md
Normal file
@@ -0,0 +1,311 @@
|
||||
# 乐观锁方案(当前实现)
|
||||
|
||||
## 方案概述
|
||||
|
||||
乐观锁是一种无锁的并发控制机制,通过版本号(version)来检测数据是否被其他事务修改。适用于读多写少、冲突率较低的场景。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### 工作流程
|
||||
|
||||
```
|
||||
用户A读取库存 (version=1, stock=100)
|
||||
用户B读取库存 (version=1, stock=100)
|
||||
↓
|
||||
用户A扣减库存 (version=1 → 2, stock=99) ✓ 成功
|
||||
用户B扣减库存 (version=1 → 2, stock=99) ✗ 失败(version不匹配)
|
||||
↓
|
||||
用户B重试:读取最新库存 (version=2, stock=99)
|
||||
用户B扣减库存 (version=2 → 3, stock=98) ✓ 成功
|
||||
```
|
||||
|
||||
### 关键机制
|
||||
|
||||
通过 MyBatis-Plus 的 `@Version` 注解实现:
|
||||
|
||||
```java
|
||||
@Version
|
||||
private Integer version;
|
||||
```
|
||||
|
||||
更新时自动添加 version 条件:
|
||||
```sql
|
||||
UPDATE inventory
|
||||
SET remaining_stock = remaining_stock - #{quantity},
|
||||
version = version + 1,
|
||||
update_time = NOW()
|
||||
WHERE inventory_code = #{inventoryCode}
|
||||
AND version = #{version}
|
||||
```
|
||||
|
||||
## 代码实现
|
||||
|
||||
### 实体类定义
|
||||
|
||||
```java
|
||||
@Data
|
||||
@TableName("inventory")
|
||||
public class InventoryDO implements Serializable {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String inventoryCode;
|
||||
private String routeCode;
|
||||
private String scheduleCode;
|
||||
private Integer totalStock;
|
||||
private Integer soldStock;
|
||||
private Integer remainingStock;
|
||||
|
||||
// 乐观锁版本号
|
||||
@Version
|
||||
private Integer version;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
```
|
||||
|
||||
### Repository 实现
|
||||
|
||||
```java
|
||||
@Repository
|
||||
public class InventoryRepositoryImpl implements InventoryRepository {
|
||||
|
||||
@Autowired
|
||||
private InventoryMapper inventoryMapper;
|
||||
|
||||
@Override
|
||||
public boolean updateWithVersion(InventoryDO inventory) {
|
||||
// MyBatis-Plus 自动处理 version 字段
|
||||
int rows = inventoryMapper.updateById(inventory);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InventoryDO findByInventoryCode(String inventoryCode) {
|
||||
return inventoryMapper.selectOne(
|
||||
Wrappers.lambdaQuery(InventoryDO.class)
|
||||
.eq(InventoryDO::getInventoryCode, inventoryCode)
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service 实现
|
||||
|
||||
```java
|
||||
private Result<Boolean> lockFixedScheduleInventory(InventoryOperationDTO dto) {
|
||||
// 查询班次
|
||||
FixedScheduleDO schedule = fixedScheduleRepository.getOne(
|
||||
Wrappers.lambdaQuery(FixedScheduleDO.class)
|
||||
.eq(FixedScheduleDO::getScheduleCode, dto.getScheduleCode())
|
||||
);
|
||||
|
||||
if (schedule == null) {
|
||||
return Result.fail("SCHEDULE_NOT_FOUND", "班次不存在");
|
||||
}
|
||||
|
||||
// 查询库存(包含 version)
|
||||
InventoryDO inventory = inventoryRepository.findByInventoryCode(
|
||||
schedule.getInventoryCode()
|
||||
);
|
||||
|
||||
if (inventory == null) {
|
||||
return Result.fail("INVENTORY_NOT_FOUND", "库存不存在");
|
||||
}
|
||||
|
||||
Integer beforeQty = inventory.getRemainingStock();
|
||||
|
||||
// 扣减库存
|
||||
inventory.setRemainingStock(inventory.getRemainingStock() - dto.getQuantity());
|
||||
inventory.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
// 使用乐观锁更新(自动检查 version)
|
||||
boolean success = inventoryRepository.updateWithVersion(inventory);
|
||||
|
||||
if (!success) {
|
||||
return Result.fail("INVENTORY_LOCK_FAILED",
|
||||
"库存锁定失败,可能库存不足或并发冲突");
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
saveInventoryLog(
|
||||
OrderTypeEnum.FIXED.getCode(),
|
||||
dto.getScheduleCode(),
|
||||
null,
|
||||
schedule.getInventoryCode(),
|
||||
InventoryOperationTypeEnum.LOCK.getCode(),
|
||||
dto.getQuantity(),
|
||||
beforeQty,
|
||||
beforeQty - dto.getQuantity(),
|
||||
dto.getOrderNo(),
|
||||
"锁定库存"
|
||||
);
|
||||
|
||||
return Result.success(true);
|
||||
}
|
||||
```
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE `inventory` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`inventory_code` VARCHAR(64) NOT NULL COMMENT '库存编码',
|
||||
`route_code` VARCHAR(64) NOT NULL COMMENT '线路编码',
|
||||
`schedule_code` VARCHAR(64) COMMENT '班次编码',
|
||||
`total_stock` INT NOT NULL DEFAULT 0 COMMENT '总库存',
|
||||
`sold_stock` INT NOT NULL DEFAULT 0 COMMENT '已售数量',
|
||||
`remaining_stock` INT NOT NULL DEFAULT 0 COMMENT '剩余库存',
|
||||
`version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_inventory_code` (`inventory_code`),
|
||||
KEY `idx_route_code` (`route_code`),
|
||||
KEY `idx_schedule_code` (`schedule_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';
|
||||
```
|
||||
|
||||
## 性能分析
|
||||
|
||||
### 优势
|
||||
|
||||
- **实现简单**:只需添加 version 字段和注解
|
||||
- **无锁开销**:不会阻塞其他事务
|
||||
- **适合低冲突**:冲突率低时性能优秀
|
||||
- **数据库压力小**:不需要持有锁
|
||||
|
||||
### 劣势
|
||||
|
||||
- **高并发失败率高**:冲突时需要重试
|
||||
- **重试开销**:失败后需要重新查询和更新
|
||||
- **用户体验差**:高并发时大量用户看到"库存不足"
|
||||
|
||||
### 性能指标
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| QPS | ~1000 |
|
||||
| 响应时间 | 20-50ms |
|
||||
| 冲突率 | <10% 时表现良好 |
|
||||
| 成功率 | 冲突率 10% 时约 90% |
|
||||
|
||||
## 适用场景
|
||||
|
||||
### 推荐使用
|
||||
|
||||
- 普通电商场景(非秒杀)
|
||||
- 并发量 < 1000 QPS
|
||||
- 库存数量较大(冲突率低)
|
||||
- 对实时性要求不高
|
||||
|
||||
### 不推荐使用
|
||||
|
||||
- 秒杀场景(并发量极高)
|
||||
- 库存数量极少(冲突率高)
|
||||
- 对成功率要求极高
|
||||
- 需要严格的顺序保证
|
||||
|
||||
## 优化建议
|
||||
|
||||
### 客户端重试机制
|
||||
|
||||
```java
|
||||
public Result<Boolean> lockInventoryWithRetry(InventoryOperationDTO dto) {
|
||||
int maxRetries = 3;
|
||||
int retryCount = 0;
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
Result<Boolean> result = lockInventory(dto);
|
||||
|
||||
if (result.isSuccess()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 只有并发冲突才重试
|
||||
if ("INVENTORY_LOCK_FAILED".equals(result.getCode())) {
|
||||
retryCount++;
|
||||
// 指数退避
|
||||
try {
|
||||
Thread.sleep(50 * (1 << retryCount));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 其他错误直接返回
|
||||
return result;
|
||||
}
|
||||
|
||||
return Result.fail("RETRY_EXHAUSTED", "重试次数已用尽");
|
||||
}
|
||||
```
|
||||
|
||||
### 前置库存检查
|
||||
|
||||
```java
|
||||
// 先检查库存,避免无效更新
|
||||
if (inventory.getRemainingStock() < dto.getQuantity()) {
|
||||
return Result.fail("INSUFFICIENT_INVENTORY", "库存不足");
|
||||
}
|
||||
|
||||
// 再执行乐观锁更新
|
||||
boolean success = inventoryRepository.updateWithVersion(inventory);
|
||||
```
|
||||
|
||||
### 监控告警
|
||||
|
||||
```java
|
||||
// 记录冲突率
|
||||
if (!success) {
|
||||
metricsService.incrementCounter("inventory.conflict",
|
||||
"inventoryCode", inventory.getInventoryCode());
|
||||
|
||||
// 冲突率超过阈值告警
|
||||
double conflictRate = calculateConflictRate(inventory.getInventoryCode());
|
||||
if (conflictRate > 0.3) {
|
||||
alertService.sendAlert("库存冲突率过高: " + conflictRate);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 故障处理
|
||||
|
||||
### 版本号溢出
|
||||
|
||||
```java
|
||||
// 定期重置 version(在低峰期执行)
|
||||
@Scheduled(cron = "0 0 3 * * ?")
|
||||
public void resetVersion() {
|
||||
inventoryMapper.update(null,
|
||||
Wrappers.lambdaUpdate(InventoryDO.class)
|
||||
.set(InventoryDO::getVersion, 0)
|
||||
.gt(InventoryDO::getVersion, 1000000)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 死锁检测
|
||||
|
||||
虽然乐观锁不会产生数据库死锁,但需要注意:
|
||||
|
||||
```java
|
||||
// 避免在事务中多次更新同一库存
|
||||
@Transactional
|
||||
public void processOrder(OrderDTO order) {
|
||||
// ✗ 错误:同一事务中多次更新
|
||||
lockInventory(order.getItem1());
|
||||
lockInventory(order.getItem2());
|
||||
|
||||
// ✓ 正确:批量更新或分开事务
|
||||
lockInventoryBatch(Arrays.asList(order.getItem1(), order.getItem2()));
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
乐观锁方案是当前系统的实现方式,适合中低并发场景。实现简单,维护成本低,但在高并发场景下会出现大量冲突。如果业务增长导致并发量增加,建议升级到 Redis 原子操作方案。
|
||||
448
docs/高并发库存方案/02-悲观锁方案.md
Normal file
448
docs/高并发库存方案/02-悲观锁方案.md
Normal file
@@ -0,0 +1,448 @@
|
||||
# 悲观锁方案
|
||||
|
||||
## 方案概述
|
||||
|
||||
悲观锁通过数据库的 `SELECT FOR UPDATE` 语句在查询时锁定记录,确保同一时刻只有一个事务能修改数据。适用于冲突率高、对一致性要求严格的场景。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### 工作流程
|
||||
|
||||
```
|
||||
事务A: SELECT ... FOR UPDATE (获取行锁)
|
||||
↓
|
||||
事务B: SELECT ... FOR UPDATE (等待锁释放)
|
||||
↓
|
||||
事务A: UPDATE ... (修改数据)
|
||||
↓
|
||||
事务A: COMMIT (释放锁)
|
||||
↓
|
||||
事务B: 获取锁,继续执行
|
||||
```
|
||||
|
||||
### 锁机制
|
||||
|
||||
```sql
|
||||
-- 查询并锁定记录
|
||||
SELECT * FROM inventory
|
||||
WHERE inventory_code = 'INV001'
|
||||
FOR UPDATE;
|
||||
|
||||
-- 其他事务会被阻塞,直到当前事务提交或回滚
|
||||
```
|
||||
|
||||
## 代码实现
|
||||
|
||||
### Mapper 接口
|
||||
|
||||
```java
|
||||
@Mapper
|
||||
public interface InventoryMapper extends BaseMapper<InventoryDO> {
|
||||
|
||||
/**
|
||||
* 查询并锁定库存(悲观锁)
|
||||
*/
|
||||
@Select("SELECT * FROM inventory WHERE inventory_code = #{inventoryCode} FOR UPDATE")
|
||||
InventoryDO selectForUpdate(@Param("inventoryCode") String inventoryCode);
|
||||
}
|
||||
```
|
||||
|
||||
### Repository 实现
|
||||
|
||||
```java
|
||||
@Repository
|
||||
public class InventoryRepositoryImpl implements InventoryRepository {
|
||||
|
||||
@Autowired
|
||||
private InventoryMapper inventoryMapper;
|
||||
|
||||
@Override
|
||||
public InventoryDO findByInventoryCodeForUpdate(String inventoryCode) {
|
||||
return inventoryMapper.selectForUpdate(inventoryCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update(InventoryDO inventory) {
|
||||
int rows = inventoryMapper.updateById(inventory);
|
||||
return rows > 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service 实现
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class PessimisticLockInventoryService {
|
||||
|
||||
@Autowired
|
||||
private InventoryRepository inventoryRepository;
|
||||
|
||||
@Autowired
|
||||
private InventoryLogRepository inventoryLogRepository;
|
||||
|
||||
@Autowired
|
||||
private FixedScheduleRepository fixedScheduleRepository;
|
||||
|
||||
/**
|
||||
* 锁定库存(悲观锁)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Boolean> lockInventory(InventoryOperationDTO dto) {
|
||||
// 查询班次
|
||||
FixedScheduleDO schedule = fixedScheduleRepository.getOne(
|
||||
Wrappers.lambdaQuery(FixedScheduleDO.class)
|
||||
.eq(FixedScheduleDO::getScheduleCode, dto.getScheduleCode())
|
||||
);
|
||||
|
||||
if (schedule == null) {
|
||||
return Result.fail("SCHEDULE_NOT_FOUND", "班次不存在");
|
||||
}
|
||||
|
||||
// 查询并锁定库存(SELECT FOR UPDATE)
|
||||
InventoryDO inventory = inventoryRepository.findByInventoryCodeForUpdate(
|
||||
schedule.getInventoryCode()
|
||||
);
|
||||
|
||||
if (inventory == null) {
|
||||
return Result.fail("INVENTORY_NOT_FOUND", "库存不存在");
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
if (inventory.getRemainingStock() < dto.getQuantity()) {
|
||||
return Result.fail("INSUFFICIENT_INVENTORY", "库存不足");
|
||||
}
|
||||
|
||||
Integer beforeQty = inventory.getRemainingStock();
|
||||
|
||||
// 扣减库存
|
||||
inventory.setRemainingStock(inventory.getRemainingStock() - dto.getQuantity());
|
||||
inventory.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
// 更新库存(不需要版本号检查)
|
||||
inventoryRepository.update(inventory);
|
||||
|
||||
// 记录日志
|
||||
saveInventoryLog(
|
||||
schedule.getInventoryCode(),
|
||||
InventoryOperationTypeEnum.LOCK.getCode(),
|
||||
dto.getQuantity(),
|
||||
beforeQty,
|
||||
beforeQty - dto.getQuantity(),
|
||||
dto.getOrderNo(),
|
||||
"锁定库存"
|
||||
);
|
||||
|
||||
LogUtil.info("库存锁定成功(悲观锁), inventoryCode={}, quantity={}, orderNo={}",
|
||||
schedule.getInventoryCode(), dto.getQuantity(), dto.getOrderNo());
|
||||
|
||||
return Result.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放库存
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Boolean> releaseInventory(InventoryOperationDTO dto) {
|
||||
FixedScheduleDO schedule = fixedScheduleRepository.getOne(
|
||||
Wrappers.lambdaQuery(FixedScheduleDO.class)
|
||||
.eq(FixedScheduleDO::getScheduleCode, dto.getScheduleCode())
|
||||
);
|
||||
|
||||
if (schedule == null) {
|
||||
return Result.fail("SCHEDULE_NOT_FOUND", "班次不存在");
|
||||
}
|
||||
|
||||
// 查询并锁定库存
|
||||
InventoryDO inventory = inventoryRepository.findByInventoryCodeForUpdate(
|
||||
schedule.getInventoryCode()
|
||||
);
|
||||
|
||||
if (inventory == null) {
|
||||
return Result.fail("INVENTORY_NOT_FOUND", "库存不存在");
|
||||
}
|
||||
|
||||
Integer beforeQty = inventory.getRemainingStock();
|
||||
|
||||
// 增加库存
|
||||
inventory.setRemainingStock(inventory.getRemainingStock() + dto.getQuantity());
|
||||
inventory.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
inventoryRepository.update(inventory);
|
||||
|
||||
// 记录日志
|
||||
saveInventoryLog(
|
||||
schedule.getInventoryCode(),
|
||||
InventoryOperationTypeEnum.RELEASE.getCode(),
|
||||
dto.getQuantity(),
|
||||
beforeQty,
|
||||
beforeQty + dto.getQuantity(),
|
||||
dto.getOrderNo(),
|
||||
"释放库存"
|
||||
);
|
||||
|
||||
return Result.success(true);
|
||||
}
|
||||
|
||||
private void saveInventoryLog(String inventoryCode, String operationType,
|
||||
Integer quantity, Integer beforeQty,
|
||||
Integer afterQty, String orderNo, String remark) {
|
||||
InventoryLogDO log = new InventoryLogDO();
|
||||
log.setInventoryCode(inventoryCode);
|
||||
log.setOperationType(operationType);
|
||||
log.setQuantity(quantity);
|
||||
log.setBeforeQty(beforeQty);
|
||||
log.setAfterQty(afterQty);
|
||||
log.setOrderNo(orderNo);
|
||||
log.setRemark(remark);
|
||||
log.setCreateTime(LocalDateTime.now());
|
||||
|
||||
inventoryLogRepository.save(log);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 数据库配置
|
||||
|
||||
### 事务隔离级别
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/travel?useUnicode=true&characterEncoding=utf8
|
||||
hikari:
|
||||
# 连接池配置
|
||||
maximum-pool-size: 50
|
||||
minimum-idle: 10
|
||||
connection-timeout: 30000
|
||||
idle-timeout: 600000
|
||||
max-lifetime: 1800000
|
||||
|
||||
jpa:
|
||||
properties:
|
||||
hibernate:
|
||||
# 使用 READ_COMMITTED 隔离级别
|
||||
connection:
|
||||
isolation: 2
|
||||
```
|
||||
|
||||
### 索引优化
|
||||
|
||||
```sql
|
||||
-- 确保 inventory_code 有索引,避免锁表
|
||||
CREATE UNIQUE INDEX uk_inventory_code ON inventory(inventory_code);
|
||||
|
||||
-- 查看锁等待情况
|
||||
SELECT * FROM information_schema.innodb_locks;
|
||||
SELECT * FROM information_schema.innodb_lock_waits;
|
||||
```
|
||||
|
||||
## 性能分析
|
||||
|
||||
### 优势
|
||||
|
||||
- **强一致性**:完全避免超卖
|
||||
- **无需重试**:不会出现并发冲突
|
||||
- **逻辑简单**:不需要版本号管理
|
||||
- **适合高冲突**:冲突率高时性能优于乐观锁
|
||||
|
||||
### 劣势
|
||||
|
||||
- **性能较低**:锁等待导致吞吐量下降
|
||||
- **可能死锁**:多个资源加锁顺序不当
|
||||
- **阻塞严重**:高并发时大量请求排队
|
||||
- **数据库压力大**:长时间持有连接
|
||||
|
||||
### 性能指标
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| QPS | ~500 |
|
||||
| 响应时间 | 50-100ms |
|
||||
| 锁等待时间 | 10-50ms |
|
||||
| 死锁概率 | <0.1% |
|
||||
|
||||
## 适用场景
|
||||
|
||||
### 推荐使用
|
||||
|
||||
- 库存数量极少(如限量商品)
|
||||
- 对一致性要求极高
|
||||
- 并发量适中(< 500 QPS)
|
||||
- 不能容忍任何超卖
|
||||
|
||||
### 不推荐使用
|
||||
|
||||
- 高并发秒杀场景
|
||||
- 对响应时间要求严格
|
||||
- 库存数量大,冲突率低
|
||||
- 需要极高吞吐量
|
||||
|
||||
## 优化建议
|
||||
|
||||
### 减少锁持有时间
|
||||
|
||||
```java
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Boolean> lockInventory(InventoryOperationDTO dto) {
|
||||
// ✗ 错误:事务中执行耗时操作
|
||||
InventoryDO inventory = inventoryRepository.findByInventoryCodeForUpdate(code);
|
||||
sendNotification(dto); // 耗时操作
|
||||
inventory.setRemainingStock(inventory.getRemainingStock() - dto.getQuantity());
|
||||
inventoryRepository.update(inventory);
|
||||
|
||||
// ✓ 正确:先完成数据库操作,再执行耗时操作
|
||||
InventoryDO inventory = inventoryRepository.findByInventoryCodeForUpdate(code);
|
||||
inventory.setRemainingStock(inventory.getRemainingStock() - dto.getQuantity());
|
||||
inventoryRepository.update(inventory);
|
||||
// 事务提交后再发送通知
|
||||
TransactionSynchronizationManager.registerSynchronization(
|
||||
new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
sendNotification(dto);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 避免死锁
|
||||
|
||||
```java
|
||||
// ✗ 错误:不同顺序加锁可能死锁
|
||||
// 事务A: 锁定 INV001 → 锁定 INV002
|
||||
// 事务B: 锁定 INV002 → 锁定 INV001
|
||||
|
||||
// ✓ 正确:统一加锁顺序
|
||||
public void lockMultipleInventories(List<String> inventoryCodes) {
|
||||
// 按 inventoryCode 排序,保证加锁顺序一致
|
||||
Collections.sort(inventoryCodes);
|
||||
|
||||
for (String code : inventoryCodes) {
|
||||
InventoryDO inventory = inventoryRepository.findByInventoryCodeForUpdate(code);
|
||||
// 处理库存
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 锁超时设置
|
||||
|
||||
```java
|
||||
@Transactional(
|
||||
rollbackFor = Exception.class,
|
||||
timeout = 5 // 5秒超时
|
||||
)
|
||||
public Result<Boolean> lockInventory(InventoryOperationDTO dto) {
|
||||
// 如果 5 秒内无法获取锁,抛出异常
|
||||
InventoryDO inventory = inventoryRepository.findByInventoryCodeForUpdate(
|
||||
dto.getInventoryCode()
|
||||
);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 监控锁等待
|
||||
|
||||
```java
|
||||
@Aspect
|
||||
@Component
|
||||
public class LockMonitorAspect {
|
||||
|
||||
@Around("@annotation(Transactional)")
|
||||
public Object monitorLock(ProceedingJoinPoint pjp) throws Throwable {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
return pjp.proceed();
|
||||
} finally {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
// 锁等待时间超过阈值告警
|
||||
if (duration > 1000) {
|
||||
LogUtil.warn("事务执行时间过长: {}ms, method={}",
|
||||
duration, pjp.getSignature());
|
||||
|
||||
// 发送告警
|
||||
alertService.sendAlert("数据库锁等待时间过长: " + duration + "ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 故障处理
|
||||
|
||||
### 死锁检测与恢复
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class DeadlockRecoveryService {
|
||||
|
||||
@Autowired
|
||||
private InventoryService inventoryService;
|
||||
|
||||
public Result<Boolean> lockInventoryWithDeadlockRetry(InventoryOperationDTO dto) {
|
||||
int maxRetries = 3;
|
||||
int retryCount = 0;
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
return inventoryService.lockInventory(dto);
|
||||
|
||||
} catch (DeadlockLoserDataAccessException e) {
|
||||
retryCount++;
|
||||
LogUtil.warn("检测到死锁,重试第 {} 次", retryCount);
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
return Result.fail("DEADLOCK_RETRY_EXHAUSTED", "死锁重试次数已用尽");
|
||||
}
|
||||
|
||||
// 随机延迟后重试,避免再次死锁
|
||||
try {
|
||||
Thread.sleep(new Random().nextInt(100));
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result.fail("LOCK_FAILED", "库存锁定失败");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 锁超时处理
|
||||
|
||||
```sql
|
||||
-- 查看当前锁等待
|
||||
SELECT
|
||||
r.trx_id waiting_trx_id,
|
||||
r.trx_mysql_thread_id waiting_thread,
|
||||
r.trx_query waiting_query,
|
||||
b.trx_id blocking_trx_id,
|
||||
b.trx_mysql_thread_id blocking_thread,
|
||||
b.trx_query blocking_query
|
||||
FROM information_schema.innodb_lock_waits w
|
||||
INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id
|
||||
INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;
|
||||
|
||||
-- 杀死阻塞的事务
|
||||
KILL <blocking_thread>;
|
||||
```
|
||||
|
||||
## 与乐观锁对比
|
||||
|
||||
| 维度 | 悲观锁 | 乐观锁 |
|
||||
|------|--------|--------|
|
||||
| 并发控制 | 加锁阻塞 | 版本检查 |
|
||||
| 冲突处理 | 排队等待 | 失败重试 |
|
||||
| 性能 | 低(~500 QPS) | 中(~1000 QPS) |
|
||||
| 一致性 | 强一致 | 强一致 |
|
||||
| 实现复杂度 | 简单 | 简单 |
|
||||
| 适用场景 | 高冲突 | 低冲突 |
|
||||
|
||||
## 总结
|
||||
|
||||
悲观锁方案通过数据库行锁保证强一致性,适合库存数量少、冲突率高的场景。虽然性能不如乐观锁和 Redis 方案,但在对一致性要求极高的场景下是最可靠的选择。需要注意避免死锁和减少锁持有时间。
|
||||
561
docs/高并发库存方案/03-Redis原子操作方案.md
Normal file
561
docs/高并发库存方案/03-Redis原子操作方案.md
Normal file
@@ -0,0 +1,561 @@
|
||||
# Redis 原子操作方案(推荐)
|
||||
|
||||
## 方案概述
|
||||
|
||||
使用 Redis 的原子操作(INCRBY/DECRBY)+ Lua 脚本实现高性能库存扣减,配合异步消息队列同步数据库,适用于高并发秒杀场景。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### 架构设计
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────┐ ┌──────────┐ ┌─────────┐
|
||||
│ 用户请求 │ ───> │ Redis │ ───> │ 消息队列 │ ───> │ MySQL │
|
||||
└─────────┘ └─────────┘ └──────────┘ └─────────┘
|
||||
原子扣减 异步同步 持久化存储
|
||||
QPS: 50000 削峰填谷 最终一致性
|
||||
```
|
||||
|
||||
### 工作流程
|
||||
|
||||
```
|
||||
请求到达
|
||||
↓
|
||||
Redis 原子扣减(Lua 脚本)
|
||||
↓
|
||||
扣减成功?
|
||||
├─ 是 → 发送 MQ 消息 → 异步更新 MySQL
|
||||
└─ 否 → 返回库存不足
|
||||
```
|
||||
|
||||
## 代码实现
|
||||
|
||||
### Maven 依赖
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- RocketMQ -->
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
<version>2.2.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Redisson(分布式锁) -->
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
<version>3.23.5</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
### Redis 配置
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(
|
||||
RedisConnectionFactory factory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(factory);
|
||||
|
||||
// 使用 Jackson 序列化
|
||||
Jackson2JsonRedisSerializer<Object> serializer =
|
||||
new Jackson2JsonRedisSerializer<>(Object.class);
|
||||
|
||||
template.setKeySerializer(new StringRedisSerializer());
|
||||
template.setValueSerializer(serializer);
|
||||
template.setHashKeySerializer(new StringRedisSerializer());
|
||||
template.setHashValueSerializer(serializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Lua 脚本(库存扣减)
|
||||
|
||||
```lua
|
||||
-- inventory_deduct.lua
|
||||
-- KEYS[1]: 库存 key (inventory:{inventoryCode})
|
||||
-- ARGV[1]: 扣减数量
|
||||
-- ARGV[2]: 最小库存(通常为 0)
|
||||
|
||||
local stock = redis.call('GET', KEYS[1])
|
||||
|
||||
-- 库存不存在
|
||||
if not stock then
|
||||
return -1
|
||||
end
|
||||
|
||||
stock = tonumber(stock)
|
||||
local quantity = tonumber(ARGV[1])
|
||||
local minStock = tonumber(ARGV[2])
|
||||
|
||||
-- 库存不足
|
||||
if stock < quantity or stock - quantity < minStock then
|
||||
return -2
|
||||
end
|
||||
|
||||
-- 扣减库存
|
||||
redis.call('DECRBY', KEYS[1], quantity)
|
||||
|
||||
-- 返回剩余库存
|
||||
return stock - quantity
|
||||
```
|
||||
|
||||
### Redis 库存服务
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class RedisInventoryService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
private static final String INVENTORY_KEY_PREFIX = "inventory:";
|
||||
private static final String INVENTORY_LOCK_PREFIX = "inventory:lock:";
|
||||
|
||||
// Lua 脚本 SHA1(预加载)
|
||||
private String deductScriptSha;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 预加载 Lua 脚本
|
||||
String script = loadLuaScript("inventory_deduct.lua");
|
||||
deductScriptSha = redisTemplate.execute(
|
||||
(RedisCallback<String>) connection ->
|
||||
connection.scriptLoad(script.getBytes())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查库存
|
||||
*/
|
||||
public Result<Boolean> checkInventory(String inventoryCode, Integer quantity) {
|
||||
String key = INVENTORY_KEY_PREFIX + inventoryCode;
|
||||
|
||||
Integer stock = (Integer) redisTemplate.opsForValue().get(key);
|
||||
|
||||
if (stock == null) {
|
||||
// Redis 中不存在,从数据库加载
|
||||
stock = loadInventoryFromDB(inventoryCode);
|
||||
if (stock == null) {
|
||||
return Result.fail("INVENTORY_NOT_FOUND", "库存不存在");
|
||||
}
|
||||
}
|
||||
|
||||
if (stock < quantity) {
|
||||
return Result.fail("INSUFFICIENT_INVENTORY", "库存不足");
|
||||
}
|
||||
|
||||
return Result.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定库存(使用 Lua 脚本保证原子性)
|
||||
*/
|
||||
public Result<Boolean> lockInventory(InventoryOperationDTO dto) {
|
||||
String key = INVENTORY_KEY_PREFIX + dto.getInventoryCode();
|
||||
|
||||
// 执行 Lua 脚本
|
||||
Long result = redisTemplate.execute(
|
||||
(RedisCallback<Long>) connection ->
|
||||
connection.evalSha(
|
||||
deductScriptSha,
|
||||
ReturnType.INTEGER,
|
||||
1,
|
||||
key.getBytes(),
|
||||
String.valueOf(dto.getQuantity()).getBytes(),
|
||||
"0".getBytes()
|
||||
)
|
||||
);
|
||||
|
||||
if (result == null || result == -1) {
|
||||
return Result.fail("INVENTORY_NOT_FOUND", "库存不存在");
|
||||
}
|
||||
|
||||
if (result == -2) {
|
||||
return Result.fail("INSUFFICIENT_INVENTORY", "库存不足");
|
||||
}
|
||||
|
||||
// 发送 MQ 消息,异步更新数据库
|
||||
InventoryChangeMessage message = new InventoryChangeMessage();
|
||||
message.setInventoryCode(dto.getInventoryCode());
|
||||
message.setOperationType("LOCK");
|
||||
message.setQuantity(dto.getQuantity());
|
||||
message.setOrderNo(dto.getOrderNo());
|
||||
message.setTimestamp(System.currentTimeMillis());
|
||||
|
||||
rocketMQTemplate.asyncSend(
|
||||
"inventory-change-topic",
|
||||
message,
|
||||
new SendCallback() {
|
||||
@Override
|
||||
public void onSuccess(SendResult sendResult) {
|
||||
LogUtil.info("库存变更消息发送成功: {}", message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Throwable e) {
|
||||
LogUtil.error("库存变更消息发送失败: {}", message, e);
|
||||
// 补偿机制:记录到失败表,定时重试
|
||||
saveFailedMessage(message);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return Result.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放库存
|
||||
*/
|
||||
public Result<Boolean> releaseInventory(InventoryOperationDTO dto) {
|
||||
String key = INVENTORY_KEY_PREFIX + dto.getInventoryCode();
|
||||
|
||||
// 增加库存
|
||||
Long newStock = redisTemplate.opsForValue()
|
||||
.increment(key, dto.getQuantity());
|
||||
|
||||
// 发送 MQ 消息
|
||||
InventoryChangeMessage message = new InventoryChangeMessage();
|
||||
message.setInventoryCode(dto.getInventoryCode());
|
||||
message.setOperationType("RELEASE");
|
||||
message.setQuantity(dto.getQuantity());
|
||||
message.setOrderNo(dto.getOrderNo());
|
||||
message.setTimestamp(System.currentTimeMillis());
|
||||
|
||||
rocketMQTemplate.asyncSend("inventory-change-topic", message, null);
|
||||
|
||||
return Result.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库加载库存到 Redis
|
||||
*/
|
||||
private Integer loadInventoryFromDB(String inventoryCode) {
|
||||
InventoryDO inventory = inventoryRepository.findByInventoryCode(inventoryCode);
|
||||
|
||||
if (inventory == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String key = INVENTORY_KEY_PREFIX + inventoryCode;
|
||||
Integer stock = inventory.getRemainingStock();
|
||||
|
||||
// 设置到 Redis,过期时间 1 小时
|
||||
redisTemplate.opsForValue().set(key, stock, 1, TimeUnit.HOURS);
|
||||
|
||||
return stock;
|
||||
}
|
||||
|
||||
/**
|
||||
* 库存预热(系统启动或定时任务)
|
||||
*/
|
||||
@Scheduled(cron = "0 0 * * * ?")
|
||||
public void warmUpInventory() {
|
||||
LogUtil.info("开始库存预热...");
|
||||
|
||||
// 查询所有活跃库存
|
||||
List<InventoryDO> inventories = inventoryRepository.findActiveInventories();
|
||||
|
||||
for (InventoryDO inventory : inventories) {
|
||||
String key = INVENTORY_KEY_PREFIX + inventory.getInventoryCode();
|
||||
redisTemplate.opsForValue().set(
|
||||
key,
|
||||
inventory.getRemainingStock(),
|
||||
1,
|
||||
TimeUnit.HOURS
|
||||
);
|
||||
}
|
||||
|
||||
LogUtil.info("库存预热完成,共预热 {} 条库存", inventories.size());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MQ 消费者(异步同步数据库)
|
||||
|
||||
```java
|
||||
@Component
|
||||
@RocketMQMessageListener(
|
||||
topic = "inventory-change-topic",
|
||||
consumerGroup = "inventory-sync-consumer"
|
||||
)
|
||||
public class InventorySyncConsumer implements RocketMQListener<InventoryChangeMessage> {
|
||||
|
||||
@Autowired
|
||||
private InventoryRepository inventoryRepository;
|
||||
|
||||
@Autowired
|
||||
private InventoryLogRepository inventoryLogRepository;
|
||||
|
||||
@Override
|
||||
public void onMessage(InventoryChangeMessage message) {
|
||||
try {
|
||||
// 查询库存
|
||||
InventoryDO inventory = inventoryRepository.findByInventoryCode(
|
||||
message.getInventoryCode()
|
||||
);
|
||||
|
||||
if (inventory == null) {
|
||||
LogUtil.error("库存不存在: {}", message.getInventoryCode());
|
||||
return;
|
||||
}
|
||||
|
||||
Integer beforeQty = inventory.getRemainingStock();
|
||||
Integer beforeSold = inventory.getSoldStock();
|
||||
|
||||
// 根据操作类型更新数据库
|
||||
switch (message.getOperationType()) {
|
||||
case "LOCK":
|
||||
inventory.setRemainingStock(
|
||||
inventory.getRemainingStock() - message.getQuantity()
|
||||
);
|
||||
break;
|
||||
|
||||
case "DEDUCT":
|
||||
inventory.setSoldStock(
|
||||
inventory.getSoldStock() + message.getQuantity()
|
||||
);
|
||||
break;
|
||||
|
||||
case "RELEASE":
|
||||
inventory.setRemainingStock(
|
||||
inventory.getRemainingStock() + message.getQuantity()
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
LogUtil.error("未知操作类型: {}", message.getOperationType());
|
||||
return;
|
||||
}
|
||||
|
||||
inventory.setUpdateTime(LocalDateTime.now());
|
||||
inventoryRepository.save(inventory);
|
||||
|
||||
// 记录日志
|
||||
saveInventoryLog(
|
||||
message.getInventoryCode(),
|
||||
message.getOperationType(),
|
||||
message.getQuantity(),
|
||||
beforeQty,
|
||||
inventory.getRemainingStock(),
|
||||
message.getOrderNo()
|
||||
);
|
||||
|
||||
LogUtil.info("库存同步成功: {}", message);
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtil.error("库存同步失败: {}", message, e);
|
||||
throw new RuntimeException("库存同步失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveInventoryLog(String inventoryCode, String operationType,
|
||||
Integer quantity, Integer beforeQty,
|
||||
Integer afterQty, String orderNo) {
|
||||
InventoryLogDO log = new InventoryLogDO();
|
||||
log.setInventoryCode(inventoryCode);
|
||||
log.setOperationType(operationType);
|
||||
log.setQuantity(quantity);
|
||||
log.setBeforeQty(beforeQty);
|
||||
log.setAfterQty(afterQty);
|
||||
log.setOrderNo(orderNo);
|
||||
log.setCreateTime(LocalDateTime.now());
|
||||
|
||||
inventoryLogRepository.save(log);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 消息实体
|
||||
|
||||
```java
|
||||
@Data
|
||||
public class InventoryChangeMessage implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String inventoryCode;
|
||||
private String operationType;
|
||||
private Integer quantity;
|
||||
private String orderNo;
|
||||
private Long timestamp;
|
||||
}
|
||||
```
|
||||
|
||||
## 数据一致性保证
|
||||
|
||||
### Redis 与 MySQL 数据同步
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class InventoryConsistencyService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private InventoryRepository inventoryRepository;
|
||||
|
||||
/**
|
||||
* 定时对账(每小时执行)
|
||||
*/
|
||||
@Scheduled(cron = "0 0 * * * ?")
|
||||
public void reconcileInventory() {
|
||||
LogUtil.info("开始库存对账...");
|
||||
|
||||
List<InventoryDO> inventories = inventoryRepository.findAll();
|
||||
int inconsistentCount = 0;
|
||||
|
||||
for (InventoryDO inventory : inventories) {
|
||||
String key = INVENTORY_KEY_PREFIX + inventory.getInventoryCode();
|
||||
Integer redisStock = (Integer) redisTemplate.opsForValue().get(key);
|
||||
|
||||
if (redisStock == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否一致
|
||||
if (!redisStock.equals(inventory.getRemainingStock())) {
|
||||
inconsistentCount++;
|
||||
|
||||
LogUtil.warn("库存不一致: inventoryCode={}, Redis={}, MySQL={}",
|
||||
inventory.getInventoryCode(), redisStock,
|
||||
inventory.getRemainingStock());
|
||||
|
||||
// 以 MySQL 为准,修正 Redis
|
||||
redisTemplate.opsForValue().set(
|
||||
key,
|
||||
inventory.getRemainingStock(),
|
||||
1,
|
||||
TimeUnit.HOURS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LogUtil.info("库存对账完成,发现 {} 条不一致数据", inconsistentCount);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 连接池配置
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
database: 0
|
||||
lettuce:
|
||||
pool:
|
||||
max-active: 200
|
||||
max-idle: 50
|
||||
min-idle: 10
|
||||
max-wait: 3000ms
|
||||
timeout: 3000ms
|
||||
```
|
||||
|
||||
### Pipeline 批量操作
|
||||
|
||||
```java
|
||||
public void batchLoadInventory(List<String> inventoryCodes) {
|
||||
redisTemplate.executePipelined(new RedisCallback<Object>() {
|
||||
@Override
|
||||
public Object doInRedis(RedisConnection connection) {
|
||||
for (String code : inventoryCodes) {
|
||||
InventoryDO inventory = inventoryRepository.findByInventoryCode(code);
|
||||
if (inventory != null) {
|
||||
String key = INVENTORY_KEY_PREFIX + code;
|
||||
connection.set(
|
||||
key.getBytes(),
|
||||
String.valueOf(inventory.getRemainingStock()).getBytes()
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 性能指标
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| QPS | ~50000 |
|
||||
| 响应时间 | 2-5ms |
|
||||
| 成功率 | 99.9% |
|
||||
| Redis 内存 | ~1KB/库存 |
|
||||
|
||||
## 故障处理
|
||||
|
||||
### Redis 故障降级
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class InventoryFallbackService {
|
||||
|
||||
@Autowired
|
||||
private RedisInventoryService redisInventoryService;
|
||||
|
||||
@Autowired
|
||||
private InventoryRepository inventoryRepository;
|
||||
|
||||
public Result<Boolean> lockInventoryWithFallback(InventoryOperationDTO dto) {
|
||||
try {
|
||||
// 优先使用 Redis
|
||||
return redisInventoryService.lockInventory(dto);
|
||||
|
||||
} catch (RedisConnectionException e) {
|
||||
LogUtil.error("Redis 连接失败,降级到数据库", e);
|
||||
|
||||
// 降级到数据库乐观锁
|
||||
return lockInventoryFromDB(dto);
|
||||
}
|
||||
}
|
||||
|
||||
private Result<Boolean> lockInventoryFromDB(InventoryOperationDTO dto) {
|
||||
InventoryDO inventory = inventoryRepository.findByInventoryCode(
|
||||
dto.getInventoryCode()
|
||||
);
|
||||
|
||||
if (inventory == null) {
|
||||
return Result.fail("INVENTORY_NOT_FOUND", "库存不存在");
|
||||
}
|
||||
|
||||
inventory.setRemainingStock(
|
||||
inventory.getRemainingStock() - dto.getQuantity()
|
||||
);
|
||||
|
||||
boolean success = inventoryRepository.updateWithVersion(inventory);
|
||||
|
||||
if (!success) {
|
||||
return Result.fail("INVENTORY_LOCK_FAILED", "库存锁定失败");
|
||||
}
|
||||
|
||||
return Result.success(true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
Redis 原子操作方案是高并发场景的最佳选择,性能提升 50 倍以上。通过 Lua 脚本保证原子性,配合消息队列实现最终一致性,是秒杀系统的标准方案。
|
||||
558
docs/高并发库存方案/04-消息队列削峰方案.md
Normal file
558
docs/高并发库存方案/04-消息队列削峰方案.md
Normal file
@@ -0,0 +1,558 @@
|
||||
# 消息队列削峰方案
|
||||
|
||||
## 方案概述
|
||||
|
||||
使用消息队列(RocketMQ/Kafka)将同步扣减库存改为异步处理,通过削峰填谷降低数据库压力,适用于流量波动大、可接受异步响应的场景。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### 架构设计
|
||||
|
||||
```
|
||||
┌─────────┐ ┌──────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ 用户请求 │ ───> │ 消息队列 │ ───> │ 消费者 │ ───> │ MySQL │
|
||||
└─────────┘ └──────────┘ └─────────┘ └─────────┘
|
||||
立即返回 削峰填谷 异步扣减 持久化
|
||||
QPS: 无限 缓冲能力强 QPS: 10000 最终一致
|
||||
```
|
||||
|
||||
### 工作流程
|
||||
|
||||
```
|
||||
用户下单
|
||||
↓
|
||||
发送 MQ 消息(立即返回订单号)
|
||||
↓
|
||||
消费者接收消息
|
||||
↓
|
||||
扣减库存(数据库)
|
||||
↓
|
||||
扣减成功?
|
||||
├─ 是 → 更新订单状态为"已确认"
|
||||
└─ 否 → 更新订单状态为"库存不足",发送通知
|
||||
```
|
||||
|
||||
## 代码实现
|
||||
|
||||
### Maven 依赖
|
||||
|
||||
```xml
|
||||
<dependencies>
|
||||
<!-- RocketMQ -->
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
<version>2.2.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
|
||||
### RocketMQ 配置
|
||||
|
||||
```yaml
|
||||
rocketmq:
|
||||
name-server: localhost:9876
|
||||
producer:
|
||||
group: inventory-producer-group
|
||||
send-message-timeout: 3000
|
||||
retry-times-when-send-failed: 2
|
||||
consumer:
|
||||
group: inventory-consumer-group
|
||||
consume-thread-min: 20
|
||||
consume-thread-max: 64
|
||||
```
|
||||
|
||||
### 消息实体
|
||||
|
||||
```java
|
||||
@Data
|
||||
public class InventoryLockMessage implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String orderNo;
|
||||
private String inventoryCode;
|
||||
private String scheduleCode;
|
||||
private String rollingScheduleCode;
|
||||
private String orderType;
|
||||
private Integer quantity;
|
||||
private Long userId;
|
||||
private Long timestamp;
|
||||
}
|
||||
```
|
||||
|
||||
### 生产者(订单服务)
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class OrderServiceImpl implements OrderService {
|
||||
|
||||
@Autowired
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
/**
|
||||
* 创建订单(异步扣减库存)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<String> createOrder(CreateOrderDTO dto) {
|
||||
// 生成订单号
|
||||
String orderNo = generateOrderNo();
|
||||
|
||||
// 创建订单(状态:待确认)
|
||||
OrderDO order = new OrderDO();
|
||||
order.setOrderNo(orderNo);
|
||||
order.setUserId(dto.getUserId());
|
||||
order.setScheduleCode(dto.getScheduleCode());
|
||||
order.setQuantity(dto.getQuantity());
|
||||
order.setStatus("PENDING"); // 待确认
|
||||
order.setCreateTime(LocalDateTime.now());
|
||||
|
||||
orderRepository.save(order);
|
||||
|
||||
// 发送库存锁定消息
|
||||
InventoryLockMessage message = new InventoryLockMessage();
|
||||
message.setOrderNo(orderNo);
|
||||
message.setInventoryCode(dto.getInventoryCode());
|
||||
message.setScheduleCode(dto.getScheduleCode());
|
||||
message.setOrderType(dto.getOrderType());
|
||||
message.setQuantity(dto.getQuantity());
|
||||
message.setUserId(dto.getUserId());
|
||||
message.setTimestamp(System.currentTimeMillis());
|
||||
|
||||
// 异步发送消息
|
||||
rocketMQTemplate.asyncSend(
|
||||
"inventory-lock-topic",
|
||||
message,
|
||||
new SendCallback() {
|
||||
@Override
|
||||
public void onSuccess(SendResult sendResult) {
|
||||
LogUtil.info("库存锁定消息发送成功: orderNo={}", orderNo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Throwable e) {
|
||||
LogUtil.error("库存锁定消息发送失败: orderNo={}", orderNo, e);
|
||||
|
||||
// 更新订单状态为失败
|
||||
order.setStatus("FAILED");
|
||||
order.setRemark("消息发送失败");
|
||||
orderRepository.save(order);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 立即返回订单号
|
||||
return Result.success(orderNo);
|
||||
}
|
||||
|
||||
private String generateOrderNo() {
|
||||
return "ORD" + System.currentTimeMillis() +
|
||||
new Random().nextInt(1000);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 消费者(库存服务)
|
||||
|
||||
```java
|
||||
@Component
|
||||
@RocketMQMessageListener(
|
||||
topic = "inventory-lock-topic",
|
||||
consumerGroup = "inventory-lock-consumer",
|
||||
consumeMode = ConsumeMode.ORDERLY // 顺序消费
|
||||
)
|
||||
public class InventoryLockConsumer implements RocketMQListener<InventoryLockMessage> {
|
||||
|
||||
@Autowired
|
||||
private InventoryRepository inventoryRepository;
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
@Autowired
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
@Override
|
||||
public void onMessage(InventoryLockMessage message) {
|
||||
LogUtil.info("收到库存锁定消息: {}", message);
|
||||
|
||||
try {
|
||||
// 查询库存
|
||||
InventoryDO inventory = inventoryRepository.findByInventoryCode(
|
||||
message.getInventoryCode()
|
||||
);
|
||||
|
||||
if (inventory == null) {
|
||||
handleLockFailed(message, "库存不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查库存
|
||||
if (inventory.getRemainingStock() < message.getQuantity()) {
|
||||
handleLockFailed(message, "库存不足");
|
||||
return;
|
||||
}
|
||||
|
||||
// 扣减库存(使用乐观锁)
|
||||
Integer beforeQty = inventory.getRemainingStock();
|
||||
inventory.setRemainingStock(inventory.getRemainingStock() - message.getQuantity());
|
||||
inventory.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
boolean success = inventoryRepository.updateWithVersion(inventory);
|
||||
|
||||
if (!success) {
|
||||
// 乐观锁冲突,重新消费
|
||||
throw new RuntimeException("库存更新失败,触发重试");
|
||||
}
|
||||
|
||||
// 更新订单状态为已确认
|
||||
OrderDO order = orderRepository.findByOrderNo(message.getOrderNo());
|
||||
if (order != null) {
|
||||
order.setStatus("CONFIRMED");
|
||||
order.setUpdateTime(LocalDateTime.now());
|
||||
orderRepository.save(order);
|
||||
}
|
||||
|
||||
// 发送库存锁定成功通知
|
||||
sendNotification(message.getUserId(), message.getOrderNo(), "订单已确认");
|
||||
|
||||
LogUtil.info("库存锁定成功: orderNo={}, inventoryCode={}, quantity={}",
|
||||
message.getOrderNo(), message.getInventoryCode(), message.getQuantity());
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtil.error("库存锁定失败: {}", message, e);
|
||||
throw new RuntimeException("库存锁定失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLockFailed(InventoryLockMessage message, String reason) {
|
||||
// 更新订单状态为失败
|
||||
OrderDO order = orderRepository.findByOrderNo(message.getOrderNo());
|
||||
if (order != null) {
|
||||
order.setStatus("FAILED");
|
||||
order.setRemark(reason);
|
||||
order.setUpdateTime(LocalDateTime.now());
|
||||
orderRepository.save(order);
|
||||
}
|
||||
|
||||
// 发送失败通知
|
||||
sendNotification(message.getUserId(), message.getOrderNo(),
|
||||
"订单创建失败: " + reason);
|
||||
|
||||
LogUtil.warn("库存锁定失败: orderNo={}, reason={}",
|
||||
message.getOrderNo(), reason);
|
||||
}
|
||||
|
||||
private void sendNotification(Long userId, String orderNo, String content) {
|
||||
// 发送站内信/短信/推送通知
|
||||
NotificationMessage notification = new NotificationMessage();
|
||||
notification.setUserId(userId);
|
||||
notification.setOrderNo(orderNo);
|
||||
notification.setContent(content);
|
||||
notification.setTimestamp(System.currentTimeMillis());
|
||||
|
||||
rocketMQTemplate.asyncSend("notification-topic", notification, null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 订单状态查询
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/order")
|
||||
public class OrderController {
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
/**
|
||||
* 查询订单状态
|
||||
*/
|
||||
@GetMapping("/{orderNo}/status")
|
||||
public Result<OrderStatusVO> getOrderStatus(@PathVariable String orderNo) {
|
||||
OrderDO order = orderRepository.findByOrderNo(orderNo);
|
||||
|
||||
if (order == null) {
|
||||
return Result.fail("ORDER_NOT_FOUND", "订单不存在");
|
||||
}
|
||||
|
||||
OrderStatusVO vo = new OrderStatusVO();
|
||||
vo.setOrderNo(order.getOrderNo());
|
||||
vo.setStatus(order.getStatus());
|
||||
vo.setStatusDesc(getStatusDesc(order.getStatus()));
|
||||
vo.setCreateTime(order.getCreateTime());
|
||||
vo.setUpdateTime(order.getUpdateTime());
|
||||
|
||||
return Result.success(vo);
|
||||
}
|
||||
|
||||
private String getStatusDesc(String status) {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "订单处理中,请稍候...";
|
||||
case "CONFIRMED":
|
||||
return "订单已确认,请尽快支付";
|
||||
case "FAILED":
|
||||
return "订单创建失败";
|
||||
default:
|
||||
return "未知状态";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 消息可靠性保证
|
||||
|
||||
### 生产者事务消息
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class TransactionalOrderService {
|
||||
|
||||
@Autowired
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
@Autowired
|
||||
private OrderRepository orderRepository;
|
||||
|
||||
/**
|
||||
* 使用事务消息保证订单创建和消息发送的一致性
|
||||
*/
|
||||
public Result<String> createOrderWithTransaction(CreateOrderDTO dto) {
|
||||
String orderNo = generateOrderNo();
|
||||
|
||||
// 发送事务消息
|
||||
Message<InventoryLockMessage> message = MessageBuilder
|
||||
.withPayload(buildMessage(dto, orderNo))
|
||||
.build();
|
||||
|
||||
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
|
||||
"inventory-lock-topic",
|
||||
message,
|
||||
dto // 传递给本地事务的参数
|
||||
);
|
||||
|
||||
if (result.getLocalTransactionState() == LocalTransactionState.COMMIT_MESSAGE) {
|
||||
return Result.success(orderNo);
|
||||
} else {
|
||||
return Result.fail("ORDER_CREATE_FAILED", "订单创建失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地事务监听器
|
||||
*/
|
||||
@RocketMQTransactionListener
|
||||
class OrderTransactionListener implements RocketMQLocalTransactionListener {
|
||||
|
||||
@Override
|
||||
public RocketMQLocalTransactionState executeLocalTransaction(
|
||||
Message msg, Object arg) {
|
||||
try {
|
||||
CreateOrderDTO dto = (CreateOrderDTO) arg;
|
||||
|
||||
// 执行本地事务:创建订单
|
||||
OrderDO order = new OrderDO();
|
||||
order.setOrderNo(extractOrderNo(msg));
|
||||
order.setUserId(dto.getUserId());
|
||||
order.setStatus("PENDING");
|
||||
order.setCreateTime(LocalDateTime.now());
|
||||
|
||||
orderRepository.save(order);
|
||||
|
||||
// 本地事务成功,提交消息
|
||||
return RocketMQLocalTransactionState.COMMIT;
|
||||
|
||||
} catch (Exception e) {
|
||||
LogUtil.error("本地事务执行失败", e);
|
||||
// 本地事务失败,回滚消息
|
||||
return RocketMQLocalTransactionState.ROLLBACK;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
|
||||
// 回查本地事务状态
|
||||
String orderNo = extractOrderNo(msg);
|
||||
OrderDO order = orderRepository.findByOrderNo(orderNo);
|
||||
|
||||
if (order != null) {
|
||||
return RocketMQLocalTransactionState.COMMIT;
|
||||
} else {
|
||||
return RocketMQLocalTransactionState.ROLLBACK;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 消费者幂等性
|
||||
|
||||
```java
|
||||
@Component
|
||||
@RocketMQMessageListener(
|
||||
topic = "inventory-lock-topic",
|
||||
consumerGroup = "inventory-lock-consumer"
|
||||
)
|
||||
public class IdempotentInventoryConsumer implements RocketMQListener<InventoryLockMessage> {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private InventoryRepository inventoryRepository;
|
||||
|
||||
@Override
|
||||
public void onMessage(InventoryLockMessage message) {
|
||||
String idempotentKey = "inventory:lock:" + message.getOrderNo();
|
||||
|
||||
// 使用 Redis 实现幂等性
|
||||
Boolean success = redisTemplate.opsForValue()
|
||||
.setIfAbsent(idempotentKey, "1", 10, TimeUnit.MINUTES);
|
||||
|
||||
if (Boolean.FALSE.equals(success)) {
|
||||
LogUtil.warn("重复消息,跳过处理: orderNo={}", message.getOrderNo());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 处理库存锁定
|
||||
processInventoryLock(message);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 处理失败,删除幂等键,允许重试
|
||||
redisTemplate.delete(idempotentKey);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void processInventoryLock(InventoryLockMessage message) {
|
||||
// 库存扣减逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 批量消费
|
||||
|
||||
```java
|
||||
@Component
|
||||
@RocketMQMessageListener(
|
||||
topic = "inventory-lock-topic",
|
||||
consumerGroup = "inventory-lock-consumer",
|
||||
consumeMode = ConsumeMode.CONCURRENTLY,
|
||||
messageModel = MessageModel.CLUSTERING,
|
||||
consumeThreadMax = 64
|
||||
)
|
||||
public class BatchInventoryConsumer implements RocketMQListener<List<InventoryLockMessage>> {
|
||||
|
||||
@Override
|
||||
public void onMessage(List<InventoryLockMessage> messages) {
|
||||
LogUtil.info("批量消费 {} 条消息", messages.size());
|
||||
|
||||
// 按 inventoryCode 分组
|
||||
Map<String, List<InventoryLockMessage>> groupedMessages = messages.stream()
|
||||
.collect(Collectors.groupingBy(InventoryLockMessage::getInventoryCode));
|
||||
|
||||
// 批量处理
|
||||
for (Map.Entry<String, List<InventoryLockMessage>> entry : groupedMessages.entrySet()) {
|
||||
String inventoryCode = entry.getKey();
|
||||
List<InventoryLockMessage> msgs = entry.getValue();
|
||||
|
||||
// 计算总扣减量
|
||||
int totalQuantity = msgs.stream()
|
||||
.mapToInt(InventoryLockMessage::getQuantity)
|
||||
.sum();
|
||||
|
||||
// 一次性扣减
|
||||
boolean success = deductInventoryBatch(inventoryCode, totalQuantity);
|
||||
|
||||
if (success) {
|
||||
// 更新所有订单状态
|
||||
msgs.forEach(msg -> updateOrderStatus(msg.getOrderNo(), "CONFIRMED"));
|
||||
} else {
|
||||
// 逐个处理
|
||||
msgs.forEach(this::processIndividually);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 性能指标
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| 接收 QPS | 无限制 |
|
||||
| 消费 QPS | ~10000 |
|
||||
| 消息延迟 | 100-500ms |
|
||||
| 吞吐量 | 10万条/秒 |
|
||||
|
||||
## 适用场景
|
||||
|
||||
### 推荐使用
|
||||
|
||||
- 流量波动大(如促销活动)
|
||||
- 可接受异步响应
|
||||
- 需要削峰填谷
|
||||
- 对实时性要求不高(秒级延迟可接受)
|
||||
|
||||
### 不推荐使用
|
||||
|
||||
- 需要立即返回库存结果
|
||||
- 对实时性要求极高
|
||||
- 用户无法接受异步通知
|
||||
- 系统架构简单,不想引入 MQ
|
||||
|
||||
## 故障处理
|
||||
|
||||
### 消息堆积
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class MessageBacklogMonitor {
|
||||
|
||||
@Scheduled(fixedRate = 60000)
|
||||
public void monitorBacklog() {
|
||||
// 查询消息堆积情况
|
||||
long backlog = getMessageBacklog("inventory-lock-topic",
|
||||
"inventory-lock-consumer");
|
||||
|
||||
if (backlog > 10000) {
|
||||
LogUtil.warn("消息堆积严重: {} 条", backlog);
|
||||
|
||||
// 动态扩容消费者
|
||||
scaleConsumers(backlog);
|
||||
|
||||
// 发送告警
|
||||
alertService.sendAlert("消息队列堆积: " + backlog + " 条");
|
||||
}
|
||||
}
|
||||
|
||||
private void scaleConsumers(long backlog) {
|
||||
// 根据堆积量动态调整消费线程数
|
||||
int targetThreads = (int) Math.min(backlog / 100, 128);
|
||||
// 调整消费线程池大小
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 消费失败重试
|
||||
|
||||
```yaml
|
||||
rocketmq:
|
||||
consumer:
|
||||
# 最大重试次数
|
||||
max-reconsume-times: 3
|
||||
# 重试间隔(秒)
|
||||
suspend-current-queue-time-millis: 1000
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
消息队列削峰方案通过异步处理降低系统压力,适合流量波动大的场景。需要注意消息可靠性、幂等性和用户体验(异步通知)。可与 Redis 方案结合使用,实现更高性能。
|
||||
557
docs/高并发库存方案/05-分段锁方案.md
Normal file
557
docs/高并发库存方案/05-分段锁方案.md
Normal file
@@ -0,0 +1,557 @@
|
||||
# 分段锁方案(库存分片)
|
||||
|
||||
## 方案概述
|
||||
|
||||
将大库存拆分成多个小库存段,每个段独立加锁,降低锁冲突概率,提高并发性能。类似 ConcurrentHashMap 的分段锁思想。
|
||||
|
||||
## 核心原理
|
||||
|
||||
### 分段策略
|
||||
|
||||
```
|
||||
总库存 1000
|
||||
↓ 拆分成 10 个段
|
||||
段1: 100 段2: 100 段3: 100 ... 段10: 100
|
||||
↓
|
||||
并发请求分散到不同段
|
||||
请求A → 段1 请求B → 段2 请求C → 段3
|
||||
↓
|
||||
锁冲突降低 90%
|
||||
```
|
||||
|
||||
### 工作流程
|
||||
|
||||
```
|
||||
用户请求
|
||||
↓
|
||||
计算分段(Hash 或轮询)
|
||||
↓
|
||||
锁定该段
|
||||
↓
|
||||
扣减该段库存
|
||||
↓
|
||||
释放锁
|
||||
```
|
||||
|
||||
## 代码实现
|
||||
|
||||
### 数据库表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE `inventory_segment` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`inventory_code` VARCHAR(64) NOT NULL COMMENT '库存编码',
|
||||
`segment_no` INT NOT NULL COMMENT '分段编号(0-9)',
|
||||
`total_stock` INT NOT NULL DEFAULT 0 COMMENT '该段总库存',
|
||||
`sold_stock` INT NOT NULL DEFAULT 0 COMMENT '该段已售数量',
|
||||
`remaining_stock` INT NOT NULL DEFAULT 0 COMMENT '该段剩余库存',
|
||||
`version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_inventory_segment` (`inventory_code`, `segment_no`),
|
||||
KEY `idx_inventory_code` (`inventory_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存分段表';
|
||||
```
|
||||
|
||||
### 实体类
|
||||
|
||||
```java
|
||||
@Data
|
||||
@TableName("inventory_segment")
|
||||
public class InventorySegmentDO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String inventoryCode;
|
||||
private Integer segmentNo;
|
||||
private Integer totalStock;
|
||||
private Integer soldStock;
|
||||
private Integer remainingStock;
|
||||
|
||||
@Version
|
||||
private Integer version;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
```
|
||||
|
||||
### Repository 实现
|
||||
|
||||
```java
|
||||
@Repository
|
||||
public class InventorySegmentRepositoryImpl implements InventorySegmentRepository {
|
||||
|
||||
@Autowired
|
||||
private InventorySegmentMapper inventorySegmentMapper;
|
||||
|
||||
@Override
|
||||
public List<InventorySegmentDO> findByInventoryCode(String inventoryCode) {
|
||||
return inventorySegmentMapper.selectList(
|
||||
Wrappers.lambdaQuery(InventorySegmentDO.class)
|
||||
.eq(InventorySegmentDO::getInventoryCode, inventoryCode)
|
||||
.orderByAsc(InventorySegmentDO::getSegmentNo)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InventorySegmentDO findByInventoryCodeAndSegment(
|
||||
String inventoryCode, Integer segmentNo) {
|
||||
return inventorySegmentMapper.selectOne(
|
||||
Wrappers.lambdaQuery(InventorySegmentDO.class)
|
||||
.eq(InventorySegmentDO::getInventoryCode, inventoryCode)
|
||||
.eq(InventorySegmentDO::getSegmentNo, segmentNo)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateWithVersion(InventorySegmentDO segment) {
|
||||
int rows = inventorySegmentMapper.updateById(segment);
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTotalRemainingStock(String inventoryCode) {
|
||||
return inventorySegmentMapper.selectList(
|
||||
Wrappers.lambdaQuery(InventorySegmentDO.class)
|
||||
.eq(InventorySegmentDO::getInventoryCode, inventoryCode)
|
||||
).stream()
|
||||
.mapToInt(InventorySegmentDO::getRemainingStock)
|
||||
.sum();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service 实现
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class SegmentedInventoryService {
|
||||
|
||||
@Autowired
|
||||
private InventorySegmentRepository segmentRepository;
|
||||
|
||||
@Autowired
|
||||
private InventoryLogRepository inventoryLogRepository;
|
||||
|
||||
private static final int SEGMENT_COUNT = 10; // 分段数量
|
||||
private final AtomicInteger roundRobinCounter = new AtomicInteger(0);
|
||||
|
||||
/**
|
||||
* 检查库存
|
||||
*/
|
||||
public Result<Boolean> checkInventory(String inventoryCode, Integer quantity) {
|
||||
int totalStock = segmentRepository.getTotalRemainingStock(inventoryCode);
|
||||
|
||||
if (totalStock < quantity) {
|
||||
return Result.fail("INSUFFICIENT_INVENTORY", "库存不足");
|
||||
}
|
||||
|
||||
return Result.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定库存(分段锁)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Boolean> lockInventory(InventoryOperationDTO dto) {
|
||||
int remainingQuantity = dto.getQuantity();
|
||||
List<SegmentLockRecord> lockedSegments = new ArrayList<>();
|
||||
|
||||
// 轮询方式选择起始分段
|
||||
int startSegment = roundRobinCounter.getAndIncrement() % SEGMENT_COUNT;
|
||||
|
||||
// 尝试从多个分段扣减库存
|
||||
for (int i = 0; i < SEGMENT_COUNT && remainingQuantity > 0; i++) {
|
||||
int segmentNo = (startSegment + i) % SEGMENT_COUNT;
|
||||
|
||||
// 查询该分段库存
|
||||
InventorySegmentDO segment = segmentRepository
|
||||
.findByInventoryCodeAndSegment(dto.getInventoryCode(), segmentNo);
|
||||
|
||||
if (segment == null || segment.getRemainingStock() <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 计算本次扣减数量
|
||||
int deductQty = Math.min(remainingQuantity, segment.getRemainingStock());
|
||||
|
||||
// 扣减该段库存(乐观锁)
|
||||
Integer beforeQty = segment.getRemainingStock();
|
||||
segment.setRemainingStock(segment.getRemainingStock() - deductQty);
|
||||
segment.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
boolean success = segmentRepository.updateWithVersion(segment);
|
||||
|
||||
if (!success) {
|
||||
// 乐观锁冲突,回滚已锁定的分段
|
||||
rollbackLockedSegments(lockedSegments);
|
||||
return Result.fail("INVENTORY_LOCK_FAILED", "库存锁定失败,请重试");
|
||||
}
|
||||
|
||||
// 记录已锁定的分段
|
||||
lockedSegments.add(new SegmentLockRecord(
|
||||
segmentNo, deductQty, beforeQty, beforeQty - deductQty
|
||||
));
|
||||
|
||||
remainingQuantity -= deductQty;
|
||||
}
|
||||
|
||||
// 检查是否全部扣减成功
|
||||
if (remainingQuantity > 0) {
|
||||
rollbackLockedSegments(lockedSegments);
|
||||
return Result.fail("INSUFFICIENT_INVENTORY", "库存不足");
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
for (SegmentLockRecord record : lockedSegments) {
|
||||
saveInventoryLog(
|
||||
dto.getInventoryCode(),
|
||||
record.getSegmentNo(),
|
||||
InventoryOperationTypeEnum.LOCK.getCode(),
|
||||
record.getQuantity(),
|
||||
record.getBeforeQty(),
|
||||
record.getAfterQty(),
|
||||
dto.getOrderNo()
|
||||
);
|
||||
}
|
||||
|
||||
LogUtil.info("分段库存锁定成功: inventoryCode={}, quantity={}, segments={}",
|
||||
dto.getInventoryCode(), dto.getQuantity(), lockedSegments.size());
|
||||
|
||||
return Result.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放库存
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Boolean> releaseInventory(InventoryOperationDTO dto) {
|
||||
// 查询该订单锁定了哪些分段(从日志表查询)
|
||||
List<InventoryLogDO> logs = inventoryLogRepository.findByOrderNo(dto.getOrderNo());
|
||||
|
||||
for (InventoryLogDO log : logs) {
|
||||
if (!"LOCK".equals(log.getOperationType())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 查询分段
|
||||
InventorySegmentDO segment = segmentRepository
|
||||
.findByInventoryCodeAndSegment(
|
||||
log.getInventoryCode(),
|
||||
log.getSegmentNo()
|
||||
);
|
||||
|
||||
if (segment == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 释放库存
|
||||
segment.setRemainingStock(
|
||||
segment.getRemainingStock() + log.getQuantity()
|
||||
);
|
||||
segment.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
segmentRepository.updateWithVersion(segment);
|
||||
|
||||
// 记录日志
|
||||
saveInventoryLog(
|
||||
log.getInventoryCode(),
|
||||
log.getSegmentNo(),
|
||||
InventoryOperationTypeEnum.RELEASE.getCode(),
|
||||
log.getQuantity(),
|
||||
segment.getRemainingStock() - log.getQuantity(),
|
||||
segment.getRemainingStock(),
|
||||
dto.getOrderNo()
|
||||
);
|
||||
}
|
||||
|
||||
return Result.success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 回滚已锁定的分段
|
||||
*/
|
||||
private void rollbackLockedSegments(List<SegmentLockRecord> lockedSegments) {
|
||||
for (SegmentLockRecord record : lockedSegments) {
|
||||
try {
|
||||
InventorySegmentDO segment = segmentRepository
|
||||
.findByInventoryCodeAndSegment(
|
||||
record.getInventoryCode(),
|
||||
record.getSegmentNo()
|
||||
);
|
||||
|
||||
if (segment != null) {
|
||||
segment.setRemainingStock(
|
||||
segment.getRemainingStock() + record.getQuantity()
|
||||
);
|
||||
segmentRepository.updateWithVersion(segment);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LogUtil.error("回滚分段库存失败: segment={}", record, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void saveInventoryLog(String inventoryCode, Integer segmentNo,
|
||||
String operationType, Integer quantity,
|
||||
Integer beforeQty, Integer afterQty, String orderNo) {
|
||||
InventoryLogDO log = new InventoryLogDO();
|
||||
log.setInventoryCode(inventoryCode);
|
||||
log.setSegmentNo(segmentNo);
|
||||
log.setOperationType(operationType);
|
||||
log.setQuantity(quantity);
|
||||
log.setBeforeQty(beforeQty);
|
||||
log.setAfterQty(afterQty);
|
||||
log.setOrderNo(orderNo);
|
||||
log.setCreateTime(LocalDateTime.now());
|
||||
|
||||
inventoryLogRepository.save(log);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分段锁定记录
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
private static class SegmentLockRecord {
|
||||
private Integer segmentNo;
|
||||
private Integer quantity;
|
||||
private Integer beforeQty;
|
||||
private Integer afterQty;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 库存初始化
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class InventorySegmentInitService {
|
||||
|
||||
@Autowired
|
||||
private InventorySegmentRepository segmentRepository;
|
||||
|
||||
/**
|
||||
* 初始化库存分段
|
||||
*/
|
||||
public void initInventorySegments(String inventoryCode, Integer totalStock) {
|
||||
int segmentCount = 10;
|
||||
int stockPerSegment = totalStock / segmentCount;
|
||||
int remainder = totalStock % segmentCount;
|
||||
|
||||
for (int i = 0; i < segmentCount; i++) {
|
||||
InventorySegmentDO segment = new InventorySegmentDO();
|
||||
segment.setInventoryCode(inventoryCode);
|
||||
segment.setSegmentNo(i);
|
||||
|
||||
// 最后一个分段包含余数
|
||||
int stock = (i == segmentCount - 1)
|
||||
? stockPerSegment + remainder
|
||||
: stockPerSegment;
|
||||
|
||||
segment.setTotalStock(stock);
|
||||
segment.setRemainingStock(stock);
|
||||
segment.setSoldStock(0);
|
||||
segment.setVersion(0);
|
||||
segment.setCreateTime(LocalDateTime.now());
|
||||
segment.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
segmentRepository.save(segment);
|
||||
}
|
||||
|
||||
LogUtil.info("库存分段初始化完成: inventoryCode={}, totalStock={}, segments={}",
|
||||
inventoryCode, totalStock, segmentCount);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 分段策略对比
|
||||
|
||||
### 轮询策略(推荐)
|
||||
|
||||
```java
|
||||
// 优点:负载均衡,分布均匀
|
||||
// 缺点:需要维护计数器
|
||||
int segmentNo = roundRobinCounter.getAndIncrement() % SEGMENT_COUNT;
|
||||
```
|
||||
|
||||
### Hash 策略
|
||||
|
||||
```java
|
||||
// 优点:同一用户总是访问同一分段,缓存友好
|
||||
// 缺点:可能分布不均
|
||||
int segmentNo = Math.abs(userId.hashCode()) % SEGMENT_COUNT;
|
||||
```
|
||||
|
||||
### 随机策略
|
||||
|
||||
```java
|
||||
// 优点:实现简单
|
||||
// 缺点:分布可能不均
|
||||
int segmentNo = ThreadLocalRandom.current().nextInt(SEGMENT_COUNT);
|
||||
```
|
||||
|
||||
## 性能分析
|
||||
|
||||
### 优势
|
||||
|
||||
- **并发性能高**:锁冲突降低 90%
|
||||
- **可扩展性好**:可动态调整分段数
|
||||
- **适合大库存**:库存越大,效果越明显
|
||||
- **实现灵活**:可结合乐观锁或悲观锁
|
||||
|
||||
### 劣势
|
||||
|
||||
- **实现复杂**:需要管理多个分段
|
||||
- **数据量增加**:存储空间增加 N 倍
|
||||
- **回滚复杂**:失败时需要回滚多个分段
|
||||
- **查询复杂**:需要聚合多个分段
|
||||
|
||||
### 性能指标
|
||||
|
||||
| 指标 | 数值 |
|
||||
|------|------|
|
||||
| QPS | ~20000 |
|
||||
| 响应时间 | 10-20ms |
|
||||
| 锁冲突率 | <1% |
|
||||
| 存储开销 | 10倍 |
|
||||
|
||||
## 适用场景
|
||||
|
||||
### 推荐使用
|
||||
|
||||
- 库存数量巨大(> 10000)
|
||||
- 并发量极高(> 5000 QPS)
|
||||
- 可接受存储开销
|
||||
- 需要高可用性
|
||||
|
||||
### 不推荐使用
|
||||
|
||||
- 库存数量少(< 1000)
|
||||
- 并发量低
|
||||
- 存储资源紧张
|
||||
- 实现复杂度要求低
|
||||
|
||||
## 优化建议
|
||||
|
||||
### 动态调整分段数
|
||||
|
||||
```java
|
||||
public int calculateOptimalSegmentCount(int totalStock, int expectedQPS) {
|
||||
// 根据库存和 QPS 计算最优分段数
|
||||
// 每个分段至少 100 库存
|
||||
int maxSegments = totalStock / 100;
|
||||
|
||||
// 每 1000 QPS 需要 1 个分段
|
||||
int requiredSegments = expectedQPS / 1000;
|
||||
|
||||
return Math.min(maxSegments, Math.max(requiredSegments, 10));
|
||||
}
|
||||
```
|
||||
|
||||
### 分段再平衡
|
||||
|
||||
```java
|
||||
@Scheduled(cron = "0 0 2 * * ?")
|
||||
public void rebalanceSegments() {
|
||||
List<String> inventoryCodes = inventoryRepository.findAllActiveCodes();
|
||||
|
||||
for (String code : inventoryCodes) {
|
||||
List<InventorySegmentDO> segments = segmentRepository.findByInventoryCode(code);
|
||||
|
||||
// 计算平均库存
|
||||
int totalStock = segments.stream()
|
||||
.mapToInt(InventorySegmentDO::getRemainingStock)
|
||||
.sum();
|
||||
int avgStock = totalStock / segments.size();
|
||||
|
||||
// 检查是否需要再平衡
|
||||
boolean needRebalance = segments.stream()
|
||||
.anyMatch(s -> Math.abs(s.getRemainingStock() - avgStock) > avgStock * 0.3);
|
||||
|
||||
if (needRebalance) {
|
||||
rebalanceInventorySegments(code, segments, avgStock);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 与其他方案结合
|
||||
|
||||
### 分段锁 + Redis
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class HybridInventoryService {
|
||||
|
||||
@Autowired
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Autowired
|
||||
private InventorySegmentRepository segmentRepository;
|
||||
|
||||
/**
|
||||
* Redis 分段 + 数据库分段
|
||||
*/
|
||||
public Result<Boolean> lockInventory(InventoryOperationDTO dto) {
|
||||
// 选择分段
|
||||
int segmentNo = selectSegment(dto);
|
||||
|
||||
// Redis 中扣减
|
||||
String redisKey = String.format("inventory:%s:segment:%d",
|
||||
dto.getInventoryCode(), segmentNo);
|
||||
|
||||
Long newStock = redisTemplate.opsForValue()
|
||||
.decrement(redisKey, dto.getQuantity());
|
||||
|
||||
if (newStock < 0) {
|
||||
// 回滚 Redis
|
||||
redisTemplate.opsForValue().increment(redisKey, dto.getQuantity());
|
||||
return Result.fail("INSUFFICIENT_INVENTORY", "库存不足");
|
||||
}
|
||||
|
||||
// 异步同步到数据库
|
||||
asyncSyncToDatabase(dto, segmentNo);
|
||||
|
||||
return Result.success(true);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 故障处理
|
||||
|
||||
### 分段数据不一致
|
||||
|
||||
```java
|
||||
@Scheduled(cron = "0 0 * * * ?")
|
||||
public void checkSegmentConsistency() {
|
||||
List<String> inventoryCodes = inventoryRepository.findAllActiveCodes();
|
||||
|
||||
for (String code : inventoryCodes) {
|
||||
// 查询主库存
|
||||
InventoryDO mainInventory = inventoryRepository.findByInventoryCode(code);
|
||||
|
||||
// 查询分段库存总和
|
||||
int segmentTotal = segmentRepository.getTotalRemainingStock(code);
|
||||
|
||||
// 检查是否一致
|
||||
if (!mainInventory.getRemainingStock().equals(segmentTotal)) {
|
||||
LogUtil.error("分段库存不一致: inventoryCode={}, main={}, segments={}",
|
||||
code, mainInventory.getRemainingStock(), segmentTotal);
|
||||
|
||||
// 以分段为准,修正主库存
|
||||
mainInventory.setRemainingStock(segmentTotal);
|
||||
inventoryRepository.save(mainInventory);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
分段锁方案通过将大库存拆分成多个小段,大幅降低锁冲突,适合超大库存的高并发场景。实现复杂度较高,但性能提升明显。可与 Redis 方案结合,实现更高性能。
|
||||
63
docs/高并发库存方案/概览.md
Normal file
63
docs/高并发库存方案/概览.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# 高并发库存解决方案
|
||||
|
||||
## 概述
|
||||
|
||||
本文档详细介绍了在高并发场景下,如何保证库存不超卖的多种技术方案。每种方案都包含原理分析、代码实现、性能对比和适用场景。
|
||||
|
||||
## 方案对比总览
|
||||
|
||||
| 方案 | QPS | 响应时间 | 超卖风险 | 实现复杂度 | 适用场景 |
|
||||
|------|-----|---------|---------|-----------|---------|
|
||||
| 乐观锁 | ~1000 | 50ms | 无 | 低 | 冲突率低 |
|
||||
| 悲观锁 | ~500 | 100ms | 无 | 低 | 强一致性 |
|
||||
| Redis 原子操作 | ~50000 | 2ms | 无 | 中 | 高并发 |
|
||||
| 消息队列削峰 | ~10000 | 异步 | 无 | 高 | 流量削峰 |
|
||||
| 分段锁 | ~20000 | 10ms | 无 | 高 | 超大库存 |
|
||||
|
||||
## 方案列表
|
||||
|
||||
1. [乐观锁方案](./01-乐观锁方案.md) - 当前实现
|
||||
2. [悲观锁方案](./02-悲观锁方案.md) - SELECT FOR UPDATE
|
||||
3. [Redis 原子操作方案](./03-Redis原子操作方案.md) - 推荐方案
|
||||
4. [消息队列削峰方案](./04-消息队列削峰方案.md) - 异步处理
|
||||
5. [分段锁方案](./05-分段锁方案.md) - 库存分片
|
||||
|
||||
## 推荐方案
|
||||
|
||||
根据不同的业务场景,推荐使用:
|
||||
|
||||
### 高并发秒杀场景
|
||||
**推荐:Redis 原子操作方案**
|
||||
- QPS: 50000+
|
||||
- 响应时间: 2-5ms
|
||||
- 适合:库存数量大,并发量极高
|
||||
|
||||
### 普通电商场景
|
||||
**推荐:乐观锁方案**
|
||||
- QPS: 1000-3000
|
||||
- 响应时间: 20-50ms
|
||||
- 适合:并发量适中,实现简单
|
||||
|
||||
### 强一致性场景
|
||||
**推荐:悲观锁方案**
|
||||
- QPS: 500-1000
|
||||
- 响应时间: 50-100ms
|
||||
- 适合:对一致性要求极高,库存数量少
|
||||
|
||||
## 架构演进路径
|
||||
|
||||
```
|
||||
阶段1:乐观锁(当前)
|
||||
↓ 并发量增加
|
||||
阶段2:Redis 原子操作
|
||||
↓ 流量波动大
|
||||
阶段3:Redis + 消息队列
|
||||
↓ 库存量巨大
|
||||
阶段4:分段锁 + Redis
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [当前库存实现分析](./当前实现分析.md)
|
||||
- [性能压测报告](./性能压测报告.md)
|
||||
- [故障处理方案](./故障处理方案.md)
|
||||
Reference in New Issue
Block a user