1. 项目背景与核心需求
新日用品公司仓储系统是一个典型的B2C电商后端管理系统,主要解决日用品行业特有的仓储管理痛点。日用品行业SKU数量庞大(通常在5000-20000个之间)、周转快(平均周转周期7-15天)、批次管理复杂(涉及保质期、促销批次等),传统Excel或简单进销存系统已无法满足业务需求。
我在实际开发中发现,这类系统需要特别关注三个核心指标:
- 库存准确率(要求≥99.5%)
- 订单处理时效(从下单到出库≤15分钟)
- 盘点效率(全仓盘点≤4小时)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计
2.1 整体技术栈选型
采用Spring Boot 3.1 + MyBatis-Plus + Redis + RabbitMQ的技术组合。这里特别说明几个关键选择:
- 放弃JPA选择MyBatis-Plus:日用品行业有大量定制化报表需求,需要灵活SQL控制
- Redis采用Cluster模式:预计峰值QPS在3000左右,单节点无法满足
- RabbitMQ做异步解耦:订单创建与库存扣减必须分离,避免长事务
java复制// 典型的多级缓存配置示例
@Configuration
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(Object.class)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withCacheConfiguration("inventory", config.entryTtl(Duration.ofMinutes(30)))
.build();
}
}
2.2 数据库设计要点
设计了8个核心表,这里重点说明三个关键表结构:
- 库存主表(inventory)
sql复制CREATE TABLE `inventory` (
`id` bigint NOT NULL AUTO_INCREMENT,
`sku_code` varchar(32) NOT NULL COMMENT '国际条码',
`batch_no` varchar(24) NOT NULL COMMENT '生产批次',
`location_code` varchar(16) NOT NULL COMMENT '库位编码',
`quantity` int NOT NULL DEFAULT '0' COMMENT '可用库存',
`locked_quantity` int NOT NULL DEFAULT '0' COMMENT '锁定库存',
`expire_date` date DEFAULT NULL COMMENT '到期日',
`version` int NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_sku_location` (`sku_code`,`location_code`,`batch_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
- 库存流水表(inventory_log)
sql复制CREATE TABLE `inventory_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`sku_code` varchar(32) NOT NULL,
`batch_no` varchar(24) NOT NULL,
`location_code` varchar(16) NOT NULL,
`change_quantity` int NOT NULL COMMENT '变动数量(正数入库,负数出库)',
`remain_quantity` int NOT NULL COMMENT '变动后库存',
`biz_type` tinyint NOT NULL COMMENT '1-采购入库 2-销售出库...',
`biz_no` varchar(32) NOT NULL COMMENT '关联单号',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_sku` (`sku_code`),
KEY `idx_biz` (`biz_type`,`biz_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
重要提示:日用品行业必须记录完整的批次信息和库存变动流水,这是后续效期预警、批次追溯的基础。
3. 核心业务逻辑实现
3.1 库存扣减的并发控制
采用"乐观锁+库存预占"模式解决超卖问题:
java复制@Transactional
public boolean deductInventory(String skuCode, String batchNo, int quantity) {
// 1. 查询可用库存(带乐观锁)
Inventory inventory = inventoryMapper.selectOne(new QueryWrapper<Inventory>()
.eq("sku_code", skuCode)
.eq("batch_no", batchNo)
.last("FOR UPDATE")); // 关键点:加行锁
if (inventory == null || inventory.getQuantity() < quantity) {
return false;
}
// 2. 扣减可用库存并增加锁定库存
inventory.setQuantity(inventory.getQuantity() - quantity);
inventory.setLockedQuantity(inventory.getLockedQuantity() + quantity);
inventory.setVersion(inventory.getVersion() + 1);
// 3. 带版本号更新
int affected = inventoryMapper.update(inventory,
new UpdateWrapper<Inventory>()
.eq("id", inventory.getId())
.eq("version", inventory.getVersion() - 1));
if (affected == 0) {
throw new OptimisticLockingFailureException("库存并发修改冲突");
}
// 4. 记录流水
InventoryLog log = new InventoryLog();
// ...设置日志字段
inventoryLogMapper.insert(log);
return true;
}
3.2 效期预警实现
通过定时任务+Redis ZSet实现高效效期管理:
java复制@Scheduled(cron = "0 0 8 * * ?") // 每天8点执行
public void expireWarningTask() {
// 1. 查询3天内到期的商品
LocalDate warningDate = LocalDate.now().plusDays(3);
List<Inventory> warningList = inventoryMapper.selectList(
new QueryWrapper<Inventory>()
.le("expire_date", warningDate)
.gt("quantity", 0));
// 2. 按SKU分组统计
Map<String, Integer> warningMap = warningList.stream()
.collect(Collectors.groupingBy(
Inventory::getSkuCode,
Collectors.summingInt(Inventory::getQuantity)));
// 3. 存入Redis ZSet(score=到期时间戳)
String redisKey = "inventory:expire_warning:" + LocalDate.now();
RedisTemplate<String, String> redisTemplate = getRedisTemplate();
warningMap.forEach((skuCode, quantity) -> {
Inventory inventory = warningList.stream()
.filter(i -> i.getSkuCode().equals(skuCode))
.findFirst()
.orElse(null);
if (inventory != null) {
long expireTimestamp = inventory.getExpireDate().atStartOfDay()
.toInstant(ZoneOffset.of("+8")).toEpochMilli();
redisTemplate.opsForZSet().add(
redisKey,
skuCode + "|" + quantity,
expireTimestamp);
}
});
// 4. 设置Redis key的过期时间(7天)
redisTemplate.expire(redisKey, 7, TimeUnit.DAYS);
}
4. 性能优化实践
4.1 热点库存解决方案
对于促销商品(如双11爆款),采用三级缓存策略:
- JVM缓存(Caffeine):存储最热门的10个SKU,过期时间5秒
- Redis缓存:存储前100个热门SKU,过期时间30秒
- 数据库:真实库存
java复制public Inventory getInventoryWithCache(String skuCode) {
// 1. 先查JVM缓存
Inventory inventory = caffeineCache.getIfPresent(skuCode);
if (inventory != null) {
return inventory;
}
// 2. 查Redis缓存
String redisKey = "inventory:hot:" + skuCode;
String redisValue = redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotBlank(redisValue)) {
inventory = JSON.parseObject(redisValue, Inventory.class);
caffeineCache.put(skuCode, inventory); // 回填JVM缓存
return inventory;
}
// 3. 查数据库
inventory = inventoryMapper.selectOne(new QueryWrapper<Inventory>()
.eq("sku_code", skuCode)
.last("LIMIT 1"));
if (inventory != null) {
// 异步更新缓存
CompletableFuture.runAsync(() -> {
redisTemplate.opsForValue().set(
redisKey,
JSON.toJSONString(inventory),
30, TimeUnit.SECONDS);
});
}
return inventory;
}
4.2 批量操作优化
使用MyBatis的批量插入功能提升入库效率:
java复制public void batchInsertInventory(List<Inventory> list) {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
InventoryMapper mapper = session.getMapper(InventoryMapper.class);
for (int i = 0; i < list.size(); i++) {
mapper.insert(list.get(i));
if (i % 500 == 0 || i == list.size() - 1) {
session.flushStatements(); // 每500条提交一次
}
}
session.commit();
}
}
5. 踩坑经验与避坑指南
5.1 分布式事务问题
场景:订单创建与库存扣减需要保持一致性
错误做法:直接使用@Transactional跨服务调用
正确方案:
- 采用本地消息表+定时任务补偿
- 或者使用Seata AT模式(适合中小规模系统)
java复制// 本地消息表示例
public void createOrder(OrderDTO orderDTO) {
// 1. 保存订单主表
orderMapper.insert(order);
// 2. 写入本地消息表
TransactionMessage message = new TransactionMessage();
message.setBizType("ORDER_CREATE");
message.setBizId(order.getId());
message.setContent(JSON.toJSONString(orderDTO));
messageMapper.insert(message);
// 3. 发送MQ消息(不保证一定成功)
try {
rabbitTemplate.convertAndSend(
"inventory.event.exchange",
"inventory.deduct",
new InventoryDeductDTO(order.getId(), orderDTO.getItems()));
} catch (Exception e) {
log.error("MQ发送失败,依赖定时任务补偿", e);
}
}
5.2 库存快照问题
场景:财务需要每日库存快照对账
错误做法:直接select * from inventory
正确方案:
- 建立专门的快照表
- 使用MySQL事件每天凌晨自动执行
sql复制CREATE EVENT `inventory_snapshot_event`
ON SCHEDULE EVERY 1 DAY STARTS '2023-01-01 02:00:00'
DO
BEGIN
INSERT INTO inventory_snapshot(snapshot_date, sku_code, quantity)
SELECT CURDATE(), sku_code, quantity FROM inventory;
END
6. 扩展功能建议
6.1 可视化库位管理
基于Three.js实现3D库位可视化:
javascript复制// 前端核心代码示例
function initWarehouse3D() {
const container = document.getElementById('container');
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
// 加载库位模型
const loader = new THREE.GLTFLoader();
loader.load('models/location.glb', function(gltf) {
scene.add(gltf.scene);
});
// 鼠标悬停显示库存信息
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener('mousemove', (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);
if (intersects.length > 0) {
const locationCode = intersects[0].object.userData.locationCode;
// 通过AJAX获取库存详情...
}
});
}
6.2 移动端PDA功能
集成ZXing实现扫码盘点:
java复制// Android端扫码核心代码
public class ScanActivity extends AppCompatActivity {
private BarcodeView barcodeView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scan);
barcodeView = findViewById(R.id.barcode_view);
barcodeView.decodeContinuous(result -> {
runOnUiThread(() -> {
String skuCode = result.getText();
// 调用API查询库存...
});
});
}
@Override
protected void onResume() {
super.onResume();
barcodeView.resume();
}
}
7. 部署方案建议
7.1 容器化部署
使用Docker Compose编排服务:
yaml复制version: '3.8'
services:
app:
image: openjdk:17-jdk
working_dir: /app
volumes:
- ./target/warehouse.jar:/app/app.jar
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=prod
command: java -jar app.jar
redis:
image: redis:7.0
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
redis_data:
7.2 监控配置
Prometheus + Grafana监控关键指标:
yaml复制# application.yml配置示例
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
tags:
application: warehouse-system
关键监控指标建议:
- 库存操作平均耗时(histogram_warehouse_inventory_op_duration_seconds)
- 库存扣减失败率(gauge_warehouse_deduct_failure_rate)
- 缓存命中率(gauge_warehouse_cache_hit_ratio)
8. 项目演进路线
8.1 短期优化(1-3个月)
- 引入Elasticsearch实现多维度库存搜索
- 增加库存预测功能(基于历史销售数据)
- 完善OpenAPI文档(Swagger UI)
8.2 中期规划(3-6个月)
- 对接AGV机器人实现智能拣货
- 开发供应商协同门户
- 引入区块链技术实现批次溯源
8.3 长期愿景(6-12个月)
- 构建智能补货系统(机器学习驱动)
- 全国多仓联动管理
- 与ERP、CRM系统深度集成
在实际开发过程中,我发现日用品仓储系统最关键的三个原则是:数据准确性优先、操作可追溯、异常可预警。特别是在促销期间,系统需要提前做好压力测试,建议使用JMeter模拟至少3倍日常峰值的并发量进行全链路压测。
