SpringBoot: Redis 模拟高并发商品秒杀测试
秒杀系统的架构设计秒杀系统,是典型的短时大量突发访问类问题。对这类问题,有三种优化性能的思路:写入内存而不是写入硬盘异步处理而不是同步处理分布式处理用上这三招,不论秒杀时负载多大,都能轻松应对, Redis正好能完美满足上述三点。因此,用Redis就能轻松实现秒杀系统。秒杀测试代码编写:package com.xiao.springbootredisseckill.controller;impor
秒杀系统的架构设计
秒杀系统,是典型的短时大量突发访问类问题。对这类问题,有三种优化性能的思路:
- 写入内存而不是写入硬盘
- 异步处理而不是同步处理
- 分布式处理
用上这三招,不论秒杀时负载多大,都能轻松应对, Redis正好能完美满足上述三点。因此,用Redis就能轻松实现秒杀系统。
秒杀测试代码编写:
package com.xiao.springbootredisseckill.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.atomic.AtomicInteger;
@RestController
public class TestController {
@Autowired
private RedisTemplate redisTemplate;
// 记录实际卖出的商品数量
private AtomicInteger successNum = new AtomicInteger(0);
// 设置库存
@GetMapping("/setStockNum")
public String setStockNum(@RequestParam Integer num){
// 初始化库存数量
redisTemplate.opsForValue().set("product_stock",num);
//初始化实际卖出的商品数量0
successNum.set(0);
return "Ok";
}
// 秒杀核心代码
@GetMapping("/grabBuy")
public String grabBuy() {
Integer sku = (Integer)redisTemplate.opsForValue().get("product_stock");
sku = sku - 1;
if (sku < 0) {
return "库存不足";
}
redisTemplate.opsForValue().set("product_stock", sku);
//记录实际卖出的商品数量
return "减少库存成功,共减少" + successNum.incrementAndGet();
}
// 获取库存数量
@GetMapping("/getStockNum")
public Object getStockNum() {
Integer product_stock = Integer.parseInt((String) redisTemplate.opsForValue().get("product_stock"));
System.out.println(product_stock);
return product_stock;
}
@GetMapping(value = "/successNum")
public String successNum() {
return "顾客成功抢到的商品数量:" + successNum.get();
}
}
测试:
库存设为 100, 访问 127.0.0.1:8080/grabBuy 进行抢购。为了让测试场景更像实际抢购实际场景,这里使用 Apache 其下的 JMeter 压力测试工具,具体如何使用请自行百度
访问127.0.0.1:8080/setStockNum 设置库存,或者使用 rdm 设置库存
JMeter 配置压力测试脚本
测试结果:
访问 127.0.0.1:8080/successNum, 查看用户实际抢购成功的商品数量:
???? 为什么用户实际抢购数量为 1422 呢? 远超于库存 100 ,出现了超卖情况,如果实际应用中出现这样的错误,那跟用户怕是讲不清了 …
原因分析:
从上面测试结果,我们知道,高并发请求: 127.0.0.1:8080/grabBuy, 就会出现超卖现象。
其原因其实就是是库存数量product_stock的读和写操作不在同一个原子操作上,导致类似不可重复读的现象。可以类比多线程的问题。
什么是原子性呢?
在化学反应中,原子是不能再分的。在计算机中也就是某个操作不可分割的,就叫原子性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。
什么是不可重复读呢?
在学习数据库相关知识的时候,我们知道了事务的隔离级别:丢失更新 、脏读、不可重复读、幻读,其中不可重复读就是在同一事务中,多次读取同一数据但是返回不同的结果,也就是有其他事务更改了这些数据。
解决办法:
提供两种解决方法:
①: 通过redis事务解决超卖问题
@GetMapping("/grabBuy")
public String grabBuy() {
/*
* 解决商品超卖 方法一
* */
redisTemplate.setEnableTransactionSupport(true);
List<Object> result = (List<Object>) redisTemplate.execute(new SessionCallback<List<Object>>() {
@Override
public List<Object> execute(RedisOperations redisOperations) throws DataAccessException {
// 监听
redisOperations.watch("product_stock");
Integer product_stock = (Integer)redisOperations.opsForValue().get("product_stock");
// 开启事务
redisOperations.multi();
// 必要的空查询
redisOperations.opsForValue().get("product_stock");
product_stock = product_stock - 1;
if (product_stock < 0) {
return null;
}
// 剩余库存存入
redisOperations.opsForValue().set("product_stock", product_stock);
return redisOperations.exec();
}
});
if (result != null && result.size() > 0) {
return "减少库存成功,共减少" + successNum.incrementAndGet();
}
return "库存不足";
}
②:通过使用 redisson 的 Rlock 加锁方式解决超卖问题
pom.xml 引入依赖包:
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.6</version>
</dependency>
秒杀代码编写:
@GetMapping("/grabBuy")
public String grabBuy() {
/*
* 解决商品超卖 方法二
* */
RLock rLock = redissonClient.getLock("product");
try {
rLock.lock();
Integer product_num = (Integer)redisTemplate.opsForValue().get("product_stock");
System.out.println(product_num);
if (product_num < 1) {
return "库存不足!";
}
// 自减一
redisTemplate.opsForValue().decrement("product_stock");
return "减少库存成功,共减少" + successNum.incrementAndGet();
} finally {
rLock.unlock();
}
}
注意: redissonClient.getLock() 括号内的参数千万不能取redis数据库脸面已经含有的键名,否则报如下错误,我找半天才发现这里的错误。
org.redisson.client.RedisException: ERR Error running script (call to f_3ffe249c16dee540ac8ab32e39bd408626b9aff7): @user_script:1: WRONGTYPE Operation against a key holding the wrong kind of value . channel: [id: 0xfc81693b, L:/127.0.0.1:11732 - R:localhost/127.0.0.1:6379] command: (EVAL), params: [if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('h..., 2, product_stock, redisson_lock__channel:{product_stock}, 0, 30000, 93656223-0013-4a55-a2d6-06ec7df0cda2:71]
结果展示:
成功解决超卖问题!
更多推荐
所有评论(0)