跳到主要内容Spring Cloud 商品服务实战:库存、缓存与分布式锁设计 | 极客日志JavaSaaSjava
Spring Cloud 商品服务实战:库存、缓存与分布式锁设计
针对电商高并发场景下的商品服务,重点解决库存超卖、缓存穿透击穿雪崩及分布式并发控制问题。通过数据库乐观锁配合重试机制防止超卖,利用布隆过滤器与互斥锁应对缓存异常,结合 Redisson 分布式锁保障集群环境下的数据一致性。最终提供从项目搭建到性能测试的完整落地方案,确保系统在高负载下稳定运行。
ByteFlow11 浏览 背景与挑战
在微服务架构的电商体系中,商品服务是整个业务链路的核心枢纽。它承接前端展示、支撑订单扣减、联动促销活动,而其中的库存管理、缓存设计和分布式锁更是决定系统稳定性的关键。很多开发者在落地时,往往会遭遇三大核心痛点:高并发下库存超卖、缓存穿透/击穿/雪崩导致服务雪崩、分布式环境下并发控制失效。
本文将聚焦这三大核心业务,深入拆解底层原理与设计思路,助力你快速搭建能支撑高并发场景的商品服务。
1. 前置认知:核心价值与高并发痛点
1.1 核心价值
商品服务作为电商微服务体系的'基础数据中心',核心价值体现在三个维度:
- 数据支撑:提供商品基础信息(名称、价格、规格)、库存数据,为订单、购物车等服务提供依赖;
- 库存管控:确保库存数据精准,避免超卖/少卖,保障交易合规性;
- 高并发承载:通过缓存、并发控制等设计,支撑大促期间的高 QPS 查询与库存扣减需求。
商品服务在微服务体系中的核心地位可通过下图直观展示:

