Java高并发系统设计及其优化策略——秒杀系统(一)

1、秒杀系统分析

1.1秒杀系统业务分析

1、秒杀系统的核心是对库存的处理,业务流程图如下所示
Java高并发系统设计及其优化策略——秒杀系统(一)_第1张图片
2、用户针对库存业务分析
1、减库存
2、记录购买明细(记录秒杀成功信息)
1)记录谁购买成功了
2)成功的时间/有效期
Java高并发系统设计及其优化策略——秒杀系统(一)_第2张图片

1.2 秒杀系统技术分析

1、为什么需要事务?
一旦用户秒杀成功系统需要做两步操作,减库存以及记录购买明细。利用数据库可以实现这操作的”事务”特性。如果没有控制事务,可能会发生如下情况:
1、减库存成功而记录购买明细失败,会导致少卖
2、记录购买明细成功而减库存失败,会导致超卖

2、数据落地方案选择
关于数据如何落地,有MySQL和NoSQL这两种方案选择。

1、MySQL是关系型数据库,它有多种存储引擎,只有InnoDB存储引擎支持事务。使用InnoDB存储引擎可以帮助我们完成减库存和记录购买明细的事务操作,InnoDB支持行级锁和表级锁,默认使用行级锁。
2、NoSQL是非关系型数据库,对于事务的支持做的不是很好,NoSQL更多的适用于高并发数据读写和海量数据存储等应用场景。
所以事务机制依然是目前可靠的落地方案。

3、MySQL实现秒杀的难点分析
当一个用户成功秒杀某件商品后,其他秒杀这件商品的用户只能等到,直到上一个用户成功提交事务或者回滚事务,他才能得到锁执行秒杀操作。锁的竞争,就是采用数据库方案的难点问题。
Java高并发系统设计及其优化策略——秒杀系统(一)_第3张图片
MySQL实现秒杀的机制是:事务+行级锁
Java高并发系统设计及其优化策略——秒杀系统(一)_第4张图片

1.3 秒杀系统优化分析

秒杀系统页面流程逻辑
Java高并发系统设计及其优化策略——秒杀系统(一)_第5张图片

高并发需要优化的地方
Java高并发系统设计及其优化策略——秒杀系统(一)_第6张图片
从上图可知,需要优化的有同一时间对详情页的高访问(这里包含了动态资源、静态资源,主要费时间的是资源的加载以及渲染)、获取系统时间(由于获取系统时间非常短,所以这个位置不需要优化)、地址暴露接口(动态资源)、执行秒杀操作(主要是对数据库的访问)。

高并发解决方案
Java高并发系统设计及其优化策略——秒杀系统(一)_第7张图片
对于固定资源的高速访问可以使用CDN智能选择离用户最近的网络节点,加速用户获取数据的系统,这个过程还可以使用nigix反向代理实现负载均衡,将访问流量分配到nigix集群中来分散压力。
Java高并发系统设计及其优化策略——秒杀系统(一)_第8张图片
对于同时某一个商品的高流量访问可以采用redis集群进行缓存和分解访问压力,redis每秒支持数十万访问,redis集群则可高达百万级。
Java高并发系统设计及其优化策略——秒杀系统(一)_第9张图片
记录行为消息(将购买明细写入消息队列,秒杀业务只需从消息队列中读取相关信息进行相应的库存操作,可以避免瞬间过大流量访问秒杀业务导致系统崩溃)可以采用分布式消息队列进行分流。

对于库存的操作,使用Mysql,通过它的事务机制可以避免少卖或超卖的发生,但它的瓶颈在于网络延迟(由于判断逻辑在客户端,当与Mysql服务器进行交互时存在较大的网络延迟,另一个则是事务锁机制的耗时以及客户端频繁的对象销毁创建造成的GC),为缓解这些问题,将判断逻辑放置在服务端并采用存储过程(存储过程是预编译的,编译一次执行多次,比单sql语句效率要高)则能极大的优化网络延迟问题。

mysql瓶颈分析
Java高并发系统设计及其优化策略——秒杀系统(一)_第10张图片
通过调整逻辑,可以减少延迟,如果先执行购买明细的记录则可避免重复秒杀sql的执行,并且插入购买明细可以并行执行,而对于先执行减库存操作,再执行购买明细的记录它是针对同一行数据的(mysql针对同一行数据的操作大概每秒大概一万多次,不同行数据操作则是十几万次),存在一个行级锁的问题,即若是多个用户在购买同一个商品(减库存操作就阻塞了)则存在两个网络延迟。
Java高并发系统设计及其优化策略——秒杀系统(一)_第11张图片

