一、环境准备与基础组件搭建
在进行商城大促压测前,需要准备一套独立的环境,避免压测数据污染生产数据库。我们将使用Docker快速搭建Redis,并准备JMeter作为压测工具。
1. 安装并启动Redis
Redis用于在大促场景中处理热点数据缓存和分布式锁。执行以下命令使用Docker启动Redis服务:
```bash
docker run -d -p 6379:6379 --name redis-press-test redis:6.2-alpine
```
执行完毕后,使用redis-cli ping命令,若返回PONG则说明服务正常。
2. 下载并安装JMeter
JMeter是本次全链路压测的核心工具。请直接执行以下命令下载5.5版本(无需配置Java环境,自带运行包):
```bash
wget https://downloads.apache.org//jmeter/binaries/apache-jmeter-5.5.tgz
tar -xvf apache-jmeter-5.5.tgz
cd apache-jmeter-5.5
```
二、模拟大促秒杀后端服务
为了真实模拟大促场景,我们需要构建一个简单的Spring Boot应用,包含基于Redisson的分布式锁实现,防止库存超卖。
1. Maven依赖配置
在pom.xml中引入Web、Redis以及Redisson依赖,版本号请严格锁定:
```xml
org.springframework.boot
spring-boot-starter-web
2.7.14
org.redisson
redisson-spring-boot-starter
3.23.4
```
2. 配置文件与初始化库存
在application.yml中配置Redis连接地址,并设置一个初始化库存的接口。确保配置内容如下:
```yaml
server:
port: 8080
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0
```
3. 核心秒杀代码实现
创建SecKillController类,实现扣减库存逻辑。这里使用Redisson分布式锁来保证并发安全。请完整复制以下代码:
```java
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SecKillController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
// 初始化库存:访问此接口将库存设为1000
@GetMapping("/init")
public String init() {
stringRedisTemplate.opsForValue().set("stock:1001", "1000");
return "Stock initialized to 1000";
}
// 秒杀接口
@GetMapping("/seckill")
public String doSecKill() {
String lockKey = "lock:product:1001";
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,等待时间为0,锁持有时间为10秒
boolean locked = lock.tryLock(0, 10, java.util.concurrent.TimeUnit.SECONDS);
if (!locked) {
return "System busy, please try later";
}
String stockStr = stringRedisTemplate.opsForValue().get("stock:1001");
if (stockStr == null) {
return "Product not found";
}
int stock = Integer.parseInt(stockStr);
if (stock <= 0) {
return "Sold out";
}
// 扣减库存
stringRedisTemplate.opsForValue().set("stock:1001", String.valueOf(stock - 1));
return "Success! Remaining stock: " + (stock - 1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "Error";
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
``>
启动Spring Boot应用,访问http://localhost:8080/init确保库存初始化成功。
三、JMeter压测脚本精细化配置
打开JMeter图形界面(./bin/jmeter.sh),按照以下步骤配置压测脚本,模拟大促瞬间流量。
1. 添加线程组
右键“测试计划” -> “添加” -> “线程(用户)” -> “线程组”。配置参数如下:
- 线程数: 500(模拟500个并发用户)
- Ramp-Up Period(准备时长): 1(1秒内启动所有线程,制造尖刺流量)
- 循环次数:10(每个用户重复请求10次,总计5000次请求)
2. 配置HTTP请求默认值
右键“线程组” -> “添加” -> “配置元件” -> “HTTP请求默认值”。设置服务器名称或IP为localhost,端口号为8080。
3. 添加秒杀请求采样器
右键“线程组” -> “添加” -> “取样器” -> “HTTP请求”。配置具体接口信息:
- 协议: http
- 路径: /seckill
- 方法: GET
4. 添加监听器查看结果

右键“线程组” -> “添加” -> “监听器” -> “聚合报告” 和 “查看结果树”。聚合报告用于查看吞吐量,查看结果树用于调试请求响应内容。
四、执行压测与数据监控
保存JMeter脚本(例如seckill_test.jmx)。为了获得更准确的数据,建议使用非GUI模式运行压测:
```bash
./bin/jmeter.sh -n -t seckill_test.jmx -l result.jtl -e -o report_folder
```
参数解释:-n非GUI模式,-l生成结果文件,-e和-o生成HTML可视化报告。
1. 核心指标解读
压测结束后,打开report_folder/index.html查看报告。重点关注以下指标:
- Throughput (吞吐量): 正常情况下应达到每秒数百次。如果数值极低(如<10/sec),说明锁竞争严重或数据库连接池耗尽。
- Error % (错误率): 必须为0%。若出现大量错误,检查后端日志是否报错“Redis connection timeout”。
2. 验证库存一致性
压测结束后,访问Redis查询剩余库存:
```bash
redis-cli get "stock:1001"
```
若初始为1000,请求成功次数与剩余库存之和必须等于1000。如果结果为负数,说明代码存在并发安全问题;如果结果过大,说明大量请求被拦截在锁外。
五、性能瓶颈深度优化
在上述基础压测中,Redis分布式锁虽然保证了安全,但性能损耗较大。针对大促场景,我们采用Redis Lua脚本实现原子性扣减,去除加锁开销。
1. 优化后的Lua脚本方案
创建一个新的Controller方法doSecKillOptimized,使用DefaultRedisScript执行Lua脚本。脚本逻辑为:检查库存是否大于0,若大于0则减1并返回1,否则返回0。
首先定义Lua脚本字符串(常量):
```java
private static final String STOCK_LUA_SCRIPT = "if redis.call('get', KEYS[1]) then " +
"local stock = tonumber(redis.call('get', KEYS[1])); " +
"if (stock > 0) then " +
"redis.call('set', KEYS[1], stock - 1); " +
"return 1; " +
"end; " +
"end; " +
"return 0;";
```
优化后的Controller代码:
```java
@GetMapping("/seckill-opt")
public String doSecKillOptimized() {
DefaultRedisScript
redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(STOCK_LUA_SCRIPT);
redisScript.setResultType(Long.class);
Long result = stringRedisTemplate.execute(
redisScript,
Collections.singletonList("stock:1001"),
new ArrayList<>()
);
if (result != null && result == 1) {
return "Optimized Success";
} else {
return "Sold Out or Busy";
}
}
```
2. 再次压测验证效果
修改JMeter中的HTTP请求路径为/seckill-opt,保持线程组参数不变,再次运行压测。
- 预期结果: 吞吐量应提升至基础版的5-10倍,错误率保持为0%。
- CPU监控: 使用
top命令观察Java进程和Redis进程的CPU占用率,优化后Redis CPU会有明显上升(处理能力增强),Java CPU占用降低(减少了上下文切换)。
3. 数据库连接池防死锁配置
大促流量下,数据库连接池容易耗尽。在application.yml中强制配置HikariCP参数:
```yaml
spring:
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
connection-timeout: 3000 3秒获取连接超时
idle-timeout: 60000
max-lifetime: 1800000
```
此配置确保在高并发下,应用能快速报错而非长时间阻塞等待连接,避免雪崩效应。