1 需求分析和技术难点:
(1) 分析:
秒杀的时候:减少库存和购买记录明细两个事件保持在同一个事物中。
使用联合查询避免同一用户多次秒杀同一商品(利用在插入购物明细表中的秒杀id和用户的唯一标识来避免)。
(2) 秒杀难点:事务和行级锁的处理
(3) 实现那些秒杀系统(以天猫的秒杀系统为例)
(4) 我们如何实现秒杀功能?
① 秒杀接口暴漏
② 执行秒杀
③ 相关查询
下面我们以主要代码实现秒杀系统:
2.数据库设计和DAO层
(1) 数据库设计
-- 数据库初始化脚本
-- 创建数据库
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
('1000元秒杀iphone6',100,'2016-01-01 00:00:00','2016-01-02 00:00:00'),
('800元秒杀ipad',200,'2016-01-01 00:00:00','2016-01-02 00:00:00'),
('6600元秒杀mac book pro',300,'2016-01-01 00:00:00','2016-01-02 00:00:00'),
('7000元秒杀iMac',400,'2016-01-01 00:00:00','2016-01-02 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:已付款 2:已发货',
`create_time` TIMESTAMP NOT NULL COMMENT '创建时间',
PRIMARY KEY(seckill_id,user_phone),/*联合主键*/
KEY idx_create_time(create_time)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';
-- SHOW CREATE TABLE seckill;#显示表的创建信息
(2) Dao层和对应的实体
① Seckill.java
package com.force4us.entity;
import org.springframework.stereotype.Component;
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 com.force4us.entity;
import org.springframework.stereotype.Component;
import java.util.Date;
public class SuccessKilled {
private long seckillId;
private long userPhone;
private short state;
private Date createTime;
private 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;
}
public Seckill getSeckill() {
return seckill;
}
public void setSeckill(Seckill seckill) {
this.seckill = seckill;
}
@Override
public String toString() {
return "SuccessKilled{" +
"seckillId=" + seckillId +
", userPhone=" + userPhone +
", state=" + state +
", createTime=" + createTime +
", seckill=" + seckill +
'}';
}
}
③ SeckillDao
package com.force4us.dao;
import com.force4us.entity.Seckill;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
import java.util.Map;
public interface SeckillDao {
/**
* 减库存
* @param seckillId
* @param killTime
* @return 如果影响行数>1,表示更新库存的记录行数
*/
int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime);
/**
* 根据id查询秒杀的商品信息
* @param seckillId
* @return
*/
Seckill queryById(@Param("seckillId") long seckillId);
/**
* 根据偏移量查询秒杀商品列表
* @param offset
* @param limit
* @return
*/
List
void killByProcedure(Map
}
④ SuccessKilledDao
package com.force4us.dao;
import com.force4us.entity.SuccessKilled;
import org.apache.ibatis.annotations.Param;
public interface SuccessKilledDao {
/**
* 插入购买明细,可过滤重复
* @param seckillId
* @param userPhone
* @return 插入的行数
*/
int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
/**
* 根据秒杀商品ID查询明细SuccessKilled对象, 携带了Seckill秒杀产品对象
* @param seckillId
* @param userPhone
* @return
*/
SuccessKilled queryByIdWithSeckill(@Param("seckillId") long , @Param("userPhone") long userPhone);
}
⑤ mybatis配置文件:
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
⑥ SeckillDao.xml
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
UPDATE seckill
SET number = number - 1
WHERE seckill_id = #{seckillId}
AND start_time #{killTime}
AND end_time >= #{killTime}
AND number > 0
SELECT *
FROM seckill
WHERE seckill_id = #{seckillId}
SELECT *
FROM seckill
ORDER BY create_time DESC
limit #{offset},#{limit}
CALL excuteSeckill(
#{seckillId, jdbcType=BIGINT, mode=IN},
#{phone, jdbcType=BIGINT, mode=IN},
#{killTime, jdbcType=TIMESTAMP, mode=IN},
#{result, jdbcType=INTEGER, mode=OUT}
)
⑦ SuccessKilledDao.xml
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
INSERT ignore INTO success_killed(seckill_id,user_phone,state)
VALUES (#{seckillId},#{userPhone},0)
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_killed sk
INNER JOIN seckill s ON sk.seckill_id = s.seckill_id
WHERE sk.seckill_id=#{seckillId} and sk.user_phone=#{userPhone}
⑧ Mybatis整合Service:spring-dao.xml
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:contex="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">
3 Service层
① SeckillService
package com.force4us.service;
import com.force4us.dto.Exposer;
import com.force4us.dto.SeckillExecution;
import com.force4us.entity.Seckill;
import com.force4us.exception.RepeatKillException;
import com.force4us.exception.SeckillCloseException;
import com.force4us.exception.SeckillException;
import java.util.List;
/**业务接口:站在使用者(程序员)的角度设计接口
* 三个方面:1.方法定义粒度,方法定义的要非常清楚2.参数,要越简练越好
* 3.返回类型(return 类型一定要友好/或者return异常,我们允许的异常)
*/
public interface SeckillService {
/**
* 查询全部秒杀记录
* @return
*/
List
/**
* 查询单个秒杀记录
* @param seckillId
* @return
*/
Seckill getById(long seckillId);
/**
* 在秒杀开启时输出秒杀接口的地址,否则输出系统时间和秒杀时间
*/
Exposer exportSeckillUrl(long seckillId);
/**
* 执行秒杀操作,有可能失败,有可能成功,所以要抛出我们允许的异常
* @param seckillId
* @param userPhone
* @param md5
* @return
* @throws SeckillException
* @throws RepeatKillException
* @throws SeckillCloseException
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillCloseException;
SeckillExecution executeSeckillProcedure(long seckillId,long userPhone,String md5)
throws SeckillException,RepeatKillException,SeckillCloseException;
}
② SeckillServiceImpl
package com.force4us.service.impl;
import com.force4us.dao.SeckillDao;
import com.force4us.dao.SuccessKilledDao;
import com.force4us.dao.cache.RedisDao;
import com.force4us.dto.Exposer;
import com.force4us.dto.SeckillExecution;
import com.force4us.entity.Seckill;
import com.force4us.entity.SuccessKilled;
import com.force4us.enums.SeckillStatEnum;
import com.force4us.exception.RepeatKillException;
import com.force4us.exception.SeckillCloseException;
import com.force4us.exception.SeckillException;
import com.force4us.service.SeckillService;
import org.apache.commons.collections4.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 javax.annotation.Resource;
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());
@Autowired
private SeckillDao seckillDao;
@Autowired
private SuccessKilledDao successKilledDao;
@Autowired
private RedisDao redisDao;
//加入一个混淆字符串(秒杀接口)的salt,为了我避免用户猜出我们的md5值,值任意给,越复杂越好
private final String salt = "sadjgioqwelrhaljflutoiu293480523*&%*&*#";
public List
return seckillDao.queryAll(0, 4);
}
public Seckill getById(long seckillId) {
return seckillDao.queryById(seckillId);
}
public Exposer exportSeckillUrl(long seckillId) {
//缓存优化
//1。访问redi
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() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) {
return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
}
//秒杀开启,返回秒杀商品的id、用给接口加密的md5
String md5 = getMD5(seckillId);
return new Exposer(true, md5, seckillId);
}
private String getMD5(long seckillId) {
String base = seckillId + "/" + salt;
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);
//看是否该明细被重复插入,即用户是否重复秒杀
if (insertCount <= 0) {
throw new RepeatKillException("seckill repeated");
} else {
//减库存,热点商品竞争,update方法会拿到行级锁
int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
if (updateCount <= 0) {
//没有更新库存记录,说明秒杀结束 rollback
throw new SeckillCloseException("seckill 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());
}
}
public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
if (md5 == null || !md5.equals(getMD5(seckillId))) {
return new SeckillExecution(seckillId, SeckillStatEnum.DATE_REWRITE);
}
Date time = new Date();
Map
map.put("seckillId", seckillId);
map.put("phone", userPhone);
map.put("killTime", time);
map.put("result", null);
try {
seckillDao.killByProcedure(map);
int result = MapUtils.getInteger(map, "result", -2);
if (result == 1) {
SuccessKilled successKill = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKill);
} else {
return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
}
}
}
③ 异常的处理:
a.SeckillCloseException
package com.force4us.exception;
public class SeckillCloseException extends SeckillException{
public SeckillCloseException(String message) {
super(message);
}
public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
}
}
b. SeckillException
package com.force4us.exception;
public class RepeatKillException extends SeckillException{
public RepeatKillException(String message) {
super(message);
}
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
c. RepeatKillException
package com.force4us.exception;
public class SeckillException extends RuntimeException{
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
④ 枚举SeckillStatEnum
package com.force4us.enums;
public enum SeckillStatEnum {
SUCCESS(1,"秒杀成功"),
END(0,"秒杀结束"),
REPEAT_KILL(-1,"重复秒杀"),
INNER_ERROR(-2,"系统异常"),
DATE_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;
}
}
⑤ spring_spring.xml文件
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
4.Web层,JSP页面和JS
(1) 详情页流程逻辑逻辑
(2) 配置web.xml
[html] view plain copy
(3) SeckillResult
package com.force4us.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;
}
}
(4) spring-web.xml
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
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/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
(5) SeckillController中:
package com.force4us.web;
import com.force4us.dto.Exposer;
import com.force4us.dto.SeckillExecution;
import com.force4us.dto.SeckillResult;
import com.force4us.entity.Seckill;
import com.force4us.enums.SeckillStatEnum;
import com.force4us.exception.RepeatKillException;
import com.force4us.exception.SeckillCloseException;
import com.force4us.exception.SeckillException;
import com.force4us.service.SeckillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.test.annotation.Repeat;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
@Controller
@RequestMapping("/seckill")
public class SeckillController {
@Autowired
private SeckillService seckillService;
@RequestMapping(value = "/list",method= RequestMethod.GET)
public String list(Model model) {
List
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 "forward:/seckill/list";
}
model.addAttribute("seckill", seckill);
return "detail";
}
//ajax ,json暴露秒杀接口的方法
@RequestMapping(value="/{seckillId}/exposer",method = RequestMethod.POST,produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult
SeckillResult
try {
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
result = new SeckillResult
} catch (Exception e) {
e.printStackTrace();
result = new SeckillResult
}
return result;
}
@RequestMapping(value="/{seckillId}/{md5}/execution", method = RequestMethod.POST,
produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult
@PathVariable("md5") String md5,
@CookieValue(value="killPhone", required = false) Long phone){
if(phone == null){
return new SeckillResult
}
SeckillResult
try {
SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId,phone, md5);
return new SeckillResult
} catch (RepeatKillException e1) {
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
return new SeckillResult
} catch(SeckillCloseException e2){
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END);
return new SeckillResult
}catch(Exception e){
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
return new SeckillResult
}
}
@RequestMapping(value = "/time/now", method = RequestMethod.GET)
@ResponseBody
public SeckillResult
Date now = new Date();
return new SeckillResult
}
@RequestMapping("/test")
public String test(){
return "helloworld";
}
}
(6) list.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@include file="common/tag.jsp"%>
<%@include file="/WEB-INF/jsp/common/head.jsp"%>
(7) details.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@include file="common/tag.jsp"%>
<%@include file="common/head.jsp"%>
${seckill.name}
<%--显示time图标--%>
<%--展示倒计时--%>
<%--登录弹出层 输入电话--%>
<%--jQuery Cookie操作插件--%>
<%--jQuery countDown倒计时插件--%>
$(function(){
seckill.detail.init({
seckillId:${seckill.seckillId},
startTime:${seckill.startTime.time},
endTime:${seckill.endTime.time}
});
})
(8) seckill.js
//存放主要交互逻辑的js代码
// javascript 模块化(package.类.方法)
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';
}
},
//验证手机号
validatePhone: function(phone){
if(phone && phone.length == 11 && !isNaN(phone)){
return 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{
$('#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);
alert('result: ' + result);
}
});
}
},
handlerSeckill: 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 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('' + stateInfo + '');
}
});
});
node.show();
} else {
//未开启秒杀(浏览器计时偏差)
var now = exposer['now'];
var start = exposer['start'];
var end = exposer['end'];
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 + 1000);//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);
}
}
}
5.优化:
由于减少库存和购买明细需要在同一事物当中,在次中间会出现网络延迟,GC,缓存,数据库的并发等,所以需要进行优化。
(1) 使用Redis优化:具体代码看上面。
(2) 调整业务逻辑:先进行insert,插入购买明细,然后进行减少库存数量,具体代码看上面。
(3) 调用存储过程seckill.sql
-- 秒杀执行存储过程
DELIMITER $$ -- console ;转换为$$
--定义存储参数
--参数:in 输入参数;out输出参数
-- rowCount():返回上一条修改类型sql(delete,insert,update)的影响行数
-- rowCount: 0:未修改数据 >0:表示修改的行数 <0:sql错误/未执行修改sql
CREATE PROCEDURE excuteSeckill(IN fadeSeckillId INT,IN fadeUserPhone VARCHAR (15),IN fadeKillTime TIMESTAMP ,OUT fadeResult INT)
BEGIN
DECLARE insert_count INT DEFAULT 0;
START TRANSACTION ;
INSERT ignore success_kill(seckill_id,user_phone,status,create_time) VALUES(fadeSeckillId,fadeUserPhone,0,fadeKillTime); --先插入购买明细
SELECT ROW_COUNT() INTO insert_count;
IF(insert_count = 0) THEN
ROLLBACK ;
SET fadeResult = -1; --重复秒杀
ELSEIF(insert_count < 0) THEN
ROLLBACK ;
SET fadeResult = -2; --内部错误
ELSE --已经插入购买明细,接下来要减少库存
UPDATE seckill SET number = number -1 WHERE seckill_id = fadeSeckillId AND start_time < fadeKillTime AND end_time > fadeKillTime AND number > 0;
SELECT ROW_COUNT() INTO insert_count;
IF (insert_count = 0) THEN
ROLLBACK ;
SET fadeResult = 0; --库存没有了,代表秒杀已经关闭
ELSEIF (insert_count < 0) THEN
ROLLBACK ;
SET fadeResult = -2; --内部错误
ELSE
COMMIT ; --秒杀成功,事务提交
SET fadeResult = 1; --秒杀成功返回值为1
END IF;
END IF;
END
$$
DELIMITER ;
SET @fadeResult = -3;
-- 执行存储过程
CALL excuteSeckill(1003,18810464493,NOW(),@fadeResult);
-- 获取结果
SELECT @fadeResult;
--存储过程
-- 1、存储过程优化:事务行级锁持有的时间
-- 2、不要过度依赖存储过程
6.系统部署:
PS:若想通过源码更好的理解Java实现高并发秒杀,请:https://github.com/luomingkui/seckill