提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
【项目实战】秒杀场景超高含金量项目,3000 QPS 高并发秒杀系统实现
视频链接见文章底部
一、项目导入
1. 导入代码
2. 阅读代码
前后端分离的跨域问题?(CORS)
比如80端口 请求9000端口
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
CorsConfiguration config = new CorsConfiguration();
// 允许cookies跨域
config.setAllowCredentials(true);
// #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
config.addAllowedOrigin("*");
// #允许访问的头信息,*表示全部
config.addAllowedHeader("*");
// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
config.setMaxAge(18000L);
// 允许提交请求的方法,*表示全部允许
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
跨域 --> 客户端需要询问服务端是否允许跨域
在发起api请求前,会构造一个预请求。疫预请求的方法是options类型
且请求参数,请求体都不存在。预请求的作用是请求到服务端,如果服务端允许跨域,就会通过预请求携带对应的响应头信息回来。
如果浏览器检测到响应头中有允许当前页面跨域,就会发起真实请求
cors:是一套机制,用于浏览器校验跨域请求。(同源策略)
基本理念:只要服务器明确表示允许则,校验通过。明确拒绝或没有表示,百不通过
微服务中如何获得用户真实IP?
用户---->网关---->微服务
网关可以拿到用户ip!
public class CommonFilter implements GlobalFilter {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
/**
* pre拦截逻辑
* 在请求去到微服务之前,做了两个处理
* 1.把客户端真实IP通过请求同的方式传递给微服务
* 2.在请求头中添加FEIGN_REQUEST的请求头,值为0,标记请求不是Feign调用,而是客户端调用
*/
ServerHttpRequest request =exchange.getRequest().mutate().
header(CommonConstants.REAL_IP,exchange.getRequest().getRemoteAddress().getHostString()).
header(CommonConstants.FEIGN_REQUEST_KEY,CommonConstants.FEIGN_REQUEST_FALSE).build();
全局过滤器,因为微服务的请求是网关转发过来的,getRequest().getRemoteAddress()获取到的是网关ip。
登录流程(jwt)
第0步:拿着token查
第一步:拿到用户信息,先到redis中查,查不到到数据库中查,并且存到redis中。
第二步:如果查到的信息为空或者查出的数据密码不符,往mq中发送消息(登录失败),抛出异常,提示密码或者用户名错误
第三步:密码符合,查出用户信息,创建token
private String createToken(UserInfo userInfo) {
//token创建
String token = UUID.randomUUID().toString().replace("-", "");
//把user对象存储到redis中
CommonRedisKey redisKey = CommonRedisKey.USER_TOKEN;
redisTemplate.opsForValue().set(redisKey.getRealKey(token), JSON.toJSONString(userInfo), redisKey.getExpireTime(), redisKey.getUnit());
return token;
}
过期时间是30分钟,如何保证用户体验?
在过滤器中
/**
* post拦截逻辑
* 在请求执行完微服务之后,需要刷新token在redis的时间
* 判断token不为空 && Redis还存在这个token对于的key,这时候需要延长Redis中对应key的有效时间.
*/
String token,redisKey;
if(!StringUtils.isEmpty(token =exchange.getRequest().getHeaders().getFirst(CommonConstants.TOKEN_NAME))
&& redisTemplate.hasKey(redisKey = CommonRedisKey.USER_TOKEN.getRealKey(token))){
redisTemplate.expire(redisKey, CommonRedisKey.USER_TOKEN.getExpireTime(), CommonRedisKey.USER_TOKEN.getUnit());
}
二、秒杀功能实现
1. 秒杀列表功能展示
秒杀产品列表查询
数据库设计
商品表t_product
id | bigint | 主键id |
---|---|---|
product_name | varchar | 产品名称 |
product_title | varchar | 产品标题 |
product_img | varchar | 产品图片链接 |
product_detail | longtext | 产品详细介绍 |
product_price | decimal | 产品价格 |
秒杀商品表t_seckill_product |
id product_id(对应t_product_id) seckill_price(价格) intergral(积分) stock_count(库存) start_date(开始日期) time(秒杀开始场次) | bigint bigint decimal decimal int date int |
---|
秒杀商品SeckillProductVo(t_product + t_seckill_product)
productName productTitle productImg productDetail productPrice currentCount; |
---|
逻辑:
Result<List<SeckillProductVo>> selectTodayListByTime(@RequestParam("time") Integer time)
得到秒杀商品t_seckill_product_id集合
Result<List<Product>> selectByIdList(@RequestParam("ids") List<Long> idList);
得到id的商品集合
整体思想图
商品详情页
@RequestMapping("/find")
public Result<SeckillProductVo> findById(Integer time, Long seckillId) {
return Result.success(seckillProductService.selectByIdAndTime(seckillId, time));
}
@Cacheable(key = "'selectByIdAndTime:' + #time + ':' + #seckillId")
public SeckillProductVo selectByIdAndTime(Long seckillId, Integer time) {
SeckillProduct seckillProduct = seckillProductMapper.selectByIdAndTime(seckillId, time);
Result<List<Product>> result = productFeignApi.selectByIdList(Collections.singletonList(seckillProduct.getProductId()));
if (result.hasError() || result.getData() == null || result.getData().size() == 0) {
throw new BusinessException(new CodeMsg(result.getCode(), result.getMsg()));
}
Product product = result.getData().get(0);
SeckillProductVo vo = new SeckillProductVo();
// 先将商品的属性 copy 到 vo 对象中
BeanUtils.copyProperties(product, vo);
// 再将秒杀商品的属性 copy 到 vo 对象中, 并覆盖 id 属性
BeanUtils.copyProperties(seckillProduct, vo);
return vo;
}
逻辑图
秒杀商品列表数据预热(定时任务)
zookeeper
public class SeckillProductInitJob implements SimpleJob {
@Value("${job.seckillProduct.cron}")
private String cron;
@Value("${job.seckillProduct.shardingCount}")
private Integer shardingCount;
@Value("${job.seckillProduct.shardingParameters}")
private String shardingParameters;
@Value("${job.seckillProduct.dataFlow}")
private boolean dataFlow;
@Autowired
private SeckillProductFeignApi seckillProductFeignApi;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void execute(ShardingContext shardingContext) {
// 分片参数利用场次进行分片
String time = shardingContext.getShardingParameter();
// 先清空之前的数据
String key = SeckillRedisKey.SECKILL_PRODUCT_LIST.join(time);
stringRedisTemplate.delete(key);
// 调用秒杀服务的接口, 查询秒杀商品数据
Result<List<SeckillProductVo>> result = seckillProductFeignApi.selectTodayListByTime(Integer.valueOf(time));
if (result.hasError() || result.getData() == null) {
log.warn("[秒杀商品数据预热] 查询秒杀商品数据失败, 远程服务异常. res={}", JSON.toJSONString(result));
return;
}
List<SeckillProductVo> productVoList = result.getData();
log.info("[秒杀商品数据预热] 准备开始预热秒杀商品数据, 当前场次:{}, 本次缓存的数据:{}", time, productVoList.size());
// 将数据存入 Redis : List
// key=TODAY:{time}:SECKILL:PRODUCTS
// value=SeckillProductVo => {json}
for (SeckillProductVo vo : productVoList) {
String json = JSON.toJSONString(vo);
stringRedisTemplate.opsForList().rightPush(key, json);
}
log.info("[秒杀商品数据预热] 数据预热完成...");
}
}
2. 秒杀功能1.0
业务流程
①当前用户必须登录
②基于秒杀商品id,查询秒杀商品对象
③判断当前时间是否在秒杀开始到结束的范围之内
④判断当前秒杀商品库存是否足够
⑤判断该用户是否已经在当前场次买过商品
⑥扣减秒杀库存,创建秒杀订单
@RequireLogin
@RequestMapping("/doSeckill")
public Result<String> doSeckill(Integer time, Long seckillId, @RequestHeader(CommonConstants.TOKEN_NAME) String token) {
// 1. 基于 token 获取到用户信息(必须登录)
UserInfo userInfo = this.getUserByToken(token);
// 2. 基于场次+秒杀id获取到秒杀商品对象
SeckillProductVo vo = seckillProductService.selectByIdAndTime(seckillId, time);
if (vo == null) {
throw new BusinessException(SeckillCodeMsg.REMOTE_DATA_ERROR);
}
// 3. 判断时间是否大于开始时间 && 小于 开始时间+2小时
// if (!DateUtil.isLegalTime(vo.getStartDate(), time)) {
// throw new BusinessException(SeckillCodeMsg.OUT_OF_SECKILL_TIME_ERROR);
// }
// 4. 判断用户是否重复下单
// 基于用户 + 秒杀 id + 场次查询订单, 如果存在订单, 说明用户已经下过单
OrderInfo orderInfo = orderInfoService.selectByUserIdAndSeckillId(userInfo.getPhone(), seckillId, time);
if (orderInfo != null) {
throw new BusinessException(SeckillCodeMsg.REPEAT_SECKILL);
}
// 5. 判断库存是否充足
if (vo.getStockCount() <= 0 || vo == null) {
throw new BusinessException(SeckillCodeMsg.SECKILL_STOCK_OVER);
}
// 6. 执行下单操作(减少库存, 创建订单)
String orderNo = orderInfoService.doSeckill(userInfo, vo);
return Result.success(orderNo);
}
扣减秒杀库存,创建秒杀订单
public String doSeckill(UserInfo userInfo, SeckillProductVo vo) {
// 1. 扣除秒杀商品库存
int row = seckillProductService.decrStockCount(vo.getId());
// 2. 创建秒杀订单并保存
OrderInfo orderInfo = this.buildOrderInfo(userInfo, vo);
orderInfoMapper.insert(orderInfo);
// 3. 返回订单编号
return orderInfo.getOrderNo();
}
3. JMeter压力测试
生成测试账户
http://localhost:9000/uaa/token/initData
JMeter参数配置
线程组
HTTP请求
HTTP信息头管理
CSV数据文件设置
4. 秒杀功能2.0
超卖问题分析
秒杀流程图
存在的问题:
- 库存超卖
- 用户重复下单
解释:redis库存未及时更新(缓存不同步)
获取到商品对象时,会从数据查询,假设库存为10,之后同步redis库存为10。接着走一下流程,扣减库存,数据库库存为9,但是redis依然为10。下个用户来是,会直接查询redis。
超卖问题解决
优化方案:SpringCache
扣减库存时,同时更新redis数据
@CacheEvict
虽然缓存很有用,但在某些情况下可能需要逐出(删除)缓存的数据。注释@CacheEvict就是用于此目的。它确保在特定操作下,缓存保持最新。
@CacheEvict(key = "'selectByIdAndTime:' + #time + ':' + #id")
@Override
public int decrStockCount(Long id, Integer time) {
return seckillProductMapper.decrStock(id);
}
优化后有所改善,但未解决
存在的问题:多线程并发(原子性问题)
原子性:即一个操作或者多个操作,要么全部执行,并且执行的过程不会被任何因素打断,要么就都不执行。
当多个线程同时执行到获取秒杀对象时,即同时拿到库存数为10
查库存与扣减库存不是同时进行。
优化:加锁
如图:
但性能会受到严重影响
优化:在扣除库存时,再次判断库存是否足够,并且加锁
具体加锁实现
synchronized (this){
//再次判断库存是否足够
SeckillProduct sp = seckillProductService.findById(vo.getId());
if(sp.getStockCount() <= 0){
//库存不足
throw new BusinessException(SeckillCodeMsg.SECKILL_STOCK_OVER);
}
//扣减库存
seckillProductService.decrStockCount(vo.getId(),vo.getTime());
}
存在的问题: synchronized属于JVM锁,再单机环境下是有效的,但是分布式环境下由于有多个JVM实例,因此无法生效。
优化:将锁放在redis中
setIfAbsent == redis中的setnx方法,即设置key在redis中 已经存在,返回false
如果当前线程执行位ture则认为抢到了锁,为了避免别人的锁被释放,用一个唯一标识用来表示当前线程。
加锁后,需要释放锁
redisTemplate.delete(key)
存在的问题: 正在加锁时,JVM出现问题,导致锁无法释放,即死锁问题
优化:加入过期时间
redisTemplate.opsForValue().setIfAbsent(key + "code", 5, TimeUnit.SECONDS );
TDOD: Spring提供的setIfAbsent( ) 存在并发问题,原因是改方法是通过先调用 SET + EXPIRE 指令实现的,因为时多指令组成的,此时可能会存在多个线程并发执行。常见的解决方法为使用LUA脚本实现批处理命令。
存在的问题: 假如任务处理时间超过了自动释放的时间,即锁自动释放了,下一个2号线程会加锁,但是1号线程结束任务,会释放2号线程的锁。
优化: 加入唯一标识,释放锁时需要验证,是自己的锁才可以释放。
final String key = "seckill:product:";
String threadId = IdGenerateUtil.get().nextId() + "";
redisTemplate.opsForValue().setIfAbsent(key + vo.getId(), threadId,5, TimeUnit.SECONDS );
//释放锁时只能释放自己的锁
if(threadId.equals((redisTemplate.opsForValue().get(key)))){
redisTemplate.delete(key);
}
存在的问题: 假如发生了超时释放锁的问题,即锁自动释放了,下一个2号线程会加锁,但是1号线程和2号线程存在同时扣除库存的可能。
优化:看门狗机制
个人来说就是一个key被锁住即将释放的时候,如果在当前线程还没执行完任务的时候,有新的线程进来争抢,会导致锁应该给谁的问题,这个时候官方为我们提供看门狗的机制,会自动增加锁的时间。
Redisson分布式锁
完善的优化机制①:Redisson分布式锁
加入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.22.0</version>
</dependency>
具体实现代码
public String doSeckill(UserInfo userInfo, SeckillProductVo vo) {
final String key = "seckill:product:";
//获取锁
RLock rlock = redissonClient.getLock(key);
try{
// 加锁-超时时间5秒钟
rlock.lock(5,TimeUnit.SECONDS);
//如果加锁成功,扣减库存
SeckillProduct sp = seckillProductService.findById(vo.getId());
if(sp.getStockCount() <= 0){
//库存不足
throw new BusinessException(SeckillCodeMsg.SECKILL_STOCK_OVER);
}
// 1. 扣除秒杀商品库存
int row = seckillProductService.decrStockCount(vo.getId(),vo.getTime());
if(row <= 0){
throw new BusinessException(SeckillCodeMsg.SECKILL_U);
}
}finally {
//释放锁
rlock.unlock();
}
// 2. 创建秒杀订单并保存
OrderInfo orderInfo = this.buildOrderInfo(userInfo, vo);
orderInfoMapper.insert(orderInfo);
// 3. 返回订单编号
return orderInfo.getOrderNo();
}
存在的问题: 浅谈一下Redis分布式锁存在的问题
虽然redisson帮助我们解决了锁续期的问题,但是在redis集群架构中,由于主从复制具有一定的延时,那么在极端情况下就会出现这样一个问题:当一个线程获取锁成功,并且成功向主节点保存了锁信息,当主节点还未像从节点同步锁信息时,主节点宕机了,那么当发生故障转移从节点切换为主节点时,线程加的锁就丢失了。
优化方案: RedLock
为了解决这个问题,redis引入了红锁RedLock,RedLock与大多数中间件的选举机制类似,采用过半的方式来决定操作成功还是不成功。
RLock lock = redissonClient.getLock("lock");
RLock lock2 = redissonClient2.getLock("lock");
RLock lock3 = redissonClient3.getLock("lock");
RedissonRedLock redLock = new RedissonRedLock(lock,lock2,lock3);// 分别像三台实例加锁
完善的优化机制①总结:
在使用reids做分布式锁时,并没有想象中的那么简单,高并发场景下容易出现死锁,锁被其他线程误删,锁续期,锁丢失等问题,在实际开发中应该考虑到这些问题并根据相应的解决办法来解决这些问题,从而保证系统的安全性。
乐观锁
完善的优化机制②:乐观锁在这里插入代码片
乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。如果没有修改则认为是安全的,自己才更新数据。如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常。
<update id="decrStock">
update t_seckill_product
set stock_count = stock_count - 1
where id = #{seckillId} and stock_count > 0
</update>
public String doSeckill(UserInfo userInfo, SeckillProductVo vo) {
// 1. 扣除秒杀商品库存
int row = seckillProductService.decrStockCount(vo.getId(),vo.getTime());
if(row <= 0){
throw new BusinessException(SeckillCodeMsg.SECKILL_U);
}
// 2. 创建秒杀订单并保存
OrderInfo orderInfo = this.buildOrderInfo(userInfo, vo);
orderInfoMapper.insert(orderInfo);
// 3. 返回订单编号
return orderInfo.getOrderNo();
}
并发优化
Redis库存预减方案
此时虽然通过乐观锁避免了超卖问题,但由于并发较高,还是会产生大量的请求进入扣库存环节,虽然会失败,但是会对性能产生较大影响。
因此希望能够在前置的环节中就控制并发访问任务
定时任务job
//保存库存
//外部key = seckillStockCount:{time}
//hash key = {seckillId}
//hash value = 库存
stringRedisTemplate.opsForHash().put(SeckillRedisKey.SECKILL_STOCK_COUNT_HASH.join(time), vo.getId(), vo.getStockCount());
doSeckill
//redis库存预减控制访问人数
String join = SeckillRedisKey.SECKILL_STOCK_COUNT_HASH.join(time + "");
Long increment = redisTemplate.opsForHash().increment(join, seckillId + "", -1);
// 5. 判断库存是否充足
if (increment < 0) {
throw new BusinessException(SeckillCodeMsg.SECKILL_STOCK_OVER);
}
库存售完标记
通过redis库存预减后,可以提前预知库存已经没有了,此时可以直接在jvm内存中标记该秒杀商品已经没有库存,之后的请求都没有必要访问redis。
具体实现
//本地售完标记 key= id, value = 是否已经售完
private static final Map<Long, Boolean> LOCAL_STOCK_FLAG_CACHE = new ConcurrentHashMap<>();
//增加本地缓存判断
if(LOCAL_STOCK_FLAG_CACHE.get(seckillId)){
//如果是ture,直接提示库存不足
throw new BusinessException(SeckillCodeMsg.SECKILL_STOCK_OVER);
}
if (increment < 0) {
//标记当前库存已经售完
LOCAL_STOCK_FLAG_CACHE.put(seckillId,true);
throw new BusinessException(SeckillCodeMsg.SECKILL_STOCK_OVER);
}
Redis记录用户是否重复下单
由于大部分用户都应该不会出现重复下单的问题,而此时每次从数据库查询是否已经下单,是浪费资源且性能较差的操作,因此希望可以借助redis实现记录用户下单状态,判断用户是否已经下单。
具体实现
int max = 1;
String userCountKey = SeckillRedisKey.SECKILL_ORDER_HASH.join(seckillId + "");
Long increment1 = redisTemplate.opsForHash().increment(userCountKey, userInfo.getPhone() + "", 1);
if(increment1 > max){
throw new BusinessException(SeckillCodeMsg.REPEAT_SECKILL);
}
优化结果
异步下单
具体实现
public class DefaultMQMessageCallBack implements SendCallback {
@Override
public void onSuccess(SendResult sendResult) {
log.info("[异步消息回调]:发送消息成功:{}",sendResult);
}
@Override
public void onException(Throwable throwable) {
log.error("[异步消息回调]:消息发送失败,准备重新投递消息",throwable);
}
}
目的地,参数,CallBack
public void asyncSend(String destination,Object payload,org.apache.rocketmq.client.producer.SendCallback sendCallback )
// 6. 执行下单操作(减少库存, 创建订单)
//利用RockMq发送消息,实现异步下单
rocketMQTemplate.asyncSend(MQConstant.ORDER_PEDDING_TOPIC,new OrderMessage(time, seckillId, token, userInfo.getPhone()),new DefaultMQMessageCallBack());
// String orderNo = orderInfoService.doSeckill(userInfo, vo);
return Result.success("成功加入下单队列,正在排队中...");
// return Result.success(orderNo);
}
下单操作
@RocketMQMessageListener(consumerGroup = MQConstant.ORDER_PENDING_CONSUMER_GROUP,topic = MQConstant.ORDER_PEDDING_TOPIC )
public class OrderPendingMessage implements RocketMQListener<OrderMessage> {
@Autowired
private IOrderInfoService orderInfoService;
@Autowired
private ISeckillProductService seckillProductService;
@Override
public void onMessage(OrderMessage orderMessage) {
log.info("[创建订单消息]:收到创建订单消息,准备开始创建订单{}", JSON.toJSON(orderMessage));
//调用秒杀接口
UserInfo userInfo = new UserInfo();
userInfo.setPhone(orderMessage.getUserPhone());
orderInfoService.doSeckill(userInfo, seckillProductService.selectByIdAndTime(orderMessage.getSeckillId(), orderMessage.getTime()));
}
}
存在的问题是无法给用户及时发送通知,更新订单信息。
解决方案: 长连接方案
主要的步骤为:
- 创建订单
- 将订单创建结果发送到 mq
- MQ 将结果通知给长连接服务
- 长连接服务收到消息后,利用连接将结果通知给客户端
优点:性能较高,可以实现服务端主动推送消息
缺点:实现相对复杂,且有些浏览器可能不支持
常见方案:
HTTP:就是利用 keepalive 属性,延长单个请求的连接时间,当客户端发起请求给服务端后,服务端在没有结果以前,不进行响应,始终保持连接(也可以设置超时时间),一旦服务端收到消息后,再将结果响应回去。客户端收到消息/超时后可重新发起请求再次获取消息。
- Tomcat 的 Comet 技术
- Jetty 的 Continuations
适用场景:消息较少,又需要长时间等待服务端响应结果的场景
WEBSOCKET:可以理解 web 版本的 socket 连接,就是基于 TCP 协议实现的一个连接,连接建立成功后,一般情况下不会断开,除非服务单/客户端主动断开。
具体实现:
public void onMessage(OrderMessage orderMessage) {
log.info("[创建订单消息]:收到创建订单消息,准备开始创建订单{}", JSON.toJSON(orderMessage));
OrderMQResult orderMQResult = new OrderMQResult();
try {
orderMQResult.setTime(orderMessage.getTime());
orderMQResult.setSeckillId(orderMessage.getSeckillId());
//调用秒杀接口
UserInfo userInfo = new UserInfo();
userInfo.setPhone(orderMessage.getUserPhone());
String orderNo = orderInfoService.doSeckill(userInfo, seckillProductService.selectByIdAndTime(orderMessage.getSeckillId(), orderMessage.getTime()));
//订单创建成功
orderMQResult.setOrderNo(orderNo);
}catch (Exception e){
//订单创建失败
SeckillCodeMsg seckillError = SeckillCodeMsg.SECKILL_ERROR;
orderMQResult.setCode(seckillError.getCode());
orderMQResult.setMsg(seckillError.getMsg());
}
//发送订单创建结果消息
rocketMQTemplate.asyncSend(MQConstant.ORDER_RESULT_TOPIC, orderMQResult, new DefaultMQMessageCallBack());
}
订单详情查询
主要是需要判断订单是否是自己的
@GetMapping("/find")
public Result<OrderInfo> findOrderId(String orderNo, @RequestHeader(CommonConstants.TOKEN_NAME) String token){
UserInfo userByToken = getUserByToken(token);
OrderInfo orderInfo = orderInfoService.findOrderId(orderNo);
if(orderInfo != null && userByToken.getPhone().equals(orderInfo.getUserId())){
throw new RuntimeException("非法操作");
}
return Result.success(orderInfo);
}
超时取消订单
如果订单超过一定时间,还不支付,需要自动的将订单状态取消,并重新回补库存。
发布时间:在订单下单成功后
收到消息:
- 检查订单是否已经支付,如果已经支付就不管它
- 如果没有支付,将订单状态更新为超时取消
- MySQL 库存+1
- 重新查到 MySQL 的库存后存入 Redis
- 本地售完标识清
具体实现 :
延时发送:asyncSend(主题,参数,callback,超时时间,延迟等级)
//创建订单成功,发送延时消息检查超时未支付订单
Message<OrderTimeOutMessage> build = MessageBuilder.withPayload(new OrderTimeOutMessage(orderNo, orderMessage.getSeckillId())).build();
rocketMQTemplate.asyncSend(MQConstant.ORDER_PAY_TIMEOUT_TOPIC,build, new DefaultMQMessageCallBack(), 2000, 3);
监听者
@RocketMQMessageListener(
consumerGroup = MQConstant.ORDER_PAY_TIMEOUT_GROUP,
topic = MQConstant.ORDER_PAY_TIMEOUT_TOPIC
)
@Slf4j
public class OrderPayTimeOutListener implements RocketMQListener<OrderTimeOutMessage> {
@Autowired
private IOrderInfoService orderInfoService;
@Override
public void onMessage(OrderTimeOutMessage message) {
log.info("[超时未支付检查] 收到超时为支付检查信息:{}", message);
//超时未支付检查
orderInfoService.checkPayTime(message);
}
}
checkPayTime
public void checkPayTime(OrderTimeOutMessage message) {
//根据订单编号,查询订单是否支付
//OrderInfo orderInfo = orderInfoMapper.find(message.getOrderNo());
//支付则结束,更新订单状态为超时取消
int status = orderInfoMapper.updateCancelStatus(message.getOrderNo(), OrderInfo.STATUS_TIMEOUT);
if(status >0){
//sql, 库存+1
seckillProductService.incryStockCount(message.getSeckillId());
//查询sql 后,将库存存入 redis
SeckillProduct seckillProduct = seckillProductService.findById(message.getSeckillId());
redisTemplate.opsForHash().put(SeckillRedisKey.SECKILL_STOCK_COUNT_HASH.join(seckillProduct.getTime() + ""),
seckillProduct.getId() + "", seckillProduct.getStockCount() + "");
//清除本地标志
OrderInfoController.LOCAL_STOCK_FLAG_CACHE.remove(message.getSeckillId());
}
}
关于清除本地标记问题:分布式缓存同步
由于本地标识是缓存在 JVM 中的,当秒杀服务集群部署时,可能各个不同的服务中都存在该库存售完标记,此时只是清空超时订单所在服务器的话,可能其他服务器本地售完标记还未清除,导致请求到其他服务器的用户,无法抢购商品。
OrderInfoController.LOCAL_STOCK_FLAG_CACHE.remove(message.getSeckillId());
rocketMQTemplate.asyncSend(MQConstant.CANCEL_SECKILL_OVER_SIGE_TOPIC, message.getSeckillId(), new DefaultMQMessageCallBack());
//消息模式修改为广播消息
@RocketMQMessageListener(
consumerGroup = MQConstant.CANCEL_SECKILL_OVER_SIGE_GROUP,
topic = MQConstant.CANCEL_SECKILL_OVER_SIGE_TOPIC,
messageModel = MessageModel.BROADCASTING
)
@Slf4j
public class ClearLocalStockMessageListener implements RocketMQListener<Long> {
@Override
public void onMessage(Long seckillId) {
log.info("[清除本地标识]收到清除本地标识,清除本地售完标识:{}", seckillId);
OrderInfoController.LOCAL_STOCK_FLAG_CACHE.put(seckillId, false);
}
}
步骤:
[异步消息回调]发送消息成功:SendResult(发送创建订单消息)
[创建订单消息]收到创建订单消息,准备开始创建订单
[异步消息回调]发送消息成功:SendResult(订单结果通知–ORDER_RESULT_TOPIC)
[异步消息回调]发送消息成功:SendResult(订单未支付超时检查–ORDER_PAY_TIMEOUT_TOPIC)
[超时未支付检查]收到超时未支付检查
[异步消息回调]发送消息成功:SendResult(清除本地标志广播消息–CANCEL_SECKILL_OVER_SIGE_TOPIC)
[清除本地标识]收到清楚本地标识请求,清楚本地售完标识
1
视频链接:bilibili
持续更新中