SpringCloud(十一)- 秒杀 抢购
1、流程图
1.1 数据预热
1.2 抢购
1.3 生成订单 (发送订单消息)
1.4 订单入库 (监听 消费订单消息)
1.5 查看订单状态
1.6 支付 (获取支付链接 )
1.7 支付成功 微信回调 (发送 支付成功消息)
1.8 支付成功 返回给前端成功 (监听 支付成功消息)
2、incr 和 setnx
2.1 incr
Redis Incr 命令将 key 中储存的数字值增一。 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。且将key的有效时间设置为长期有效 。
2.1.1 常见使用场景
2.1.1.1 计数
我们可能常会统计网站页面每天访问量,通过incr命令在redis中设置key,每次增加1,设置24小时过期。
2.1.1.2 限流
日常的开放平台API一般常有限流,利用redis的incr命令可以实现一般的限流操作。如限制某接口每分钟请求次数上限1000次
2.1.1.3 幂等
MQ防止重复消费也可以利用INCR命令实现,如订单防重,订单5分钟之内只能被消费一次,订单号作为redis的key
2.2 sexnx
Redis使用setnx命令实现分布式锁;
2.1.1 加锁
版本一#但是这种方式的缺点是容易产生死锁,因为客户端有可能忘记解锁,或者解锁失败。
setnx key value
版本二#给锁增加了过期时间,避免出现死锁。但这两个命令不是原子的,第二步可能会失败,依然无法避免死锁问题。
setnx key value
expire key seconds
版本三(推荐)#通过“set...nx...”命令,将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。
set key value nx ex seconds
2.1.2 解锁
解锁就是删除代表锁的那份数据。
del key
参考博客:https://blog.csdn.net/weixin_45525272/article/details/126562119
3、实现代码
主要是抢购的业务;
3.1模块分析
3.2 web模块
3.2.1 application.yml
点击查看代码
# 端口
server:
port: 8106
# 服务名
spring:
application:
name: edocmall-seckill-web
# redis 配置
redis:
host: 127.0.0.1
port: 6379
# eureka 注册中心的配置
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8096/eureka # 注册中心的地址
# 秒杀抢购自定义配置
kh96:
seckill:
buy-user-count-prefix: seckill-buy-user-count # 请求用户数量的限制头标识
buy-prod-stock-count: 100 # 初始化腔骨商品的库存数量,存入redis,实际开发中,没有此配置(初始化商品库存,在洗头膏添加抢购商品是)
buy-prod-stock-prefix: seckill-buy-prod-stock # 抢购商品数量 头标识
buy-user-lock-prefix: seckill-buy-user-lock # 锁定抢购用户的头标识
buy-prod-lock-prefix: seckill-buy-prod-lock # 锁定商品库存锁头标识
3.2.2 SeckillConfig 配置类
点击查看代码
/**
* Created On : 9/11/2022.
* <p>
* Author : huayu
* <p>
* Description: SeckillConfig
*/
@Data
@Component
@ConfigurationProperties(prefix = "kh96.seckill")
public class SeckillConfig {
/*
请求用户数量的限制头标识
*/
private String buyUserCountPrefix;
/*
初始化抢购商品的库存数量
*/
private Integer buyProdStockCount;
/*
抢购商品数量 头标识
*/
private String buyProdStockPrefix;
/*
锁定抢购用户的头标识
*/
private String buyUserLockPrefix;
/*
锁定商品库存锁头标识
*/
private String buyProdLockPrefix;
}
3.2.3 抢购接口和实现类
3.2.3.1 接口
点击查看代码
/**
* Created On : 9/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 秒杀抢购的业务接口
*/
public interface SeckillService {
/**
* @author : huayu
* @date : 9/11/2022
* @param : [prodId, stockCount]
* @return : void
* @description : 初始化商品库存到缓存
*/
boolean initProdStock2Redis(String prodId,Integer stockCount);
/**
* @author : huayu
* @date : 9/11/2022
* @param : [productId]
* @return : boolean
* @description : 校验抢购商品的请求数量是否达到上限
*/
boolean checkBuyUserCount(String productId);
/**
* @author : huayu
* @date : 9/11/2022
* @param : [prodId, buyCount]
* @return : boolean
* @description : 校验商品库存是否充足
*/
boolean checkProdStockEnough(String prodId,Integer buyCount);
/**
* @author : huayu
* @date : 9/11/2022
* @param : [userId, prodId]
* @return : boolean
* @description : 校验用户是否已经抢购过当前商品
*/
boolean checkBuyUserBought(String userId,String prodId);
/**
* @author : huayu
* @date : 9/11/2022
* @param : [userId]
* @return : boolean
* @description : 校验商品库存是否锁定
*/
boolean checkProdStockLocked(String prodId);
/**
* @author : huayu
* @date : 9/11/2022
* @param : [prodId]
* @return : boolean
* @description : 释放库存锁
*/
void unLockProdStock(String prodId);
/**
* @author : huayu
* @date : 9/11/2022
* @param : [prodId, butCount]
* @return : void
* @description : 扣减库存
*/
void subProdStockCount(String prodId, Integer butCount);
}
3.2.3.2 实现类
点击查看代码
/**
* Created On : 9/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 抢购业务接口实现类
*/
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
private RedisUtils redisUtils;
@Autowired
private SeckillConfig seckillConfig;
@Override
public boolean initProdStock2Redis(String prodId, Integer stockCount) {
// 判断redis中,是否存在已经初始化的商品库存数据,如果已经存在,不需要再次初始化
if (ObjectUtils.isEmpty(redisUtils.get(seckillConfig.getBuyProdStockPrefix() + ":" + prodId))){
// 将商品库存添加到redis中
return redisUtils.set(seckillConfig.getBuyProdStockPrefix() + ":" + prodId, stockCount);
}
// 已经存在商品库存,不需要设置
return false;
}
@Override
public boolean checkBuyUserCount(String prodId) {
// 使用redis的incr操作,校验当前商品的抢购用户数是否达到上限
return redisUtils.incr(seckillConfig.getBuyUserCountPrefix() + ":" + prodId, 1)
> Integer.parseInt(redisUtils.get(seckillConfig.getBuyProdStockPrefix() + ":" + prodId).toString()) * 2;
}
@Override
public boolean checkProdStockEnough(String prodId, Integer buyCount) {
//校验商品库存,是否大于等于用户抢购数量,如果小于,代表库存不足
//TODO 如果redis 没有查到库存(库存没初始化,或者后台误删,必须要进行数据库查询操作,获取库存,
// 要加锁,只有一个线程取操作),再同步到redis
return Integer.parseInt(redisUtils.get(seckillConfig.getBuyProdStockPrefix() + ":" + prodId).toString())
>= buyCount;
}
@Override
public boolean checkBuyUserBought(String userId, String prodId) {
//使用redis的分布式锁,sexnx,
// 如果上锁成功,代表没有买过,可以继续购买,如果上锁失败,表示购买过不能继续购买
//锁时长为正抢购活动时长
return ! redisUtils.lock( seckillConfig.getBuyUserLockPrefix()+":"+userId,
null,
15*60); //模拟 抢购时长
}
@Override
public boolean checkProdStockLocked(String prodId) {
//使用redis的分布式锁,sexnx,如果上锁成功,代表库存没有被锁,如果上锁失败代表库存被其他用户锁定不能购买
//锁库存必须要增加时长限制,防止库存锁释放失败,导致用户无法抢购,过期仍然会释放
return ! redisUtils.lock(seckillConfig.getBuyProdLockPrefix()+":"+prodId,
null,
2*60); //模拟锁2分钟
}
@Override
public void unLockProdStock(String prodId) {
//删除redis中的 库存锁
redisUtils.del(seckillConfig.getBuyProdLockPrefix()+":"+prodId);
}
@Override
public void subProdStockCount(String prodId, Integer butCount) {
//扣减 redis中缓存的商品库存数量
//TODO redis缓存中操成功后,要发异步消息 到队列 更新数据库中商品库存呢
redisUtils.decr(seckillConfig.getBuyProdStockPrefix() + ":" + prodId,
butCount);
}
}
3.2.4 远程调用订单模块进行下单
点击查看代码
/**
* Created On : 10/11/2022.
* <p>
* Author : huayu
* <p>
* Description: SeckillOrderFeignService
*/
@FeignClient(value = "edocmall-seckill-order")
public interface SeckillOrderFeignService {
/**
* @author : huayu
* @date : 10/11/2022
* @param : [userId, prodId, buyCount]
* @return : java.lang.String
* @description : 远程调用订单中心,生成秒杀抢购订单的接口
*/
@GetMapping("/createSeckillOrder")
String feignInvokeCreateSeckillOrder(@RequestParam(value = "userId") String userId,
@RequestParam(value = "prodId") String prodId,
@RequestParam(value = "buyCount") Integer buyCount);
}
3.2.5 抢购 控制层
点击查看代码
/**
* Created On : 9/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 秒杀操作入口
*/
@Slf4j
@RestController
@Api(tags = "秒杀操作")
public class SeckillController {
@Autowired
private SeckillService seckillService;
@Autowired
private SeckillOrderFeignService seckillOrderFeignService;
/**
* @author : zhukang
* @date : 2022/11/9
* @param : [userId, prodId, buyCount]
* @return : com.kgc.scd.util.RequestResult<?>
* @description : 模拟初始化商品库存,实际开发中,没有此操作,是在后台添加抢购商品预热是,直接同步到redis
*/
@GetMapping("/initProdStock")
@ApiOperation(value = "1 初始库存", notes = "抢购商品预热,初始化商品库存数量到redis中")
@ApiImplicitParams({
@ApiImplicitParam(value = "初始化商品id",name = "prodId"),
@ApiImplicitParam(value = "初始化商品数量",name = "stockCount")
})
public RequestResult<?> initProdStock(@RequestParam String prodId,
@RequestParam Integer stockCount){
log.info("------ 初始化商品:{},库存数量:{},到redis缓存中 ------", prodId, stockCount);
// 增加业务逻辑处理:防止瞬时请求过多,使用redis的incr原子操作,进行请求用户数的统计计数,如果请求已经达到了上限(比如:设定请求用户数最多是抢购商品库存的2倍),其它后续所有的请求都默认无效,返回失败:当前请求用户过多,请稍后重试
if(seckillService.initProdStock2Redis(prodId, stockCount)){
return ResultBuildUtil.success("初始化商品库存成功!");
}
// 初始化商品库存失败
return ResultBuildUtil.fail("601", "初始化商品库存失败,请确认redis库存是否已初始化!");
}
@GetMapping("/seckillBuy")
@ApiOperation(value = "2 开始秒杀", notes = "基于redis,RabbitMQ消息队列,实现有序秒杀抢购功能")
@ApiImplicitParams({
@ApiImplicitParam(value = "用户id",name = "userId"),
@ApiImplicitParam(value = "商品id",name = "prodId"),
@ApiImplicitParam(value = "抢购数量",name = "buyCount")
})
public RequestResult<?> seckillBuy(@RequestParam String userId,
@RequestParam String prodId,
@RequestParam Integer buyCount){
log.info("------ 用户:{},购买商品:{},购买数量:{},开始抢购 ------", userId, prodId, buyCount);
// TODO 增加请求许可校验(自定义注解+拦截器),校验请求此接口的所有用户是否是已登录用户,如果没有登录,提示请登录
// TODO 增加接口参数的校验,判断用户是否是系统正常用户,不能是状态异常用户,判断商品的数据是否正确(切记:涉及系统内数据不要信任请求参数,要信任系统的缓存的数据库)
// TODO 为了提高抢购入口的并发处理能力,要减少数据库交互,可以设计为根据商品编号,从redis缓存中查询商品,如果商品信息存在,则参与抢购,如果不存在,还是需要到数据库查询商品,如果数据库中存在,将商品信息存入redis缓存,如果数据库不存在,则直接提示抢购失败。
// TODO 此种场景,正常情况,没有问题,可能存在的问题,某个商品,是首次参与抢购,缓存中没有数据,但是数据库有,虽然上面的处理方式,可以解决,但是在高并发场景下,同一时刻会有大批量的请求来秒杀此商品,此时同时去缓存中获取商品数据,没有获取到,又同时去数据库查询,就会导致数据库扛不住压力,可能直接数据库挂掉。
// TODO 解决方式:缓存商品数据一般都是在后台添加抢购商品时,直接对商品进行预热处理,即:事先把参与抢购的商品直接同步到redis缓存中,这样当抢购开始,直接从redis缓存就可以获取到商品,而不会出现缓存击穿问题。
// TODO 虽然商品数据预热方式,可以解决此类问题,但是可能还会存在例外(比如:缓存中的商品由于后台失误操作,导致设置的过期时间不对,缓存时间提前过期,或者缓存数据误删除),此种情况还是需要当缓存不存在商品数据,从数据库查询,放入缓存的逻辑。
// TODO 解决方式:可以进行加锁,限制在高并发的情况下访问数据库,如果同一时刻,缓存中没有获取到商品数据库,就进入查询数据库操作,但是在进入查询前,增加分布式锁,只有获取到锁的请求,才可以查询数据库并加入到缓存中(加完就释放锁),获取不到锁的请求,直接返回失败(损失当前请求,后续请求进行弥补,只要一个操作成功,缓存中的数据就存在,后续的请求自然可以获取到数据)
// TODO 极端情况:redis无法使用,必须要增加redis的高可用,确保redis永远是有效的,考虑的模式就是集群模式下的哨兵机制。或者把大批量的请求直接放到消息队列,进行缓冲处理。
log.info("------------------------------ 进行请求用户数的统计计数,防止请求过多 -----------------------------------------");
// 增加业务逻辑处理:防止瞬时请求过多,使用redis的incr原子操作,进行请求用户数的统计计数,如果请求已经达到了上限(比如:设定请求用户数最多是抢购商品库存的2倍),其它后续所有的请求都默认无效,返回失败:当前请求用户过多,请稍后重试
if(seckillService.checkBuyUserCount(prodId)){
return ResultBuildUtil.fail("602", "抢购失败,当前抢购用户过多,请稍后重试!");
}
log.info("------------------------------ 查看库存是否充足 -----------------------------------------");
//校验商品库存数量是否充足,可以进行后续抢购,日过不足,直接抢购失败
log.info("------ 用户:{},购买商品:{},购买数量:{},库存校验 ------", userId, prodId, buyCount);
if(!seckillService.checkProdStockEnough(prodId,buyCount)){
//库存不足,返回抢购失败
log.info("------ 用户:{},购买商品:{},购买数量:{},库存不足 ------", userId, prodId, buyCount);
return ResultBuildUtil.fail("603", "抢购失败,库存不足,缺货!");
}
log.info("------------------------------ 判断用户是否抢购过 -----------------------------------------");
//增加幂等操作:当前抢购用户只能抢购一次,如果已经抢购过商品,不允许再次抢购(限制一个用户同一个抢购商品,整个抢购期间只能抢购一次)
log.info("------ 用户:{},购买商品:{},购买数量:{},锁定抢购用户,如果 已经抢购过商品,不允许再次抢购 ------", userId, prodId, buyCount);
if(seckillService.checkBuyUserBought(userId,prodId)){
//用户抢购过,返回抢购失败
log.info("------ 用户:{},购买商品:{},购买数量:{},重复抢购 ------", userId, prodId, buyCount);
return ResultBuildUtil.fail("604", "抢购失败,重复抢购!");
}
log.info("------------------------------ 用户获取库存锁 -----------------------------------------");
//执行抢购业务,先给商品库存上锁(锁库存),如果上锁成功,代表当前用户继续抢购商品,如果上锁失败,说明有人抢购,进行等待
log.info("------ 用户:{},购买商品:{},购买数量:{},(尝试获取库存锁) 执行抢购业务,先给商品库存上锁(锁库存),拿到 库存锁 才可以开始下单 ------", userId, prodId, buyCount);
while (true){
//死循环,尝试锁库存,如果锁库存成功,代表库存所已经被释放
if(!seckillService.checkProdStockLocked(prodId)){
log.info("------ 用户:{},购买商品:{},购买数量:{},锁库存成功,拿到 库存锁 ,开始下单------", userId, prodId, buyCount);
//结束循环,执行抢购下单
break;
}
log.debug("------ 用户:{},购买商品:{},购买数量:{},锁库存成功失败,等待。。。------", userId, prodId, buyCount);
}
log.info("------------------------------ 已经获得库存锁,再次判断库存是否充足 -----------------------------------------");
//考虑高并发的场景:
//多人同时校验库存成功,但是进入到抢购下单业务时,库存呢只够部分人购买,需要再确定库存是否足够
//校验商品库存数量是否充足,可以进行后续抢购,日过不足,直接抢购失败
log.info("------ 用户:{},购买商品:{},购买数量:{},锁库存后(拿到库存锁后) 再次 库存校验 ------",userId, prodId, buyCount);
if(!seckillService.checkProdStockEnough(prodId,buyCount)){
//释放库存锁,后续用户继续尝试购买
//库存剩余2两,还有三个人买,库存不足,后续两个人各买一件
//释放库存锁
seckillService.unLockProdStock(prodId);
log.info("------ 用户:{},购买商品:{},购买数量:{},再次 库存校验,库存不足 ------", userId, prodId, buyCount);
//库存不足,返回抢购失败
log.info("------ 用户:{},购买商品:{},购买数量:{},抢购下单中,库存不足 ------", userId, prodId, buyCount);
return ResultBuildUtil.fail("605", "抢购失败,库存不足,缺货!");
}
log.info("------------------------------ 开始扣减库存 -----------------------------------------");
//执行扣减商品库存
log.info("------ 用户:{},购买商品:{},购买数量:{},扣减库存 ------", userId, prodId, buyCount);
seckillService.subProdStockCount(prodId,buyCount);
log.info("------------------------------ 开始下单 -----------------------------------------");
//开始调用订单中心的生成抢购订单接口,下单并返回给前端,抢购结果
//先模拟下单成功,返回一个抢购订单号
log.info("------ 用户:{},购买商品:{},购买数量:{},开始下单 ------", userId, prodId, buyCount);
// String seckillOrderNo = "SK"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
// + UUID.randomUUID().toString().substring(0,5);
String seckillOrder = seckillOrderFeignService.feignInvokeCreateSeckillOrder(userId, prodId, buyCount);
log.info("------------------------------ 下单成功 主动释放库存锁 -----------------------------------------");
//生成抢购订单成功,立刻释放库存锁,给其他抢购用户购买
log.info("------ 用户:{},购买商品:{},购买数量:{},下单成功,释放库存锁 ------", userId, prodId, buyCount);
seckillService.unLockProdStock(prodId);
log.info("------ 用户:{},购买商品:{},购买数量:{},抢购成功 ------", userId, prodId, buyCount);
//返回抢购成功,实际开发不能返回此种格式的数据
//必须使用key和value的返回,方便前端获取订单号
return ResultBuildUtil.success("抢购成功,抢购订单"+seckillOrder);
}
}
3.3 订单模块
3.3.1 application.yml
点击查看代码
# 端口
server:
port: 8107
# 服务名
spring:
application:
name: edocmall-seckill-order
# redis
redis:
host: 127.0.0.1
port: 6379
# RabbitMQ
rabbitmq:
host: 1.117.75.57
port: 5672
username: admin
password: admin
# eureka 注册中心的配置
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8096/eureka # 注册中心的地址
3.3.2 OrderMQDirectConfig
点击查看代码
/**
* Created On : 1/11/2022.
* <p>
* Author : huayu
* <p>
* Description: Direct直连模式,自动配置类,自动创建队列,交换机,并将队列绑定到交换机,指定唯一路由
*/
@Configuration
public class OrderMQDirectConfig {
/**
* @author : huayu
* @date : 1/11/2022
* @param : []
* @return : org.springframework.amqp.core.Queue
* @description : directQueue
*/
@Bean
public Queue directQueue(){
return new Queue(OrderMQConstant.SECKILL_SAVE_ORDER_QUEUE_KH96,true);
}
/**
* @author : huayu
* @date : 1/11/2022
* @param : []
* @return : org.springframework.amqp.core.DirectExchange
* @description : 创建直连交换机
*/
@Bean
public DirectExchange directExchange(){
// 创建支持持久化的直连交换机,指定交换机的名称
return new DirectExchange(OrderMQConstant.SECKILL_SAVE_ORDER_EXCHANGE_KH96U);
}
/**
* @author : huayu
* @date : 1/11/2022
* @param : []
* @return : org.springframework.amqp.core.Binding
* @description : 将直连队列和直连交换机进行绑定,并指定绑定的唯一路由键
*/
@Bean
public Binding directBinding(){
// 将直连队列和直连交换机进行绑定,并指定绑定的唯一路由键
return BindingBuilder.bind(directQueue())
.to(directExchange())
.with(OrderMQConstant.SECKILL_SAVE_ORDER_ROUTING_KH96);
}
}
3.3.3 OrderMQConstant
点击查看代码
/**
* Created On : 1/11/2022.
* <p>
* Author : huayu
* <p>
* Description: RabbitMQ 常量类,系统的所有队列名,交换机名,路由键名等,统一进行配置管理
*/
public class OrderMQConstant {
//========================== 直连模式
/**
* Direct直连模式 队列名
*/
public static final String SECKILL_SAVE_ORDER_QUEUE_KH96 ="seckill_save_order_queue_kh96";
/**
* Direct直连模式 交换机名
*/
public static final String SECKILL_SAVE_ORDER_EXCHANGE_KH96U ="seckill_save_order_exchange_kh96";
/**
* Direct直连模式 路由键
*/
public static final String SECKILL_SAVE_ORDER_ROUTING_KH96 ="seckill_save_order_routing_kh96";
//========================== 扇形模式
/**
* Fanout 扇形模式 队列名
*/
public static final String ACCOUNT_FANOUT_QUEUE_KH96 ="account_pay_result_queue_kh96";
}
3.3.4 下订单业务层
3.3.4.1 接口
点击查看代码
/**
* Created On : 10/11/2022.
* <p>
* Author : huayu
* <p>
* Description:
*/
public interface SeckillOrderService {
/**
* @author : huayu
* @date : 11/11/2022
* @param : [seckillOrder]
* @return : void
* @description : 生成秒杀订单
*/
void saveSeckillOrder(Map<String,Object> seckillOrder);
}
3.3.4.2实现类
点击查看代码
/**
* Created On : 10/11/2022.
* <p>
* Author : huayu
* <p>
* Description: SeckillOrderServiceImpl
*/
@Service
@Slf4j
public class SeckillOrderServiceImpl implements SeckillOrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RedisUtils redisUtils;
@Override
public void saveSeckillOrder(Map<String, Object> seckillOrder) {
//发送生成抢购订单的消息到消息队列,并在redis中添加此订单的记录,模拟交互
//0 正在生成
if(redisUtils.set(seckillOrder.get("seckillOrderNo").toString(),0)){
//发送生成订单的消息
rabbitTemplate.convertAndSend(OrderMQConstant.SECKILL_SAVE_ORDER_EXCHANGE_KH96U,
OrderMQConstant.SECKILL_SAVE_ORDER_ROUTING_KH96,
seckillOrder);
}
}
}
3.3.5 SeckillOrderSaveListener
点击查看代码
/**
* Created On : 1/11/2022.
* <p>
* Author : huayu
* <p>
* Description: Direct 直连模式消费者 One
*/
@Slf4j
@Component
//指定接听的 消息队列 名字
@RabbitListener(queues = OrderMQConstant.SECKILL_SAVE_ORDER_QUEUE_KH96)
public class SeckillOrderSaveListener {
@Autowired
private RedisUtils redisUtils;
/**
* @author : huayu
* @date : 1/11/2022
* @param : [directMsgJson]
* @return : void
* @description : Direct 直连模式消费者One,消费信息
*/
//指定消息队列中的消息,交给对应的方法处理
@RabbitHandler
public void saveSeckillOrder(Map<String,Object> seckillOrder){
log.info("***** 秒杀抢购订单:{},开始入库 ******",seckillOrder.get("seckillOrderNo"));
//TODO 将消息中的订单实体对象,调入业务接口,插入到数据库
redisUtils.set(seckillOrder.get("seckillOrderNo").toString(),1);
log.info("***** 秒杀抢购订单:{},入库成功 ******",seckillOrder.get("seckillOrderNo"));
}
}
3.3.6 下单 控制层
点击查看代码
/**
* Created On : 10/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 秒杀抢购订单入口
*/
@Slf4j
@RestController
public class SeckillOrderController {
@Autowired
private SeckillOrderService seckillOrderService;
/**
* @author : huayu
* @date : 10/11/2022
* @param : [userId, prodId, buyCount]
* @return : java.lang.String
* @description : 生成秒杀抢购订单
*/
@GetMapping("/createSeckillOrder")
public String createSeckillOrder(@RequestParam String userId,
@RequestParam String prodId,
@RequestParam Integer buyCount){
log.info("****** 用户:{},购买商品:{},购买数量:{},生成抢购订单 ******", userId, prodId, buyCount);
//TODO 必须要有参数校验,必须要有用户,商品信息的校验,确定用户是否正常,商品是否还在抢购中
//TODO 再次强调所有的中心模块,数据来源,不能信任外部接口来源的参数,都必须从数据库或者缓存中获取,尤其是跟金钱相关
//TODO 所有的接口必需要校验结束,通过获取的数据,封装订单实体对象,用户不关系订单的生成业务,可以使用异步消息队列,实现晓峰,并快速响应
//模拟生成一个抢购订单号,并封装成订单实体对象,通过map集合模拟
String seckillOrderNo = "SK"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
+ UUID.randomUUID().toString().substring(0,5);
//创建订单实体对象
Map<String, Object> seckillOrder = new HashMap<>();
seckillOrder.put("userId",userId);
seckillOrder.put("prodId",prodId);
seckillOrder.put("buyCount",buyCount);
//TODO 其他的订单属性封装,比如收货人,总价格,优惠,时间等
seckillOrder.put("seckillOrderNo",seckillOrderNo);
//发送到生成订单的消息队列中,使用异步处理
seckillOrderService.saveSeckillOrder(seckillOrder);
//返回抢购订单
return seckillOrder.toString();
}
}
3.4 查询订单状态和订单支付部分不再赘述
4、测试
4.1 初始化库存
4.2 抢购
4.2.1 抢购情况
4.2.2 redis中数据变化情况
4.3 查看订单详情
4.4 订单支付
支付的时候数注意穿透的路径;
SpringCloud(十一)- 秒杀 抢购的更多相关文章
- redis使用watch完成秒杀抢购功能
Redis使用watch完成秒杀抢购功能: 使用redis中两个key完成秒杀抢购功能,mywatchkey用于存储抢购数量和mywatchlist用户存储抢购列表. 它的优点如下: 1. 首先选用内 ...
- redis使用watch完成秒杀抢购功能(转)
redis使用watch完成秒杀抢购功能: 使用redis中两个key完成秒杀抢购功能,mywatchkey用于存储抢购数量和mywatchlist用户存储抢购列表. 它的优点如下: 1. 首先选用内 ...
- redis使用watch完成秒杀抢购功能:
redis使用watch完成秒杀抢购功能: 使用redis中两个key完成秒杀抢购功能,mywatchkey用于存储抢购数量和mywatchlist用户存储抢购列表. 它的优点如下: 1. 首先选用内 ...
- zookeeper实现商品秒杀抢购
package com.test; import java.io.IOException; import java.util.List; import java.util.concurrent.Cyc ...
- springcloud(十一):熔断聚合监控Hystrix Turbine
springcloud(十一):熔断聚合监控Hystrix Turbine
- php结合Redis实现高并发下的秒杀抢购功能
实现思路 准备两个队列A和B,假设A队列的名称为stock,用于存放商品总库存信息,B队列的名称为users,用于存放抢购成功后的用户信息.每当有用户进行抢购操作时,先从A队列弹出一个元素,如果该元素 ...
- 2020-04-29:现在你有个秒杀抢购的app,用户不断大量增加,技术层面,你要怎么做
2020-04-29:现在你有个秒杀抢购的app,用户不断大量增加,技术层面,你要怎么做,才能既满足用户需求,又能扛住压力,还能帮公司合理支出?福哥答案2020-04-29: 限流(杀部分用户祭天). ...
- 利用 JS 脚本实现网页全自动秒杀抢购
利用 JS 脚本实现网页全自动秒杀抢购 倒计时页面: 倒计时未结束时,购买按钮还不能点击. 结束时,可以点击购买,点击后出现提示"付款成功" 展示效果 1.制作测试网页 首先我们来 ...
- 汽车XX网站秒杀抢购代码
.../////完整抢购代码某网站最近在举办半价秒杀 其实有技巧的 首先可以添加代码 //自动监视回车,直接回车提交document.onkeydown=function(e){var theEven ...
- php如何应对秒杀抢购高并发思路
我们常用QPS(Query Per Second,每秒处理请求数)来衡量一个web应用的吞吐率,解决每秒数万次的高并发场景,这个指标非常关键. 举个栗子:假设一个业务请求平均为100ms,同时系统内有 ...
随机推荐
- 尝试阅读理解一份linux shell脚本
以下内容为本人的学习笔记,如需要转载,请声明原文链接微信公众号「englyf」https://www.cnblogs.com/englyf/p/16721350.html 从头一二去阅读语法和命令说明 ...
- ProxySQL 防火墙白名单
ProxySQL 2.0.9 引入了防火墙功能. 在从早期版本版本中,可以通过设置查询规则来创建要阻止的黑名单,或者定义通用规则,实现白名单功能. 但是,如果面对的系统有非常多而且操作内容也不同,这时 ...
- docker 生成mysql镜像启动时自动执行sql
文章转载自:https://www.jianshu.com/p/12fc253fa37d 在docker 创建 mysql 容器时,往往需要在创建容器的过程中创建database 实例,代码如下: # ...
- logstash 读取MySQL数据到elasticsearch 相差8小时解决办法
logstash和elasticsearch是按照UTC时间的,kibana却是按照正常你所在的时区显示的,是因为kibana中可以配置时区信息. 具体看这个: logstash 的配置文件添加 fi ...
- 打印 Logger 日志时,需不需要再封装一下工具类?
在开发过程中,打印日志是必不可少的,因为日志关乎于应用的问题排查.应用监控等.现在打印日志一般都是使用 slf4j,因为使用日志门面,有助于打印方式统一,即使后面更换日志框架,也非常方便.在 < ...
- 企业运维 | MySQL关系型数据库在Docker与Kubernetes容器环境中快速搭建部署主从实践
[点击 关注「 WeiyiGeek」公众号 ] 设为「️ 星标」每天带你玩转网络安全运维.应用开发.物联网IOT学习! 希望各位看友[关注.点赞.评论.收藏.投币],助力每一个梦想. 本章目录 目录 ...
- P3250 [HNOI2016] 网络 (树剖+堆/整体二分+树上差分+树状数组)
解法1: 本题有插入路径和删除路径,在每个节点维护插入堆和删除堆,查询时两者top一样则一直弹出.如果每个节点维护的是经过他的路径,显然有些不好处理,正难则反,每个点维护不经过他的路径,那么x节点出了 ...
- FastJson序列化对象复杂时出错问题解决
FastJson序列化对象复杂时出错问题解决 针对复杂的对象,如Map<String, List<Map<String, XxxObject<A, B, C>>&g ...
- C++编程范式(函数)
1 // 2 // main.cpp 3 // test 4 // 5 // Created by Shaojun on 30/5/2020. 6 // Copyright 2020 Shaojun. ...
- mysql 判断 字段为空 的一个小误区(又忘了)
今天判断mysql是否为空 直接写某字段 例 image_url !=null 结果数据库不报错误 并且没有返回相对数据. 又忘了这个事.今天特地记录一下. 因为null 表示什么也不是, 不能= ...