JAVA高并发秒杀API项目的学习笔记


一步一步的搭建JAVA WEB项目,采用Maven构建,基于MYBatis+Spring+Spring MVC+Bootstrap技术的秒杀项目
学习的视频:http://www.imooc.com/learn/587


创建Maven项目

  • 创建目录,执行Maven命令
 mvn archetype:generate -DgroupId=org.seckill -DartifactId=seckill -DarchetypeArtifactId=maven-archetype-webapp -DarchetypeCatalog=local

问题:Maven命令执行到Generating Project in Batch mode 卡住,参考链接

  • 将项目导入到IDEA工具中
  • 修改项目配置
    1. 修改web.xml中的servlet版本,默认是2.3,其不支持JSP的EL表达式。从Tomcat中的示例的web.xml中拷贝3.0的版本配置到项目中
    2. 补全目录。项目的main目录下创建java目录,在src目录下创建test目录,test目录下创建java和sources目录
    3. 打开pom.xml,进行依赖的配置
      • 单元测试依赖:Junit4
      • 日志依赖:slf4j+logback。(lf4j是规范/接口,log4j,common-logging,logback是日志的实现)
      • 数据库依赖:mysql-connector-java、c3p0
      • DAO框架:mybatis依赖:mybatis
      • Servlet web相关依赖:standard、jstl、jackson-databind、servlet-api
      • Spring依赖:spring-core、spring-beans、spring-context、spring-jdbc、spring-tx、spring-web、spring-webmvc、spring-test
        
          
          
            junit
            junit
            4.11
            test
          
    
           
               org.slf4j
               slf4j-api
               1.7.12
           
            
                ch.qos.logback
                 logback-core
                1.1.1
            
            
            
                ch.qos.logback
                logback-classic
                1.1.1
            
            
            
                mysql
                mysql-connector-java
                5.1.35
                runtime
            
            
                c3p0
                c3p0
                0.9.1.2
            
            
            
                  org.mybatis
                  mybatis
                  3.3.0
            
            
            
                org.mybatis
                mybatis-spring
                1.2.3
            
            
            
                taglibs
                standard
                1.1.2
            
            
                jstl
                jstl
                1.2
            
             
                 com.fasterxml.jackson.core
                 jackson-databind
                 2.5.4
             
            
                javax.servlet
                javax.servlet-api
                3.1.0
            
    
            
            
            
                org.springframework
                spring-core
                4.1.7.RELEASE
            
            
                org.springframework
                spring-beans
                4.1.7.RELEASE
            
            
                org.springframework
                spring-context
                4.1.7.RELEASE
            
            
            
                org.springframework
                spring-jdbc
                4.1.7.RELEASE
            
            
                org.springframework
                spring-tx
                4.1.7.RELEASE
            
            
            
                org.springframework
                spring-web
                4.1.7.RELEASE
            
            
                org.springframework
                spring-webmvc
                4.1.7.RELEASE
            
            
            
                org.springframework
                spring-test
                4.1.7.RELEASE
            
        
    

