文章目录
SpringBoot+Redis+消息队列 技术的抢购方案
提前部署好Redis和RabbitMQ
RabbitMQ部署参考:https://cloud.tencent.com/developer/article/2303666
13.1简单抢购
创建springboot项目,导入redis相关依赖
<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>
server:
port: 80
spring:
redis:
# 连接超时 毫秒
connectTimeout: 1800
# 连接超时时间
timeout: 60s
host: 127.0.0.1
port: 6379
database: 1
password: 123456
jedis:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 3600ms
首先模拟商品初始化信息
@SpringBootApplication
public class HzQianggouApplication {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public static void main(String[] args) {
SpringApplication.run(HzQianggouApplication.class, args);
}
@Bean
public void goodsInfo() {
//初始化抢购商品信息,假设有100个商品
stringRedisTemplate.opsForValue()
.set("goods:1001","100");
}
}
package com.hz.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/qg")
public class QiangGouController01 {
@Autowired
private StringRedisTemplate stringRedisTemplate;
Long userId = 110L;//用户id
Long goodsId = 1001L;//商品id
@RequestMapping("/qg01")
public String qianggou(){
//判断是否购买过次商品
if(stringRedisTemplate.opsForValue()
.get("user:"+userId+":goods:"+goodsId) != null){
return "抢购失败,您已经购买过次商品";
}
//判断库存是否足够
Integer kc = Integer.parseInt(stringRedisTemplate.opsForValue().get("goods:" + goodsId));
if (kc <= 0){
return "抢购失败,商品已售罄";
}
kc--;
stringRedisTemplate.opsForValue().set("goods:"+goodsId,kc.toString());
//添加抢购成功标识 1:抢购成功 0:抢购失败
stringRedisTemplate.opsForValue().set("user:"+userId+":goods:"+goodsId,"1");
return "抢购成功";
}
}
13.2 模拟高并下发抢购
jmeter工具使用
首先安装jmeter工具,模拟高并发
解压即能用,找到bin下的jmeter.bat双击运行。
设置,放大
模拟1000个人每秒点击3次
实现
package com.hz.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/qg02")
public class QiangGouController02 {
@Autowired
private StringRedisTemplate stringRedisTemplate;
Long userId = 110L;//用户id
Long goodsId = 1001L;//商品id
@RequestMapping("/qginfo01")
public String qianggou(){
userId ++;//模拟多个用户进来抢购
//判断是否购买过次商品
if(stringRedisTemplate.opsForValue()
.get("user:"+userId+":goods:"+goodsId) != null){
return "抢购失败,您已经购买过次商品";
}
//判断库存是否足够
Integer kc = Integer.parseInt(stringRedisTemplate.opsForValue().get("goods:" + goodsId));
if (kc <= 0){
return "抢购失败,商品已售罄";
}
kc--;
stringRedisTemplate.opsForValue().set("goods:"+goodsId,kc.toString());
//添加抢购成功标识 1:抢购成功 0:抢购失败
stringRedisTemplate.opsForValue().set("user:"+userId+":goods:"+goodsId,"1");
return "抢购成功";
}
}
结果显示496个用户抢到,但是商品只有100个,出现数据不安全问题。解决方案如下。
13.3数据安全问题
加锁synchronized
再次运行,测试
数据正确。但是执行速度比较慢。
分布式锁
如果是分布式系统的话,synchronized无法解决。
【RedisUtils】
package com.hz.utils;
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){
// 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);
}
}
package com.hz.controller;
import com.hz.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/qg03")
public class QiangGouController03 {
// @Autowired
// private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisUtil redisUtil;
Long userId = 110L;//用户id
Long goodsId = 1001L;//商品id
@RequestMapping("/qginfo01")
public String qianggou(){
userId ++;//模拟多个用户进来抢购
while (!redisUtil.lock("look_",3L)){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
String msg= "";
//判断是否购买过次商品
if(redisUtil.getStr("user:"+userId+":goods:"+goodsId) == null){
//判断库存是否足够
Integer kc = Integer.parseInt(redisUtil.getStr("goods:" + goodsId));
if (kc <= 0){
msg = "抢购失败,商品已售罄";
}else{
kc--;
redisUtil.setStr("goods:"+goodsId,kc.toString());
//添加抢购成功标识 1:抢购成功 0:抢购失败
redisUtil.setStr("user:"+userId+":goods:"+goodsId,"1");
msg = "抢购成功";
}
}else {
msg = "抢购失败,您已经购买过次商品";
}
// 释放锁
redisUtil.unLock("look_");
return msg;
}
}
测试
13.4 消息队列完成抢购
为了方便,直接在启动类中初始化商品信息
@SpringBootApplication
public class HzQinggou02Application {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public static void main(String[] args) {
SpringApplication.run(HzQinggou02Application.class, args);
}
@Bean
public void info(){
//初始化抢购商品
stringRedisTemplate.opsForValue()
.increment("goods:1001",100);
}
}
【yml配置文件】
server:
port: 80
spring:
redis:
# 连接超时 毫秒
connectTimeout: 1800
# 连接超时时间
timeout: 60s
host: 127.0.0.1
port: 6379
database: 1
password: 123456
jedis:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: 3600ms
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
listener:
simple:
acknowledge-mode: manual #手动反馈
prefetch: 1 #限流 (消费者每次从队列中获得的消息数量)
concurrency: 1 #消费者数据量
max-concurrency: 1 #消费者最大数据量
auto-startup: true #是否自动启动
【实体类】
@Data
@ToString
public class UserGoods implements Serializable {
private Integer userId;
private String goodsId;
}
【redis工具类】
package com.hz.utils;
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);
}
public boolean isKey(String key, String val){
return stringRedisTemplate.opsForHash().hasKey(key,val);
}
// public Long get(String key){
// return redisTemplate.opsForSet().members(key);
// }
public void set(String key,String val,String val1){
stringRedisTemplate.opsForHash().put(key,val,val);
}
/**
* 设置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);
}
}
【队列配置类】
package com.hz.config;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitConfig {
public static final String QUEUE_NAME = "test_qg";
/*
* 队列配置
* // durable 持久化队列
//QueueBuilder.Overflow.rejectPublish 枚举类型 当队列满时,多余的消息直接拒绝接收,多余的消息被丢弃
*/
@Bean
public Queue queue() {
return QueueBuilder.durable(QUEUE_NAME)
.maxLength(50)
.overflow(QueueBuilder
.Overflow.rejectPublish)
.build();
}
}
【controller】
package com.hz.controller;
import com.hz.config.RabbitConfig;
import com.hz.pojo.UserGoods;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/rabbit")
public class RabbitController {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
Integer userId = 110;//用户ID
String goodsId = "1001";//要抢购的商品ID
@GetMapping("/qg")
public String test(){
userId++;
UserGoods userGoods = new UserGoods();
userGoods.setUserId(userId);
userGoods.setGoodsId(goodsId);
//发送消息到队列
rabbitTemplate.convertAndSend(RabbitConfig.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 "等待";
// }
// }
}
【队列监听器RabbitQMListener】
package com.hz.controller;
import com.hz.config.RabbitConfig;
import com.hz.pojo.UserGoods;
import com.hz.utils.RedisUtil;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.utils.SerializationUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class RabbitQMListener {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RabbitListener(queues = RabbitConfig.QUEUE_NAME)
public void onMessage(Channel channel, Message message) {
// 用于操作Redis中的Hash类型数据结构。
HashOperations hashOperations = stringRedisTemplate
.opsForHash();
// 用于操作Redis中的String类型数据结构。
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);
//添加抢购标识 0:抢购成功 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){
e.printStackTrace();
try {
//放回队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
}
【测试】
启动MQ
启动Redis
启动SpringBoot程序后输入网址
http://localhost:80/rabbit/qg
标签:SpringBoot,示例,Redis,springframework,key,org,import,public,String
From: https://blog.csdn.net/weixin_54555405/article/details/145183150