【秒杀系统业务分析
在秒杀系统当中有两个核心的表:秒杀商品(kill_product)与秒杀明细(kill_item),具体的逻辑是一个用户秒杀商品的库存减一,秒杀明细的记录增加一条。这两步作是处于同一事务之中。
- 当秒杀日期尚未达到会提示用户秒杀尚未开始;
- 当用户多次秒杀同一商品会提示用户重复秒杀;
- 当秒杀日期过期或者秒杀商品的库存为零会提示用户秒杀结束。
【秒杀项目结构
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<KillProduct,String>{
/**
* 查看可以开始秒杀商品
* @param now 开始时间点
* @return 秒杀商品明细
*/
List<KillProduct> 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<KillItem,String> {
/**
* 查看秒杀商品的秒杀记录
* @param killProductId 秒杀商品Id
* @return 秒杀记录详情
*/
List<KillItem> 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<KillProduct> 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<KillProduct> 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();
}
}
【项目效果
秒杀列表
秒杀详情
秒杀成功
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。