UDN-企业互联网技术人气社区

板块导航

浏览  : 803
回复  : 1

[资讯] SSM实现秒杀系统案例

[复制链接]
瞌睡虫的头像 楼主
发表于 2016-11-2 10:55:39 | 显示全部楼层 |阅读模式
    好久没写博客了,因为一直在忙项目和其他工作中的事情,最近有空,刚好看到了一个秒杀系统的设计,感觉还是非常不错的一个系统,于是在这里分享一下。
秒杀场景主要两个点:
1:流控系统,防止后端过载或不必要流量进入,因为慕课要求课程的长度和简单性,没有加。
2:减库存竞争,减库存的update必然涉及exclusive lock ,持有锁的时间越短,并发性越高。

    对于抢购系统来说,首先要有可抢购的活动,而且这些活动具有促销性质,比如直降500元。其次要求可抢购的活动类目丰富,用户才有充分的选择性。马上就双十一了,用户剁手期间增量促销活动量非常多,可能某个活动力度特别大,大多用户都在抢,必然对系统是一个考验。这样抢购系统具有秒杀特性,并发访问量高,同时用户也可选购多个限时抢商品,与普通商品一起进购物车结算。这种大型活动的负载可能是平时的几十倍,所以通过增加硬件、优化瓶颈代码等手段是很难达到目标的,所以抢购系统得专门设计。
在这里以秒杀单个功能点为例,以ssm框架+MySQL+Redis等技术来说明。

一、数据库设计
    使用mysql数据库:这里主要是两个表,主要是一个商品表和一个购买明细表,在这里用户的购买信息的登录注册这里就不做了,用户购买时需要使用手机号码来进行秒杀操作,购买成功使用的是商品表id和购买明细的用户手机号码做为双主键。

[sql] view plain copy print?在CODE上查看代码片派生到我的代码片
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=utf8 COMMENT '秒杀库存表'  


--初始化数据  
INSERT INTO seckill(NAME,number,start_time,end_time)  
VALUES  
('4000元秒杀ipone7',300,'2016-11-5 00:00:00','2016-11-6 00:00:00'),  
('3000元秒杀ipone6',200,'2016-11-5 00:00:00','2016-11-6 00:00:00'),  
('2000元秒杀ipone5',100,'2016-11-5 00:00:00','2016-11-6 00:00:00'),  
('1000元秒杀小米5',100,'2016-11-5 00:00:00','2016-11-6 00:00:00');  