1.2 高并发痛点
在高并发场景(如大促、秒杀)下,最易遭遇三大痛点:
- 库存超卖:多个线程同时扣减库存时,因并发控制不当,导致实际扣减数量超过库存总量;
- 缓存三大问题:缓存穿透(查询不存在的商品)、缓存击穿(热点商品缓存过期)、缓存雪崩(大量缓存同时过期),均可能导致数据库压力激增;
- 分布式锁失效:微服务集群部署下,本地锁无法跨服务生效,导致并发控制失效。
2. 技术选型:构建高可用商品服务的技术栈清单
本文采用 Spring Cloud Alibaba 生态,结合成熟的中间件,具体选型如下:
| 技术领域 | 技术选型 | 选型理由 |
|---|
| 核心框架 | Spring Boot 3.2 + Spring Cloud Alibaba 2023.0.1.0 | 主流微服务框架,生态完善 |
| 数据持久层 | MyBatis-Plus 3.5.5 | 简化开发,支持乐观锁、分页 |
| 数据库 | MySQL 8.0 | 稳定高效,支持行级锁 |
| 缓存中间件 | Redis 7.0 | 高性能内存数据库 |
| 分布式锁 | Redisson 3.23.3 | 基于 Redis 实现,支持自动续期 |
| 服务注册发现 | Nacos 2.3.2 | 阿里开源,轻量高效 |
| 工具类 | Hutool 5.8.20 | 简化重复开发 |
| 性能测试 | JMeter 5.6 | 模拟高并发场景 |
3. 环境搭建:初始化与基础配置
3.1 项目初始化
创建 Spring Boot 项目(命名为 product-service),引入核心依赖,pom.xml 关键配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>product-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>product-service</name>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2023.0.1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
3.2 基础配置
编写 application.yml 配置文件,配置数据库、Redis、Nacos 等核心参数:
server:
port: 8081
spring:
application:
name: product-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/product_service?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: root123456
redis:
host: localhost
port: 6379
database: 0
timeout: 3000ms
cloud:
nacos:
discovery:
server-addr: localhost:8848
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.productservice.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
redisson:
singleServerConfig:
address: redis://localhost:6379
database: 0
timeout: 3000ms
3.3 项目结构搭建
product-service/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/productservice/
│ │ │ ├── ProductServiceApplication.java
│ │ │ ├── entity/
│ │ │ ├── mapper/
│ │ │ ├── service/
│ │ │ ├── controller/
│ │ │ ├── config/
│ │ │ ├── util/
│ │ │ └── exception/
│ │ └── resources/
│ │ ├── mapper/
│ │ ├── application.yml
│ │ └── db/
│ └── test/
└── pom.xml
4. 核心模块一:库存管理(精准扣减 + 防超卖)
库存管理的核心目标是确保库存数据精准,杜绝超卖。本文采用'数据库乐观锁'实现库存扣减,兼顾性能与数据一致性。
4.1 数据模型设计
设计 product(商品表)和 stock(库存表),将商品基础信息与库存数据分离。
4.1.1 数据库脚本
创建 product_service 数据库,执行以下 SQL 脚本:
CREATE TABLE `product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品 ID',
`product_name` varchar(100) NOT NULL COMMENT '商品名称',
`price` decimal(10,2) NOT NULL COMMENT '商品价格',
`spec` varchar(200) DEFAULT NULL COMMENT '商品规格',
`status` tinyint(1) DEFAULT 1 COMMENT '状态:1-上架,0-下架',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
CREATE TABLE `stock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '库存 ID',
`product_id` bigint(20) NOT NULL COMMENT '商品 ID',
`total_stock` int(11) NOT NULL DEFAULT 0 COMMENT '总库存',
`lock_stock` int(11) NOT NULL DEFAULT 0 COMMENT '已锁定库存',
`available_stock` int(11) NOT NULL DEFAULT 0 COMMENT '可用库存',
`version` int(11) NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁用)',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';
4.1.2 实体类编写
编写 Product 和 Stock 实体类(使用 Lombok 简化代码):
package com.example.productservice.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("product")
public class Product {
@TableId(type = IdType.AUTO)
private Long id;
private String productName;
private BigDecimal price;
private String spec;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
package com.example.productservice.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("stock")
public class Stock {
@TableId(type = IdType.AUTO)
private Long id;
private Long productId;
private Integer totalStock;
private Integer lockStock;
private Integer availableStock;
@Version
private Integer version;
private LocalDateTime updateTime;
}
4.2 库存核心操作实现
4.2.1 数据层开发
编写 StockMapper 接口及 XML 文件,实现库存扣减的 SQL(乐观锁核心):
package com.example.productservice.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.productservice.entity.Stock;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface StockMapper extends BaseMapper<Stock> {
int deductStock(@Param("productId") Long productId, @Param("deductCount") Integer deductCount, @Param("version") Integer version);
Stock selectStockByProductId(@Param("productId") Long productId);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.productservice.mapper.StockMapper">
<update>
UPDATE stock
SET available_stock = available_stock - #{deductCount},
lock_stock = lock_stock + #{deductCount},
version = version + 1
WHERE product_id = #{productId}
AND available_stock >= #{deductCount}
AND version = #{version}
</update>
<select resultType="com.example.productservice.entity.Stock">
SELECT id, product_id, total_stock, lock_stock, available_stock, version, update_time
FROM stock WHERE product_id = #{productId}
</select>
</mapper>
4.2.2 业务层开发
编写 StockServiceImpl 实现类,封装库存核心操作,重点处理扣减库存的重试逻辑(乐观锁失败时重试):
@Service
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> implements StockService {
@Resource
private StockMapper stockMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Result<?> deductStock(Long productId, Integer deductCount) {
if (productId == null || deductCount == null || deductCount <= 0) {
return Result.fail("参数错误");
}
int maxRetry = 3;
int retryCount = 0;
while (retryCount < maxRetry) {
Stock stock = stockMapper.selectStockByProductId(productId);
if (stock == null) {
return Result.fail("商品库存不存在");
}
if (stock.getAvailableStock() < deductCount) {
return Result.fail("库存不足");
}
int affectRows = stockMapper.deductStock(productId, deductCount, stock.getVersion());
if (affectRows > 0) {
return Result.success("库存扣减成功");
}
retryCount++;
if (retryCount >= maxRetry) {
return Result.fail("库存扣减失败,请重试");
}
try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
return Result.fail("库存扣减失败");
}
}
4.3 库存流转流程
库存流转的核心场景包括'下单扣减''订单取消解锁''订单支付确认''退货补增',完整流程如下:
5. 核心模块二:缓存设计(穿透 + 击穿 + 雪崩解决方案)
商品查询是高并发场景的核心需求,直接查询数据库会导致压力过大。但缓存使用不当会引发'穿透、击穿、雪崩'三大问题。
5.1 缓存核心流程
商品缓存的核心流程是'缓存优先查询':先查 Redis,命中则返回;未命中则查库并写缓存。商品信息更新时,同步更新缓存或删除缓存。
5.2 缓存三大问题解决方案
5.2.1 缓存穿透(查询不存在的商品)
问题:恶意用户频繁查询不存在的商品 ID,直接穿透到数据库。
解决方案:布隆过滤器拦截 + 缓存空值(短期缓存不存在的商品结果)。
@Component
public class CacheUtil {
@Resource
private StringRedisTemplate stringRedisTemplate;
private final BloomFilter<Long> productBloomFilter = BloomFilter.create(100000, 0.0001);
public void initBloomFilter(Long... productIds) {
for (Long productId : productIds) {
productBloomFilter.add(productId);
}
}
public String getCacheWithPenetrationProtection(String key, Long productId) {
if (!productBloomFilter.contains(productId)) {
return null;
}
String value = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isBlank(value)) {
stringRedisTemplate.opsForValue().set(key, "", 1, TimeUnit.MINUTES);
return null;
}
return value;
}
}
5.2.2 缓存击穿(热点商品缓存过期)
问题:热点商品缓存过期瞬间,大量请求穿透到数据库。
解决方案:热点商品永不过期(后台定时更新)+ 非热点商品互斥锁。
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
@Resource
private CacheUtil cacheUtil;
private final Lock cacheLock = new ReentrantLock();
@Override
public Result<Product> getProductById(Long productId) {
String cacheKey = "product:info:" + productId;
String cacheValue = cacheUtil.getCacheWithPenetrationProtection(cacheKey, productId);
if (StringUtils.hasText(cacheValue)) {
return Result.success(objectMapper.readValue(cacheValue, Product.class));
}
Product product = null;
try {
if (cacheLock.tryLock(500, TimeUnit.MILLISECONDS)) {
cacheValue = cacheUtil.getCacheWithPenetrationProtection(cacheKey, productId);
if (StringUtils.hasText(cacheValue)) {
return Result.success(objectMapper.readValue(cacheValue, Product.class));
}
product = productMapper.selectById(productId);
if (product == null) return Result.fail("商品不存在");
boolean isHotProduct = productId <= 100;
cacheUtil.setCacheWithBreakdownProtection(cacheKey, objectMapper.writeValueAsString(product), isHotProduct, 30, TimeUnit.MINUTES);
} else {
return Result.fail("查询繁忙,请重试");
}
} finally {
if (cacheLock.isHeldByCurrentThread()) cacheLock.unlock();
}
return Result.success(product);
}
}
5.2.3 缓存雪崩(大量缓存同时过期)
问题:大量缓存同时过期,导致数据库压力激增。
解决方案:缓存过期时间添加随机值(分散过期时间)。
在 CacheUtil 的 setCacheWithBreakdownProtection 方法中,已实现'过期时间加随机值'逻辑:
long randomExpire = expireTime + (long) (Math.random() * 300);
stringRedisTemplate.opsForValue().set(key, value, randomExpire, timeUnit);
5.3 缓存一致性保障
商品信息更新时,需确保缓存与数据库数据一致,采用'先更新数据库,再删除缓存'的策略。
@Override
@Transactional(rollbackFor = Exception.class)
public Result<?> updateProduct(Product product) {
boolean updateResult = this.updateById(product);
if (!updateResult) return Result.fail("商品更新失败");
String cacheKey = "product:info:" + product.getId();
cacheUtil.deleteCache(cacheKey);
return Result.success("商品更新成功");
}
6. 核心模块三:分布式锁(Redisson 实现)
在微服务集群部署场景下,本地锁无法跨服务生效。本文基于 Redisson 实现分布式锁。
6.1 分布式锁核心原理
Redisson 基于 Redis 实现分布式锁,核心采用 Redis 的 SETNX 命令,同时支持自动续期、可重入等企业级特性。
6.2 Redisson 分布式锁核心配置
Redisson 已通过 Starter 集成,只需在 application.yml 中配置 Redis 连接信息。若需自定义锁参数,可添加如下配置:
redisson:
singleServerConfig:
address: redis://localhost:6379
database: 0
timeout: 3000ms
lock:
defaultLockWatchdogTimeout: 30000
6.3 分布式锁 + 乐观锁 双重保障库存扣减
为进一步提升库存扣减的并发安全性,采用'分布式锁(跨服务并发控制) + 乐观锁(数据库层并发控制)'的双重保障策略。
修改 StockServiceImpl 的 deductStock 方法,添加分布式锁逻辑:
@Service
public class StockServiceImpl extends ServiceImpl<StockMapper, Stock> implements StockService {
@Resource
private RedissonClient redissonClient;
@Override
@Transactional(rollbackFor = Exception.class)
public Result<?> deductStock(Long productId, Integer deductCount) {
String lockKey = "stock:deduct:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
boolean lockAcquired = lock.tryLock(5, TimeUnit.SECONDS);
if (!lockAcquired) return Result.fail("系统繁忙,请稍后重试");
int maxRetry = 3;
int retryCount = 0;
while (retryCount < maxRetry) {
Stock stock = stockMapper.selectStockByProductId(productId);
if (stock == null) return Result.fail("商品库存不存在");
if (stock.getAvailableStock() < deductCount) return Result.fail("库存不足");
int affectRows = stockMapper.deductStock(productId, deductCount, stock.getVersion());
if (affectRows > 0) return Result.success("库存扣减成功");
retryCount++;
if (retryCount >= maxRetry) return Result.fail("库存扣减失败,请重试");
try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return Result.fail("获取锁异常,请稍后重试");
} finally {
if (lock.isHeldByCurrentThread()) lock.unlock();
}
return Result.fail("库存扣减失败,请重试");
}
}
- 锁粒度:按商品 ID 粒度加锁,避免全局锁。
- 锁等待:设置最大等待时间,避免用户无限等待。
- 自动续期:解决长耗时业务导致锁超时释放的问题。
- 锁释放:在
finally 块中释放锁,避免死锁。
7. 实战测试:高并发场景下的功能与性能验证
通过 JMeter 模拟高并发场景,验证库存防超卖、缓存有效性、分布式锁并发控制三大核心功能。
7.1 测试环境准备
- 基础数据:添加商品(ID=1,名称='测试商品',价格=99.9),初始化库存(总库存=1000)。
- JMeter 配置:线程数=1000,循环次数=1,调用库存扣减接口。
- 服务部署:启动 2 个商品服务实例,注册到 Nacos,模拟集群部署。
7.2 测试步骤与预期结果
- 库存防超卖测试:1000 个并发请求,最终可用库存=0,无超卖。
- 缓存有效性测试:第一次查询查库,后续查询命中缓存,响应时间显著降低。
- 分布式锁并发控制测试:集群环境下,无重复扣减或超卖。
7.3 性能测试指标
| 测试指标 | 单机服务(8081) | 集群服务(8081+8082) | 优化目标 |
|---|
| 平均响应时间 | 50ms | 30ms | < 100ms |
| 95% 响应时间 | 100ms | 60ms | < 200ms |
| QPS | 2000 | 3500 | > 1000 |
| 错误率 | 0% | 0% | 0% |
8. 避坑指南:企业级落地的 6 个核心注意点
-
避坑 1:分布式锁粒度太粗
- 问题:使用全局锁,所有商品竞争同一把锁。
- 方案:按商品 ID 粒度加锁,细化锁的范围。
-
避坑 2:缓存更新策略错误
- 问题:采用'先删缓存,再更数据库',导致脏数据。
- 方案:采用'先更数据库,再删缓存',配合定时任务校验。
-
避坑 3:乐观锁重试次数过多
- 问题:无限重试,阻塞系统。
- 方案:设置最大重试次数(如 3 次)。
-
避坑 4:分布式锁未释放
- 问题:异常时未释放锁,导致死锁。
- 方案:在
finally 块中释放锁。
-
避坑 5:缓存空值未设置过期时间
- 问题:Redis 积累大量空值缓存。
- 方案:缓存空值时设置短期过期时间。
-
避坑 6:库存字段设计不合理
- 问题:仅设计总库存,未区分可用和锁定库存。
- 方案:设计总库存、可用库存、锁定库存三个字段。
9. 总结与展望
9.1 核心总结
本文完整实现了企业级 Spring Cloud 商品服务,聚焦三大核心业务:
- 库存管理:采用'总库存 + 可用库存 + 锁定库存'的字段设计,结合乐观锁与重试机制,实现精准库存扣减,杜绝超卖;
- 缓存设计:针对缓存穿透、击穿、雪崩三大问题,提供布隆过滤器 + 缓存空值、热点商品永不过期 + 互斥锁、过期时间加随机值的完整解决方案;
- 分布式锁:基于 Redisson 实现分布式锁,按商品 ID 细化锁粒度,配合乐观锁实现双重并发控制。
9.2 进阶扩展方向
- 库存预占与超时释放:实现订单创建时预占库存,超过支付时间自动释放;
- 多级缓存设计:引入本地缓存(Caffeine)+ Redis 分布式缓存;
- 分布式事务:集成 Seata 实现分布式事务;
- 热点商品隔离:对热点商品进行单独的缓存和库存管理;
- 监控与告警:集成 Prometheus + Grafana,监控库存变化、缓存命中率等。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online