2、秒杀系统实现

2.1 DAO层实现

数据库脚本,创建秒杀商品表、秒杀成功明细表。

/*数据初始化脚本*/
---创建数据库
CREATE DATABASE seckill;
---使用数据库
use seckill;
---创建秒杀库存表
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=utf-8 comment='秒杀库存表';

---初始化数据
INSERT INTO
  seckill(name,number,start_time,end_time)
VALUES
  ('1000元秒杀iphone6',100,'2018-08-08 00:00:00','2018-08-09 00:00:00'),
  ('500元秒杀ipad2',200,'2018-08-08 00:00:00','2018-08-09 00:00:00'),
  ('300元秒杀小米4',300,'2018-08-08 00:00:00','2018-08-09 00:00:00'),
  ('200元秒杀红米note',400,'2018-08-08 00:00:00','2018-08-09 00:00:00');

---秒杀成功明细表
---用户登录认证相关的信息
CREATE TABLE success_killed(
'seckill_id' bigint NOT NULL comment '秒杀商品id',
'user_phone' bigint NOT NULL comment '用户手机号',
'state' tinyint NOT NULL DEFAULT -1 comment '状态标识:-1:无效 0:成功 1:已付款',
'create_time' TIMESTAMP NOT NULL comment '创建时间',
PRIMARY KEY (seckill_id,user_phone),/*联合主键*/
key idx_create_time(create_time) /*创建索引*/
)ENGINE=InnoDB DEFAULT charset=utf-8 comment='秒杀成功明细表';

整合Spring和MyBatis
在resources下创建jdbc.properties文件

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/seckill?useUnicode=true&characterEncoding=utf8
username=root
password=root

在resources下创建mybatis-config.xml文件



<configuration>
    
    <settings>
        
        <setting name="useGeneratedKeys" value="true"/>
         
        <setting name="useColumnLabel" value="true" />
        
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    settings>
configuration>

在resources目录下创建一个新的目录spring(存放所有spring相关的配置),spring目录下创建spring-dao.xml文件


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       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">

    
    
    <context:property-placeholder location="classpath:jdbc.properties"/>

    
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        
        <property name="driverClass" value="${driver}" />
        <property name="jdbcUrl" value="${url}" />
        <property name="user" value="${username}" />
        <property name="password" value="${password}" />

        
        <property name="maxPoolSize" value="30" />
        <property name="minPoolSize" value="10" />
        
        <property name="autoCommitOnClose" value="false" />
        
        <property name="checkoutTimeout" value="1000" />
        
        <property name="acquireIncrement" value="2" />
     bean>

    
    
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        
        <property name="dataSource" ref="dataSource" />
        
        <property name="configLocation" value="classpath:mybatis-config.xml" />
        
        <property name="typeAliasesPackage" value="com.seckill.entity" />
        
        <property name="mapperLocations" value="classpath:mapper/*.xml" />
    bean>

    
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
        
        <property name="basePackage" value="com.seckill.dao" />
    bean>

    
    <bean id="redisDao" class="com.seckill.dao.cache.RedisDao">
        <constructor-arg index="0" value="localhost"/>
        <constructor-arg index="1" value="6379" />
    bean>

beans>

resources下创建mapper目录,mapper下专门存放dao映射文件。
SeckillDao.xml



<mapper namespace="cn.ctgu.seckill.dao.SeckillDao">
    <update id="reduceNumber">
        /*具体的sql*/
        update
          seckill
        set
          number=number-1
        where seckill_id=#{seckillId}
        and start_time#{killTime}
        and end_time>=#{killTime}
        and number >0;
    update>

    <select id="queryById" resultType="Seckill" parameterType="long">
        select Seckill_id,name,number,start_time,end_time,create_time
        from seckill
        where seckill_id=#{seckillId}
    select>

    <select id="queryAll" resultType="Seckill">
        select seckill_id,name,number,start_time,end_time,create_time
        from seckill
        order by create_time desc
        limit #{offset},#{limit}
    select>

    
    <select id="killByprocedure" statementType="CALLABLE">
        call execute_seckill(
        #{seckillId,jdbcType=BIGINT,mode=IN},
        #{phone,jdbcType=BIGINT,mode=IN},
         #{killTime,jdbcType=TIMESTAMP,mode=IN},
        #{result,jdbcType=INTEGER,mode=OUT},
        )
    select>

mapper>

SuccessKilledDao.xml