--秒杀成功明细表  
--用户登录认证相关的信息  
CREATE TABLE success_kill(  
    seckill_id  BIGINT NOT NULL AUTO_INCREMENT 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 AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT '秒杀成功明细表'  

SELECT * FROM seckill;  
SELECT * FROM success_kill;  

这里我们还用到了一个存储过程,所以我们新建一个存储过程来处理,对于近日产品公司来说存储过程的使用还是比较多的,所以存储过程也还是要会写的。
[sql] view plain copy print?在CODE上查看代码片派生到我的代码片
DELIMITER $$  

CREATE PROCEDURE seckill.execute_seckill  
  (IN v_seckill_id BIGINT, IN v_phone BIGINT,  
   IN v_kill_time TIMESTAMP, OUT r_result INT)  
  BEGIN  
    DECLARE insert_count INT DEFAULT 0;  
    START TRANSACTION;  
    INSERT IGNORE INTO success_kill(seckill_id,user_phone,create_time,state)  
        VALUE(v_seckill_id,v_phone,v_kill_time,0);  
    SELECT ROW_COUNT() INTO insert_count;  
    IF(insert_count = 0) THEN  
       ROLLBACK;  
       SET r_result = -1;  
    ELSEIF(insert_count < 0) THEN  
       ROLLBACK;  
       SET r_result = -2;  
    ELSE  
       UPDATE seckill  
       SET number = number - 1  
       WHERE seckill_id = v_seckill_id  
         AND end_time > v_kill_time  
         AND start_time < v_kill_time  
         AND number > 0;  
       SELECT ROW_COUNT() INTO insert_count;  
       IF(insert_count = 0) THEN  
         ROLLBACK;  
         SET r_result = 0;  
       ELSEIF (insert_count < 0) THEN  
          ROLLBACK;  
          SET r_result = -2;  
        ELSE   
          COMMIT;  
          SET r_result = 1;  
        END IF;   
    END IF;  
   END;  
$$  

DELIMITER ;  

SET @r_result = -3;  
CALL execute_seckill(1000,13813813822,NOW(),@r_result);  
SELECT @r_result;  

先可以看一下页面的展示情况:


二、实体类
因为我们有两个表,所以自然建两个实体bean啦!新建一个Seckill.Java

[java] view plain copy print?在CODE上查看代码片派生到我的代码片
private long seckillId;  
private String name;  
private int number;  
private Date startTime;  
private Date endTime;  
private Date createTime;  

实现其getter/setter方法。

再新建一个SuccessKill。
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
private long seckillId;  
    private long userPhone;  
    private short state;  
    private Date createTime;  

    private Seckill seckill;  

实现其getter/setter方法。

三、DAO接口层
接口我们也是来两个:SeckillDao.java和SuccessKillDao.java
内容分别为:
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
public interface SeckillDao {  

    //减库存  
    int reduceNumber(@Param("seckillId")long seckillId,@Param("killTime")Date killTime);  

    Seckill queryById(long seckilled);  

    List<Seckill>  queryAll(@Param("offset") int offset,@Param("limit") int limit);  
     public void seckillByProcedure(Map<String, Object> paramMap);  
}  

[java] view plain copy print?在CODE上查看代码片派生到我的代码片
public interface SuccessKillDao {  

    /**
     * 插入购买明细
     *  
     * @param seckillId
     * @param userPhone
     * @return
     */  
    int insertSuccessKill(@Param("seckillId")long seckillId,@Param("userPhone")long userPhone);  

    /**
     * 根据id查询
     *  
     * @param seckill
     * @return
     */  
    SuccessKill  queryByIdWithSeckill(@Param("seckillId")long seckillId,@Param("userPhone")long userPhone);  


}  

四、mapper处理
在mybatis中对上面的接口进行实现,这里可以通过mybatis来实现。
[html] view plain copy print?在CODE上查看代码片派生到我的代码片
<mapper  namespace="cn.tf.seckill.dao.SeckillDao">  
        <update id="reduceNumber"  >  
        update  seckill set number=number-1  where seckill_id=#{seckillId}  
        and start_time <![CDATA[<=]]>#{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="seckillByProcedure" 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>  

[html] view plain copy print?在CODE上查看代码片派生到我的代码片
<mapper namespace="cn.tf.seckill.dao.SuccessKillDao">  
    <insert id="insertSuccessKill">  
            insert ignore into success_kill(seckill_id,user_phone,state)  
            values (#{seckillId},#{userPhone},0)  
    </insert>  

  <select id="queryByIdWithSeckill"  resultType="SuccessKill">  
        select   
            sk.seckill_id,  
            sk.user_phone,  
            sk.create_time,  
            sk.state,  
            s.seckill_id   "seckill.seckill_id",  
            s.name   "seckill.name",  
            s.number  "seckill.number",  
            s.start_time  "seckill.start_time",  
            s.end_time  "seckill.end_time",  
            s.create_time  "seckill.create_time"      
        from success_kill sk  
        inner join seckill s on sk.seckill_id=s.seckill_id  
        where sk.seckill_id=#{seckillId} and sk.user_phone=#{userPhone}   

  </select>  


</mapper>  

五、redis缓存处理

   在这里我们说的库存不是真正意义上的库存,其实是该促销可以抢购的数量,真正的库存在基础库存服务。用户点击『提交订单』按钮后,在抢购系统中获取了资格后才去基础库存服务中扣减真正的库存;而抢购系统控制的就是资格/剩余数。传统方案利用数据库行锁,但是在促销高峰数据库压力过大导致服务不可用,目前采用redis集群(16分片)缓存促销信息,例如促销id、促销剩余数、抢次数等,抢的过程中按照促销id散列到对应分片,实时扣减剩余数。当剩余数为0或促销删除,价格恢复原价。

这里使用的是redis来进行处理。这里使用的是序列化工具RuntimeSchema。
在pom.xml中配置如下:
[html] view plain copy print?在CODE上查看代码片派生到我的代码片
<dependency>  
            <groupId>redis.clients</groupId>  
            <artifactId>jedis</artifactId>  
            <version>2.7.2</version>  
        </dependency>  

        <dependency>  
            <groupId>com.dyuproject.protostuff</groupId>  
            <artifactId>protostuff-API</artifactId>  
            <version>1.0.8</version>  
        </dependency>  
        <dependency>  
            <groupId>com.dyuproject.protostuff</groupId>  
            <artifactId>protostuff-core</artifactId>  
            <version>1.0.8</version>  
        </dependency>  
        <dependency>  
            <groupId>com.dyuproject.protostuff</groupId>  
            <artifactId>protostuff-runtime</artifactId>  
            <version>1.0.8</version>  
        </dependency>  



    然后我们引入之后,直接在这个dao中进行处理即可,就是把数据从redis中读取出来以及把数据存到redis中,如果redis中有这个数据就直接读,如果没有就存进去。

[java] view plain copy print?在CODE上查看代码片派生到我的代码片
public class RedisDao {  

    private Logger logger = LoggerFactory.getLogger(this.getClass());  
    private JedisPool jedisPool;  
    private int port;  
    private String ip;  

    public RedisDao(String ip, int port) {  
        this.port = port;  
        this.ip = ip;  
    }  

    //Serialize function  
    private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);  

    public Seckill getSeckill(long seckillId) {  
        jedisPool = new JedisPool(ip, port);  
        //redis operate  
        try {  
            Jedis jedis = jedisPool.getResource();  
            try {  
                String key = "seckill:" + seckillId;  
                //由于redis内部没有实现序列化方法,而且jdk自带的implaments Serializable比较慢,会影响并发,因此需要使用第三方序列化方法.  
                byte[] bytes = jedis.get(key.getBytes());  
                if(null != bytes){  
                    Seckill seckill = schema.newMessage();  
                    ProtostuffIOUtil.mergeFrom(bytes,seckill,schema);  
                    //reSerialize  
                    return seckill;  
                }  
            } finally {  
                jedisPool.close();  
            }  
        } catch (Exception e) {  
            logger.error(e.getMessage(),e);  
        }  

        return null;  
    }  

    public String putSeckill(Seckill seckill) {  
        jedisPool = new JedisPool(ip, port);  
        //set Object(seckill) ->Serialize -> byte[]  
        try{  
            Jedis jedis = jedisPool.getResource();  
            try{  
                String key = "seckill:"+seckill.getSeckillId();  
                byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));  
                //time out  cache  
                int timeout = 60*60;  
                String result = jedis.setex(key.getBytes(),timeout,bytes);  
                return result;  
            }finally {  
                jedisPool.close();  
            }  
        }catch (Exception e){  
            logger.error(e.getMessage(),e);  
        }  
        return null;  
    }  
}  

