抢购功能
功能实现流程:判断用户是否已经抢购;判断是否还有商品库存;减库存;生产订单;保存购买记录;返回操作结果。
创建 SpringBoot 项目,引入 Redis 等相关依赖,配置 yml 文件
在启动时加载类中初始化抢购对象
@SpringBootApplication
public class RushApplication {
@Resource
private StringRedisTemplate stringRedisTemplate;
public static void main(String[] args) {
SpringApplication.run(RushApplication.class, args);
}
@Bean
public void info(){
// 初始化抢购对象
stringRedisTemplate.opsForValue().set("goods:1001", "100");
}
}
在控制器中模拟抢购流程
@RestController
@RequestMapping("/index")
public class IndexController {
@Resource
private StringRedisTemplate stringRedisTemplate;
// 模拟抢购用户和商品
Long userId = 1101L;
Long goodsId = 1001L;
@GetMapping("/rush")
public String index(){
// 判断是否有抢购标识(已成功参与抢购)
if (stringRedisTemplate.opsForValue().get("user:"+userId+":"+goodsId) != null){
return "已经抢购过该商品!";
}
// 判断是否还有库存
Integer stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("goods:"+goodsId));
if (stock <= 0){
return "抢购失败,该商品已售罄!";
}
// 减库存
stock--;
stringRedisTemplate.opsForValue().set("goods:"+goodsId, stock.toString());
// 添加抢购标识
stringRedisTemplate.opsForValue().set("user:"+userId+":"+goodsId, "0"); // 已购买
return "抢购成功!";
}
}
启动程序,初始库存为 100
模拟用户抢购,调用接口:http://localhost:8080/index/rush
库存减少,抢购标识成功添加。
浏览器显示 抢购成功!
,再次调用接口显示 已经抢购过该商品!
悲观锁
userId++
模拟多用户抢购,方法添加 synchronized
关键字实现悲观锁。
@GetMapping("/rush")
public synchronized String index(){
userId++;
// ......
}
}
使用 apache-jmeter 压力测试工具,线程组中线程属性设置如下。
设置 HTTP 请求,添加路径 http://localhost:8080/index/rush
运行查看结果,100个模拟用户抢购完全部库存,没有出现错误。
分布式锁
RedisUtil Redis工具类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedisTemplate<Object, Object> redisTemplate;
@Resource(name = "stringRedisTemplate")
ValueOperations<String, String> valOpsStr;
@Resource(name = "redisTemplate")
ValueOperations<Object, Object> valOpsObj;
/**
* 根据指定key获取String
* @param key
* @return
*/
public String getStr(String key){
return valOpsStr.get(key);
}
/**
* 设置Increment缓存
* @param key
*/
public long getIncrement(String key){
return valOpsStr.increment(key);
}
/**
* 设置Increment缓存
* @param key
* @param val
*/
public long setIncrement(String key, long val){
return valOpsStr.increment(key,val);
}
/**
* 设置Str缓存
* @param key
* @param val
*/
public void setStr(String key, String val){
valOpsStr.set(key,val);
}
/***
* 设置Str缓存
* @param key
* @param val
* @param expire 超时时间
*/
public void setStr(String key, String val,Long expire){
valOpsStr.set(key,val,expire, TimeUnit.MINUTES);
}
/**
* 删除指定key
* @param key
*/
public void del(String key){
stringRedisTemplate.delete(key);
}
/**
* 根据指定o获取Object
* @param o
* @return
*/
public Object getObj(Object o){
return valOpsObj.get(o);
}
/**
* 设置obj缓存
* @param o1
* @param o2
*/
public void setObj(Object o1, Object o2){
valOpsObj.set(o1, o2);
}
/**
* 删除Obj缓存
* @param o
*/
public void delObj(Object o){
redisTemplate.delete(o);
}
/***
* 加锁的方法
* @return
*/
public boolean lock(String key,Long expire){
RedisConnection redisConnection=redisTemplate.getConnectionFactory().getConnection();
//设置序列化方法
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
if(redisConnection.setNX(key.getBytes(),new byte[]{1})){
redisTemplate.expire(key,expire,TimeUnit.SECONDS);
redisConnection.close();
return true;
}else{
redisConnection.close();
return false;
}
}
/***
* 解锁的方法
* @param key
*/
public void unLock(String key){
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.delete(key);
}
}
SETNX
是 Redis 数据库中的一个命令,它是 “SET if Not eXists” 的缩写。这个命令的作用是设置一个键值对,但前提是这个键(key)在数据库中尚未存在。如果键已经存在,SETNX
命令不会执行任何操作,并且返回 0;如果键不存在,则会设置键值对,并返回 1。
基本语法:
SETNX key value
key
:想要设置的键。value
:想要关联到键的值。
SETNX
命令经常用于实现分布式锁,因为它的原子性操作可以确保多个客户端不会同时设置同一个键。
使用工具类中的加锁解锁方法:
// 加锁
redisUtil.lock(luckKey,expire)
// 解锁(释放锁)
redisUtil.unLock(luckKey);
修改过后的 Controller
@RestController
@RequestMapping("/index")
public class IndexController {
@Resource
private RedisUtil redisUtil;
// 模拟抢购用户和商品
Long userId = 1101L;
Long goodsId = 1001L;
@GetMapping("/rush")
public synchronized String index(){
userId++;
while (!redisUtil.lock("lock_" + goodsId,2L)){
// 休眠
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String msg = "";
// 判断是否有抢购标识(未成功参与抢购)
if (redisUtil.getStr("user:"+userId+":"+goodsId) == null){
// 判断是否还有库存
Integer stock = Integer.parseInt(redisUtil.getStr("goods:"+goodsId));
if (stock <= 0){
msg = "抢购失败,该商品已售罄!";
} else {
// 减库存
stock--;
redisUtil.setStr("goods:"+goodsId, stock.toString());
// 添加抢购标识
redisUtil.setStr("user:"+userId+":"+goodsId, "0"); // 已购买
msg = "抢购成功!";
}
} else { // (成功参与抢购)
msg = "已经抢购过该商品!";
}
// 释放锁
redisUtil.unLock("lock_" + goodsId);
return msg;
}
}
测试
消息队列
引入相关依赖,配置 yml 文件
<!-- rabbitmq -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
spring:
redis:
password: 123456
host: 127.0.0.1
port: 6379
database: 1
rabbitmq:
port: 5672
host: 127.0.0.1
listener:
simple:
acknowledge-mode: manual # 手动反馈
prefetch: 1 # 限流
concurrency: 1 # 并发数(消费者数量)
max-concurrency: 1 # 最大并发数(最大消费者数量)
auto-startup: true # 是否自动启动
启动时创建队列
@Configuration
public class RabbitMQConfig {
public static final String QUEUE_NAME = "test_qg";
@Bean
public Queue queue(){
return QueueBuilder.durable(QUEUE_NAME)
// 设置队列最大长度
.maxLength(50)
// 队列溢出后,拒绝消息
.overflow(QueueBuilder.Overflow.rejectPublish)
.build();
}
}
用户抢购商品类
@Data
@ToString
public class UserGoods implements Serializable {
private Integer userId;
private String goodsId;
}
监听器
@Component
public class RabbitMQListener {
@Resource
private StringRedisTemplate stringRedisTemplate;
@RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
public void receiveMessage(Channel channel, Message message) {
HashOperations hashOperations = stringRedisTemplate.opsForHash();
ValueOperations valueOperations = stringRedisTemplate.opsForValue();
try {
// 反序列化 将字节数组转为对象
UserGoods userGoods = (UserGoods) SerializationUtils.deserialize(message.getBody());
//判断是否还有库存 并且已经购买过
if(!(hashOperations.hasKey(userGoods.getGoodsId()+":success",userGoods.getUserId().toString()))
&&Integer.valueOf(valueOperations.get("goods:"+userGoods.getGoodsId()).toString())>0){
//减库存
valueOperations.increment("goods:"+userGoods.getGoodsId(),-1);
//添加抢购标识
hashOperations.put(userGoods.getGoodsId()+":success",userGoods.getUserId().toString(),"0");//抢到了
}else{
hashOperations.put(userGoods.getGoodsId()+":error",userGoods.getUserId().toString(),"1");
}
// 手动反馈
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e){
// 放回队列
try {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
} catch (IOException ex) {
e.printStackTrace();
}
e.printStackTrace();
}
}
}
控制器
@RestController
@RequestMapping("/rabbit")
public class RabbitmqController {
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
// 模拟抢购用户和商品
Integer userId = 1100;
String goodsId = "1001";
@GetMapping("/index")
public String index(){
userId++;
UserGoods userGoods = new UserGoods();
userGoods.setUserId(userId);
userGoods.setGoodsId(goodsId);
rabbitTemplate.convertAndSend(RabbitMQConfig.QUEUE_NAME,userGoods);
return "用户"+userId+"正在抢购"+goodsId;
}
@GetMapping("/refund")
public String refund(Integer userId,String goodsId){
HashOperations hashOperations = stringRedisTemplate.opsForHash();
if(hashOperations.hasKey(goodsId+":success",userId.toString())){
return "抢购成功!";
}else if(hashOperations.hasKey(goodsId+":error",userId.toString())){
return "抢购失败!";
}else {
return "等待";
}
}
}
运行程序,去到 http://127.0.0.1:15672/
队列 test_qg
创建成功
使用 jmeter 测试高并发下接口性能
结果