Java 实现高并发秒杀

1 需求分析和技术难点:

(1) 分析:

     秒杀的时候:减少库存和购买记录明细两个事件保持在同一个事物中。

     使用联合查询避免同一用户多次秒杀同一商品(利用在插入购物明细表中的秒杀id和用户的唯一标识来避免)。

(2) 秒杀难点:事务和行级锁的处理

(3) 实现那些秒杀系统(以天猫的秒杀系统为例)

(4) 我们如何实现秒杀功能?

     ① 秒杀接口暴漏

     ② 执行秒杀

     ③ 相关查询

     下面我们以主要代码实现秒杀系统:

 

2.数据库设计和DAO层

(1) 数据库设计

 
  1. -- 数据库初始化脚本

  2.  
  3. -- 创建数据库

  4. CREATE DATABASE seckill;

  5. -- 使用数据库

  6. use seckill;

  7. CREATE TABLE seckill(

  8. `seckill_id` BIGINT NOT NUll AUTO_INCREMENT COMMENT '商品库存ID',

  9. `name` VARCHAR(120) NOT NULL COMMENT '商品名称',

  10. `number` int NOT NULL COMMENT '库存数量',

  11. `start_time` TIMESTAMP NOT NULL COMMENT '秒杀开始时间',

  12. `end_time` TIMESTAMP NOT NULL COMMENT '秒杀结束时间',

  13. `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

  14. PRIMARY KEY (seckill_id),

  15. key idx_start_time(start_time),

  16. key idx_end_time(end_time),

  17. key idx_create_time(create_time)

  18. )ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表';

  19.  
  20. -- 初始化数据

  21. INSERT into seckill(name,number,start_time,end_time)

  22. VALUES

  23. ('1000元秒杀iphone6',100,'2016-01-01 00:00:00','2016-01-02 00:00:00'),

  24. ('800元秒杀ipad',200,'2016-01-01 00:00:00','2016-01-02 00:00:00'),

  25. ('6600元秒杀mac book pro',300,'2016-01-01 00:00:00','2016-01-02 00:00:00'),

  26. ('7000元秒杀iMac',400,'2016-01-01 00:00:00','2016-01-02 00:00:00');

  27.  
  28. -- 秒杀成功明细表

  29. -- 用户登录认证相关信息(简化为手机号)

  30. CREATE TABLE success_killed(

  31. `seckill_id` BIGINT NOT NULL COMMENT '秒杀商品ID',

  32. `user_phone` BIGINT NOT NULL COMMENT '用户手机号',

  33. `state` TINYINT NOT NULL DEFAULT -1 COMMENT '状态标识:-1:无效 0:成功 1:已付款 2:已发货',

  34. `create_time` TIMESTAMP NOT NULL COMMENT '创建时间',

  35. PRIMARY KEY(seckill_id,user_phone),/*联合主键*/

  36. KEY idx_create_time(create_time)

  37. )ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';

  38.  
  39. -- SHOW CREATE TABLE seckill;#显示表的创建信息

 

(2) Dao层和对应的实体

① Seckill.java

 
  1. package com.force4us.entity;

  2. import org.springframework.stereotype.Component;

  3. import java.util.Date;

  4.  
  5. public class Seckill {

  6. private long seckillId;

  7. private String name;

  8. private int number;

  9. private Date startTime;

  10. private Date endTime;

  11. private Date createTime;

  12. public long getSeckillId() {

  13. return seckillId;

  14. }

  15. public void setSeckillId(long seckillId) {

  16. this.seckillId = seckillId;

  17. }

  18. public String getName() {

  19. return name;

  20. }

  21. public void setName(String name) {

  22. this.name = name;

  23. }

  24. public int getNumber() {

  25. return number;

  26. }

  27. public void setNumber(int number) {

  28. this.number = number;

  29. }

  30. public Date getStartTime() {

  31. return startTime;

  32. }

  33. public void setStartTime(Date startTime) {

  34. this.startTime = startTime;

  35. }

  36. public Date getEndTime() {

  37. return endTime;

  38. }

  39. public void setEndTime(Date endTime) {

  40. this.endTime = endTime;

  41. }

  42.  
  43. public Date getCreateTime() {

  44. return createTime;

  45. }

  46. public void setCreateTime(Date createTime) {

  47. this.createTime = createTime;

  48. }

  49. @Override

  50. public String toString() {

  51. return "Seckill{" +

  52. "seckillId=" + seckillId +

  53. ", name='" + name + '\'' +

  54. ", number=" + number +

  55. ", startTime=" + startTime +

  56. ", endTime=" + endTime +

  57. ", createTime=" + createTime +

  58. '}';

  59. }

  60. }

②  SuccessKilled.java

 
  1. package com.force4us.entity;

  2. import org.springframework.stereotype.Component;

  3. import java.util.Date;

  4. public class SuccessKilled {

  5. private long seckillId;

  6. private long userPhone;

  7. private short state;

  8. private Date createTime;

  9. private Seckill seckill;

  10. public long getSeckillId() {

  11. return seckillId;

  12. }

  13. public void setSeckillId(long seckillId) {

  14. this.seckillId = seckillId;

  15. }

  16. public long getUserPhone() {

  17. return userPhone;

  18. }

  19. public void setUserPhone(long userPhone) {

  20. this.userPhone = userPhone;

  21. }

  22. public short getState() {

  23. return state;

  24. }

  25. public void setState(short state) {

  26. this.state = state;

  27. }

  28. public Date getCreateTime() {

  29. return createTime;

  30. }

  31. public void setCreateTime(Date createTime) {

  32. this.createTime = createTime;

  33. }

  34. public Seckill getSeckill() {

  35. return seckill;

  36. }

  37. public void setSeckill(Seckill seckill) {

  38. this.seckill = seckill;

  39. }

  40. @Override

  41. public String toString() {

  42. return "SuccessKilled{" +

  43. "seckillId=" + seckillId +

  44. ", userPhone=" + userPhone +

  45. ", state=" + state +

  46. ", createTime=" + createTime +

  47. ", seckill=" + seckill +

  48. '}';

  49. }

  50. }

③  SeckillDao

 
  1. package com.force4us.dao;

  2. import com.force4us.entity.Seckill;

  3. import org.apache.ibatis.annotations.Param;

  4. import java.util.Date;

  5. import java.util.List;

  6. import java.util.Map;

  7.  
  8. public interface SeckillDao {

  9. /**

  10. * 减库存

  11. * @param seckillId

  12. * @param killTime

  13. * @return 如果影响行数>1,表示更新库存的记录行数

  14. */

  15. int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime);

  16. /**

  17. * 根据id查询秒杀的商品信息

  18. * @param seckillId

  19. * @return

  20. */

  21. Seckill queryById(@Param("seckillId") long seckillId);

  22. /**

  23. * 根据偏移量查询秒杀商品列表

  24. * @param offset

  25. * @param limit

  26. * @return

  27. */

  28. List queryAll(@Param("offset") int offset, @Param("limit") int limit);

  29. void killByProcedure(Map paramMap);

  30. }

④  SuccessKilledDao

 
  1. package com.force4us.dao;

  2. import com.force4us.entity.SuccessKilled;

  3. import org.apache.ibatis.annotations.Param;

  4. public interface SuccessKilledDao {

  5. /**

  6. * 插入购买明细,可过滤重复

  7. * @param seckillId

  8. * @param userPhone

  9. * @return 插入的行数

  10. */

  11. int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);

  12. /**

  13. * 根据秒杀商品ID查询明细SuccessKilled对象, 携带了Seckill秒杀产品对象

  14. * @param seckillId

  15. * @param userPhone

  16. * @return

  17. */

  18. SuccessKilled queryByIdWithSeckill(@Param("seckillId") long , @Param("userPhone") long userPhone);

  19. }

⑤ mybatis配置文件:

 
  1. PUBLIC "-//mybatis.org//DTD Config 3.0//EN"

  2. "http://mybatis.org/dtd/mybatis-3-config.dtd">

  3.  
  4.  
  5.  
  6.  

⑥ SeckillDao.xml

 
  1. PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"

  2. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

  3.  
  4. UPDATE seckill

  5. SET number = number - 1

  6. WHERE seckill_id = #{seckillId}

  7. AND start_time #{killTime}

  8. AND end_time >= #{killTime}

  9. AND number > 0

  10.  
  11.  
  12.  

⑦ SuccessKilledDao.xml

 
  1. PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"

  2. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

  3.  
  4. INSERT ignore INTO success_killed(seckill_id,user_phone,state)

  5. VALUES (#{seckillId},#{userPhone},0)

  6.  

⑧ Mybatis整合Service:spring-dao.xml

 
  1. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  2. xmlns:contex="http://www.springframework.org/schema/context"

  3. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

  4.  
  5.  
  6.  
  7.  
  8.  
  9.  
  10.  
  11.  
  12.  
  13.  

3 Service层

① SeckillService

 
  1. package com.force4us.service;

  2. import com.force4us.dto.Exposer;

  3. import com.force4us.dto.SeckillExecution;

  4. import com.force4us.entity.Seckill;

  5. import com.force4us.exception.RepeatKillException;

  6. import com.force4us.exception.SeckillCloseException;

  7. import com.force4us.exception.SeckillException;

  8. import java.util.List;

  9.  
  10. /**业务接口:站在使用者(程序员)的角度设计接口

  11. * 三个方面:1.方法定义粒度,方法定义的要非常清楚2.参数,要越简练越好

  12. * 3.返回类型(return 类型一定要友好/或者return异常,我们允许的异常)

  13. */

  14. public interface SeckillService {

  15.  
  16. /**

  17. * 查询全部秒杀记录

  18. * @return

  19. */

  20. List getSeckillList();

  21.  
  22. /**

  23. * 查询单个秒杀记录

  24. * @param seckillId

  25. * @return

  26. */

  27. Seckill getById(long seckillId);

  28.  
  29. /**

  30. * 在秒杀开启时输出秒杀接口的地址,否则输出系统时间和秒杀时间

  31. */

  32. Exposer exportSeckillUrl(long seckillId);

  33.  
  34. /**

  35. * 执行秒杀操作,有可能失败,有可能成功,所以要抛出我们允许的异常

  36. * @param seckillId

  37. * @param userPhone

  38. * @param md5

  39. * @return

  40. * @throws SeckillException

  41. * @throws RepeatKillException

  42. * @throws SeckillCloseException

  43. */

  44. SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)

  45. throws SeckillException, RepeatKillException, SeckillCloseException;

  46.  
  47. SeckillExecution executeSeckillProcedure(long seckillId,long userPhone,String md5)

  48. throws SeckillException,RepeatKillException,SeckillCloseException;

  49. }

② SeckillServiceImpl

 
  1. package com.force4us.service.impl;

  2. import com.force4us.dao.SeckillDao;

  3. import com.force4us.dao.SuccessKilledDao;

  4. import com.force4us.dao.cache.RedisDao;

  5. import com.force4us.dto.Exposer;

  6. import com.force4us.dto.SeckillExecution;

  7. import com.force4us.entity.Seckill;

  8. import com.force4us.entity.SuccessKilled;

  9. import com.force4us.enums.SeckillStatEnum;

  10. import com.force4us.exception.RepeatKillException;

  11. import com.force4us.exception.SeckillCloseException;

  12. import com.force4us.exception.SeckillException;

  13. import com.force4us.service.SeckillService;

  14. import org.apache.commons.collections4.MapUtils;

  15. import org.slf4j.Logger;

  16. import org.slf4j.LoggerFactory;

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

  18. import org.springframework.stereotype.Service;

  19. import org.springframework.transaction.annotation.Transactional;

  20. import org.springframework.util.DigestUtils;

  21. import javax.annotation.Resource;

  22. import java.util.Date;

  23. import java.util.HashMap;

  24. import java.util.List;

  25. import java.util.Map;

  26.  
  27. @Service

  28. public class SeckillServiceImpl implements SeckillService {

  29. //日志对象

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

  31.  
  32. @Autowired

  33. private SeckillDao seckillDao;

  34.  
  35. @Autowired

  36. private SuccessKilledDao successKilledDao;

  37.  
  38. @Autowired

  39. private RedisDao redisDao;

  40.  
  41. //加入一个混淆字符串(秒杀接口)的salt,为了我避免用户猜出我们的md5值,值任意给,越复杂越好

  42. private final String salt = "sadjgioqwelrhaljflutoiu293480523*&%*&*#";

  43.  
  44. public List getSeckillList() {

  45. return seckillDao.queryAll(0, 4);

  46. }

  47.  
  48. public Seckill getById(long seckillId) {

  49. return seckillDao.queryById(seckillId);

  50. }

  51.  
  52. public Exposer exportSeckillUrl(long seckillId) {

  53. //缓存优化

  54. //1。访问redi

  55.  
  56.  
  57. Seckill seckill = redisDao.getSeckill(seckillId);

  58. if (seckill == null) {

  59. //2.访问数据库

  60. seckill = seckillDao.queryById(seckillId);

  61. if (seckill == null) {//说明查不到这个秒杀产品的记录

  62. return new Exposer(false, seckillId);

  63. } else {

  64. //3,放入redis

  65. redisDao.putSeckill(seckill);

  66. }

  67.  
  68. }

  69. Date startTime = seckill.getStartTime();

  70. Date endTime = seckill.getEndTime();

  71. Date nowTime = new Date();

  72. //若是秒杀未开启

  73. if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) {

  74. return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());

  75. }

  76.  
  77. //秒杀开启,返回秒杀商品的id、用给接口加密的md5

  78. String md5 = getMD5(seckillId);

  79. return new Exposer(true, md5, seckillId);

  80. }

  81.  
  82. private String getMD5(long seckillId) {

  83. String base = seckillId + "/" + salt;

  84. String md5 = DigestUtils.md5DigestAsHex(base.getBytes());

  85. return md5;

  86. }

  87.  
  88. @Transactional

  89. /**

  90. * 使用注解控制事务方法的优点:

  91. * 1.开发团队达成一致约定,明确标注事务方法的编程风格

  92. * 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部

  93. * 3.不是所有的方法都需要事务,如只有一条修改操作、只读操作不要事务控制

  94. */

  95. public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {

  96. if (md5 == null || !md5.equals(getMD5(seckillId))) {

  97. throw new SeckillException("seckill data rewrite");

  98. }

  99. //执行秒杀逻辑:减库存+记录购买行为

  100. Date nowTime = new Date();

  101. try {

  102. //否则更新了库存,秒杀成功,增加明细

  103. int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);

  104. //看是否该明细被重复插入,即用户是否重复秒杀

  105. if (insertCount <= 0) {

  106. throw new RepeatKillException("seckill repeated");

  107. } else {

  108.  
  109. //减库存,热点商品竞争,update方法会拿到行级锁

  110. int updateCount = seckillDao.reduceNumber(seckillId, nowTime);

  111. if (updateCount <= 0) {

  112. //没有更新库存记录,说明秒杀结束 rollback

  113. throw new SeckillCloseException("seckill is closed");

  114. } else {

  115. //秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息 commit

  116. SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);

  117. return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);

  118. }

  119.  
  120. }

  121. } catch (SeckillCloseException e1) {

  122. throw e1;

  123. } catch (RepeatKillException e2) {

  124. throw e2;

  125. } catch (Exception e) {

  126. logger.error(e.getMessage(), e);

  127. //所有编译器异常,转化成运行期异常

  128. throw new SeckillException("seckill inner error:" + e.getMessage());

  129. }

  130. }

  131.  
  132. public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {

  133. if (md5 == null || !md5.equals(getMD5(seckillId))) {

  134. return new SeckillExecution(seckillId, SeckillStatEnum.DATE_REWRITE);

  135. }

  136. Date time = new Date();

  137. Map map = new HashMap();

  138. map.put("seckillId", seckillId);

  139. map.put("phone", userPhone);

  140. map.put("killTime", time);

  141. map.put("result", null);

  142. try {

  143. seckillDao.killByProcedure(map);

  144. int result = MapUtils.getInteger(map, "result", -2);

  145. if (result == 1) {

  146. SuccessKilled successKill = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);

  147. return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKill);

  148. } else {

  149. return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));

  150. }

  151. } catch (Exception e) {

  152. logger.error(e.getMessage(), e);

  153. return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);

  154. }

  155. }

  156. }

③ 异常的处理:

a.SeckillCloseException

 
  1. package com.force4us.exception;

  2.  
  3. public class SeckillCloseException extends SeckillException{

  4. public SeckillCloseException(String message) {

  5. super(message);

  6. }

  7. public SeckillCloseException(String message, Throwable cause) {

  8. super(message, cause);

  9. }

  10. }

b. SeckillException

 
  1. package com.force4us.exception;

  2.  
  3. public class RepeatKillException extends SeckillException{

  4. public RepeatKillException(String message) {

  5. super(message);

  6. }

  7. public RepeatKillException(String message, Throwable cause) {

  8. super(message, cause);

  9. }

  10. }

c. RepeatKillException

 
  1. package com.force4us.exception;

  2.  
  3. public class SeckillException extends RuntimeException{

  4. public SeckillException(String message) {

  5. super(message);

  6. }

  7. public SeckillException(String message, Throwable cause) {

  8. super(message, cause);

  9. }

  10. }

④ 枚举SeckillStatEnum

 
  1. package com.force4us.enums;

  2. public enum SeckillStatEnum {

  3. SUCCESS(1,"秒杀成功"),

  4. END(0,"秒杀结束"),

  5. REPEAT_KILL(-1,"重复秒杀"),

  6. INNER_ERROR(-2,"系统异常"),

  7. DATE_REWRITE(-3,"数据篡改");

  8. private int state;

  9. private String stateInfo;

  10. SeckillStatEnum(int state, String stateInfo){

  11. this.state = state;

  12. this.stateInfo = stateInfo;

  13. }

  14. public int getState() {

  15. return state;

  16. }

  17. public String getStateInfo() {

  18. return stateInfo;

  19. }

  20. public static SeckillStatEnum stateOf(int index){

  21. for(SeckillStatEnum state : values()){

  22. if(state.getState() == index){

  23. return state;

  24. }

  25. }

  26. return null;

  27. }

  28. }

⑤ spring_spring.xml文件

 

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

 

4.Web层,JSP页面和JS

(1) 详情页流程逻辑逻辑

(2) 配置web.xml[html] view plain copy

  1.   
  2.   
  3.   
  4.   
  5.          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  6.          xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee  
  7.                       http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"  
  8.          version="3.1"  
  9.          metadata-complete="true">  
  10.       
  11.       
  12.       
  13.         seckill-dispatcher  
  14.         org.springframework.web.servlet.DispatcherServlet  
  15.           
  16.           
  17.             contextConfigLocation  
  18.             classpath:spring/spring-*.xml  
  19.           
  20.       
  21.   
  22.       
  23.         seckill-dispatcher  
  24.         /  
  25.       
  26.   
  27.   

(3) SeckillResult

 
  1. package com.force4us.dto;

  2. //将所有的ajax请求返回类型,全部封装成json数据

  3. public class SeckillResult {

  4. private boolean success;

  5. private T data;

  6. private String error;

  7. public SeckillResult(boolean success, T data) {

  8. this.success = success;

  9. this.data = data;

  10. }

  11. public SeckillResult(boolean success, String error) {

  12. this.success = success;

  13. this.error = error;

  14. }

  15. public boolean isSuccess() {

  16. return success;

  17. }

  18. public void setSuccess(boolean success) {

  19. this.success = success;

  20. }

  21. public T getData() {

  22. return data;

  23. }

  24. public void setData(T data) {

  25. this.data = data;

  26. }

  27. public String getError() {

  28. return error;

  29. }

  30. public void setError(String error) {

  31. this.error = error;

  32. }

  33. }

(4) spring-web.xml

 
  1. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"

  2. xmlns:context="http://www.springframework.org/schema/context"

  3. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

  4.  
  5.  
  6.  
  7.  

(5) SeckillController中:

 
  1. package com.force4us.web;

  2. import com.force4us.dto.Exposer;

  3. import com.force4us.dto.SeckillExecution;

  4. import com.force4us.dto.SeckillResult;

  5. import com.force4us.entity.Seckill;

  6. import com.force4us.enums.SeckillStatEnum;

  7. import com.force4us.exception.RepeatKillException;

  8. import com.force4us.exception.SeckillCloseException;

  9. import com.force4us.exception.SeckillException;

  10. import com.force4us.service.SeckillService;

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

  12. import org.springframework.stereotype.Controller;

  13. import org.springframework.test.annotation.Repeat;

  14. import org.springframework.ui.Model;

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

  16. import java.util.Date;

  17. import java.util.List;

  18.  
  19. @Controller

  20. @RequestMapping("/seckill")

  21. public class SeckillController {

  22.  
  23. @Autowired

  24. private SeckillService seckillService;

  25.  
  26.  
  27.  
  28. @RequestMapping(value = "/list",method= RequestMethod.GET)

  29. public String list(Model model) {

  30. List list = seckillService.getSeckillList();

  31. model.addAttribute("list",list);

  32. return "list";

  33. }

  34.  
  35. @RequestMapping(value = "/{seckillId}/detail",method = RequestMethod.GET)

  36. public String detail(@PathVariable("seckillId") Long seckillId, Model model){

  37. if(seckillId == null){

  38. return "redirect:/seckill/list";

  39. }

  40. Seckill seckill = seckillService.getById(seckillId);

  41. if(seckill == null){

  42. return "forward:/seckill/list";

  43. }

  44. model.addAttribute("seckill", seckill);

  45. return "detail";

  46. }

  47.  
  48. //ajax ,json暴露秒杀接口的方法

  49. @RequestMapping(value="/{seckillId}/exposer",method = RequestMethod.POST,produces = {"application/json;charset=UTF-8"})

  50. @ResponseBody

  51. public SeckillResult exposer(@PathVariable("seckillId") Long seckillId){

  52. SeckillResult result;

  53.  
  54. try {

  55. Exposer exposer = seckillService.exportSeckillUrl(seckillId);

  56. result = new SeckillResult(true,exposer);

  57. } catch (Exception e) {

  58. e.printStackTrace();

  59. result = new SeckillResult(false,e.getMessage());

  60. }

  61.  
  62. return result;

  63. }

  64.  
  65.  
  66. @RequestMapping(value="/{seckillId}/{md5}/execution", method = RequestMethod.POST,

  67. produces = {"application/json;charset=UTF-8"})

  68. @ResponseBody

  69. public SeckillResult execute(@PathVariable("seckillId") Long seckillId,

  70. @PathVariable("md5") String md5,

  71. @CookieValue(value="killPhone", required = false) Long phone){

  72. if(phone == null){

  73. return new SeckillResult(false,"未注册");

  74. }

  75.  
  76. SeckillResult result;

  77.  
  78. try {

  79. SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId,phone, md5);

  80. return new SeckillResult(true,execution);

  81. } catch (RepeatKillException e1) {

  82. SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);

  83. return new SeckillResult(true,execution);

  84. } catch(SeckillCloseException e2){

  85. SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END);

  86. return new SeckillResult(true,execution);

  87. }catch(Exception e){

  88. SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);

  89. return new SeckillResult(true,execution);

  90. }

  91. }

  92.  
  93. @RequestMapping(value = "/time/now", method = RequestMethod.GET)

  94. @ResponseBody

  95. public SeckillResult time(){

  96. Date now = new Date();

  97. return new SeckillResult(true,now.getTime());

  98. }

  99.  
  100.  
  101. @RequestMapping("/test")

  102. public String test(){

  103. return "helloworld";

  104. }

  105. }

(6) list.jsp

 
  1. <%@ page contentType="text/html;charset=UTF-8" language="java" %>

  2. <%@include file="common/tag.jsp"%>

  3. 秒杀列表页

  4. <%@include file="/WEB-INF/jsp/common/head.jsp"%>

  5.  
  6.  
  7. 秒杀列表

  • 名称 库存 开始时间 结束时间 创建时间 详情页
    ${sk.name} ${sk.number}

  • 详情

  •  
  •  
  •  
  •  
  • (7) details.jsp

     
    1. <%@ page contentType="text/html;charset=UTF-8" language="java" %>

    2. <%@include file="common/tag.jsp"%>

    3. 秒杀详情页

    4. <%@include file="common/head.jsp"%>

    5.  
    6.  
    7. ${seckill.name}

  •  
  • <%--显示time图标--%>

  • <%--展示倒计时--%>

  •  
  • <%--登录弹出层 输入电话--%>

  •  
  •  
  • 秒杀电话:

  •  
  • placeholder="填写手机号^o^" class="form-control">

  •  
  • <%--验证信息--%>

  • Submit

  •  
  •  
  •  
  •  
  • <%--jQuery Cookie操作插件--%>

  • <%--jQuery countDown倒计时插件--%>

  •  
  •  
  • (8) seckill.js

     
    1. //存放主要交互逻辑的js代码

    2. // javascript 模块化(package.类.方法)

    3.  
    4. var seckill = {

    5. //封装秒杀相关ajax的url

    6. URL: {

    7. now: function () {

    8. return '/seckill/time/now';

    9. },

    10. exposer: function (seckillId) {

    11. return '/seckill/' + seckillId + '/exposer';

    12. },

    13. execution: function (seckillId, md5) {

    14. return '/seckill/' + seckillId + '/' + md5 + '/execution';

    15. }

    16. },

    17.  
    18. //验证手机号

    19. validatePhone: function(phone){

    20. if(phone && phone.length == 11 && !isNaN(phone)){

    21. return true;

    22. }else{

    23. return false;

    24. }

    25.  
    26. },

    27.  
    28. //详情页秒杀逻辑

    29. detail:{

    30. //详情页初始化

    31. init:function (params) {

    32. //手机验证和登录,计时交互

    33. //规划我们的交互流程

    34. //在cookie中查找手机号

    35. var killPhone = $.cookie('killPhone');

    36. //验证手机号

    37. if(!seckill.validatePhone(killPhone)){

    38. //绑定手机,控制输出

    39. var killPhoneModal = $('#killPhoneModal');

    40. killPhoneModal.modal({

    41. show:true,//显示弹出层

    42. backdrop:'static',//禁止位置关闭

    43. keyboard:false//关闭键盘事件

    44. });

    45.  
    46. $('#killPhoneBtn').click(function () {

    47. var inputPhone = $('#killPhoneKey').val();

    48. console.log("inputPhone" + inputPhone);

    49. if(seckill.validatePhone(inputPhone)){

    50. //电话写入cookie,7天过期

    51. $.cookie('killPhone',inputPhone,{expires:7, path:'/seckill'});

    52. //验证通过,刷新页面

    53. window.location.reload();

    54. }else{

    55. $('#killPhoneMessage').hide().html('').show(300);

    56. }

    57. });

    58. }

    59. //已经登录

    60. //计时交互

    61.  
    62. var startTime = params['startTime'];

    63. var endTime = params['endTime'];

    64. var seckillId = params['seckillId'];

    65. $.get(seckill.URL.now(), {}, function (result) {

    66. if (result && result['success']) {

    67. var nowTime = result['data'];

    68. //时间判断 计时交互

    69. seckill.countDown(seckillId, nowTime, startTime, endTime);

    70. } else {

    71. console.log('result: ' + result);

    72. alert('result: ' + result);

    73. }

    74. });

    75. }

    76. },

    77.  
    78. handlerSeckill: function (seckillId, node) {

    79. //获取秒杀地址,控制显示器,执行秒杀

    80. node.hide().html('');

    81.  
    82. $.post(seckill.URL.exposer(seckillId), {}, function (result) {

    83. //在回调函数种执行交互流程

    84. if (result && result['success']) {

    85. var exposer = result['data'];

    86. if (exposer['exposed']) {

    87. //开启秒杀

    88. //获取秒杀地址

    89. var md5 = exposer['md5'];

    90. var killUrl = seckill.URL.execution(seckillId, md5);

    91. console.log("killUrl: " + killUrl);

    92. //绑定一次点击事件

    93. $('#killBtn').one('click', function () {

    94. //执行秒杀请求

    95. //1.先禁用按钮

    96. $(this).addClass('disabled');//,<-$(this)===('#killBtn')->

    97. //2.发送秒杀请求执行秒杀

    98. $.post(killUrl, {}, function (result) {

    99. if (result && result['success']) {

    100. var killResult = result['data'];

    101. var state = killResult['state'];

    102. var stateInfo = killResult['stateInfo'];

    103. //显示秒杀结果

    104. node.html('' + stateInfo + '');

    105. }

    106. });

    107. });

    108. node.show();

    109. } else {

    110. //未开启秒杀(浏览器计时偏差)

    111. var now = exposer['now'];

    112. var start = exposer['start'];

    113. var end = exposer['end'];

    114. seckill.countDown(seckillId, now, start, end);

    115. }

    116. } else {

    117. console.log('result: ' + result);

    118. }

    119. });

    120.  
    121. },

    122.  
    123.  
    124. countDown: function (seckillId, nowTime, startTime, endTime) {

    125. console.log(seckillId + '_' + nowTime + '_' + startTime + '_' + endTime);

    126. var seckillBox = $('#seckill-box');

    127. if (nowTime > endTime) {

    128. //秒杀结束

    129. seckillBox.html('秒杀结束!');

    130. } else if (nowTime < startTime) {

    131. //秒杀未开始,计时事件绑定

    132. var killTime = new Date(startTime + 1000);//todo 防止时间偏移

    133. seckillBox.countdown(killTime, function (event) {

    134. //时间格式

    135. var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒 ');

    136. seckillBox.html(format);

    137. }).on('finish.countdown', function () {

    138. //时间完成后回调事件

    139. //获取秒杀地址,控制现实逻辑,执行秒杀

    140. console.log('______fininsh.countdown');

    141. seckill.handlerSeckill(seckillId, seckillBox);

    142. });

    143. } else {

    144. //秒杀开始

    145. seckill.handlerSeckill(seckillId, seckillBox);

    146. }

    147. }

    148. }

    5.优化:

         由于减少库存和购买明细需要在同一事物当中,在次中间会出现网络延迟,GC,缓存,数据库的并发等,所以需要进行优化。

    (1) 使用Redis优化:具体代码看上面。

    (2) 调整业务逻辑:先进行insert,插入购买明细,然后进行减少库存数量,具体代码看上面。

    (3) 调用存储过程seckill.sql

     
    1. -- 秒杀执行存储过程

    2. DELIMITER $$ -- console ;转换为$$

    3. --定义存储参数

    4. --参数:in 输入参数;out输出参数

    5. -- rowCount():返回上一条修改类型sql(delete,insert,update)的影响行数

    6. -- rowCount: 0:未修改数据 >0:表示修改的行数 <0:sql错误/未执行修改sql

    7. CREATE PROCEDURE excuteSeckill(IN fadeSeckillId INT,IN fadeUserPhone VARCHAR (15),IN fadeKillTime TIMESTAMP ,OUT fadeResult INT)

    8. BEGIN

    9. DECLARE insert_count INT DEFAULT 0;

    10. START TRANSACTION ;

    11. INSERT ignore success_kill(seckill_id,user_phone,status,create_time) VALUES(fadeSeckillId,fadeUserPhone,0,fadeKillTime); --先插入购买明细

    12. SELECT ROW_COUNT() INTO insert_count;

    13. IF(insert_count = 0) THEN

    14. ROLLBACK ;

    15. SET fadeResult = -1; --重复秒杀

    16. ELSEIF(insert_count < 0) THEN

    17. ROLLBACK ;

    18. SET fadeResult = -2; --内部错误

    19. ELSE --已经插入购买明细,接下来要减少库存

    20. UPDATE seckill SET number = number -1 WHERE seckill_id = fadeSeckillId AND start_time < fadeKillTime AND end_time > fadeKillTime AND number > 0;

    21. SELECT ROW_COUNT() INTO insert_count;

    22. IF (insert_count = 0) THEN

    23. ROLLBACK ;

    24. SET fadeResult = 0; --库存没有了,代表秒杀已经关闭

    25. ELSEIF (insert_count < 0) THEN

    26. ROLLBACK ;

    27. SET fadeResult = -2; --内部错误

    28. ELSE

    29. COMMIT ; --秒杀成功,事务提交

    30. SET fadeResult = 1; --秒杀成功返回值为1

    31. END IF;

    32. END IF;

    33. END

    34. $$

    35.  
    36. DELIMITER ;

    37.  
    38. SET @fadeResult = -3;

    39. -- 执行存储过程

    40. CALL excuteSeckill(1003,18810464493,NOW(),@fadeResult);

    41. -- 获取结果

    42. SELECT @fadeResult;

    43.  
    44. --存储过程

    45. -- 1、存储过程优化:事务行级锁持有的时间

    46. -- 2、不要过度依赖存储过程

    6.系统部署:

     

    PS:若想通过源码更好的理解Java实现高并发秒杀,请:https://github.com/luomingkui/seckill

     

     

    你可能感兴趣的:(Java 实现高并发秒杀)