还需要在spring中进行配置:我这里的地址使用的是我服务器的地址。
[html] view plain copy print?在CODE上查看代码片派生到我的代码片
<bean id="redisDao" class="cn.tf.seckill.dao.cache.RedisDao">  
    <constructor-arg index="0" value="115.28.16.234"></constructor-arg>  
       <constructor-arg index="1" value="6379"></constructor-arg>  
</bean>   



六、service接口及其实现
    接下来就是service的处理了。这里主要是由两个重要的业务接口。
1、暴露秒杀 和 执行秒杀 是两个不同业务,互不影响  2、暴露秒杀 的逻辑可能会有更多变化,现在是时间上达到要求才能暴露,说不定下次加个别的条件才能暴露,基于业务耦合度考虑,分开比较好。3、重新更改暴露秒杀接口业务时,不会去影响执行秒杀接口,对于测试都是有好处的。。。另外 不好的地方是前端需要调用两个接口才能执行秒杀。

[java] view plain copy print?在CODE上查看代码片派生到我的代码片
//从使用者角度设计接口,方法定义粒度,参数,返回类型  
public interface SeckillService {  

    List<Seckill>  getSeckillList();  

    Seckill getById(long seckillId);  
    //输出秒杀开启接口地址  
    Exposer  exportSeckillUrl(long seckillId);  

