高并发秒杀优化操作介绍
高并发秒杀优化操作
1. 减少数据库的操作
判断是否重复抢购这个操作可以优化,大致思路是把用户订单放到Redis里,键中加上用户,抢购时判断是否已存在信息。来代替查询数据库
具体操作
1
2
3//生成订单时
redisTemplate.opsForValue().set("order:" + user.getId() + ":" + goods.getId(),
JsonUtil.object2JsonStr(seckillOrder));1
2
3
4
5
6
7//秒杀操作前
SeckillOrder seckillOrder = (SeckillOrder) redisTemplate.opsForValue().get("order:" +user.getId()
+ ":" + goodsId);
//如果取出的信息不为空代表已经购买过此产品,不能再参与(限购为1情况下)
if(seckillOrder != null){
return RespBean.error(RespBeanEnum.REPEATE_ERROR);
}
库存可以在初始化时候就加入到Redis里,秒杀的时候直接减去Redis里的库存,然后再使用RabbitMQ消息队列分别进行处理
具体操作
- 配置初始化库存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//接口调用implements InitializingBean
public class SecKillController implements InitializingBean{
//实现这个接口的方法
//系统初始化,把商品库存数量加载到Redis
public void afterPropertiesSet() throws Exception{
//从数据库找出有秒杀商品
List<GoodsVo> list = goodsService.findGoodsVo();
if(CollectionUtils.isEmpty(list)){
return;
}
//把每个秒杀产品的库存信息加载到Redis里
list.forEach(goodsVo ->
redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(),
goodsVo.getStockCount()));
}
}- 秒杀接口里配置库存减少操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public RespBean doSeckill(User user,Long goodsId){
//判断用户是否存在
//一堆代码判断是否重复抢购
//判断是否重复抢购后
ValueOperations valueOperations = redisTemplate.opsForValue();
//预减库存,每次执行自动-1
Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
//如果库存减了之后小于零就执行这个情况,个人感觉这里可能会有问题
if (stock < 0){
//每次执行自动加一,防止库存出现-1的情况
valueOperations.increment("seckillGoods:" + goodsId);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
//生成订单,以后会替换成SeckillMessage
Order order = orderService.seckill(user,goods);
return RespBean.success(Order);
}
把秒杀请求封装成一个对象发送给RabbitMQ。把操作异步掉,用消息队列达到流量削锋的目的。
具体操作
创建封装对象SeckillMessage
1
2
3
4
5public class SeckillMessage{
private User user;
private Long goodId;
//getter、setter、构造器省略
}选择使用RabbitMQ的Topic模式,进行config配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class RabbitMQTopicConfig{
private static final String QUEUE = "seckillQueue";
private static final String EXCHANGE = "seckillExchange";
public Queue queue(){
return new Queue(QUEUE);
}
public TopicExchange topicExchange(){
return new TopicExchange(EXCHANGE);
}
public Binding binding(){
return BindingBuilder.bind(queue()).to(topicExchange()).with("seckill.#");
}
}MQSender配置
1
2
3
4
5
6
7
8
9
10
11
12
13
//lombok的注解
public class MQSender {
private RabbitTemplate rabbitTemplate;
public void sendSeckillMessage(String message){
log.info("发送消息:" + message);
rabbitTemplate.converAndSend("seckillExchange", "seckill.message", message);
}
}配置完后回到Controller接口修改,加入MQSender
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private MQSender mqSender;
public RespBean doSeckill(User user,Long goodsId){
//判断用户是否存在
//一堆代码判断是否重复抢购
//判断是否重复抢购后
ValueOperations valueOperations = redisTemplate.opsForValue();
//预减库存,每次执行自动-1
Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
//如果库存减了之后小于零就执行这个情况,个人感觉这里可能会有问题
if (stock < 0){
//每次执行自动加一,防止库存出现-1的情况
valueOperations.increment("seckillGoods:" + goodsId);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
//替换处
SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
//使用RabbitMQ实现异步,把订单生成放入另一部分操作,这样能快速返回信息。这里是JsonUtil是自己编写的工具类,在下文
mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage));
//前端收到0,要作出"排队中"的响应
return RespBean.success(0);
}MQReceiver,接受sender消息,异步掉操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//lombok的注解
public class MQReceiver {
private RabbitTemplate rabbitTemplate;
private IGoodsService goodsService;
private RedisTemplate redisTemplate;
private OrderService orderService;
public void receive(String message){
//先打印下看看能不能拿到正确的message
log.info("接受到的消息:" + message);
//转换消息从JSON到对象
SeckillMessage seckillMessage = JsonUtil.jsonStr2Object(message, SeckillMessage.class);
//获取goodId和用户
User user = seckillMessage.getUser();
Long goodsId = seckillMessage.getGoodId();
//获取商品信息
GoodsVo goodsVo = goodsService.findGoodsVoByGoodsId(goodsId);
//判断库存
if(goodsVo.getStockCount() < 1){
return;
}
//判断是否重复抢购
SeckillOrder seckillOrder =
(SeckillOrder) redisTemplate.opsForValue().get("order:" + user.getId() + ":"
+ goodsId);
if(seckillOrder != null){
return;
}
//下单操作
orderService.seckill(user,goodsVo);
}
}
内存标记,减少库存卖光之后对redis的访问
具体操作
在Controller里设置个内存标记
1
2//Long对应不同的商品id
private Map<Long,Boolean> EmptyStockMap = new HashMap<>();之前的初始化方法重写,加入内存标记初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void afterPropertiesSet() throws Exception{
//从数据库找出有秒杀商品
List<GoodsVo> list = goodsService.findGoodsVo();
if(CollectionUtils.isEmpty(list)){
return;
}
//把每个秒杀产品的库存信息加载到Redis里
list.forEach(goodsVo -> {
redisTemplate.opsForValue().set("seckillGoods:" + goodsVo.getId(),
goodsVo.getStockCount()));
EmptyStockMap.put(goodsVo.getId(),false);
}
}接口里具体实现内存标记判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private MQSender mqSender;
public RespBean doSeckill(User user,Long goodsId){
//判断用户是否存在
//一堆代码判断是否重复抢购
//判断是否重复抢购后
ValueOperations valueOperations = redisTemplate.opsForValue();
//预减库存,每次执行自动-1
Long stock = valueOperations.decrement("seckillGoods:" + goodsId);
//内存标记判断,减少Redis访问
if(EmptyStockMap.get(goodsId)){
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
//如果库存减了之后小于零就执行这个情况,个人感觉这里可能会有问题
if (stock < 0){
//修改处,标记内存
EmptyStockMap.put(goodsId,true);
//每次执行自动加一,防止库存出现-1的情况
valueOperations.increment("seckillGoods:" + goodsId);
return RespBean.error(RespBeanEnum.EMPTY_STOCK);
}
SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);
//这里是JsonUtil是自己编写的工具类,在下文
mqSender.sendSeckillMessage(JsonUtil.object2JsonStr(seckillMessage));
//前端收到0,要作出"排队中"的响应
return RespBean.success(0);
}
轮询判断是否秒杀成功,因为之前只返回了一个排队中
具体操作
在秒杀的Controller里再新建一个接口,专门获取秒杀结果,返回一个orderId代表成功,返回-1代表秒杀失败,返回0代表排队中
Controller
1
2
3
4
5
6
7
8
public RespBean getResult(User user, Long goodsId){
if(user == null){
return RespBean.error(RespBeanEnum.SESSION_ERROR);
}
Long orderId = seckillOrderService.getResult(user,goodsId);
return RespBean.success(orderId);
}Service层创建接口并实现(相关自动加载注解就不写了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Long getResult(User user, Long goodsId){
SeckillOrder seckillOrder = seckillOrderMapper.selectOne(new QueryWrapper<SeckillOrder)().eq("user_id", user.getId()).eq("goods_id",goodsId);
//如果订单表里有这个用户下了这个产品的订单,就说明秒杀成功,返回订单编号。
if(seckillOrder != null){
return seckillOrder.getOrderId();
}else if(redisTemplate.hasKey("isStockEmpty:" + goodsId)){
//如果Redis里这个产品的isStockEmpty标记为是,就返回库存为空的标记
return -1L;
}else{
//如果没有这个订单,就代表秒杀失败
return 0L;
}
}在生成订单的时候要加上判断库存为空
1
2
3
4
5
6
7
8
9
10
11
12
public Order seckill(User user,GoodsVo goods){
ValueOperations valueOperations = redisTemplate.opsForValue();
//秒杀商品表减库存操作...
//判断是否还有库存
if(seckillGoods.getStockCount() < 1){
valueOperations.set("isStockEmpty:" + goods.getId(), "0");
return null;
}
}接下来就是前端写轮询发送请求判断了,若返回-1就是秒杀失败不用轮询,若返回0排队中就要写设置间隔几秒发送请求到getResult接口。
脚本优化(再说)
经历以上操作,超卖问题是解决了,但是存在一个问题,单个用户多次秒杀会使得redis库存多次减少并且超出限购。还会让redis库存扣完但是数据库没有,所以需要优化。**(这个方法好像没有用,自己预想是用用户标记)**
lua脚本实现分布式锁,具体作用看视频
RedisConfig调用
在配置一个stock.lua判断库存
RedisConfig配置
Controller里判断库存修改
1
2
private RedsiScript<Long> script;
JSONUtil 或者Maven添加fastJson依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74package com.bank.seckill2022.utils;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.List;
/**
* 功能描述:
* @param: Json工具类
* @return:
* @author 来自于网络
* @date: 2022/3/25 23:36
*/
public class JSONUtil {
private static ObjectMapper objectMapper = new ObjectMapper();
/**
* 将对象转换成json字符串
*
* @param obj
* @return
*/
public static String object2JsonStr(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
//打印异常信息
e.printStackTrace();
}
return null;
}
/**
* 将字符串转换为对象
*
* @param <T> 泛型
*/
public static <T> T jsonStr2Object(String jsonStr, Class<T> clazz) {
try {
return objectMapper.readValue(jsonStr.getBytes("UTF-8"), clazz);
} catch (JsonParseException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 将json数据转换成pojo对象list
* <p>Title: jsonToList</p>
* <p>Description: </p>
*
* @param jsonStr
* @param beanType
* @return
*/
public static <T> List<T> jsonToList(String jsonStr, Class<T> beanType) {
JavaType javaType =
objectMapper.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list = objectMapper.readValue(jsonStr, javaType);
return list;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}