"1.0" encoding="UTF-8" ?>
"-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
"cn.ctgu.seckill.dao.SuccessKilledDao">
    "insertSuccessKilled">
        /*主键冲突,报错,采用忽略*/
        insert ignore into success_killed(seckill_id,user_phone)
        values(#{seckillId},#{userPhone})
    

    

SuccessKilledDao.java

package cn.ctgu.seckill.dao;

import cn.ctgu.seckill.domain.SuccessKilled;
import org.apache.ibatis.annotations.Param;

public interface SuccessKilledDao {
    //插入购买明细,可过滤重复
    int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone")long userPhone);

    //根据id查询successKilled并携带秒杀产品对象实体
    SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId,@Param("userPhone")long userPhone);

}

SeckillDao.java

package cn.ctgu.seckill.dao;

import cn.ctgu.seckill.domain.Seckill;
import org.apache.ibatis.annotations.Param;

import java.util.Date;
import java.util.List;
import java.util.Map;

public interface SeckillDao {
    //减库存
    int reduceNumber(@Param("seckillId")long seckillId,@Param("killTime")Date killTime);
    //通过id查询商品秒杀列表
    Seckill queryById(long seckillId);
    //根据偏移量查询商品秒杀列表
    List queryAll(@Param("offset") int offet, @Param("limit")int limit);
    //使用存储过程实现秒杀
    void killByProcedure(Map paramMap);
}

RedisDao.java

package cn.ctgu.seckill.dao.cache;

import cn.ctgu.seckill.domain.Seckill;
import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;



public class RedisDao {
    private final Logger logger= LoggerFactory.getLogger(this.getClass());
    private final JedisPool jedisPool;
    private RuntimeSchemaschema=RuntimeSchema.createFrom(Seckill.class);

    public RedisDao(String ip,int port){
        jedisPool=new JedisPool(ip,port);
    }
    public Seckill getSeckill(long seckillId){
        //redis操作逻辑
        try{
            Jedis jedis=jedisPool.getResource();
            try {
                String key="seckill:"+seckillId;
                //并没有实现内部序列化操作
                //get->byte[]->反序列化->Object([Seckill])
                //采用自定义序列化
                //protosbuff:pojo
                byte[]bytes=jedis.get(key.getBytes());
                //缓存重获取到
                if(bytes!=null){
                    //反序列化需要先创建一个空对象用于存储
                    Seckill seckill=schema.newMessage();
                    ProtostuffIOUtil.mergeFrom(bytes,seckill,schema);
                    //seckill被反序列化
                    return seckill;
                }
            }finally {
                jedis.close();
            }
        }catch (Exception e){
            logger.error(e.getMessage(),e);
        }

        return null;
    }
    public String putSeckill(Seckill seckill){
        //set Object(Seckill)->序列化->byte[]
        try{
            Jedis jedis=jedisPool.getResource();
            try{
                String key="seckill:"+seckill.getSeckillId();
                byte[]bytes=ProtostuffIOUtil.toByteArray(seckill,schema,
                        LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
                //超时缓存一个小时
                int timeout=60*60;//一个小时
                String result=jedis.setex(key.getBytes(),timeout,bytes);
                return result;
            }finally {
                jedis.close();
            }
        }catch (Exception e){
            logger.error(e.getMessage(),e);
        }

        return null;
    }

}

2.2 Service层实现

配置事务,resources/spring下创建spring-service.xml文件


<beans xmlns="http://www.springframework.org/schema/beans"
       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">

    
    <context:component-scan base-package="com.seckill.service" />

    
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        
        <property name="dataSource" ref="dataSource" />
    bean>

    
    <tx:annotation-driven transaction-manager="transactionManager" />
beans>

SeckillService.java

package cn.ctgu.seckill.service;

import cn.ctgu.seckill.domain.Seckill;
import cn.ctgu.seckill.dto.Exposer;
import cn.ctgu.seckill.dto.SeckillExecution;
import cn.ctgu.seckill.exception.RepeatKillException;
import cn.ctgu.seckill.exception.SeckillCloseException;
import cn.ctgu.seckill.exception.SeckillException;

import java.util.List;

/*
*
* 业务接口:站在使用者的角度去设计接口
* 三个方面:方法定义粒度,参数,返回类型(return 类型/异常)
*
* */
public interface SeckillService {
    /*
    *
    * 查询所有秒杀记录
    * */
    List getSeckillList();
    /*
    *
    * 查询单个秒杀记录
    *
    * */
    Seckill getById(long seckillId);
    /*
    *
    * 秒杀开启时输出秒杀接口地址
    * 否则输出系统时间和秒杀时间
    * */
    Exposer exportSeckillUrl(long seckillId);