数据库的设计

  • 在项目main目录下创建sql目录,新建 schema.sql,作为数据库的创建脚本
  • 脚本代码如下:
        -- 数据库初始化脚本
    
        -- 创建数据库
        CREATE DATABASE seckill;
    
        -- 使用数据库
        use seckill;
    
        -- 创建秒杀库存表:使用InnoDB引擎,其支持事务。主键自增设置为从1000开始,字符格式设置为UTF8
        CREATE TABLE seckill(
          seckill_id bigint NOT NULL AUTO_INCREMENT COMMENT '商品库存id',
          name varchar(120) NOT NULL COMMENT '商品名称',
          number int NOT NULL COMMENT '库存数量',
          start_time timestamp NOT NULL COMMENT '秒杀开启时间',
          end_time timestamp NOT NULL COMMENT '秒杀结束时间',
          create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
            PRIMARY KEY (seckill_id),
          KEY idx_start_time(start_time),
          KEY idx_end_time(end_time),
          KEY idx_create_time(create_time)
        )ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表';
    
        -- 秒杀成功明细表
        CREATE TABLE success_killed(
          seckill_id bigint NOT NULL COMMENT '秒杀商品id',
          user_phone int NOT NULL COMMENT '用户手机号',
          state tinyint NOT NULL  COMMENT '状态标示:-1指无效,0指成功,1指已付款',
          create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
          PRIMARY KEY (seckill_id,user_phone),
          KEY idx_create_time(create_time)
        )ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';
    
        -- 初始化数据
        INSERT INTO seckill(name,number,start_time,end_time)
        VALUES
        ('1000元秒杀iphone6',100,'2016-06-28 00:00:00','2016-06-29 00:00:00'),
        ('500元秒杀iphone5',200,'2016-06-28 00:00:00','2016-06-29 00:00:00'),
        ('200元秒杀小米4',300,'2016-06-28 00:00:00','2016-06-29 00:00:00'),
        ('100元秒杀红米note',400,'2016-06-28 00:00:00','2016-06-29 00:00:00');
    
    
        -- show create table seckill;
        -- 为什么手写DDL,记录每次上线的DDL修改
    

