java实现秒杀系统_java并发编程学习19--基于springboot的秒杀系统实现1--项目介绍...

【秒杀系统业务分析

在秒杀系统当中有两个核心的表:秒杀商品(kill_product)与秒杀明细(kill_item),具体的逻辑是一个用户秒杀商品的库存减一,秒杀明细的记录增加一条。这两步作是处于同一事务之中。

当秒杀日期尚未达到会提示用户秒杀尚未开始;

当用户多次秒杀同一商品会提示用户重复秒杀;

当秒杀日期过期或者秒杀商品的库存为零会提示用户秒杀结束。

【秒杀项目结构

bd42b93dbf8203541cf1cbd3a3608d2e.png

java目录下

web:controller以及rest接口

applicationService:所有的写操作业务逻辑接口

queryService:所有的读操作业务逻辑接口

dao:数据传输层包括:mysql以及redis

common:所有的常量以及枚举

aop:针对request进行拦截,在日志中打印每个接口耗时毫秒值

configuration:所有的配置信息

exception:所有的业务异常

dto:数据传输对象

resources目录下

static:存放静态资源:javascript,css,图片

template:H5模板,我们的项目采用的是Thymeleaf

application.properties:通用的配置信息

application-*.properties:根据环境不同而不同的配置信息,比如开发环境数据库地址

test目录下

单元测试代码

【Entity设计

秒杀商品实体:注意一下:product_id只是用于表示秒杀商品是属于哪一个实体商品,本项目不会用到该字段

import lombok.Data;

import javax.persistence.Column;

import javax.persistence.Entity;

import javax.persistence.Id;

import javax.persistence.Table;

import java.util.Date;

/**

* 秒杀产品实体类

* @author ibm

* @since 0

* @date 2018/3/22

*/

@Entity

@Table(name = "kill_product")

@Data

public class KillProduct {

/**

* ID

*/

@Id

@Column(name = "id")

private String id;

/**

* 产品ID

*/

@Column(name = "product_id")

private String productId;

/**

* 秒杀描述信息

*/

@Column(name = "kill_description")

private String killDescription;

/**

* 库存数量

*/

@Column(name = "number")

private String number;

/**

* 秒杀开始时间

*/

@Column(name = "start_time")

private Date startTime;

/**

* 秒杀结束时间

*/

@Column(name = "end_time")

private Date endTime;

}

秒杀明细实体:记录一次成功的秒杀,类上关于Procedure的注解是为了提供高并发调用存储过程支持而加入的。

import lombok.Data;

import javax.persistence.*;

import java.util.Date;

/**

* 秒杀明细实体类

* @author ibm

* @since 0

* @date 2018/3/22

*/

@Entity

@Table(name = "kill_item")

@NamedStoredProcedureQuery(name = "executeSeckill", procedureName = "execute_seckill", parameters = {

@StoredProcedureParameter(mode = ParameterMode.IN, name = "v_id", type = String.class),

@StoredProcedureParameter(mode = ParameterMode.IN, name = "v_kill_product_id", type = String.class),

@StoredProcedureParameter(mode = ParameterMode.IN, name = "v_mobile", type = Long.class),

@StoredProcedureParameter(mode = ParameterMode.IN, name = "v_kill_time", type = Date.class),

@StoredProcedureParameter(mode = ParameterMode.OUT, name = "r_result", type = Integer.class) })

@Data

public class KillItem {

/**

* 记录ID

*/

@Id

@Column(name = "id")

private String id;

/**

* 秒杀产品id

*/

@Column(name = "kill_product_id")

private String killProductId;

/**

* 用户手机号码

*/

@Column(name = "mobile")

private String mobile;

/**

* 秒杀成功时间

*/

@Column(name = "kill_time")

private Date killTime;

}

【JPA设计

秒杀商品的JPA的核心方法就是修改库存

import com.example.seckill.dao.entity.KillProduct;

import org.springframework.data.jpa.repository.JpaRepository;

import org.springframework.data.jpa.repository.Modifying;

import org.springframework.data.jpa.repository.Query;

import java.util.Date;

import java.util.List;

/**

* @author ibm

* @since 0

* @date 2018/3/22

*/

public interface KillProductJpaRepo extends JpaRepository{

/**

* 查看可以开始秒杀商品

* @param now 开始时间点

* @return 秒杀商品明细

*/

List findAllByStartTimeAfter(Date now);

/**

* 减少库存,库存等于0就不再减少

* @param id 秒杀商品id

* @param time 执行秒杀的时间

* @return 执行的行数

*/

@Modifying

@Query(value = "UPDATE kill_product SET number = number - 1 WHERE id = ?1 AND number >= 1 AND end_time > ?2",

nativeQuery = true)

int reduceNumber(String id,Date time);

}