    //执行秒杀操作
    SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
            throws SeckillException,RepeatKillException,SeckillCloseException;

    //执行秒杀操作,存储过程实现
    SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5);
}

SeckillServiceImpl.java

package cn.ctgu.seckill.service.impl;

import cn.ctgu.seckill.dao.SeckillDao;
import cn.ctgu.seckill.dao.SuccessKilledDao;
import cn.ctgu.seckill.dao.cache.RedisDao;
import cn.ctgu.seckill.domain.Seckill;
import cn.ctgu.seckill.domain.SuccessKilled;
import cn.ctgu.seckill.dto.Exposer;
import cn.ctgu.seckill.dto.SeckillExecution;
import cn.ctgu.seckill.enums.SeckillStatEnum;
import cn.ctgu.seckill.exception.RepeatKillException;
import cn.ctgu.seckill.exception.SeckillCloseException;
import cn.ctgu.seckill.exception.SeckillException;
import cn.ctgu.seckill.service.SeckillService;
import org.apache.commons.collections.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;


import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class SeckillServiceImpl implements SeckillService{

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

    //注入service依赖
    @Autowired
    private SeckillDao seckillDao;

    @Autowired
    private SuccessKilledDao successKilledDao;

    @Autowired
    private RedisDao redisDao;

    //md5盐值字符串,用于混淆md5
    private final String slat="faldksjfhaehfasje";

    public List getSeckillList() {
        return seckillDao.queryAll(0,4);
    }

    public Seckill getById(long seckillId) {
        return seckillDao.queryById(seckillId);
    }

    public Exposer exportSeckillUrl(long seckillId) {
        //缓存优化:超时的基础上维护一致性
        /*
        *1、访问redis
        * */
        Seckill seckill=redisDao.getSeckill(seckillId);

        if(seckill==null){
            //2、访问数据库
            seckill=seckillDao.queryById(seckillId);
            if(seckill==null){
                return new Exposer(false,seckillId);
            }else{
                //3、放入redis
                redisDao.putSeckill(seckill);
            }

        }
        Date startTime=seckill.getStartTime();
        Date endTime=seckill.getEndTime();
        //系统当前时间
        Date nowTime=new Date();
        if(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);

    }
    private String getMD5(long seckillId){
        String base=seckillId+"/"+slat;
        String md5= DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }
    @Transactional
    /*
    * 使用注解控制事务方法的优点:
    * 1、开发团队达成一致约定,明确事务方法的编程风格
    * 2、保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部
    * 3、不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制
    *
    * */
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
            throws SeckillException, RepeatKillException, SeckillCloseException {
        if(md5==null || md5.equals(getMD5(seckillId))){
            throw new SeckillException("seckill data rewrite");
        }
        //执行秒杀逻辑:减库存+记录购买行为
        Date nowTime=new Date();

        try {
            //记录购买行为
            int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
            //唯一:seckillId,userPhone
            if (insertCount <= 0) {
                //重复秒杀
                throw new RepeatKillException("seckilled repeat");
            } else {
                //减库存,热点商品的竞争
                int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
                if (updateCount <= 0) {
                    //没有更新到记录,秒杀结束,rollback
                    throw new SeckillCloseException("seckilled is closed");
                } else {
                    //秒杀成功,commit
                    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());
        }
    }

    @Override
    public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
        if(md5==null||!md5.equals(getMD5(seckillId))){
            return new SeckillExecution(seckillId,SeckillStatEnum.DATA_REWRITE);
        }
        Date killTime=new Date();
        Map map=new HashMap();
        map.put("seckillId",seckillId);
        map.put("phone",userPhone);
        map.put("killTime",killTime);
        map.put("result",null);
        //执行存储过程,result被赋值
        try{
            seckillDao.killByProcedure(map);
            //获取result
            int result= MapUtils.getInteger(map,"reuslt",-2);
            if(result==1){
                SuccessKilled sk=successKilledDao.
                        queryByIdWithSeckill(seckillId,userPhone);
                return new SeckillExecution(seckillId,SeckillStatEnum.SUCCESS,sk);
            }else{
                return new SeckillExecution(seckillId,SeckillStatEnum.stateOf(result));
            }
        }catch (Exception e){
            logger.error(e.getMessage(),e);
            return new SeckillExecution(seckillId,SeckillStatEnum.INNER_ERROR);
        }


    }

}