    /**
     * 执行描述操作
     *  
     * @param seckillId
     * @param userPhone
     * @param md5
     */  
    SeckillExecution executeSeckill(long seckillId,long userPhone,String md5)  throws SeckillCloseException,RepeatKillException,SeckillException;  
      /**
     * 通过存储过程执行秒杀
     * @param seckillId
     * @param userPhone
     * @param md5  
     */  
    SeckillExecution executeSeckillByProcedure(long seckillId, long userPhone, String md5);   


}   



实现的过程就比较复杂了,这里加入了前面所说的存储过程还有redis缓存。这里做了一些异常的处理,以及数据字典的处理。
[java] view plain copy print?在CODE上查看代码片派生到我的代码片
@Service  
public class SeckillServiceImpl implements SeckillService{  

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

    @Autowired  
    private SeckillDao  seckillDao;  
    @Autowired  
    private SuccessKillDao successKillDao;  
     @Autowired  
    private RedisDao redisDao;  

    //加盐处理  
    private final String slat="xvzbnxsd^&&*)(*()kfmv4165323DGHSBJ";  


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

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


    public Exposer exportSeckillUrl(long seckillId) {  

        //优化点:缓存优化  

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


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


    @Transactional  
    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 = successKillDao.insertSuccessKill(seckillId, userPhone);  
            if (insertCount <= 0) {  
                //重复秒杀  
                throw new RepeatKillException("Seckill repeated");  
            } else {  
                //减库存  
                int updateCount = seckillDao.reduceNumber(seckillId, nowTime);  
                if (updateCount <= 0) {  
                    //没有更新到记录,秒杀结束  
                    throw new SeckillCloseException("Seckill is closed");  
                } else {  
                    //秒杀成功  
                    SuccessKill successKilled = successKillDao.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());  
            //所有编译期异常转换为运行时异常  
            throw new SeckillException("Seckill inner error" + e.getMessage());  
        }  

    }  

    /**
     * @param seckillId
     * @param userPhone
     * @param md5
     * @return
     * @throws SeckillException
     * @throws RepeteKillException
     * @throws SeckillCloseException
     */  
    public SeckillExecution executeSeckillByProcedure(long seckillId, long userPhone, String md5) {  
        if (md5 == null || (!md5.equals(getMD5(seckillId)))) {  
            throw new SeckillException("Seckill data rewrite");  
        }  
        Date killTime = new Date();  
        Map<String, Object> map = new HashMap<String, Object>();  
        map.put("seckillId", seckillId);  
        map.put("phone", userPhone);  
        map.put("killTime", killTime);  
        map.put("result", null);  
        //执行存储过程,result被赋值  
        try {  
            seckillDao.seckillByProcedure(map);  
            //获取result  
            int result = MapUtils.getInteger(map, "result", -2);  
            if (result == 1) {  
                SuccessKill successKilled = successKillDao.queryByIdWithSeckill(seckillId, userPhone);  
                return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);  
            } else {  
                return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));  
            }  
        } catch (Exception e) {  
            logger.error(e.getMessage(), e);  
            return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);  
        }  
    }  

    private String getMD5(long seckillId) {  
        String base = seckillId + "/" + slat;  
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());  
        return md5;  
    }  
}  


七、Controller层处理
在springMVC中,是基于restful风格来对访问地址进行处理,所以我们在控制层也这样进行处理。