秒杀明细的JPA核心就是增加一条成功秒杀的明细,这里还会提供一个针对存储过程调用的方法

import com.example.seckill.dao.entity.KillItem;

import org.springframework.data.jpa.repository.JpaRepository;

import org.springframework.data.jpa.repository.Modifying;

import org.springframework.data.jpa.repository.Query;

import org.springframework.data.jpa.repository.query.Procedure;

import org.springframework.data.repository.query.Param;

import java.util.Date;

import java.util.List;

/**

* @author ibm

* @since 0

* @date 2018/3/22

*/

public interface KillItemJpaRepo extends JpaRepository {

/**

* 查看秒杀商品的秒杀记录

* @param killProductId 秒杀商品Id

* @return 秒杀记录详情

*/

List findAllByKillProductIdOrderByKillTimeDesc(String killProductId);

/**

* 保存秒杀记录

* @param id 预生成的主键

* @param killProductId 秒杀商品id

* @param mobile 执行秒杀用户手机号

* @return 执行的行数

*/

@Modifying

@Query(value = "INSERT IGNORE INTO kill_item(id,kill_product_id,mobile) values(?1,?2,?3)",

nativeQuery = true)

int insertKillItem(String id,String killProductId,long mobile);

@Procedure(procedureName = "execute_seckill")

int executeProcedure(@Param("v_id")String killItemId,

@Param("v_kill_product_id")String killProductId,

@Param("v_mobile")long mobile,

@Param("v_kill_time")Date killTime);

}

【applicationService设计

applicationService会提供两个方法一个是将事务交个spring控制的方式,另个一个是将事务直接交给MySQL控制的,而高并发一个重要的优化点就是减少行级锁的持有时间,而有效的方式就是取消spring提供的声明式事务,将事务完全交个MySQL,这样网络延迟与GC的时间都可以得到节约。并且我们也需要在提供了秒杀地址的时候,返回一个md5的加密数据,保证秒杀不会被篡改数据。

import com.example.seckill.applicationService.ISecKillApplicationService;

import com.example.seckill.common.status.KillStatus;

import com.example.seckill.common.utils.IdUtil;

import com.example.seckill.common.utils.Md5Util;

import com.example.seckill.configuration.cache.RedisCacheName;

import com.example.seckill.dao.entity.KillItem;

import com.example.seckill.dao.repository.KillItemJpaRepo;

import com.example.seckill.dao.repository.KillProductJpaRepo;

import com.example.seckill.dto.Execution;

import com.example.seckill.exception.KillClosedException;

import com.example.seckill.exception.RepeatKillException;

import com.example.seckill.exception.SecKillException;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.cache.annotation.CacheConfig;

import org.springframework.cache.annotation.CacheEvict;

import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Transactional;

import org.springframework.util.StringUtils;

import java.util.Date;

/**

* @author ibm

*/

@CacheConfig(cacheNames = RedisCacheName.KILL_PRODUCT)

@Service

public class SecKillApplicationServiceImpl implements ISecKillApplicationService{

@Autowired

private KillProductJpaRepo killProductJpaRepo;

@Autowired

private KillItemJpaRepo killItemJpaRepo;

@Override

@CacheEvict(keyGenerator = "keyGenerator")

@Transactional(rollbackFor = RuntimeException.class)

public Execution executeSecKill(String killProductId, long mobile, String md5) throws SecKillException, RepeatKillException, KillClosedException {

if(StringUtils.isEmpty(md5) || !md5.equals(Md5Util.getMd5(killProductId))){

throw new SecKillException(KillStatus.REWRITE.getInfo());

}

//执行秒杀逻辑:减库存 + 插入秒杀明细

try{

Date now = new Date();

int updateCount = killProductJpaRepo.reduceNumber(killProductId,now);

if(updateCount <= 0){

throw new KillClosedException(KillStatus.END.getInfo());

}else {

//记录秒杀明细

String itemId = IdUtil.getObjectId();

int insertCount = killItemJpaRepo.insertKillItem(itemId,killProductId,mobile);

if(insertCount <= 0){

throw new RepeatKillException(KillStatus.REPEAT_KILL.getInfo());

}else {

KillItem killItem = killItemJpaRepo.findById(itemId).get();

return new Execution(killProductId, KillStatus.SUCCESS,killItem);

}

}

}catch (RepeatKillException e1){

throw e1;

}catch (KillClosedException e2){

throw e2;

}catch (Exception e){

throw new SecKillException(KillStatus.INNER_ERROR.getInfo());

}

}

@Override

public Execution executeSecKillProcedure(String killProductId, long mobile, String md5){

if(StringUtils.isEmpty(md5) || !md5.equals(Md5Util.getMd5(killProductId))){

throw new SecKillException(KillStatus.REWRITE.getInfo());

}

String itemId = IdUtil.getObjectId();

int reuslt = killItemJpaRepo.executeProcedure(itemId,killProductId,mobile,new Date());

if(KillStatus.SUCCESS.getValue() == reuslt){

KillItem killItem = killItemJpaRepo.findById(itemId).get();

return new Execution(killProductId, KillStatus.SUCCESS,killItem);

}else if(KillStatus.REPEAT_KILL.getValue() == reuslt){

throw new RepeatKillException(KillStatus.REPEAT_KILL.getInfo());

}else if(KillStatus.END.getValue() == reuslt){

throw new KillClosedException(KillStatus.END.getInfo());

}else {

throw new SecKillException(KillStatus.INNER_ERROR.getInfo());

}

}

}