2.3 Web层实现

整合SpringMVC ,配置web.xml

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                      http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         version="3.1"
         metadata-complete="true">

    
    
    <servlet>
        <servlet-name>seckill-dispatcherservlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
        
        <init-param>
            <param-name>contextConfigLocationparam-name>
            <param-value>classpath:spring/spring-*.xmlparam-value>
        init-param>
    servlet>

    <servlet-mapping>
        <servlet-name>seckill-dispatcherservlet-name>
        
        <url-pattern>/url-pattern>
    servlet-mapping>

web-app>

resources/spring下创建spring-web.xml文件


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       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/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    
    
    
    <mvc:annotation-driven />

    
    <mvc:default-servlet-handler />

    
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass"  value="org.springframework.web.servlet.view.JstlView"/>
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    bean>

    
    <context:component-scan base-package="com.seckill.web" />
beans>

接口设计,秒杀功能的流程:秒杀接口暴露 -> 执行秒杀 -> 相关查询,秒杀API的URL设计如下:
Java高并发系统设计及其优化策略——秒杀系统(一)_第12张图片

SeckillController.java

package cn.ctgu.seckill.web;

import cn.ctgu.seckill.domain.Seckill;
import cn.ctgu.seckill.dto.Exposer;
import cn.ctgu.seckill.dto.SeckillExecution;
import cn.ctgu.seckill.dto.SeckillResult;
import cn.ctgu.seckill.enums.SeckillStatEnum;
import cn.ctgu.seckill.exception.RepeatKillException;
import cn.ctgu.seckill.exception.SeckillCloseException;
import cn.ctgu.seckill.service.SeckillService;
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.web.bind.annotation.*;

import java.util.Date;
import java.util.List;

@Controller
@RequestMapping("/seckill")//url:模块/资源/{id}/细分/seckill/list
public class SeckillController {

    private final Logger logger= LoggerFactory.getLogger(this.getClass());
    @Autowired
    private SeckillService seckillService;

    @RequestMapping(value="list",method= RequestMethod.GET)
    public String list(Model model){
        //获取列表页
        List list=seckillService.getSeckillList();
        model.addAttribute("list",list);
        //list.jsp+model=ModelAndView
        return "list";//返回的是/WEB-INF/jsp/"list".jsp
    }

    @RequestMapping(value="/{seckillId}/detail",method=RequestMethod.GET)
    public String detail(@PathVariable("seckill")Long seckiiId,Model model){
        if(seckiiId==null){
            return "redirect:/seckill/list";
        }
        Seckill seckill=seckillService.getById(seckiiId);
        if(seckill==null){
            return "forward:/seckill/list";
        }
        model.addAttribute("seckill",seckill);
        return "detail";
    }

    //返回ajax json
    @RequestMapping(value="/{seckillId}/exporder",
            method = RequestMethod.GET,
            produces = {"application/json;charset=UTF-8"})
    @ResponseBody
    public SeckillResultexporser(Long seckillId) {
        SeckillResult result;
        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;
    }

    //执行秒杀
    @ResponseBody
    @RequestMapping(value="/{seckillId}/{md5}/execution",
                method = RequestMethod.POST,
                produces = {"application/json;charset=UTF-8"})
    public SeckillResultexecute(@PathVariable("seckillId")Long seckillId,
                                                  @PathVariable("md5")String md5,
                                                  @CookieValue(value = "killPhone",required = false) Long phone){
        if(phone==null){
            return new SeckillResult(false,"未注册");
        }
        SeckillResultresult;
        try{
            //存储过程调用
            SeckillExecution execution=seckillService.executeSeckillProcedure(seckillId,phone,md5);
            return new SeckillResult(true,execution);
        }catch (RepeatKillException e){
            SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
            return new SeckillResult(true,execution);
        }catch (SeckillCloseException e){
            SeckillExecution execution = new SeckillExecution(seckillId,SeckillStatEnum.END);
            return new SeckillResult(true,execution);
        }catch (Exception e){
            logger.error(e.getMessage(),e);
            SeckillExecution execution = new SeckillExecution(seckillId,SeckillStatEnum.INNER_ERROR);
            return new SeckillResult(true,execution);
        }


    }

    //返回当前时间
    @ResponseBody
    @RequestMapping(value="/time/now",method=RequestMethod.GET)
    public SeckillResulttime(){
        Date now=new Date();
        return new SeckillResult(true,now.getTime());
    }

}

其他代码
domain层
Seckill.java