[java] view plain copy print?在CODE上查看代码片派生到我的代码片
@Controller  
@RequestMapping("/seckill")  
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<Seckill> 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> exposer(@PathVariable("seckillId") Long seckillId){  
            SeckillResult<Exposer> result;  
            try {  
                Exposer exposer = seckillService.exportSeckillUrl(seckillId);  
                result = new SeckillResult<Exposer>(true,exposer);  
            } catch (Exception e) {  
                result = new SeckillResult<Exposer>(false, e.getMessage());  
            }  
            return result;  
        }  

        @RequestMapping(value = "/{seckillId}/{md5}/execution",  
                        method = RequestMethod.POST,  
                        produces = {"application/json;charset=UTF-8"})  
        @ResponseBody  
        public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId")Long seckillId,  
                                                       @PathVariable("md5")String md5,   
                                                       @CookieValue(value = "killPhone", required = false)Long phone){  
            if(phone == null){  
                return new SeckillResult<>(false, "未注册");  
            }  

            try {  
                 SeckillExecution execution = seckillService.executeSeckillByProcedure(seckillId, phone, md5);  
                return new SeckillResult<SeckillExecution>(true, execution);  
            } catch (SeckillCloseException e) {  
                SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);  
                return new SeckillResult<SeckillExecution>(false, execution);  
            } catch (RepeatKillException e) {  
                SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END);  
                return new SeckillResult<SeckillExecution>(false, execution);  
            } catch (Exception e) {  
                logger.error(e.getMessage(), e);  
                SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);  
                return new SeckillResult<SeckillExecution>(false, execution);  
            }  
        }  

        @RequestMapping(value = "/time/now", method = RequestMethod.GET)  
        @ResponseBody  
        public SeckillResult<Long> time(){  
            Date now = new Date();  
            return new SeckillResult<>(true, now.getTime());  
        }  
}  

八、前台处理
后台数据处理完之后就是前台了,对于页面什么的就直接使用bootstrap来处理了,直接调用bootstrap的cdn链接地址。
页面的代码我就不贴出来了,可以到源码中进行查看,都是非常经典的几个页面。值得一提的是这个js的分模块处理。

[JavaScript] view plain copy print?在CODE上查看代码片派生到我的代码片
//存放主要交互逻辑的js代码  
// JavaScript 模块化(package.类.方法)  

