20

Java秒杀系统:商品秒杀代码实战

 5 years ago
source link: https://www.tuicool.com/articles/JrYZ3mi
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

内容:

“商品秒杀”功能模块是建立在“商品详情”功能模块的基础之上,对于这一功能模块而言,其主要的核心流程在于:前端发起抢购请求,该请求将携带着一些请求数据:待秒杀Id跟当前用户Id等数据;后端接口在接收到请求之后,将执行一系列的判断与秒杀处理逻辑,最终将处理结果返回给到前端。

YJfiEjn.jpg!web

其中,后端接口的这一系列判断与秒杀处理逻辑还是挺复杂的,Debug将其绘制成了如下的流程图:

eMZ7ZbY.jpg!web

从该业务流程图中可以看出,后端接口在接收前端用户的秒杀请求时,其核心处理逻辑为:

(1)首先判断当前用户是否已经抢购过该商品了,如果否,则代表用户没有抢购过该商品,可以进入下一步的处理逻辑

(2)判断该商品可抢的剩余数量,即库存是否充足(即是否大于0),如果是,则进入下一步的处理逻辑

(3)扣减库存,并更新数据库的中对应抢购记录的库存(一般是减一操作),判断更新库存的数据库操作是否成功了,如果是,则创建用户秒杀成功的订单,并异步发送短信或者邮件通知信息通知用户

(4)以上的操作逻辑如果有任何一步是不满足条件的,则直接结束整个秒杀的流程,即秒杀失败!

接下来,我们仍然基于MVC的开发模式,采用代码实战实现这一功能模块!

(1)首先是在KillController 控制器开发接收“前端用户秒杀请求”的功能方法,其中,该方法需要接收前端请求过来的“待秒杀Id”,而当前用户的Id可以通过上一篇博文介绍的Shiro 的会话模块Session进行获取!

其源代码如下所示:

private static final String prefix = "kill"; 
  
@Autowired 
private IKillService killService; 
  
@Autowired 
private ItemKillSuccessMapper itemKillSuccessMapper; 
  
/*** 
 * 商品秒杀核心业务逻辑 
 */ 
@RequestMapping(value = prefix+"/execute",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) 
@ResponseBody 
public BaseResponse execute(@RequestBody @Validated KillDto dto, BindingResult result, HttpSession session){ 
 if (result.hasErrors() || dto.getKillId()<=0){ 
 return new BaseResponse(StatusCode.InvalidParams); 
} 
//获取当前登录用户的信息 
 Object uId=session.getAttribute("uid"); 
 if (uId==null){ 
 return new BaseResponse(StatusCode.UserNotLogin); 
 } 
 Integer userId= (Integer)uId ; 
 BaseResponse response=new BaseResponse(StatusCode.Success); 
 try { 
 Boolean res=killService.killItem(dto.getKillId(),userId); 
 if (!res){ 
 return new BaseResponse(StatusCode.Fail.getCode(),"哈哈~商品已抢购完毕或者不在抢购时间段哦!"); 
 } 
 }catch (Exception e){ 
 response=new BaseResponse(StatusCode.Fail.getCode(),e.getMessage()); 
 } 
 return response; 
}复制代码 

其中,KillDto对象主要封装了“待秒杀Id”等字段信息,其主要用于接收前端过来的用户秒杀请求信息,源代码如下所示:

@Data 
@ToString 
public class KillDto implements Serializable{ 
 @NotNull 
 private Integer killId; 
  
 private Integer userId; //在整合shiro之后,userId字段可以不需要了!因为通过session进行获取了 
}复制代码 

(2)紧接着是开发 killService.killItem(dto.getKillId(),userId) 的功能,该功能对应的代码的编写逻辑可以参见本文刚开始介绍时的流程图!其完整源代码如下所示:

@Autowired 
private ItemKillSuccessMapper itemKillSuccessMapper; 
  
@Autowired 
private ItemKillMapper itemKillMapper; 
  
@Autowired 
private RabbitSenderService rabbitSenderService; 
  
//商品秒杀核心业务逻辑的处理 
@Override 
public Boolean killItem(Integer killId, Integer userId) throws Exception { 
 Boolean result=false; 
  
 //TODO:判断当前用户是否已经抢购过当前商品 
 if (itemKillSuccessMapper.countByKillUserId(killId,userId) <= 0){ 
 //TODO:查询待秒杀商品详情 
 ItemKill itemKill=itemKillMapper.selectById(killId); 
  
 //TODO:判断是否可以被秒杀canKill=1? 
 if (itemKill!=null && 1==itemKill.getCanKill() ){ 
 //TODO:扣减库存-减一 
 int res=itemKillMapper.updateKillItem(killId); 
  
 //TODO:扣减是否成功?是-生成秒杀成功的订单,同时通知用户秒杀成功的消息 
 if (res>0){ 
 commonRecordKillSuccessInfo(itemKill,userId); 
  
 result=true; 
 } 
 } 
 }else{ 
 throw new Exception("您已经抢购过该商品了!"); 
 } 
 return result; 
}复制代码 