【rest设计

提供的接口:

秒杀列表,使用Thymeleaf模板返回

秒杀详情,使用Thymeleaf模板返回

获取秒杀地址与md5(Ajax),使用json返回

获取系统时间,使用json返回

执行秒杀(Ajax),使用json返回

import com.example.seckill.applicationService.ISecKillApplicationService;

import com.example.seckill.dao.entity.KillProduct;

import com.example.seckill.dto.Execution;

import com.example.seckill.dto.Exposer;

import com.example.seckill.exception.SecKillException;

import com.example.seckill.queryService.ISecKillQueryService;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.ui.Model;

import org.springframework.util.StringUtils;

import org.springframework.web.bind.annotation.*;

import java.util.List;

import java.util.Optional;

/**

* 秒杀相关web接口

* @author ibm

* @since 0

* @date 2018/3/22

*/

@Controller

@RequestMapping("/secKill")

public class SecKillRest {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

private final ISecKillQueryService secKillQueryService;

private final ISecKillApplicationService secKillApplicationService;

@Autowired

public SecKillRest(ISecKillQueryService secKillQueryService,ISecKillApplicationService secKillApplicationService){

this.secKillQueryService = secKillQueryService;

this.secKillApplicationService = secKillApplicationService;

}

/**

* 秒杀列表页

* @param model 封装返回对象使用

* @return 列表页视图

*/

@GetMapping("/list")

public String getList(Model model){

List list = secKillQueryService.getKillProductList();

model.addAttribute("list",list);

return "/list";

}

/**

* 秒杀详情页

* @param killProductId 秒杀商品Id

* @param model 封装返回对象使用

* @return 详情页视图

*/

@GetMapping("/{killProductId}/detail")

public String getDetail(@PathVariable("killProductId")String killProductId, Model model){

if(StringUtils.isEmpty(killProductId)){

return "redirect:/secKill/list";

}

Optional killProductOptional = secKillQueryService.getKillProductById(killProductId);

if(!killProductOptional.isPresent()){

return "forward:/secKill/list";

}

KillProduct killProduct = killProductOptional.get();

model.addAttribute("killProduct",killProduct);

return "detail";

}

/**

* 查看秒杀商品是否暴露

* @param killProductId 秒杀商品Id

* @return 是否暴露

*/

@PostMapping("/{killProductId}/expose")

@ResponseBody

public Exposer expose(@PathVariable("killProductId") String killProductId){

return secKillQueryService.exportSecKillUrl(killProductId);

}

/**

* 执行秒杀

* @param killProductId 秒杀商品Id

* @param md5 加密值

* @param mobile 用户登陆手机号

* @return 秒杀结果

*/

@PostMapping("/{killProductId}/{md5}/execute")

@ResponseBody

public Execution execute(@PathVariable("killProductId") String killProductId,

@PathVariable("md5")String md5,

@CookieValue("killPhone") Long mobile){

if(mobile == null){

throw new SecKillException("用户未登录");

}

return secKillApplicationService.executeSecKillProcedure(killProductId,mobile,md5);

}

/**

* 获取当前系统时间

* @return

*/

@GetMapping("/time/now")

@ResponseBody

public Long time(){

return System.currentTimeMillis();

}

}

【项目效果

秒杀列表

8b47deb5cb7f89b475533838bdab61bd.png

秒杀详情

e184b511211744b030fc4971a2265499.png

7d3550a82f9ccb571f8ca029446ab881.png

秒杀成功

ee2291d52321b455430f3fa2a1d9ffa1.png

【项目地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值