var seckill = {  

    //封装秒杀相关ajax的url  
    URL: {  
        now: function () {  
            return '/SecKill/seckill/time/now';  
        },  
        exposer: function (seckillId) {  
            return '/SecKill/seckill/' + seckillId + '/exposer';  
        },  
        execution: function (seckillId, md5) {  
            return '/SecKill/seckill/' + seckillId + '/' + md5 + '/execution';  
        }  
    },  

    //验证手机号  
    validatePhone: function (phone) {  
        if (phone && phone.length == 11 && !isNaN(phone)) {  
            return true;//直接判断对象会看对象是否为空,空就是undefine就是false; isNaN 非数字返回true  
        } else {  
            return false;  
        }  
    },  

    //详情页秒杀逻辑  
    detail: {  
        //详情页初始化  
        init: function (params) {  
            //手机验证和登录,计时交互  
            //规划我们的交互流程  
            //在cookie中查找手机号  
            var killPhone = $.cookie('killPhone');  
            //验证手机号  
            if (!seckill.validatePhone(killPhone)) {  
                //绑定手机 控制输出  
                var killPhoneModal = $('#killPhoneModal');  
                killPhoneModal.modal({  
                    show: true,//显示弹出层  
                    backdrop: 'static',//禁止位置关闭  
                    keyboard: false//关闭键盘事件  
                });  

                $('#killPhoneBtn').click(function () {  
                    var inputPhone = $('#killPhoneKey').val();  
                    console.log("inputPhone: " + inputPhone);  
                    if (seckill.validatePhone(inputPhone)) {  
                        //电话写入cookie(7天过期)  
                        $.cookie('killPhone', inputPhone, {expires: 7, path: '/SecKill'});  
                        //验证通过  刷新页面  
                        window.location.reload();  
                    } else {  
                        //todo 错误文案信息抽取到前端字典里  
                        $('#killPhoneMessage').hide().html('<label class="label label-danger">手机号错误!</label>').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'];  

                    //解决计时误差  
                    var userNowTime = new Date().getTime();  
                    console.log('nowTime:' + nowTime);  
                    console.log('userNowTime:' + userNowTime);  

                    //计算用户时间和系统时间的差,忽略中间网络传输的时间(本机测试大约为50-150毫秒)  
                    var deviationTime = userNowTime - nowTime;  
                    console.log('deviationTime:' + deviationTime);  
                    //考虑到用户时间可能和服务器时间不一致,开始秒杀时间需要加上时间差  
                    startTime = startTime + deviationTime;  
                    //  


                    //时间判断 计时交互  
                    seckill.countDown(seckillId, nowTime, startTime, endTime);  
                } else {  
                    console.log('result: ' + result);  
                    alert('result: ' + result);  
                }  
            });  
        }  
    },  

    handlerSeckill: function (seckillId, node) {  
        //获取秒杀地址,控制显示器,执行秒杀  
        node.hide().html('<button class="btn btn-primary btn-lg" id="killBtn">开始秒杀</button>');  

        $.post(seckill.URL.exposer(seckillId), {}, function (result) {  
            //在回调函数种执行交互流程  
            if (result && result['success']) {  
                var exposer = result['data'];  
                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');//,<-$(this)===('#killBtn')->  
                        //2.发送秒杀请求执行秒杀  
                        $.post(killUrl, {}, function (result) {  
                            if (result && result['success']) {  
                                var killResult = result['data'];  
                                var state = killResult['state'];  
                                var stateInfo = killResult['stateInfo'];  
                                //显示秒杀结果  
                                node.html('<span class="label label-success">' + stateInfo + '</span>');  
                            }  
                        });  
                    });  
                    node.show();  
                } else {  
                    //未开启秒杀(由于浏览器计时偏差,以为时间到了,结果时间并没到,需要重新计时)  
                    var now = exposer['now'];  
                    var start = exposer['start'];  
                    var end = exposer['end'];  
                    var userNowTime = new Date().getTime();  
                    var deviationTime = userNowTime - nowTime;  
                    start = start + deviationTime;  
                    seckill.countDown(seckillId, now, start, end);  
                }  
            } else {  
                console.log('result: ' + result);  
            }  
        });  

    },  

    countDown: function (seckillId, nowTime, startTime, endTime) {  
        console.log(seckillId + '_' + nowTime + '_' + startTime + '_' + endTime);  
        var seckillBox = $('#seckill-box');  
        if (nowTime > endTime) {  
            //秒杀结束  
            seckillBox.html('秒杀结束!');  
        } else if (nowTime < startTime) {  
            //秒杀未开始,计时事件绑定  
            var killTime = new Date(startTime);//todo 防止时间偏移  
            seckillBox.countdown(killTime, function (event) {  
                //时间格式  
                var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒 ');  
                seckillBox.html(format);  
            }).on('finish.countdown', function () {  
                //时间完成后回调事件  
                //获取秒杀地址,控制现实逻辑,执行秒杀  
                console.log('______fininsh.countdown');  
                seckill.handlerSeckill(seckillId, seckillBox);  
            });  
        } else {  
            //秒杀开始  
            seckill.handlerSeckill(seckillId, seckillBox);  
        }  
    }  

}  

    用户秒杀之前需要先登记用户的手机号码,这个号码会保存在cookie中。
20161101170144887.png



    到了秒杀开始时间段,用户就可以点击按钮进行秒杀操作。
20161101165934091.png


    每个用户只能秒杀一次,不能重复秒杀,如果重复执行,会显示重复秒杀。
20161101165949403.png


秒杀倒计时:
20161101170002372.png


总结:
    其实在真实的秒杀系统中,我们是不直接对数据库进行操作的,我们一般是会放到redis中进行处理,企业的秒杀目前应该考虑使用redis,而不是mysql。其实高并发是个伪命题,根据业务场景,数据规模,架构的变化而变化。开发高并发相关系统的基础知识大概有:多线程,操作系统IO模型,分布式存储,负载均衡和熔断机制,消息服务,甚至还包括硬件知识。每块知识都需要一定的学习周期,需要几年的时间总结和提炼。

来源:开源中国

作者:朱培




相关帖子

发表于 2016-11-2 13:57:15 | 显示全部楼层
学习一下
使用道具 举报

回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部