其中,itemKillMapper.selectById(killId); 表示用于获取待秒杀商品的详情信息,这在前面的篇章中已经介绍过了;而 itemKillMapper.updateKillItem(killId); 主要用于扣减库存(在这里是减1操作),其对应的动态Sql如下所示:

<!--抢购商品,剩余数量减一--> 
 <update id="updateKillItem"> 
 UPDATE item_kill 
 SET total = total - 1 
 WHERE 
 id = #{killId} 
 </update>复制代码 

(3)值得一提的是,在上面 KillService执行killItem功能方法时,还开发了一个通用的方法:用户秒杀成功后创建秒杀订单、并异步发送通知消息给到用户秒杀成功的信息!该方法为 commonRecordKillSuccessInfo(itemKill,userId); 其完整的源代码如下所示:

/** 
 * 通用的方法-用户秒杀成功后创建订单-并进行异步邮件消息的通知 
 * @param kill 
 * @param userId 
 * @throws Exception 
 */ 
private void commonRecordKillSuccessInfo(ItemKill kill, Integer userId) throws Exception{ 
 //TODO:记录抢购成功后生成的秒杀订单记录 
  
 ItemKillSuccess entity=new ItemKillSuccess(); 
 String orderNo=String.valueOf(snowFlake.nextId()); 
  
 //entity.setCode(RandomUtil.generateOrderCode()); //传统时间戳+N位随机数 
 entity.setCode(orderNo); //雪花算法 
 entity.setItemId(kill.getItemId()); 
 entity.setKillId(kill.getId()); 
 entity.setUserId(userId.toString()); 
 entity.setStatus(SysConstant.OrderStatus.SuccessNotPayed.getCode().byteValue()); 
 entity.setCreateTime(DateTime.now().toDate()); 
 //TODO:学以致用,举一反三 -> 仿照单例模式的双重检验锁写法 
 if (itemKillSuccessMapper.countByKillUserId(kill.getId(),userId) <= 0){ 
 int res=itemKillSuccessMapper.insertSelective(entity); 
  
 if (res>0){ 
 //TODO:进行异步邮件消息的通知=rabbitmq+mail 
 rabbitSenderService.sendKillSuccessEmailMsg(orderNo); 
  
 //TODO:入死信队列,用于 “失效” 超过指定的TTL时间时仍然未支付的订单 
 rabbitSenderService.sendKillSuccessOrderExpireMsg(orderNo); 
 } 
 } 
}复制代码 

该方法涉及的功能模块稍微比较多,即主要包含了“分布式唯一ID-雪花算法的应用”、“整合RabbitMQ异步发送通知消息给用户”、“基于JavaMail开发发送邮件的功能”、“死信队列失效超时未支付的订单”等等,这些功能模块将在后面的小节一步一步展开进行介绍!

(4)最后是需要在前端页面info.jsp开发“提交用户秒杀请求”的功能,其部分核心源代码如下所示:

JnArMbM.jpg!web

其中,提交的数据是采用application/json的格式提交的,即json的格式!并采用POST的请求方法进行交互!

(5)将整个系统、项目采用外置的tomcat运行起来,观察控制台的输出信息,如果没有报错信息,则代表整体的实战代码没有语法级别的错误!点击“详情”按钮,登录成功后,进入“待秒杀商品的的详情”,可以查看当前待秒杀商品的详情信息;点击“抢购”按钮,即可进入“秒杀”环节,后端经过一系列的逻辑处理之后,将处理的结果返回给到前端,如下图所示:

yInUBzE.jpg!web

与此同时,当前用户的邮箱中将收到一条“秒杀成功”的邮件信息,表示当前用户已经成功秒杀抢到当前商品了,如下图所示:

VFvyIbu.jpg!web

除此之外,在数据库表item_kill_success中也将会生成一笔“秒杀成功的订单记录”,如下图所示:

emYVriv.jpg!web

当然,对于“邮件的通知”和“秒杀成功生成的订单的订单编号”的功能,在本节我们主要是分享介绍了秒杀系统中用户的“秒杀/抢购请求”功能!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK