目录
一、优化思路
主要就是减少数据库的访问:
1. 将商品的库存信息放入Redis中
2. 收到秒杀请求,Redis预减库存,库存不足直接返回,否则进行下一步操作
3. 请求入队,立即返回排队中
4. 请求出队,生成订单,减少库存
5. 客户轮询,是否秒杀成功
二、具体实现
2.1 秒杀接口改造
SeckillController中的seckillOrder方法
改造过程:
1. 系统初始化的时候将要秒杀的商品库存放入redis中
2. 秒杀开始时,先读取redis中的库存信息,判断是否还有剩余
3. 如果已经没有库存,那么直接返回,不再继续往下执行
4. 如果库存充足,那么先对redis中的库存进行更新,然后发消息到rabbitmq队列当中。
2.1.1 系统初始化
在这里,让SeckillController实现InitializingBean接口,然后在afterPropertiesSet方法中加载秒杀商品库存信息。
/**
* 系统初始化,初始化秒杀商品数量
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
//1.查询可以秒杀的商品
List<SeckillGoods> seckillGoods = this.seckillService.querySeckillGoods();
if (seckillGoods == null || seckillGoods.size() == 0){
return;
}
BoundHashOperations<String,Object,Object> hashOperations = this.stringRedisTemplate.boundHashOps(KEY_PREFIX);
if (hashOperations.hasKey(KEY_PREFIX)){
hashOperations.delete(KEY_PREFIX);
}
seckillGoods.forEach(goods -> {
hashOperations.put(goods.getSkuId().toString(),goods.getStock().toString());
});
}
2.1.2 库存更新
String result = "排队中";
//1.读取库存,减一后更新缓存
BoundHashOperations<String,Object,Object> hashOperations = this.stringRedisTemplate.boundHashOps(KEY_PREFIX);
Long stock = hashOperations.increment(seckillGoods.getSkuId().toString(), -1);
//2.库存不足直接返回
if (stock < 0){
localOverMap.put(seckillGoods.getSkuId(),true);
return ResponseEntity.ok(result);
}
2.1.3 发消息
//3.库存充足,请求入队
//3.1 获取用户信息
UserInfo userInfo = LoginInterceptor.getLoginUser();
SeckillMessage seckillMessage = new SeckillMessage(userInfo,seckillGoods);
//3.2 发送消息
this.seckillService.sendMessage(seckillMessage);
这里面要发送的消息内容包含两部分,第一个部分就是用户信息(通过登录拦截器从cookie中解析token获得),第二部分就是秒杀商品的信息,所以这里面将其封装为消息对象SeckillMessage。
SeckillMessage
package com.leyou.seckill.vo;
import com.leyou.auth.entity.UserInfo;
import com.leyou.user.pojo.User;
/**
* @Author: 98050
* @Time: 2018-11-15 20:19
* @Feature: 秒杀信息
*/
public class SeckillMessage {
/**
* 用户信息
*/
private UserInfo userInfo;
/**
* 秒杀商品
*/
private SeckillGoods seckillGoods;
public SeckillMessage() {
}
public SeckillMessage(UserInfo userInfo, SeckillGoods seckillGoods) {
this.userInfo = userInfo;
this.seckillGoods = seckillGoods;
}
public UserInfo getUserInfo() {
return userInfo;
}
public void setUserInfo(UserInfo userInfo) {
this.userInfo = userInfo;
}
public SeckillGoods getSeckillGoods() {
return seckillGoods;
}
public void setSeckillGoods(SeckillGoods seckillGoods) {
this.seckillGoods = seckillGoods;
}
}
sendMessage
/**
* 发送消息到秒杀队列当中
* @param seckillMessage
*/
@Override
public void sendMessage(SeckillMessage seckillMessage) {
String json = JsonUtils.serialize(seckillMessage);
System.out.println(json);
try {
this.amqpTemplate.convertAndSend("order.seckill", json);
}catch (Exception e){
LOGGER.error("秒杀商品消息发送异常,商品id:{}",seckillMessage.getSeckillGoods().getSkuId(),e);
}
}
2.1.4 RabbitMQ配置
2.1.5 优化redis
为了减少对redis的无用访问,设置一个标记位,当库存不足时直接返回即可,不用再访问redis。
最终代码
@PostMapping("seck")
public ResponseEntity<String> seckillOrder(@RequestBody SeckillGoods seckillGoods){
String result = "排队中";
//内存标记,减少redis访问
boolean over = localOverMap.get(seckillGoods.getSkuId());
if (over){
return ResponseEntity.ok(result);
}
//1.读取库存,减一后更新缓存
BoundHashOperations<String,Object,Object> hashOperations = this.stringRedisTemplate.boundHashOps(KEY_PREFIX);
String s = (String) hashOperations.get(seckillGoods.getSkuId().toString());
if (s == null){
return ResponseEntity.ok(result);
}
int stock = Integer.valueOf(s) - 1;
//2.库存不足直接返回
if (stock < 0){
localOverMap.put(seckillGoods.getSkuId(),true);
return ResponseEntity.ok(result);
}
//3.更新库存
hashOperations.delete(seckillGoods.getSkuId().toString());
hashOperations.put(seckillGoods.getSkuId().toString(),String.valueOf(stock));
//4.库存充足,请求入队
//4.1 获取用户信息
UserInfo userInfo = LoginInterceptor.getLoginUser();
SeckillMessage seckillMessage = new SeckillMessage(userInfo,seckillGoods);
//4.2 发送消息
this.seckillService.sendMessage(seckillMessage);
return ResponseEntity.ok(result);
}
2.2 订单服务监听
在订单微服务中设置消息队列监听器,读取消息队列的信息,然后创建订单。
2.2.1 监听器配置
在application.yml中配置rabbitmq
创建listener用来接收消息。
- 创建队列:leyou.order.seckill.queue
- 绑定交换机:leyou.order.exchange
- 交换机类型:Topic
- 接收消息类型:order.seckill
2.2.2 消息解析
SeckillMessage seckillMessage = JsonUtils.parse(seck,SeckillMessage.class);
UserInfo userInfo = seckillMessage.getUserInfo();
SeckillGoods seckillGoods = seckillMessage.getSeckillGoods();
2.2.3 库存判断
//1.首先判断库存是否充足
Stock stock = stockMapper.selectByPrimaryKey(seckillGoods.getSkuId());
if (stock.getSeckillStock() <= 0 || stock.getStock() <= 0){
return;
}
2.2.4 用户是否秒杀成功
默认规定一个用户只能秒杀一个商品,所以需要查询秒杀订单表tb_seckill_order。同时在创建tb_seckill_order表的时候要创建唯一索引:
双重保险。
//2.判断此用户是否已经秒杀到了
Example example = new Example(SeckillOrder.class);
example.createCriteria().andEqualTo("userId",userInfo.getId()).andEqualTo("skuId",seckillGoods.getSkuId());
List<SeckillOrder> list = this.seckillOrderMapper.selectByExample(example);
if (list.size() > 0){
return;
}
2.2.5 下订单
和普通订单的创建没有什么区别,但是此时缺少对地址的管理,所以这里面就采用默认地址,这个可以再后期进行业务逻辑的优化(根据用户id查询用户的默认地址,又增加了一次数据库的访问)
//3.下订单
//构造order对象
Order order = new Order();
order.setPaymentType(1);
order.setTotalPay(seckillGoods.getSeckillPrice());
order.setActualPay(seckillGoods.getSeckillPrice());
order.setPostFee(0+"");
order.setReceiver("李四");
order.setReceiverMobile("15812312312");
order.setReceiverCity("西安");
order.setReceiverDistrict("碑林区");
order.setReceiverState("陕西");
order.setReceiverZip("000000000");
order.setInvoiceType(0);
order.setSourceType(2);
OrderDetail orderDetail = new OrderDetail();
orderDetail.setSkuId(seckillGoods.getSkuId());
orderDetail.setNum(1);
orderDetail.setTitle(seckillGoods.getTitle());
orderDetail.setImage(seckillGoods.getImage());
orderDetail.setPrice(seckillGoods.getSeckillPrice());
orderDetail.setOwnSpec(this.skuMapper.selectByPrimaryKey(seckillGoods.getSkuId()).getOwnSpec());
order.setOrderDetails(Arrays.asList(orderDetail));
//3.1 生成orderId
long orderId = idWorker.nextId();
//3.2 初始化数据
order.setBuyerNick(userInfo.getUsername());
order.setBuyerRate(false);
order.setCreateTime(new Date());
order.setOrderId(orderId);
order.setUserId(userInfo.getId());
//3.3 保存数据
this.orderMapper.insertSelective(order);
//3.4 保存订单状态
OrderStatus orderStatus = new OrderStatus();
orderStatus.setOrderId(orderId);
orderStatus.setCreateTime(order.getCreateTime());
//初始状态未未付款:1
orderStatus.setStatus(1);
//3.5 保存数据
this.orderStatusMapper.insertSelective(orderStatus);
//3.6 在订单详情中添加orderId
order.getOrderDetails().forEach(od -> {
//添加订单
od.setOrderId(orderId);
});
//3.7 保存订单详情,使用批量插入功能
this.orderDetailMapper.insertList(order.getOrderDetails());
//3.8 修改库存
order.getOrderDetails().forEach(ord -> {
Stock stock1 = this.stockMapper.selectByPrimaryKey(ord.getSkuId());
stock1.setStock(stock1.getStock() - ord.getNum());
stock1.setSeckillStock(stock1.getSeckillStock() - ord.getNum());
this.stockMapper.updateByPrimaryKeySelective(stock1);
//新建秒杀订单
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setOrderId(orderId);
seckillOrder.setSkuId(ord.getSkuId());
seckillOrder.setUserId(userInfo.getId());
this.seckillOrderMapper.insert(seckillOrder);
});
三、Jmeter压测
3.1 数据准备
Jmeter所需数据:5000个已经申请号的用户,然后循环10次。具体数据参考上一篇。
数据库:
然后清空tb_order、tb_order_detail、tb_order_status和tb_seckill_order四个表。
3.2 测试
查看数据库中的数据:
3.3 错误分析
在聚合报告中会发现有错误,接下来进行分析
3.3.1 拒绝连接
系统配置文件:{JMeter 主目录}\bin\system.properties,将 java.net.preferIPv4Stack 设置为 true 即可
3.3.2 地址正在使用
报错:JAVA.NET.BINDEXCEPTION: ADDRESS ALREADY IN USE: CONNECT
解决方案为:
1.cmd中,用regedit命令打开注册表
2.在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters下,
1 .右击parameters,添加一个新的DWORD,名字为MaxUserPort
2 .然后双击MaxUserPort,输入数值数据为65534,基数选择十进制(如果是分布式运行的话,控制机器和负载机器都需要这样操作哦)
3.修改配置完毕之后记得重启机器才会生效
通过上述修改,问题不能得到全部解决,因为机器承受不了这么大的访问,没得办法,有其它解决办法的小伙伴请留言。
四、是否秒杀成功
增加接口,判断当前用户是否秒杀成功,即根据用户id从tb_seckill_order表中查询订单号。
4.1 Controller
/**
* 根据userId查询订单号
* @param userId
* @return
*/
@GetMapping("orderId")
public ResponseEntity<Long> checkSeckillOrder(Long userId){
Long result = this.seckillService.checkSeckillOrder(userId);
if (result == null){
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.ok(result);
}
4.2 Service
/**
* 根据用户id查询秒杀订单
* @param userId
* @return
*/
@Override
public Long checkSeckillOrder(Long userId) {
Example example = new Example(SeckillOrder.class);
example.createCriteria().andEqualTo("userId",userId);
List<SeckillOrder> seckillOrders = this.seckillOrderMapper.selectByExample(example);
if (seckillOrders == null || seckillOrders.size() == 0){
return null;
}
return seckillOrders.get(0).getOrderId();
}
然后在前端不断轮询即可。