用户在下单的时候,需要基于JWT令牌信息进行登陆人信息认证,确定当前订单是属于谁的。
针对秒杀的特殊业务场景,仅仅依靠对象缓存或者页面静态化等技术去解决服务端压力还是远远不够。对于数据库压力还是很大,所以需要异步下单,异步是最好的解决办法,但会带来一些额外的程序上的复杂性。
1.1 秒杀服务-下单实现
1)将tokenDecode工具类放入秒杀服务并声明Bean
2)更新秒杀服务启动类,添加redis配置
package com.changgou.seckill;
import com.changgou.seckill.config.TokenDecode;
import com.changgou.util.IdWorker;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.EnableScheduling;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@EnableEurekaClient
@MapperScan(basePackages = {"com.changgou.seckill.dao"})
@EnableScheduling
public class SecKillApplication {
public static void main(String[] args) {
SpringApplication.run(SecKillApplication.class,args);
}
//idwork
@Bean
public IdWorker idWorker(){
return new IdWorker(1,1);
}
//设置redistemplate的序列化
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 1.创建 redisTemplate 模版
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 2.关联 redisConnectionFactory
template.setConnectionFactory(redisConnectionFactory);
// 3.创建 序列化类
GenericToStringSerializer genericToStringSerializer = new GenericToStringSerializer(Object.class);
// 6.序列化类,对象映射设置
// 7.设置 value 的转化格式和 key 的转化格式
template.setValueSerializer(genericToStringSerializer);
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
@Bean
public TokenDecode tokenDecode(){
return new TokenDecode();
}
}
2)新建下单controller并声明方法
package com.changgou.seckill.controller;
import com.changgou.entity.Result;
import com.changgou.entity.StatusCode;
import com.changgou.seckill.config.TokenDecode;
import com.changgou.seckill.service.SecKillOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/seckillorder")
public class SecKillOrderController {
@Autowired
private TokenDecode tokenDecode;
@Autowired
private SecKillOrderService secKillOrderService;
@RequestMapping("/add")
public Result add(@RequestParam("time") String time, @RequestParam("id") Long id){
//1.动态获取到当前的登录人
String username = tokenDecode.getUserInfo().get("username");
//2.基于业务层进行秒杀下单
boolean result = secKillOrderService.add(id, time, username);
//3.返回结果
if (result){
//下单成功
return new Result(true, StatusCode.OK,"下单成功");
}else{
//下单失败
return new Result(false,StatusCode.ERROR,"下单失败");
}
}
}
public interface SecKillOrderService {
/**
* 秒杀下单
* @param id 商品id
* @param time 时间段
* @param username 登陆人姓名
* @return
*/
boolean add(Long id, String time, String username);
}
4)更改预加载秒杀商品
当预加载秒杀商品的时候,提前加载每一个商品的库存信息,后续减库存操作也会先预扣减缓存中的库存再异步扣减mysql数据。
预扣减库存会基于redis原子性操作实现
for (SeckillGoods seckillGoods : seckillGoodsList) {
redisTemplate.boundHashOps(SECKILL_GOODS_KEY +redisExtName).put(seckillGoods.getId(),seckillGoods);
//预加载库存信息
redisTemplate.OpsForValue(SECKILL_GOODS_STOCK_COUNT_KEY+seckillGoods.getId(),seckillGoods.getStockCount());
}
6)秒杀下单业务层实现
业务逻辑:
获取秒杀商品数据与库存量数据,如果没有库存则抛出异常
执行redis预扣减库存,并获取扣减之后的库存值
如果扣减完的库存值<=0, 则删除redis中对应的商品信息与库存信息
基于mq异步方式完成与mysql数据同步(最终一致性)
package com.changgou.seckill.service.impl;
import com.alibaba.fastjson.JSON;
import com.changgou.seckill.config.ConfirmMessageSender;
import com.changgou.seckill.config.RabbitMQConfig;
import com.changgou.seckill.dao.SeckillOrderMapper;
import com.changgou.seckill.pojo.SeckillGoods;
import com.changgou.seckill.pojo.SeckillOrder;
import com.changgou.seckill.service.SecKillOrderService;
import com.changgou.util.IdWorker;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Service
public class SecKillOrderServiceImpl implements SecKillOrderService {
@Autowired
private RedisTemplate redisTemplate;
public static final String SECKILL_GOODS_KEY="seckill_goods_";
public static final String SECKILL_GOODS_STOCK_COUNT_KEY="seckill_goods_stock_count_";
@Autowired
private IdWorker idWorker;
@Autowired
private ConfirmMessageSender confirmMessageSender;
@Autowired
private SeckillOrderMapper seckillOrderMapper;
@Override
public boolean add(Long id, String time, String username) {
//获取商品信息
SeckillGoods seckillGoods = (SeckillGoods) redisTemplate.boundHashOps(SECKILL_GOODS_KEY+time).get(id);
//获取库存信息
String redisStock = (String) redisTemplate.opsForValue().get(SECKILL_GOODS_STOCK_COUNT_KEY+id);
if (StringUtils.isEmpty(redisStock)){
return false;
}
int stock = Integer.parseInt(redisStock);
if (seckillGoods == null || stock<=0){
return false;
}
//执行redis的预扣减库存,并获取到扣减之后的库存值
//decrement:减 increment:加 -> Lua脚本语言
Long decrement = redisTemplate.opsForValue().decrement(SECKILL_GOODS_STOCK_COUNT_KEY + id);
if (decrement<=0){
//扣减完库存之后,当前商品已经没有库存了.
//删除redis中的商品信息与库存信息
redisTemplate.boundHashOps(SECKILL_GOODS_KEY+time).delete(id);
redisTemplate.delete(SECKILL_GOODS_STOCK_COUNT_KEY + id);
}
//发送消息(保证消息生产者对于消息的不丢失实现)
//消息体: 秒杀订单
SeckillOrder seckillOrder = new SeckillOrder();
seckillOrder.setId(idWorker.nextId());
seckillOrder.setSeckillId(id);
seckillOrder.setMoney(seckillGoods.getCostPrice());
seckillOrder.setUserId(username);
seckillOrder.setSellerId(seckillGoods.getSellerId());
seckillOrder.setCreateTime(new Date());
seckillOrder.setStatus("0");
//发送消息
confirmMessageSender.sendMessage("", RabbitMQConfig.SECKILL_ORDER_QUEUE, JSON.toJSONString(seckillOrder));
return true;
}
}
按照现有rabbitMQ的相关知识,生产者会发送消息到达消息服务器。但是在实际生产环境下,消息生产者发送的消息很有可能当到达了消息服务器之后,由于消息服务器的问题导致消息丢失,如宕机。因为消息服务器默认会将消息存储在内存中。一旦消息服务器宕机,则消息会产生丢失。因此要保证生产者的消息不丢失,要开始持久化策略。
rabbitMQ持久化:
交换机持久化
队列持久化
消息持久化
但是如果仅仅只是开启这两部分的持久化,也很有可能造成消息丢失。因为消息服务器很有可能在持久化的过程中出现宕机。因此需要通过数据保护机制来保证消息一定会成功进行持久化,否则将一直进行消息发送
RabbitMQ数据保护机制
事务机制
事务机制采用类数据库的事务机制进行数据保护,当消息到达消息服务器,首先会开启一个事务,接着进行数据磁盘持久化,只有持久化成功才会进行事务提交,向消息生产者返回成功通知,消息生产者一旦接收成功通知则不会再发送此条消息。当出现异常,则返回失败通知.消息生产者一旦接收失败通知,则继续发送该条消息。
事务机制虽然能够保证数据安全,但是此机制采用的是同步机制,会产生系统间消息阻塞,影响整个系统的消息吞吐量。从而导致整个系统的性能下降,因此不建议使用。
confirm机制
confirm模式需要基于channel进行设置, 一旦某条消息被投递到队列之后,消息队列就会发送一个确认信息给生产者,如果队列与消息是可持久化的, 那么确认消息会等到消息成功写入到磁盘之后发出.confirm的性能高,主要得益于它是异步的.生产者在将第一条消息发出之后等待确认消息的同时也可以继续发送后续的消息.当确认消息到达之后,就可以通过回调方法处理这条确认消息. 如果MQ服务宕机了,则会返回nack消息. 生产者同样在回调方法中进行后续处理。
1)更改秒杀服务配置文件
rabbitmq:
host: 192.168.200.128
publisher-confirms: true #开启confirm机制
2)开启队列持久化
@Configuration
public class RabbitMQConfig {
//秒杀商品订单消息
public static final String SECKILL_ORDER_KEY="seckill_order";
@Bean
public Queue queue(){
//开启队列持久化
return new Queue(SECKILL_ORDER_KEY,true);
}
}
4)增强rabbitTemplate
package com.changgou.seckill.config;
import com.alibaba.fastjson.JSON;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Component
public class ConfirmMessageSender implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RedisTemplate redisTemplate;
public static final String MESSAGE_CONFIRM_KEY="message_confirm_";
public ConfirmMessageSender(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
rabbitTemplate.setConfirmCallback(this);
}
@Override
//接收消息服务器返回的通知的
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack){
//成功通知
//删除redis中的相关数据
redisTemplate.delete(correlationData.getId());
redisTemplate.delete(MESSAGE_CONFIRM_KEY+correlationData.getId());
}else{
//失败通知
//从redis中获取刚才的消息内容
Map<String,String> map = (Map<String,String>)redisTemplate.opsForHash().entries(MESSAGE_CONFIRM_KEY+correlationData.getId());
//重新发送
String exchange = map.get("exchange");
String routingkey = map.get("routingkey");
String message = map.get("message");
rabbitTemplate.convertAndSend(exchange,routingkey, JSON.toJSONString(message));
}
}
//自定义消息发送方法
public void sendMessage(String exchange,String routingKey,String message){
//设置消息的唯一标识并存入到redis中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
redisTemplate.opsForValue().set(correlationData.getId(),message);
//将本次发送消息的相关元数据保存到redis中
Map<String,String> map = new HashMap<>();
map.put("exchange",exchange);
map.put("routingKey",routingKey);
map.put("message",message);
redisTemplate.opsForHash().putAll(MESSAGE_CONFIRM_KEY+correlationData.getId(),map);
//携带着本次消息的唯一标识,进行数据发送
rabbitTemplate.convertAndSend(exchange,routingKey,message,correlationData);
}
}
5)发送消息
更改下单业务层实现
@Autowired
private CustomMessageSender customMessageSender;
1)添加依赖
<dependencies>
<dependency>
<groupId>com.changgougroupId>
<artifactId>changgou_common_dbartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.changgougroupId>
<artifactId>changgou_service_order_apiartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>com.changgougroupId>
<artifactId>changgou_service_seckill_apiartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>com.changgougroupId>
<artifactId>changgou_service_goods_apiartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.amqpgroupId>
<artifactId>spring-rabbitartifactId>
dependency>
dependencies>
2)新建application.yml
server:
port: 9022
spring:
jackson:
time-zone: GMT+8
application:
name: sec-consume
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.200.128:3306/changgou_seckill?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT%2b8
username: root
password: root
main:
allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
redis:
host: 192.168.200.128
rabbitmq:
host: 192.168.200.128
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:6868/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
client:
config:
default: #配置全局的feign的调用超时时间 如果 有指定的服务配置 默认的配置不会生效
connectTimeout: 60000 # 指定的是 消费者 连接服务提供者的连接超时时间 是否能连接 单位是毫秒
readTimeout: 20000 # 指定的是调用服务提供者的 服务 的超时时间() 单位是毫秒
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled设置为false,则请求超时交给ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
thread:
# 熔断器超时时间,默认:1000/毫秒
timeoutInMilliseconds: 20000
3)新建启动类
package com.changgou.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import tk.mybatis.spring.annotation.MapperScan;
@SpringBootApplication
@EnableEurekaClient
@MapperScan(basePackages = {"com.changgou.consumer.dao"})
public class OrderConsumeApplication {
public static void main(String[] args) {
SpringApplication.run(OrderConsumeApplication.class,args);
}
}
按照现有RabbitMQ知识,可以得知当消息消费者成功接收到消息后,会进行消费并自动通知消息服务器将该条消息删除。此种方式的实现使用的是消费者自动应答机制。但是此种方式非常的不安全。
在生产环境下,当消息消费者接收到消息,很有可能在处理消息的过程中出现意外情况从而导致消息丢失,因为如果使用自动应答机制是非常不安全。
我们需要确保消费者当把消息成功处理完成之后,消息服务器才会将该条消息删除。此时要实现这种效果的话,就需要将自动应答转换为手动应答,只有在消息消费者将消息处理完,才会通知消息服务器将该条消息删除。
1)更改配置文件
rabbitmq:
host: 192.168.200.128
listener:
simple:
acknowledge-mode: manual #手动
2)定义监听类
package com.changgou.consumer.listener;
import com.alibaba.fastjson.JSON;
import com.changgou.consumer.config.RabbitMQConfig;
import com.changgou.consumer.service.SecKillOrderService;
import com.changgou.seckill.pojo.SeckillOrder;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class ConsumerListener {
@Autowired
private SecKillOrderService secKillOrderService;
@RabbitListener(queues = RabbitMQConfig.SECKILL_ORDER_QUEUE)
public void receiveSecKillOrderMessage(Message message, Channel channel){
//设置预抓取总数
try {
channel.basicQos(300);
} catch (IOException e) {
e.printStackTrace();
}
//1.转换消息格式
SeckillOrder seckillOrder = JSON.parseObject(message.getBody(), SeckillOrder.class);
//2.基于业务层完成同步mysql的操作
int result = secKillOrderService.createOrder(seckillOrder);
if (result>0){
//同步mysql成功
//向消息服务器返回成功通知
try {
/**
* 第一个参数:消息的唯一标识
* 第二个参数:是否开启批处理
*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (IOException e) {
e.printStackTrace();
}
}else{
//同步mysql失败
//向消息服务器返回失败通知
try {
/**
* 第一个参数:消息的唯一标识
* 第二个参数: true所有消费者都会拒绝这个消息,false只有当前消费者拒绝
* 第三个参数:true当前消息会进入到死信队列(延迟消息队列),false当前的消息会重新进入到原有队列中,默认回到头部
*/
channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3)定义业务层接口与实现类
public interface ConsumeService {
int handleCreateOrder(SeckillOrder order);
}
package com.changgou.consumer.service.impl;
import com.changgou.consumer.dao.SeckillGoodsMapper;
import com.changgou.consumer.dao.SeckillOrderMapper;
import com.changgou.consumer.service.SecKillOrderService;
import com.changgou.seckill.pojo.SeckillOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class SecKillOrderServiceImpl implements SecKillOrderService {
@Autowired
private SeckillGoodsMapper seckillGoodsMapper;
@Autowired
private SeckillOrderMapper seckillOrderMapper;
@Override
@Transactional
public int createOrder(SeckillOrder seckillOrder) {
//同步mysql中的数据
//1.扣减秒杀商品的库存
int result = seckillGoodsMapper.updateStockCount(seckillOrder.getSeckillId());
if (result <= 0){
return 0;
}
//2.新增秒杀订单
result = seckillOrderMapper.insertSelective(seckillOrder);
if (result <= 0){
return 0;
}
return 1;
}
}
数据库字段unsigned介绍
unsigned-----无符号,修饰int 、char
ALTER TABLE tb_seckill_goods MODIFY COLUMN stock_count int(11) UNSIGNED DEFAULT
NULL COMMENT '剩余库存数';
在秒杀这种高并发的场景下,每秒都有可能产生几万甚至十几万条消息,如果没有对消息处理量进行任何限制的话,很有可能因为过多的消息堆积从而导致消费者宕机的情况。因此官网建议对每一个消息消费者都设置处理消息总数(消息抓取总数)。
消息抓取总数的值,设置过大或者过小都不好,过小的话,会导致整个系统消息吞吐能力下降,造成性能浪费。过大的话,则很有可能导致消息过多,导致整个系统OOM。因此官网建议每一个消费者将该值设置在100-300之间。
1)更新消费者
//设置预抓取总数
channel.basicQos(300);
1)定义feign接口
@FeignClient(name="seckill")
public interface SecKillOrderFeign {
/**
* 秒杀下单
* @param time 当前时间段
* @param id 秒杀商品id
* @return
*/
@RequestMapping("/seckillorder/add")
public Result add(@RequestParam("time") String time, @RequestParam("id") Long id);
}
2)定义controller
@Controller
@CrossOrigin
@RequestMapping("/wseckillorder")
public class SecKillOrderController {
@Autowired
private SecKillOrderFeign secKillOrderFeign;
@RequestMapping("/add")
@ResponseBody
public Result add(String time,Long id){
Result result = secKillOrderFeign.add(time, id);
return result;
}
}
1)定义feign接口
@FeignClient(name="seckill")
public interface SecKillOrderFeign {
/**
* 秒杀下单
* @param time 当前时间段
* @param id 秒杀商品id
* @return
*/
@RequestMapping("/seckillorder/add")
public Result add(@RequestParam("time") String time, @RequestParam("id") Long id);
}
2)定义controller
@Controller
@CrossOrigin
@RequestMapping("/wseckillorder")
public class SecKillOrderController {
@Autowired
private SecKillOrderFeign secKillOrderFeign;
@RequestMapping("/add")
@ResponseBody
public Result add(String time,Long id){
Result result = secKillOrderFeign.add(time, id);
return result;
}
}