DAO实体和接口

  • 创建实体包org.seckill.entity

  • 创建DAO包org.seckill.dao

  • 创建SecKill实体类,生成getter和setter,重写toString

        private long secKillId; 
        private String name; 
        private int number; 
        private Date startTime; 
        private Date endTime; 
        private Date createTime; 
    
  • 创建SuccessKilled实体类,生成getter和setter,重写toString

        private long secKillId; 
        private long userPhone; 
        private short state; 
        private Date createTime; 
    
  • 创建DAO接口SecKillDao,添加减库存,根据ID查询秒杀对象,查询秒杀商品列表方法

        /**
         * 减库存
         * @param secKillId
         * @param killTime
         * @return如果影响行数大于1,表示更新的记录行数
         */
        int reduceNumber(long secKillId,Date killTime);
    
        /**
         * 根据id查询秒杀对象
         * @param secKillId
         * @return
         */
        SecKill queryById(long secKillId);
    
        /**
         * 根据偏移量查询秒杀商品列表
         * @param offset
         * @param limit
         * @return
         */
        List queryAll(int offset,int limit);
    
  • 创建DAO接口SuccessKilledDao,添加插入购买明细,根据ID查询购买明细实体的方法

        /**
         * 插入购买明细,可过滤重复
         * @param secKillId
         * @param userPhone
         * @return插入的行数
         */
        int inertSuccessKilled(long secKillId,long userPhone);
    
        /**
         *根据ID查询SuccessKilled并携带秒杀产品对象实体
         * @param secKillId
         * @return
         */
        SuccessKilled queryByIdWithSecKill(long secKillId);
    
  • 基于MyBaits实现DAO接口

    1. 创建mybatis-config.xml全局配置文件
     
     
     
         
         
             
             
    
             
             
    
             
             
     
    
    1. 创建mapper文件夹,用于存储mybatis映射文件
    2. 创建SecKilledDao.xml映射文件
     
     
     
          
         
             
             update
               seckill
             set
               number = number -1
             where seckill_id = #{secKillId}
             and start_time  #{killTime}
             and end_time >= #{killTime}
             and number > 0;
         
    
         
    
         
     
    
    1. 创建SuccessKilledDao.xml映射文件
    
    
    
        
        
           
           insert ignore into success_killed(seckill_id,user_phone)
           values (#{secKilled},#{userPhone})
        
    
        
     
    
  • mybatis整合spring

    1. 创建spring文件,用于存储spring配置文件
    2. 创建spring-dao.xml配置文件
    3. 创建jdbc.properties配置文件,用于存储数据库相关信息
    ``` 
      driver=com.mysql.jdbc.Driver
      url=jdbc:mysql://127.0.0.1:3306/seckill?useUnicode=true&characterEncoding=utf-8
      username=root
      password=purple
    ```
    
    1. 在spring-dao.xml配置文件中进行四个步骤的配置
      • 配置数据库相关参数
      • 配置数据库连接池
      • 配置sqlSessionFactory对象
      • 配置扫描dao接口包,动态实现 dao接口,并注入到spring容器中
       
       
               
               
               
    
               
               
                   
                   
                   
                   
    
                   
                   
                   
                   
                   
                   
                   
                   
               
    
               
               
                   
                   
                   
                   
                   
                   
                   
                   
               
    
               
               
                   
                   
                   
                   
               
       
    
  • Junit4与Spring进行整合,进行Junit4单元测试

    1. 创建SecKillDao的单元测试类
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration("classpath:spring/spring-dao.xml")
    public class SecKillDaoTest {
        //注入DAO实现类依赖
        @Resource
        private SecKillDao secKillDao;
    
        @Test
        public void testReduceNumber() throws Exception {
            Date killTime = new Date();
            int result = secKillDao.reduceNumber(1000L,killTime);
            System.out.println(result);
    
        }
    
        @Test
        public void testQueryById() throws Exception {
            long id = 1000;
            SecKill secKill = secKillDao.queryById(id);
            System.out.println(secKill.getName());
        }
    
        @Test
        public void testQueryAll() throws Exception {
            List secKillList = secKillDao.queryAll(0,1000);
    
            for(SecKill row : secKillList){
                System.out.println(row.toString());
            }
        }
    }
    
    1. 创建SuccessKilledDao的单元测试类
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration("classpath:spring/spring-dao.xml")
    public class SuccessKilledDaoTest {
        @Resource
        private SuccessKilledDao successKilledDao;
    
        @Test
        public void testInertSuccessKilled() throws Exception {
            int result = successKilledDao.insertSuccessKilled(1000L,28059830451L);
            System.out.println(result);
    
        }
    
        @Test
        public void testQueryByIdWithSecKill() throws Exception {
            SuccessKilled successKilled = successKilledDao.queryByIdWithSecKill(1000L,2147483647L);
            System.out.println(successKilled.toString());
        }
    } 
    
    1. 学习点
      • 单元测试类可以利用IDEA的快捷键,直接在要测试的类中进行代码的生成
      • mybatis的传参,需要在DAO接口方法的形参中使用@Param注解进行指明

业务层设计

  • 秒杀业务接口设计
    1. 创建业务包service

    2. 创建数据传输实体包dto

    3. 创建异常包exception

    4. 创建dto实体

      • 创建暴露秒杀地址DTO:Exposer
      public class Exposer {
          /**
           * 是否开启秒杀
           */
          private boolean exposed;
      
          /**
           * 秒杀ID
           */
          private long secKillId;
      
          /**
           * 一种加密措施
           */
          private String md5;
      
          /**
           *系统当前时间(毫秒值)
           */
          private long now;
      
          private long start;
      
          private long end;
      
          public Exposer(boolean exposed, String md5, long secKillId) {
              this.exposed = exposed;
              this.md5 = md5;
              this.secKillId = secKillId;
          }
      
          public Exposer(boolean exposed, long now, long start, long end) {
              this.exposed = exposed;
              this.now = now;
              this.start = start;
              this.end = end;
          }
      
          public Exposer(boolean exposed, long secKillId) {
              this.exposed = exposed;
              this.secKillId = secKillId;
          }
      
          public boolean isExposed() {
              return exposed;
          }
      
          public void setExposed(boolean exposed) {
              this.exposed = exposed;
          }
      
          public long getSecKillId() {
              return secKillId;
          }
      
          public void setSecKillId(long secKillId) {
              this.secKillId = secKillId;
              this.secKillId = secKillId;
          }
      
          public String getMd5() {
              return md5;
          }
      
          public void setMd5(String md5) {
              this.md5 = md5;
          }
      
          public long getNow() {
              return now;
          }
      
          public void setNow(long now) {
              this.now = now;
          }
      
          public long getStart() {
              return start;
          }
      
          public void setStart(long start) {
              this.start = start;
          }
      
          public long getEnd() {
              return end;
          }
      
          public void setEnd(long end) {
              this.end = end;
          }
      }
      
      
      • 创建封装秒杀执行后结果DTO:SecKillExecution
      public class SecKillExecution {
      
          private long secKillId;
      
          /**
           * 秒杀执行结果状态
           */
          private int state;
      
          /**
           * 状态表示
           */
          private String stateInfo;
      
          private SuccessKilled successKilled;
      
          public SecKillExecution(long secKillId, int state, String stateInfo, SuccessKilled successKilled) {
              this.secKillId = secKillId;
              this.state = state;
              this.stateInfo = stateInfo;
              this.successKilled = successKilled;
          }
      
          public SecKillExecution(long secKillId, int state, String stateInfo) {
              this.secKillId = secKillId;
              this.state = state;
              this.stateInfo = stateInfo;
          }
      
          public long getSecKillId() {
              return secKillId;
          }
      
          public void setSecKillId(long secKillId) {
              this.secKillId = secKillId;
          }
      
          public int getState() {
              return state;
          }
      
          public void setState(int state) {
              this.state = state;
          }
      
          public String getStateInfo() {
              return stateInfo;
          }
      
          public void setStateInfo(String stateInfo) {
              this.stateInfo = stateInfo;
          }
      
          public SuccessKilled getSuccessKilled() {
              return successKilled;
          }
      
          public void setSuccessKilled(SuccessKilled successKilled) {
              this.successKilled = successKilled;
          }
      }
      
    5. 创建异常类

      • 创建业务相关异常:SecKillException
      public class SecKillException extends RuntimeException{
          public SecKillException(String message) {
              super(message);
          }
      
          public SecKillException(String message, Throwable cause) {
              super(message, cause);
          }
      }
      
      • 创建重复秒杀异常类:RepeatKillException
      public class RepeatKillException extends SecKillException{
          public RepeatKillException(String message, Throwable cause) {
              super(message, cause);
          }
      
          public RepeatKillException(String message) {
              super(message);
          }
      }
      
      • 创建秒杀关闭异常类:SecKillCloseExce
        ption
      public class SecKillCloseException extends SecKillException{
          public SecKillCloseException(String message) {
              super(message);
          }
      
          public SecKillCloseException(String message, Throwable cause) {
              super(message, cause);
          }
      }
      
      
    6. 创建SecKillService业务接口:SecKillService

      • 创建查询所有的秒杀记录方法:getSecKillList
      • 创建查询单个秒杀记录方法:getById
      • 创建秒杀开启时输出秒杀接口地址方法:exportSecKillUrl
      • 创建执行秒杀操作方法:executeSecKill
       public interface SecKillService {
           /**
            * 查询所有的秒杀记录
            * @return
            */
            List getSecKillList();
      
           /**
            * 查询单个秒杀记录
            * @param secKillId
            * @return
            */
           SecKill getById(long secKillId);
      
           /**
            * 秒杀开启时输出秒杀接口地址
            * 否则输出系统时间和秒杀时间
            * 防止用户猜测出秒杀地址的规律
            * @param secKillId
            */
           Exposer exportSecKillUrl(long secKillId);
      
           /**
            *执行秒杀操作
            * @param secKillId
            * @param userPhone
            * @param md5
            */
           SecKillExecution executeSecKill(long secKillId,long userPhone,String md5) throws SecKillException,RepeatKillException,SecKillCloseException;
       }
      
    7. 业务接口设计的学习点

      • 站在使用者的角度进行设计接口,不要冗余设计
      • 方法定义粒度,目的明确。非常友好的让使用者调用接口
      • 参数要简炼
      • 返回类型要清晰
  • 秒杀业务接口实现

    1. 新建enums枚举包,将数据字典放到枚举中
    2. 在枚举包下创建秒杀状态枚举:SecKillStatEnum
    public enum SecKillStatEnum {
        SUCCESS(1,"秒杀成功"),
        END(0,"秒杀结束"),
        REPEAT(-1,"重复秒杀"),
        INNER_ERROR(-2,"系统异常"),
        DATA_REWRITE(-3,"数据篡改");
    
        private int state;
        private String stateInfo;
    
        SecKillStatEnum(int state, String stateInfo) {
            this.state = state;
            this.stateInfo = stateInfo;
        }
    
        public int getState() {
            return state;
        }
    
        public String getStateInfo() {
            return stateInfo;
        }
    
        public static SecKillStatEnum stateOf(int index){
            for(SecKillStatEnum state : values()) {
                if(state.getState() == index){
                    return state;
                }
            }
    
            return null;
    
        }
    }
    
    1. 在service包下新建impl包
    2. 创建SecKillServiceImpl实现类,实现SecKillService接口方法
     public class SecKillServiceImpl implements SecKillService{
         private Logger logger = LoggerFactory.getLogger(SecKillService.class);
    
         private SecKillDao secKillDao;
         private SuccessKilledDao successKilledDao;
    
         //混淆字符,用于混淆MD5
         private final String salt = "sdlkjs#$#$dfowierlkjafdmv232k3j@@##$";
    
         @Override
         public List getSecKillList() {
             return secKillDao.queryAll(0,4);
         }
    
         @Override
         public SecKill getById(long secKillId) {
             return secKillDao.queryById(secKillId);
         }
    
         @Override
         public Exposer exportSecKillUrl(long secKillId) {
             SecKill secKill = secKillDao.queryById(secKillId);
    
             if(null == secKill){
                 return  new Exposer(false,secKillId);
             }
    
             Date startTime = secKill.getStartTime();
             Date endTime = secKill.getEndTime();
             Date nowTime = new Date();
    
             if(nowTime.getTime() < startTime.getTime()
                     || nowTime.getTime() > endTime.getTime()){
                 return new Exposer(false,secKillId,nowTime.getTime(),startTime.getTime(),endTime.getTime());
             }
    
             //转化特定字符串的过程,不可逆
             String md5 = getMD5(secKillId);
    
             return new Exposer(true,md5,secKillId);
    
         }
    
         @Override
         public SecKillExecution executeSecKill(long secKillId, long userPhone, String md5)
                 throws SecKillException, RepeatKillException, SecKillCloseException {
             if(null == md5 || md5.equals(getMD5(secKillId))){
                 throw new SecKillException("seckill datarewirte");
             }
    
            try{
                //执行秒杀逻辑,减库存,记录购买行为
                Date nowTime = new Date();
                //减库存
                int updateCount = secKillDao.reduceNumber(secKillId,nowTime);
    
                if(updateCount <= 0){
                    //没有更新到记录,秒杀结束
                    throw new SecKillCloseException("seckill is Closed");
                }else{
                    //记录购买行为
                    int insertCount = successKilledDao.insertSuccessKilled(secKillId,userPhone);
    
                    //唯一:secKillId,userPhone
                    if(insertCount <= 0){
                        //重复秒杀
                        throw new RepeatKillException("seckill repeated");
                    }else{
                        //秒杀成功
                        SuccessKilled successKilled = successKilledDao.queryByIdWithSecKill(secKillId,userPhone);
    
                        return new SecKillExecution(secKillId, SecKillStatEnum.SUCCESS,successKilled);
                    }
                }
            }catch(SecKillCloseException e1){
                throw e1;
            }catch(RepeatKillException e2){
                throw e2;
            }catch (Exception e){
                 logger.error(e.getMessage(),e);
                //所有编译期异常,转化为运行期异常
                throw new SecKillException("seckill inner error:" + e.getMessage());
            }
         }
    
         /**
          * 生成MD5
          * @param secKillId
          * @return
          */
         private String getMD5(long secKillId){
             String base = secKillId + "/" + salt;
             String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
             return md5;
         }
     }
    
  • 基于Spring托管Service实现类

    1. 创建Spring的service配置spring-service.xml,进行service包下的注解类型的扫描配置
    
           
           
    
     
    
    1. 在service实现类中添加上@Service的注解,在类中的dao对象添加上@Autowired的注解
  • 配置并使用Spring声明式事务

    1. 在spring-service.xml中添加上配置事务管理器
    
        
         
    
    
    1. 在spring-service.xml中添加上配置基于注解的声明式事务
     
    
    1. 在业务类的executeSecKill方法中添加上@Transactional事务注解
    2. 学习点:使用注解控制事务方法的优点
    • 开发团队达到一致约定,明确标注事务方法的编程风格
    • 保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求,或者剥离到事务方法外部
    • 不是所有的方法都需要事务,如只有一条修改操作,只读操作就不需要事务控制
  • Service集成测试

    1. 添加上logback的日志配置文件logback.xml
     
    
     
         
         
             
                 [%-5level] %d{HH:mm:ss.SSS} [%thread] %logger{36} - %msg%n
             
    
         
    
         
         
             
             
         
    
        
    
    1. 使用IDEA为SecKillService业务接口创建单元测试类SecKillServiceTest
    2. 编写单元测试方法
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration({"classpath:spring/spring-dao.xml","classpath:spring/spring-service.xml"})
    public class SecKillServiceTest {
        private final Logger logger = LoggerFactory.getLogger(this.getClass());
        @Autowired
        private SecKillService secKillService;
        @Test
        public void testGetSecKillList() throws Exception {
            List list = secKillService.getSecKillList();
            logger.info("list={}",list);
        }
    
        @Test
        public void testGetById() throws Exception {
            SecKill secKill = secKillService.getById(1000L);
            logger.info("secKill:{}",secKill);
    
        }
    
        /**
         * 测试完整业务,注意集成测试代码完整逻辑,注意可重复执行
         * @throws Exception
         */
        @Test
        public void testSecKillLogic() throws Exception {
            long id = 1000L;
            Exposer exposer = secKillService.exportSecKillUrl(id);
    
            if(exposer.isExposed()){
                logger.info("exposer={}",exposer);
                long phone = 18059830432L;
                SecKillExecution secKillExecution = secKillService.executeSecKill(id,phone,exposer.getMd5());
                logger.info("secKillExecution:{}",secKillExecution);
            }else{
                //秒杀未开始
                logger.warn("exposer={}",exposer);
            }
        }
    
        @Test
        public void testExportSecKillUrl() throws Exception {
            long id = 1000L;
            Exposer exposer = secKillService.exportSecKillUrl(id);
            logger.info("exposer={}",exposer);
    
        }
    
        @Test
        public void testExecuteSecKill() throws Exception {
            long id = 1000L;
            long phone = 18059830452L;
            String md5 = "f1974250b060f51c4a8e48df67232d53";
    
            SecKillExecution secKillExecution = secKillService.executeSecKill(id,phone,md5);
    
            logger.info("secKillExecution:{}",secKillExecution);
    
        }
    } 
    
    1. 单元测试的学习点
      • 集成测试的业务逻辑的完整性
      • 注意测试的可重复执行

WEB层设计

  • 设计Restful接口

  • SpringMVC整合Spring

    1. 在web.xml中配置DispatcherServlet
    2. 创建web包
    3. 创建spring-web.xml配置文件
    4. 在spring-web.xml进行SpringMVC的配置
      • 开启SpringMVC注解模式
      • servlet-mapping映射路径
      • 配置jsp显示viewResolver
      • 扫描web相关的bean
      
             
             
             
              
      
              
              
              
      
              
              
                  
                  
                  
              
      
              
              
      
       
      
  • 实现秒杀相关的Restful接口

    1. 创建控制类SecKillController,实现获取列表,获取单条数据,获取系统时间,获取秒杀地址,秒杀的方法
     @Controller
     @RequestMapping("/seckill/")//模块/资源
     public class SecKillController {
         private final Logger logger = LoggerFactory.getLogger(this.getClass());
         @Autowired
         private SecKillService secKillService;
    
        @RequestMapping(name="/list",method= RequestMethod.GET)
        public String list(Model model){
            List list = secKillService.getSecKillList();
            model.addAttribute("list",list);
            return "list";
        }
    
         @RequestMapping(value="/{secKillId}/detail",method=RequestMethod.GET)
         public String detail(@PathVariable("secKillId") Long secKillId,Model model){
             if(secKillId == null){
                 return "redirect:/seckill/list";
             }
    
             SecKill secKill = secKillService.getById(secKillId);
    
             if(secKill == null){
                 return "redirect:/seckill/list";
             }
    
             model.addAttribute("secKill",secKill);
             return "detail";
         }
    
         @RequestMapping(value="/{secKillId}/exposer",method = RequestMethod.POST,
             produces = {"application/json;charset=utf-8"})
         @ResponseBody
         public SecKillResult exposer(@PathVariable("secKillId") Long secKillId){
             SecKillResult result = null;
    
             try{
                 Exposer exposer = secKillService.exportSecKillUrl(secKillId);
                 result = new SecKillResult(true,exposer);
    
             }catch(Exception e){
                 logger.error(e.getMessage(),e);
                 result = new SecKillResult(false,e.getMessage());
             }
    
             return result;
         }
    
         @RequestMapping(value="/{secKillId}/{md5}/execution",
         method = RequestMethod.POST,
         produces = {"application/json;charset=utf-8"})
         public SecKillResult excute(@PathVariable("secKillId") Long secKillId,
                                                       @PathVariable("md5") String md5,
                                                @CookieValue(value="killPhone",required = false) Long userPhone){
             //springmvc valid
             if(userPhone == null){
                 return new SecKillResult(false,"未注册");
             }
    
             SecKillResult result = null;
    
             try{
                 SecKillExecution secKillExecution = secKillService.executeSecKill(secKillId,userPhone,md5);
                 result = new SecKillResult(true,secKillExecution);
    
             }catch(RepeatKillException e){
                 SecKillExecution secKillExecution = new SecKillExecution(secKillId, SecKillStatEnum.REPEAT);
                 result = new SecKillResult(false,secKillExecution);
    
             }catch(SecKillCloseException e){
                 SecKillExecution secKillExecution = new SecKillExecution(secKillId, SecKillStatEnum.END);
                 result = new SecKillResult(false,secKillExecution);
    
             }catch(Exception e){
                 logger.error(e.getMessage(),e);
                 SecKillExecution secKillExecution = new SecKillExecution(secKillId, SecKillStatEnum.INNER_ERROR);
                 result = new SecKillResult(false,secKillExecution);
             }
    
             return result;
         }
    
         @RequestMapping(value="/time/now",method=RequestMethod.GET)
         public SecKillResult time(){
             Date now = new Date();
             return new SecKillResult(true,now.getTime());
    
         }
     }
    
  • 基于Bootstrap开发页面结构
    1. 创建jsp文件夹,创建common/header.jsp,common/tag.jsp,list.jsp,detail.jsp,并引入bootstrap框架,jquery、cookie、countdown插件,可以从百度和bootcss的CDN中引入插件。

      • 链接:http://www.bootcdn.cn/
    2. 创建js文件seckill.js,进行登录、计时的交互逻辑的编码,并在详细页面中引入

     var seckill = {
         //封装秒杀相关ajax的url
         URL: {
           now: function(){
               return '/seckill/time/now';
           },
           exposer: function(id){
               return '/seckill/' + id + '/exposer';
           },
           execution : function(id,md5){
               return '/seckill/' + id + '/' + md5 + '/execution';
           }
         },
         //处理秒杀逻辑
         handleSecKillKill: function(secKillId,node){
             node.hide().html('');
    
             $.post(seckill.URL.exposer(secKillId),{},function(result){
                 if(result && result.success){
                     var exposer = result.data;
    
                     if(exposer.exposed){
                         //开启秒杀
                          //获取秒杀地址
                         var killUrl =  seckill.URL.execution(secKillId,exposer.md5);
                         console.log('killUrl:',killUrl);
                         //绑定一次点击事件
                         $('#killBtn').one('click',function(){
                              //执行秒杀请求
                             $(this).addClass('disabled');
                             $.post(killUrl,{},function(result){
                                  if(result && result.success){
                                      var killResult = result.data;
                                      var state = killResult.state;
                                      var stateInfo = killResult.stateInfo;
    
                                      node.html(''+stateInfo+'');
                                  }
                             });
                         });
    
                         node.show();
                     }else{
                         //未开启秒杀
                         //重新计算计时逻辑
                         seckill.countdown(secKillId,exposer.now,exposer.start,exposer.end);
                     }
    
                 }else{
                     console.error('result:',result);
                 }
             });
         },
         //计时
         countdown: function(secKillId,nowTime,startTime,endTime){
             var $secKillBox = $('#seckill-box');
    
             if(nowTime > endTime){
                 $secKillBox.html('秒杀结束');
             }else if(nowTime < startTime){
                 $secKillBox.html('秒杀未开始');
                 var killTime = new Date(startTime + 1000);
    
                 $secKillBox.countdown(killTime,function(event){
                     var format = event.strftime('秒杀倒计时:%D天 %H时 %M分 %S秒');
                     $secKillBox.html(format);
                 }).on('finish.countdown',function(){
                     //获取秒杀地址,控制实现逻辑,执行秒杀
                     seckill.handleSecKillKill(secKillId,$secKillBox);
                 });
             }else{
                 //秒杀开始
                 seckill.handleSecKillKill(secKillId,$secKillBox);
             }
    
    
         },
         //验证手机号
         validatePhone: function(phone){
             if(phone && phone.length == 11 && !isNaN(phone)){
                 return true;
             }else{
                 return false;
             }
         },
         //详情页秒杀逻辑
         detail: {
             //详情页初始化
             init: function(params){
                 //用户手机验证和登录,计时交互
                 //规划交互流程
                 //在cookie中查找手机号
                 var killPhone = $.cookie('killPhone'),
                     startTime = params.startTime,
                     endTime = params.endTime,
                     secKillId = params.secKillId;
    
                 //验证手机号
                 if(!seckill.validatePhone(killPhone)){
                     var killPhoneModal = $('#killPhoneModal');
    
                     killPhoneModal.modal({
                         show: true,
                         backdrop: 'static',//禁止位置关闭
                         keyboard: false//关闭键盘事件
                     });
    
                     $('#killPhoneBtn').click(function(){
                        var inputPhone = $('#killPhoneKey').val();
                         if(seckill.validatePhone(inputPhone)){
                             //电话写入cookie
                             $.cookie('killPhone',inputPhone,{expires:7,path: '/seckill'})
                             window.location.reload();
    
                         }else{
                             //正常下会有一个前端字典
                            $('#killPhoneMessage').hide().html('').show(300);
                         }
                     });
                 }
    
                 //用户已经登录
                 //计时交互
                 $.get(seckill.URL.now(),function(result){
                     if(result && result.success){
                         var nowTime = result.data;
                         seckill.countdown(secKillId,nowTime,startTime,endTime);
    
                     }else{
                         consolw.error('result:',result);
                     }
                 });
             }
    
         }
    
     }
    
    1. 在detail.jsp页面中引入seckill.js文件,并进行初始化
     
    

你可能感兴趣的:(JAVA高并发秒杀API项目的学习笔记)