package cn.ctgu.seckill.domain;

import java.util.Date;

public class Seckill {
    private long seckillId;
    private String name;
    private int number;
    private Date startTime;
    private Date endTime;
    private Date createTime;

    public long getSeckillId() {
        return seckillId;
    }

    public void setSeckillId(long seckillId) {
        this.seckillId = seckillId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    public Date getStartTime() {
        return startTime;
    }

    public void setStartTime(Date startTime) {
        this.startTime = startTime;
    }

    public Date getEndTime() {
        return endTime;
    }

    public void setEndTime(Date endTime) {
        this.endTime = endTime;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }

    @Override
    public String toString() {
        return "Seckill{" +
                "seckillId=" + seckillId +
                ", name='" + name + '\'' +
                ", number=" + number +
                ", startTime=" + startTime +
                ", endTime=" + endTime +
                ", createTime=" + createTime +
                '}';
    }
}

SuccessKilled.java

package cn.ctgu.seckill.domain;

import java.util.Date;

public class SuccessKilled {
    private long seckillId;
    private long userPhone;
    private short state;
    private Date createTime;

    //多对一
    private Seckill seckill;

    public Seckill getSeckill() {
        return seckill;
    }

    public void setSeckill(Seckill seckill) {
        this.seckill = seckill;
    }

    public long getSeckillId() {
        return seckillId;
    }

    public void setSeckillId(long seckillId) {
        this.seckillId = seckillId;
    }

    public long getUserPhone() {
        return userPhone;
    }

    public void setUserPhone(long userPhone) {
        this.userPhone = userPhone;
    }

    public short getState() {
        return state;
    }

    public void setState(short state) {
        this.state = state;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}

dto层
Exposer.java

package cn.ctgu.seckill.dto;
/*
*
* 暴露秒杀地址DTO
*
* */
public class Exposer {
    //是否开启秒杀
    private boolean exposed;

    //一种机密措施
    private String md5;

    //id
    private long seckillId;

    //系统当前时间(毫秒)
    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 seckillId, long now, long start, long end) {
        this.exposed = exposed;
        this.seckillId=seckillId;
        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 String getMd5() {
        return md5;
    }

    public void setMd5(String md5) {
        this.md5 = md5;
    }

    public long getSeckillId() {
        return seckillId;
    }

    public void setSeckillId(long seckillId) {
        this.seckillId = seckillId;
    }

    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;
    }
}

SeckillExecution.java

package cn.ctgu.seckill.dto;

import cn.ctgu.seckill.domain.SuccessKilled;
import cn.ctgu.seckill.enums.SeckillStatEnum;
import com.sun.net.httpserver.Authenticator;

/*
*
* 封装秒杀执行结果
*
* */
public class SeckillExecution {

    private long seckillId;

    //秒杀执行结果状态
    private int state;

    //状态表示
    private String stateInfo;

    //秒杀成功对象
    private SuccessKilled successKilled;

    public SeckillExecution(long seckillId, SeckillStatEnum statEnum,int state, String stateInfo, SuccessKilled successKilled) {
        this.seckillId = seckillId;
        this.state = statEnum.getState();
        this.stateInfo = statEnum.getStateInfo();
        this.successKilled = successKilled;
    }

    public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled stateInfo) {
        this.seckillId = seckillId;
        this.state = statEnum.getState();
        this.stateInfo = statEnum.getStateInfo();
    }

    public SeckillExecution(long seckillId, SeckillStatEnum statEnum) {
        this.seckillId = seckillId;
        this.state = statEnum.getState();
    }

    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;
    }
}

SeckillResult.java

package cn.ctgu.seckill.dto;

//所有的ajax请求返回类型的结果时封装json结果
public class SeckillResult {
    private boolean success;
    private T data;
    private String error;

    public SeckillResult(boolean success, T data) {
        this.success = success;
        this.data = data;
    }

    public SeckillResult(boolean success, String error) {
        this.success = success;
        this.error = error;
    }

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public String getError() {
        return error;
    }

    public void setError(String error) {
        this.error = error;
    }
}

enums层
SeckillStatEnum.java

package cn.ctgu.seckill.enums;
/*
*
* 使用枚举表述常量数据字段
*
* */
public enum  SeckillStatEnum {
    SUCCESS(1,"秒杀成功"),
    END(0,"秒杀结束"),
    REPEAT_KILL(-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;
    }


}

Exception层
RepeatKillExecption.java

package cn.ctgu.seckill.exception;

