在秒杀系统当中有两个核心的表:秒杀商品(kill_product)与秒杀明细(kill_item),具体的逻辑是一个用户秒杀商品的库存减一,秒杀明细的记录增长一条。这两步做是处于同一事务之中。javascript
java目录下css
resources目录下java
test目录下mysql
秒杀商品实体:注意一下:product_id只是用于表示秒杀商品是属于哪个实体商品,本项目不会用到该字段git
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的注解是为了提供高并发调用存储过程支持而加入的。github
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的核心方法就是修改库存web
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核心就是增长一条成功秒杀的明细,这里还会提供一个针对存储过程调用的方法redis
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会提供两个方法一个是将事务交个spring控制的方式,另个一个是将事务直接交给MySQL控制的,而高并发一个重要的优化点就是减小行级锁的持有时间,而有效的方式就是取消spring提供的声明式事务,将事务彻底交个MySQL,这样网络延迟与GC的时间均可以获得节约。而且咱们也须要在提供了秒杀地址的时候,返回一个md5的加密数据,保证秒杀不会被篡改数据。spring
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()); } } }
提供的接口:sql
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(); } }
秒杀列表
秒杀详情
秒杀成功