/*
*
* 重复秒杀异常(运行时期异常)
*
* */
public class RepeatKillException extends SeckillException{
    public RepeatKillException(String message) {
        super(message);
    }

    public RepeatKillException(String message, Throwable cause) {
        super(message, cause);
    }
}

SeckillCloseExecption.java

package cn.ctgu.seckill.exception;

/*
*
* 秒杀关闭异常
*
* */
public class SeckillCloseException extends SeckillException{
    public SeckillCloseException(String message) {
        super(message);
    }

    public SeckillCloseException(String message, Throwable cause) {
        super(message, cause);
    }
}

SeckillExecption.java

package cn.ctgu.seckill.exception;
/*
*
* 秒杀相关的业务异常
*
* */
public class SeckillException extends RuntimeException{
    public SeckillException(String message) {
        super(message);
    }

    public SeckillException(String message, Throwable cause) {
        super(message, cause);
    }
}

2.4 前端页面
引入Bootstrap,通过cdn的方式引入Bootstrap相关的文件


<script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.min.js">script>

<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js">script>

倒计时插件


<script src="https://cdn.bootcss.com/jquery.countdown/2.2.0/jquery.countdown.js">script>

cookie插件


<script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.js">script>

list.jsp

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


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

<html>
<head>
    <title>秒杀列表页title>
    <%@include file="common/head.jsp" %>
head>
<body>

<div class="container">
    <div class="panel panel-default">
        <div class="panel-heading text-center">
            <h1>秒杀列表h1>
        div>
        <div class="panel-body">
            <table class="table table-hover">
                <thead>
                <tr>
                    <td>名称td>
                    <td>库存td>
                    <td>开始时间td>
                    <td>结束时间td>
                    <td>创建时间td>
                    <td>详情页td>
                tr>
                thead>

                <tbody>
                <c:forEach var="sk" items="${list}">
                    <tr>
                        <td>${sk.name}td>
                        <td>${sk.number}td>
                        <td>
                            <fmt:formatDate value="${sk.startTime}" pattern="yyyy-MM-dd HH:mm:ss"/>
                        td>
                        <td>
                            <fmt:formatDate value="${sk.endTime}" pattern="yyyy-MM-dd HH:mm:ss"/>
                        td>
                        <td>
                            <fmt:formatDate value="${sk.createTime}" pattern="yyyy-MM-dd HH:mm:ss"/>
                        td>
                        <td>
                            <a class="btn btn-info" href="/seckill/${sk.seckillId}/detail" target="_blank">linka>
                        td>
                    tr>
                c:forEach>

                tbody>
            table>
        div>
    div>
div>
body>

<script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.min.js">script>

<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js">script>

html>

detail.jsp

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


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

<html>
<head>
    <title>秒杀详情页title>
    <%@include file="common/head.jsp" %>
head>
<body>

<div class="container">
    <div class="panel panel-default text-center">
        <div class="panel-heading ">
            <h1>${seckill.name}h1>
        div>

        <div class="panel-body">
            <h2 class="text-danger">
                
                <span class="glyphicon glyphicon-time">span>
                
                <span class="glyphicon" id="seckill-box">span>
            h2>
        div>
    div>
div>


<div id="killPhoneModal" class="modal fade">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h3 class="modal-title text-center">
                    <span class="glyphicon glyphicon-phone">span>秒杀电话:
                h3>
            div>

            <div class="modal-body">
                <div class="row">
                    <div class="col-xs-8 col-xs-offset-2">
                        <input type="text" name="killPhone" id="killPhoneKey" placeholder="填写手机号" class="form-control"/>
                    div>
                div>
            div>

            <div class="modal-footer">
                
                <span id="killPhoneMessage" class="glyphicon">span>
                <button type="button" id="killPhoneBtn" class="btn btn-success">
                    <span class="glyphicon glyphicon-phone">span>Submit
                button>
            div>
        div>
    div>
div>

body>

<script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.min.js">script>

<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js">script>



<script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.js">script>


<script src="https://cdn.bootcss.com/jquery.countdown/2.2.0/jquery.countdown.js">script>


<script type="text/javascript" src="/resources/script/seckill.js">script>
<script type="text/javascript">
    $(function () {
        //使用EL表达式传入参数
        seckill.detail.init({
            seckillId: ${seckill.seckillId},
            startTime: ${seckill.startTime.time},   //毫秒
            endTime:   ${seckill.endTime.time}
        });
    });
script>

html>

seckill.js

//存放主要交互逻辑js代码
//javascript 模块化
var seckill = {
    //封装秒杀相关ajax的URL
    URL : {
        now : function () {
            return '/seckill/time/now';
        },
        exposer: function (seckillId) {
            return '/seckill/' + seckillId + "/exposer";
        },
        execution:function (seckillId,md5) {
            return '/seckill/' + seckillId + '/' + md5 + "/execution";
        }
    },
    //处理秒杀逻辑
    handleSeckill : function (seckillId,node) {
        node.hide().html('');
        $.post(seckill.URL.exposer(seckillId),{},function (result) {
            //在回调函数中,执行交互流程
            if(result && result['success']){
                var exposer = result['data'];
                console.log(exposer);
                if(exposer['exposed']){
                    //开启秒杀,获取秒杀地址
                    var md5 = exposer['md5'];
                    var killUrl = seckill.URL.execution(seckillId,md5);
                    console.log("killUrl:" + killUrl);
                    //绑定一次点击事件
                    $("#killBtn").one('click',function () {
                        //执行秒杀请求操作
                        //1.先禁用按钮
                        $(this).addClass('disabled');
                        //2.发送秒杀请求执行秒杀
                        $.post(killUrl,{},function (result) {
                            if(result && result['success']){
                                var killResult = result['data'];
                                var state = killResult['state'];
                                var stateInfo = killResult['stateInfo'];
                                //3.显示秒杀结果
                                node.html('' + stateInfo + '');
                            }
                        })
                    });

                    node.show();
                }else{
                    //未开启秒杀
                    var now = exposer['now'];
                    var start = exposer['start'];
                    var end = exposer['end'];
                    //重新计算计时逻辑
                    seckill.countdown(seckillId,now,start,start,end);
                }
            }else{
                console.log('result:' + result);
            }
        });
    },
    //验证手机号
    validatePhone:function (phone) {
        if(phone && phone.length == 11 && !isNaN(phone)){
            return true;
        }else{
            return false;
        }
    },
    countdown:function (seckillId,nowTime,startTime,endTime) {
        var seckillBox = $("#seckill-box");
        //时间判断
        if(nowTime > endTime){
            //秒杀结束
            seckillBox.html("秒杀结束!");
        }else if(nowTime < startTime){
            //秒杀未开始,计时事件绑定
            var killTime = new Date(startTime + 1000);
            console.log('nowTime:' + nowTime + ' ,startTime:' + startTime + ' ,endTime:'+endTime);
            seckillBox.countdown(killTime,function (event) {
               //时间格式
               var format = event.strftime('秒杀计时: %D天 %H时 %M分 %S秒');
               seckillBox.html(format);
               //时间完成后回调时间
            }).on('finish.countdown',function () {
                //获取秒杀地址,控制实现逻辑,执行秒杀
                seckill.handleSeckill(seckillId,seckillBox);
            });

        }else{
            //秒杀开始
            console.log('已开始');
            seckill.handleSeckill(seckillId,seckillBox);
        }
    },
    //详情页秒杀逻辑
    detail: {
        //详情页初始化
        init : function (params) {
            //手机验证和登录,计时交互 - 规划交互流程
            //在cookie中查找手机号
            var killPhone = $.cookie('killPhone');

            //验证手机号,判断用户是否登录
            if (!seckill.validatePhone(killPhone)) {
                //绑定phone
                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);
                    }
                });
            }

            //已经登录
            //计时交互
            var startTime = params['startTime'];
            var endTime = params['endTime'];
            var seckillId = params['seckillId'];
            $.get(seckill.URL.now(),{},function (result) {
                if(result && result['success']){
                    var nowTime = result['data'];
                    //时间判断,计时交互
                    seckill.countdown(seckillId,nowTime,startTime,endTime);
                }else{
                    console.log('result:' + result);
                }
            })
        }
    }
}

3、系统演示

秒杀列表页面
Java高并发系统设计及其优化策略——秒杀系统(一)_第13张图片

输入手机号
Java高并发系统设计及其优化策略——秒杀系统(一)_第14张图片
系统没有做登录页面,此处使用本地cookie作为替代方案。如果cookie中没有手机号,则在弹出页面填写正确手机号就可以进入秒杀页面。

秒杀成功页面
Java高并发系统设计及其优化策略——秒杀系统(一)_第15张图片

秒杀未开始页面
Java高并发系统设计及其优化策略——秒杀系统(一)_第16张图片

秒杀还未开始时通过插件显示倒计时,倒计时时间以服务器时间为准。

资源地址
GitHub代码:秒杀系统代码

你可能感兴趣的:(java,项目)