├── README.md ├── SQL └── schema.sql ├── pom.xml └── src ├── main ├── java │ └── cn │ │ └── colg │ │ ├── core │ │ ├── BaseController.java │ │ ├── BaseEntity.java │ │ └── BaseServiceImpl.java │ │ ├── dao │ │ ├── SeckillMapper.java │ │ └── SuccessKilledMapper.java │ │ ├── dto │ │ ├── Exposer.java │ │ ├── SeckillExecution.java │ │ └── SeckillResult.java │ │ ├── entity │ │ ├── Seckill.java │ │ └── SuccessKilled.java │ │ ├── enums │ │ └── SeckillStatEnum.java │ │ ├── exception │ │ ├── RepeatKillException.java │ │ ├── SeckillCloseException.java │ │ └── SeckillException.java │ │ ├── service │ │ ├── SeckillService.java │ │ ├── SuccessKilledService.java │ │ └── impl │ │ │ ├── SeckillServiceImpl.java │ │ │ └── SuccessKilledServiceImpl.java │ │ └── web │ │ └── SeckillController.java ├── resources │ ├── conf │ │ ├── jdbc.properties │ │ └── resource.properties │ ├── logback.xml │ ├── mybatis │ │ ├── mapper │ │ │ ├── SeckillMapper.xml │ │ │ └── SuccessKilledMapper.xml │ │ └── mybatis-config.xml │ └── spring │ │ ├── spring-dao.xml │ │ ├── spring-service.xml │ │ ├── spring-trans.xml │ │ └── spring-web.xml └── webapp │ ├── WEB-INF │ ├── jsp │ │ ├── common │ │ │ ├── head.jsp │ │ │ └── tag.jsp │ │ ├── detail.jsp │ │ └── list.jsp │ └── web.xml │ ├── index.jsp │ └── resources │ └── script │ └── seckill.js └── test └── java └── cn └── colg ├── core └── BaseTester.java ├── dao ├── SeckillMapperTest.java └── SuccessKilledMapperTest.java └── service └── SeckillServiceTest.java /README.md: -------------------------------------------------------------------------------- 1 | ### Java高并发秒杀系统API 2 | 3 | #### 慕课网课程: 4 | 1. [Java高并发秒杀API之业务分析与DAO层 ](https://www.imooc.com/learn/630) 5 | 2. [Java高并发秒杀API之Service层](https://www.imooc.com/learn/631) 6 | 3. [Java高并发秒杀API之web层](https://www.imooc.com/learn/630) 7 | 4. [Java高并发秒杀API之高并发优化](https://www.imooc.com/learn/632) 8 | 5. [Java秒杀系统方案优化 高性能高并发实战](https://coding.imooc.com/class/168.html) 9 | 10 | #### 秒杀功能 11 | - 秒杀接口暴露 12 | - 执行秒杀 13 | - 相关查询 14 | 15 | #### 代码开发阶段 16 | - [Dao设计编码:接口设计+SQL编写](https://github.com/colg-cloud/seckill/tree/master/src/main/java/cn/colg/dao) 17 | - [Service设计编码:业务逻辑实现](https://github.com/colg-cloud/seckill/tree/master/src/main/java/cn/colg/service) 18 | - [Web设计编码:前端业务交互](https://github.com/colg-cloud/seckill/tree/master/src/main/java/cn/colg/web) 19 | 20 | --- 21 | 22 | #### 技术总结 23 | ##### 联合主键,避免重复秒杀 24 | ``` 25 | -- 秒杀成功明细表 26 | -- 联合主键 27 | PRIMARY KEY (seckill_id, user_phone), 28 | ``` 29 | 在这里使用**秒杀商品id+用户手机号**作为秒杀成功的一个联合主键。当用户使用该手机秒杀同一件商品时从数据库层面来说是不允许的。 30 | 可以从单元测试的日志查看: 31 | ``` 32 | private static final Log log = LogFactory.get(); 33 | 34 | @Test 35 | public void testInsertSuccessKilled() { 36 | String seckillId = "c18c169938c311e89fa754ee75c6aeb0"; 37 | String userPhone = "18701012345"; 38 | int insertCount = successKilledMapper.insertSuccessKilled(seckillId, userPhone); 39 | log.info("insertCount: {}", insertCount); 40 | } 41 | ``` 42 | 执行结果: 43 | 1. 第一次:`INFO cn.colg.dao.SuccessKilledMapperTest - insertCount: 1` 表示插入成功 44 | 2. 第二次:`INFO cn.colg.dao.SuccessKilledMapperTest - insertCount: 0` 表示插入失败 45 | 46 | ##### 使用注解控制事务方法的优点 47 | ``` 48 | 49 | 50 | 51 | @Transactional(propagation=Propagation.REQUIRED) :如果有事务, 那么加入事务, 没有的话新建一个(默认情况下) 52 | @Transactional(propagation=Propagation.NOT_SUPPORTED) :容器不为这个方法开启事务 53 | @Transactional(propagation=Propagation.REQUIRES_NEW) :不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务 54 | @Transactional(propagation=Propagation.MANDATORY) :必须在一个已有的事务中执行,否则抛出异常 55 | @Transactional(propagation=Propagation.NEVER) :必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反) 56 | @Transactional(propagation=Propagation.SUPPORTS) :如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务. 57 | @Transactional(timeout=30) //默认是30秒 58 | ``` 59 | 1. 开发团队达成一致的风格,明确标注事务方法的编程风格。 60 | 2. 保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部。 61 | 3. 不是所有的方法都需要事务,如:只有一条修改操作,只读操作不需要事务控制 62 | 63 | ###### 错误处理 64 | [According to TLD or attribute directive in tag file, attribute value does not accept any expressions](https://blog.csdn.net/jasper_success/article/details/6693434) 65 | ``` 66 | <%@ taglib uri="http://java.sun.com/jstl/core" prefix="c"%> 改为 <%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c"%> 67 | <%@ taglib uri="http://java.sun.com/jstl/fmt" prefix="fmt"%> 改为 <%@ taglib uri="http://java.sun.com/jstl/fmt_rt" prefix="fmt"%> 68 | ``` 69 | 70 | #### 使用说明 71 | 1. 首先从github上把项目传到本地,可以直接下载项目的压缩包,点击Clone or download,然后Download Zip。也可以通过git,使用git clone https://github.com/colg-cloud/seckill 命令,把项目克隆到本地 72 | 2. 然后修改数据库连接信息,在resources目录下jdbc.properties配置文件中修改 73 | 3. 使用maven tomcat7插件启动项目, 进入项目目录,打开cmd输入:`mvn tomcat7:run` 74 | -------------------------------------------------------------------------------- /SQL/schema.sql: -------------------------------------------------------------------------------- 1 | -- 数据库初始化脚本 2 | 3 | -- 创建数据库 4 | CREATE DATABASE seckill; 5 | -- 使用数据库 6 | USE seckill; 7 | -- 创建秒杀库存表 8 | CREATE TABLE seckill( 9 | seckill_id VARCHAR(64) NOT NULL COMMENT '商品库存id', 10 | NAME VARCHAR(128) NOT NULL COMMENT '商品名称', 11 | number INT NOT NULL COMMENT '库存数量', 12 | start_time DATETIME NOT NULL COMMENT '秒杀开始时间', 13 | end_time DATETIME NOT NULL COMMENT '秒杀结束时间', 14 | create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 15 | PRIMARY KEY (seckill_id), 16 | -- 创建索引 17 | KEY idx_start_time(start_time), 18 | KEY idx_end_time(end_time), 19 | KEY idx_create_time(create_time) 20 | ) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT '秒杀库存表'; 21 | 22 | -- 初始化数据 23 | INSERT INTO 24 | seckill(seckill_id, NAME, number, start_time, end_time) 25 | VALUES 26 | (REPLACE(UUID(), '-', ''), '1000元秒杀iphone6', 100, '2015-11-01 00:00:00', '2015-11-02 00:00:00'), 27 | (REPLACE(UUID(), '-', ''), '500元秒杀ipad2', 200, '2015-11-01 00:00:00', '2015-11-02 00:00:00'), 28 | (REPLACE(UUID(), '-', ''), '300元秒杀小米4', 300, '2015-11-01 00:00:00', '2015-11-02 00:00:00'), 29 | (REPLACE(UUID(), '-', ''), '200元秒杀红米note', 400, '2015-11-01 00:00:00', '2015-11-02 00:00:00'); 30 | 31 | -- 秒杀成功明细表 32 | -- 用户登录认证相关的信息 33 | CREATE TABLE success_killed( 34 | seckill_id VARCHAR(64) NOT NULL COMMENT '秒杀商品id', 35 | user_phone VARCHAR(32) NOT NULL COMMENT '用户手机号', 36 | state CHAR(2) NOT NULL DEFAULT -1 COMMENT '状态标识(-1:无效,0:成功,1:已付款)', 37 | create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 38 | -- 联合主键 39 | PRIMARY KEY (seckill_id, user_phone), 40 | -- 索引 41 | KEY idx_create_time(create_time) 42 | ) ENGINE=INNODB CHARSET=utf8 COMMENT '秒杀成功明细表'; 43 | 44 | 45 | 46 | -- 为什么手写ddl 47 | -- 记录每次上线的ddl修改 48 | -- 上线v1.1版本 49 | ALTER TABLE seckill 50 | DROP INDEX idx_create_time, 51 | ADD INDEX idx_c_s(start_time, create_time); 52 | 53 | -- 上线v1.2 54 | -- ddl -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | cn.colg 4 | seckill 5 | 0.0.1-SNAPSHOT 6 | war 7 | Java高并发秒杀API 8 | 9 | 10 | 11 | 12 | junit 13 | junit 14 | 4.12 15 | test 16 | 17 | 18 | 19 | 20 | 25 | 26 | 27 | ch.qos.logback 28 | logback-classic 29 | 1.2.3 30 | runtime 31 | 32 | 33 | 34 | 35 | mysql 36 | mysql-connector-java 37 | 5.1.38 38 | 39 | 40 | com.alibaba 41 | druid 42 | 1.1.9 43 | 44 | 45 | 46 | 47 | org.mybatis 48 | mybatis 49 | 3.4.6 50 | 51 | 52 | 53 | org.mybatis 54 | mybatis-spring 55 | 1.3.2 56 | 57 | 58 | 59 | 60 | taglibs 61 | standard 62 | 1.1.2 63 | 64 | 65 | jstl 66 | jstl 67 | 1.2 68 | 69 | 70 | com.alibaba 71 | fastjson 72 | 1.2.47 73 | 74 | 75 | javax.servlet 76 | javax.servlet-api 77 | 3.1.0 78 | provided 79 | 80 | 81 | 82 | 83 | 84 | 85 | org.springframework 86 | spring-orm 87 | 4.3.15.RELEASE 88 | 89 | 90 | 91 | org.springframework 92 | spring-webmvc 93 | 4.3.15.RELEASE 94 | 95 | 96 | 97 | org.springframework 98 | spring-test 99 | 4.3.15.RELEASE 100 | test 101 | 102 | 103 | 104 | cn.hutool 105 | hutool-all 106 | 4.0.9 107 | 108 | 109 | 110 | 111 | 112 | ${project.artifactId}} 113 | 114 | 115 | 116 | org.apache.tomcat.maven 117 | tomcat7-maven-plugin 118 | 2.2 119 | 120 | UTF-8 121 | UTF-8 122 | / 123 | 8080 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/core/BaseController.java: -------------------------------------------------------------------------------- 1 | package cn.colg.core; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | import cn.colg.service.SeckillService; 6 | import cn.colg.service.SuccessKilledService; 7 | 8 | /** 9 | * Controller 的基类,用于抽取注入的Service 10 | * 11 | * @author Yang Lei 12 | */ 13 | public abstract class BaseController { 14 | 15 | @Autowired 16 | protected SeckillService seckillService; 17 | @Autowired 18 | protected SuccessKilledService successKilledService; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/core/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package cn.colg.core; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.serializer.SerializerFeature; 5 | 6 | /** 7 | * entity的基类,建议所有实体类都继承 8 | * 9 | * @author colg 10 | */ 11 | public abstract class BaseEntity { 12 | 13 | @Override 14 | public String toString() { 15 | return JSON.toJSONString( 16 | this, 17 | SerializerFeature.WriteDateUseDateFormat, 18 | SerializerFeature.WriteMapNullValue, 19 | SerializerFeature.DisableCircularReferenceDetect 20 | ); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/core/BaseServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.colg.core; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | import cn.colg.dao.*; 6 | 7 | /** 8 | * ServiceImpl层基础类,用于抽取注入的Mapper 9 | * 10 | * @author Yang Lei 11 | */ 12 | public abstract class BaseServiceImpl { 13 | 14 | @Autowired 15 | protected SeckillMapper seckillMapper; 16 | @Autowired 17 | protected SuccessKilledMapper successKilledMapper; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/dao/SeckillMapper.java: -------------------------------------------------------------------------------- 1 | package cn.colg.dao; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | import cn.colg.entity.Seckill; 9 | 10 | public interface SeckillMapper { 11 | 12 | int reduceNumber(@Param("seckillId") String seckillId, @Param("killTime") Date killTime); 13 | 14 | Seckill findById(@Param("seckillId") String seckillId); 15 | 16 | List querySeckill(@Param("offet") int offet, @Param("limit") int limit); 17 | 18 | String findNameById(@Param("seckillId") String seckillId); 19 | } -------------------------------------------------------------------------------- /src/main/java/cn/colg/dao/SuccessKilledMapper.java: -------------------------------------------------------------------------------- 1 | package cn.colg.dao; 2 | 3 | import org.apache.ibatis.annotations.Param; 4 | 5 | import cn.colg.entity.SuccessKilled; 6 | 7 | public interface SuccessKilledMapper { 8 | 9 | int insertSuccessKilled(@Param("seckillId") String seckillId, @Param("userPhone") String userPhone); 10 | 11 | SuccessKilled findBySeckillId(@Param("seckillId") String seckillId, @Param("userPhone") String userPhone); 12 | } -------------------------------------------------------------------------------- /src/main/java/cn/colg/dto/Exposer.java: -------------------------------------------------------------------------------- 1 | package cn.colg.dto; 2 | 3 | import cn.colg.core.BaseEntity; 4 | 5 | /** 6 | * 暴露秒杀接口 dto 7 | * 8 | * @author colg 9 | */ 10 | public class Exposer extends BaseEntity { 11 | 12 | /** 13 | * 是否开启秒杀 14 | */ 15 | private boolean exposed; 16 | 17 | /** 18 | * 一种加密措施 19 | */ 20 | private String md5; 21 | 22 | /** 23 | * 库存商品id 24 | */ 25 | private String seckillId; 26 | 27 | /** 28 | * 系统当前时间(毫秒) 29 | */ 30 | private Long now; 31 | 32 | /** 33 | * 秒杀开始时间 34 | */ 35 | private Long start; 36 | 37 | /** 38 | * 秒杀结束时间 39 | */ 40 | private Long end; 41 | 42 | public Exposer() { 43 | } 44 | 45 | public Exposer(boolean exposed, String seckillId) { 46 | this.exposed = exposed; 47 | this.seckillId = seckillId; 48 | } 49 | 50 | public Exposer(boolean exposed, String md5, String seckillId) { 51 | this.exposed = exposed; 52 | this.md5 = md5; 53 | this.seckillId = seckillId; 54 | } 55 | 56 | public Exposer(boolean exposed, String seckillId, Long now, Long start, Long end) { 57 | this.exposed = exposed; 58 | this.seckillId = seckillId; 59 | this.now = now; 60 | this.start = start; 61 | this.end = end; 62 | } 63 | 64 | public boolean getExposed() { 65 | return exposed; 66 | } 67 | 68 | public void setExposed(boolean exposed) { 69 | this.exposed = exposed; 70 | } 71 | 72 | public String getMd5() { 73 | return md5; 74 | } 75 | 76 | public void setMd5(String md5) { 77 | this.md5 = md5; 78 | } 79 | 80 | public String getSeckillId() { 81 | return seckillId; 82 | } 83 | 84 | public void setSeckillId(String seckillId) { 85 | this.seckillId = seckillId; 86 | } 87 | 88 | public Long getNow() { 89 | return now; 90 | } 91 | 92 | public void setNow(Long now) { 93 | this.now = now; 94 | } 95 | 96 | public Long getStart() { 97 | return start; 98 | } 99 | 100 | public void setStart(Long start) { 101 | this.start = start; 102 | } 103 | 104 | public Long getEnd() { 105 | return end; 106 | } 107 | 108 | public void setEnd(Long end) { 109 | this.end = end; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/dto/SeckillExecution.java: -------------------------------------------------------------------------------- 1 | package cn.colg.dto; 2 | 3 | import cn.colg.core.BaseEntity; 4 | import cn.colg.entity.SuccessKilled; 5 | import cn.colg.enums.SeckillStatEnum; 6 | 7 | /** 8 | * 封装秒杀执行后的结果 9 | * 10 | * @author colg 11 | */ 12 | public class SeckillExecution extends BaseEntity { 13 | 14 | /** 15 | * 库存商品id 16 | */ 17 | private String seckillId; 18 | 19 | /** 20 | * 商品秒杀状态标识(-1:无效,0:成功,1:已付款) 21 | */ 22 | private String state; 23 | 24 | /** 25 | * 状态表示 26 | */ 27 | private String stateInfo; 28 | 29 | /** 30 | * 秒杀成功对象,当秒杀成功时,返回此对象 31 | */ 32 | private SuccessKilled successKilled; 33 | 34 | public SeckillExecution() { 35 | } 36 | 37 | public SeckillExecution(String seckillId, SeckillStatEnum statEnum) { 38 | this.seckillId = seckillId; 39 | this.state = statEnum.getState(); 40 | this.stateInfo = statEnum.getStateInfo(); 41 | } 42 | 43 | public SeckillExecution(String seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) { 44 | this.seckillId = seckillId; 45 | this.state = statEnum.getState(); 46 | this.stateInfo = statEnum.getStateInfo(); 47 | this.successKilled = successKilled; 48 | } 49 | 50 | public String getSeckillId() { 51 | return seckillId; 52 | } 53 | 54 | public void setSeckillId(String seckillId) { 55 | this.seckillId = seckillId; 56 | } 57 | 58 | public String getState() { 59 | return state; 60 | } 61 | 62 | public void setState(String state) { 63 | this.state = state; 64 | } 65 | 66 | public String getStateInfo() { 67 | return stateInfo; 68 | } 69 | 70 | public void setStateInfo(String stateInfo) { 71 | this.stateInfo = stateInfo; 72 | } 73 | 74 | public SuccessKilled getSuccessKilled() { 75 | return successKilled; 76 | } 77 | 78 | public void setSuccessKilled(SuccessKilled successKilled) { 79 | this.successKilled = successKilled; 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/dto/SeckillResult.java: -------------------------------------------------------------------------------- 1 | package cn.colg.dto; 2 | 3 | /** 4 | * 5 | * 所有的ajax请求返回类型,封装json结果 6 | * 7 | * @author colg 8 | * @param 9 | */ 10 | public class SeckillResult { 11 | 12 | private boolean success; 13 | private T data; 14 | private String error; 15 | 16 | public SeckillResult() { 17 | } 18 | 19 | public SeckillResult(boolean success, T data) { 20 | this.success = success; 21 | this.data = data; 22 | } 23 | 24 | public SeckillResult(boolean success, String error) { 25 | this.success = success; 26 | this.error = error; 27 | } 28 | 29 | public boolean getSuccess() { 30 | return success; 31 | } 32 | 33 | public void setSuccess(boolean success) { 34 | this.success = success; 35 | } 36 | 37 | public T getData() { 38 | return data; 39 | } 40 | 41 | public void setData(T data) { 42 | this.data = data; 43 | } 44 | 45 | public String getError() { 46 | return error; 47 | } 48 | 49 | public void setError(String error) { 50 | this.error = error; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/entity/Seckill.java: -------------------------------------------------------------------------------- 1 | package cn.colg.entity; 2 | 3 | import cn.colg.core.BaseEntity; 4 | import java.io.Serializable; 5 | import java.util.Date; 6 | import java.util.List; 7 | 8 | /** 9 | * 秒杀库存表 10 | * 11 | * @author colg 12 | */ 13 | public class Seckill extends BaseEntity implements Serializable { 14 | /** 15 | * 商品库存id 16 | */ 17 | private String seckillId; 18 | 19 | /** 20 | * 商品名称 21 | */ 22 | private String name; 23 | 24 | /** 25 | * 库存数量 26 | */ 27 | private Integer number; 28 | 29 | /** 30 | * 秒杀开始时间 31 | */ 32 | private Date startTime; 33 | 34 | /** 35 | * 秒杀结束时间 36 | */ 37 | private Date endTime; 38 | 39 | /** 40 | * 创建时间 41 | */ 42 | private Date createTime; 43 | 44 | /** 45 | * 一对多 46 | */ 47 | private List successKilleds; 48 | 49 | private static final long serialVersionUID = 1L; 50 | 51 | /** 52 | * 获取商品库存id 53 | * 54 | * @return seckill_id - 商品库存id 55 | */ 56 | public String getSeckillId() { 57 | return seckillId; 58 | } 59 | 60 | /** 61 | * /* 设置seckillId 62 | * 63 | * @param seckillId 64 | * 商品库存id 65 | */ 66 | public void setSeckillId(String seckillId) { 67 | this.seckillId = seckillId == null ? null : seckillId.trim(); 68 | } 69 | 70 | /** 71 | * 获取商品名称 72 | * 73 | * @return name - 商品名称 74 | */ 75 | public String getName() { 76 | return name; 77 | } 78 | 79 | /** 80 | * /* 设置name 81 | * 82 | * @param name 83 | * 商品名称 84 | */ 85 | public void setName(String name) { 86 | this.name = name == null ? null : name.trim(); 87 | } 88 | 89 | /** 90 | * 获取库存数量 91 | * 92 | * @return number - 库存数量 93 | */ 94 | public Integer getNumber() { 95 | return number; 96 | } 97 | 98 | /** 99 | * /* 设置number 100 | * 101 | * @param number 102 | * 库存数量 103 | */ 104 | public void setNumber(Integer number) { 105 | this.number = number; 106 | } 107 | 108 | /** 109 | * 获取秒杀开始时间 110 | * 111 | * @return start_time - 秒杀开始时间 112 | */ 113 | public Date getStartTime() { 114 | return startTime; 115 | } 116 | 117 | /** 118 | * /* 设置startTime 119 | * 120 | * @param startTime 121 | * 秒杀开始时间 122 | */ 123 | public void setStartTime(Date startTime) { 124 | this.startTime = startTime; 125 | } 126 | 127 | /** 128 | * 获取秒杀结束时间 129 | * 130 | * @return end_time - 秒杀结束时间 131 | */ 132 | public Date getEndTime() { 133 | return endTime; 134 | } 135 | 136 | /** 137 | * /* 设置endTime 138 | * 139 | * @param endTime 140 | * 秒杀结束时间 141 | */ 142 | public void setEndTime(Date endTime) { 143 | this.endTime = endTime; 144 | } 145 | 146 | /** 147 | * 获取创建时间 148 | * 149 | * @return create_time - 创建时间 150 | */ 151 | public Date getCreateTime() { 152 | return createTime; 153 | } 154 | 155 | /** 156 | * /* 设置createTime 157 | * 158 | * @param createTime 159 | * 创建时间 160 | */ 161 | public void setCreateTime(Date createTime) { 162 | this.createTime = createTime; 163 | } 164 | 165 | public List getSuccessKilleds() { 166 | return successKilleds; 167 | } 168 | 169 | public void setSuccessKilleds(List successKilleds) { 170 | this.successKilleds = successKilleds; 171 | } 172 | 173 | } -------------------------------------------------------------------------------- /src/main/java/cn/colg/entity/SuccessKilled.java: -------------------------------------------------------------------------------- 1 | package cn.colg.entity; 2 | 3 | import cn.colg.core.BaseEntity; 4 | import java.io.Serializable; 5 | import java.util.Date; 6 | 7 | /** 8 | * 秒杀成功明细表 9 | * 10 | * @author colg 11 | */ 12 | public class SuccessKilled extends BaseEntity implements Serializable { 13 | /** 14 | * 秒杀商品id 15 | */ 16 | private String seckillId; 17 | 18 | /** 19 | * 用户手机号 20 | */ 21 | private String userPhone; 22 | 23 | /** 24 | * 状态标识(-1:无效,0:成功,1:已付款) 25 | */ 26 | private String state; 27 | 28 | /** 29 | * 变通: 多对一 30 | */ 31 | private Seckill seckill; 32 | 33 | /** 34 | * 创建时间 35 | */ 36 | private Date createTime; 37 | 38 | private static final long serialVersionUID = 1L; 39 | 40 | /** 41 | * 获取秒杀商品id 42 | * 43 | * @return seckill_id - 秒杀商品id 44 | */ 45 | public String getSeckillId() { 46 | return seckillId; 47 | } 48 | 49 | /** 50 | * /* 设置seckillId 51 | * 52 | * @param seckillId 53 | * 秒杀商品id 54 | */ 55 | public void setSeckillId(String seckillId) { 56 | this.seckillId = seckillId == null ? null : seckillId.trim(); 57 | } 58 | 59 | /** 60 | * 获取用户手机号 61 | * 62 | * @return user_phone - 用户手机号 63 | */ 64 | public String getUserPhone() { 65 | return userPhone; 66 | } 67 | 68 | /** 69 | * /* 设置userPhone 70 | * 71 | * @param userPhone 72 | * 用户手机号 73 | */ 74 | public void setUserPhone(String userPhone) { 75 | this.userPhone = userPhone == null ? null : userPhone.trim(); 76 | } 77 | 78 | /** 79 | * 获取状态标识(-1:无效,0:成功,1:已付款) 80 | * 81 | * @return state - 状态标识(-1:无效,0:成功,1:已付款) 82 | */ 83 | public String getState() { 84 | return state; 85 | } 86 | 87 | /** 88 | * /* 设置state 89 | * 90 | * @param state 91 | * 状态标识(-1:无效,0:成功,1:已付款) 92 | */ 93 | public void setState(String state) { 94 | this.state = state == null ? null : state.trim(); 95 | } 96 | 97 | /** 98 | * 获取创建时间 99 | * 100 | * @return create_time - 创建时间 101 | */ 102 | public Date getCreateTime() { 103 | return createTime; 104 | } 105 | 106 | /** 107 | * /* 设置createTime 108 | * 109 | * @param createTime 110 | * 创建时间 111 | */ 112 | public void setCreateTime(Date createTime) { 113 | this.createTime = createTime; 114 | } 115 | 116 | public Seckill getSeckill() { 117 | return seckill; 118 | } 119 | 120 | public void setSeckill(Seckill seckill) { 121 | this.seckill = seckill; 122 | } 123 | 124 | } -------------------------------------------------------------------------------- /src/main/java/cn/colg/enums/SeckillStatEnum.java: -------------------------------------------------------------------------------- 1 | package cn.colg.enums; 2 | 3 | /** 4 | * 使用枚举表述常量数据字段 5 | * 6 | * @author colg 7 | */ 8 | public enum SeckillStatEnum { 9 | 10 | // 枚举类必须定义枚举值,编译才会通过 11 | 12 | SUCCESS ("1", "秒杀成功"), 13 | END ("0", "秒杀结束"), 14 | REPEAT_KILL ("-1", "重复秒杀"), 15 | INNER_ERROR ("-2", "系统异常"), 16 | DATA_REWRITE ("-3", "数据篡改"); 17 | 18 | 19 | private String state; 20 | private String stateInfo; 21 | 22 | private SeckillStatEnum(String state, String stateInfo) { 23 | this.state = state; 24 | this.stateInfo = stateInfo; 25 | } 26 | 27 | public String getState() { 28 | return state; 29 | } 30 | 31 | public String getStateInfo() { 32 | return stateInfo; 33 | } 34 | 35 | public static SeckillStatEnum stateOf(String state) { 36 | for (SeckillStatEnum statEnum : values()) { 37 | if (state.equals(statEnum.getState())) { 38 | return statEnum; 39 | } 40 | } 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/exception/RepeatKillException.java: -------------------------------------------------------------------------------- 1 | package cn.colg.exception; 2 | 3 | /** 4 | * 重复秒杀异常(运行期异常) 5 | * 6 | * @author colg 7 | */ 8 | public class RepeatKillException extends SeckillException { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | public RepeatKillException(String message) { 13 | super(message); 14 | } 15 | 16 | public RepeatKillException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/exception/SeckillCloseException.java: -------------------------------------------------------------------------------- 1 | package cn.colg.exception; 2 | 3 | /** 4 | * 秒杀关闭异常 5 | * 6 | * @author colg 7 | */ 8 | public class SeckillCloseException extends SeckillException { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | public SeckillCloseException(String message) { 13 | super(message); 14 | } 15 | 16 | public SeckillCloseException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/exception/SeckillException.java: -------------------------------------------------------------------------------- 1 | package cn.colg.exception; 2 | 3 | /** 4 | * 秒杀相关业务异常 5 | * 6 | * @author colg 7 | */ 8 | public class SeckillException extends RuntimeException { 9 | 10 | private static final long serialVersionUID = 1L; 11 | 12 | public SeckillException(String message) { 13 | super(message); 14 | } 15 | 16 | public SeckillException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/service/SeckillService.java: -------------------------------------------------------------------------------- 1 | package cn.colg.service; 2 | 3 | import java.util.List; 4 | 5 | import cn.colg.dto.Exposer; 6 | import cn.colg.dto.SeckillExecution; 7 | import cn.colg.entity.Seckill; 8 | import cn.colg.exception.RepeatKillException; 9 | import cn.colg.exception.SeckillCloseException; 10 | import cn.colg.exception.SeckillException; 11 | 12 | /** 13 | * 业务接口:站在“使用者”角度设计接口
14 | * 15 | * 三个方面:
16 | * 1. 方法定义粒度
17 | * 2. 参数
18 | * 3. 返回类型{return 类型/异常;} 19 | * 20 | * @author colg 21 | */ 22 | public interface SeckillService { 23 | 24 | /** 25 | * 查询所有秒杀记录 26 | * 27 | * @return 28 | */ 29 | List querySeckill(); 30 | 31 | /** 32 | * 查询单个秒杀记录 33 | * 34 | * @param seckillId 35 | * 商品库存id 36 | * @return 37 | */ 38 | Seckill findById(String seckillId); 39 | 40 | /** 41 | * 秒杀开启时输出秒杀接口地址,否则输出系统时间和秒杀时间 42 | * 43 | * @param seckillId 44 | * 商品库存id 45 | * @return 秒杀接口 46 | */ 47 | Exposer exportSeckillUrl(String seckillId); 48 | 49 | /** 50 | * 执行秒杀操作 51 | * 52 | * @param seckillId 53 | * 库存商品id 54 | * @param userPhone 55 | * 用户手机号 56 | * @param md5 57 | * 58 | * 59 | * @return 秒杀执行后的结果 60 | * 61 | * @throws SeckillException 62 | * 秒杀相关业务异常 63 | * @throws RepeatKillException 64 | * 重复秒杀异常 65 | * @throws SeckillCloseException 66 | * 秒杀关闭异常 67 | */ 68 | SeckillExecution executeSeckill(String seckillId, String userPhone, String md5) 69 | throws SeckillException, RepeatKillException, SeckillCloseException; 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/service/SuccessKilledService.java: -------------------------------------------------------------------------------- 1 | package cn.colg.service; 2 | 3 | import cn.colg.entity.SuccessKilled; 4 | 5 | public interface SuccessKilledService { 6 | 7 | /** 8 | * 插入购买明细,可过滤重复 9 | * 10 | * @param seckillId 11 | * 秒杀商品id 12 | * @param userPhone 13 | * 用户手机号 14 | * @return 插入的行数 15 | */ 16 | int insertSuccessKilled(String seckillId, String userPhone); 17 | 18 | /** 19 | * 根据id和手机号查询SuccessKilled并携带秒杀产品对象实体 20 | * 21 | * @param seckillId 22 | * @return 23 | */ 24 | SuccessKilled findBySeckillId(String seckillId, String userPhone); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/service/impl/SeckillServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.colg.service.impl; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | import cn.colg.core.BaseServiceImpl; 11 | import cn.colg.dto.Exposer; 12 | import cn.colg.dto.SeckillExecution; 13 | import cn.colg.entity.Seckill; 14 | import cn.colg.entity.SuccessKilled; 15 | import cn.colg.enums.SeckillStatEnum; 16 | import cn.colg.exception.RepeatKillException; 17 | import cn.colg.exception.SeckillCloseException; 18 | import cn.colg.exception.SeckillException; 19 | import cn.colg.service.SeckillService; 20 | import cn.hutool.core.util.StrUtil; 21 | import cn.hutool.crypto.SecureUtil; 22 | import cn.hutool.log.Log; 23 | import cn.hutool.log.LogFactory; 24 | 25 | /** 26 | * 27 | * 28 | * @author colg 29 | */ 30 | @Service 31 | public class SeckillServiceImpl extends BaseServiceImpl implements SeckillService { 32 | 33 | private static final Log Log = LogFactory.get(); 34 | 35 | /** 36 | * md5盐值字符串,用于混淆md5 37 | */ 38 | @Value("${SLAT}") 39 | private String slat; 40 | 41 | @Override 42 | public List querySeckill() { 43 | // 只查询4个 44 | return seckillMapper.querySeckill(0, 100); 45 | } 46 | 47 | @Override 48 | public Seckill findById(String seckillId) { 49 | return seckillMapper.findById(seckillId); 50 | } 51 | 52 | @Override 53 | public Exposer exportSeckillUrl(String seckillId) { 54 | Seckill seckill = seckillMapper.findById(seckillId); 55 | if (seckill == null) { 56 | return new Exposer(false, seckillId); 57 | } 58 | 59 | Date startTime = seckill.getStartTime(); 60 | Date endTime = seckill.getEndTime(); 61 | // 系统当前时间 62 | Date nowTime = new Date(); 63 | // 判断当前系统时间是否在秒杀期间 64 | long now = nowTime.getTime(); 65 | if (now < startTime.getTime() || now > endTime.getTime()) { 66 | return new Exposer(false, seckillId, now, startTime.getTime(), endTime.getTime()); 67 | } 68 | 69 | // 转换特定字符串的过程,不可逆 70 | String md5 = getMd5(seckillId); 71 | return new Exposer(true, md5, seckillId); 72 | } 73 | 74 | /** 75 | * 根据库存商品id和盐值生成md5 76 | * 77 | * @param seckillId 78 | * @return 79 | */ 80 | private String getMd5(String seckillId) { 81 | String data = seckillId + "/" + slat; 82 | String md5 = SecureUtil.md5(data); 83 | return md5; 84 | } 85 | 86 | /** 87 | *
 88 | 	 * 使用注解控制事务方法的优点:
 89 | 	 * 	1:开发团队达成一致约定,明确标注事务方法的变成风格。
 90 | 	 * 	2:保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部。
 91 | 	 * 	3:不是所有的方法都需要事务,如:只有一条修改操作,只读操作不需要事务控制。
 92 | 	 * 
93 | */ 94 | @Transactional(rollbackFor = Exception.class) 95 | @Override 96 | public SeckillExecution executeSeckill(String seckillId, String userPhone, String md5) 97 | throws SeckillException, RepeatKillException, SeckillCloseException { 98 | if (StrUtil.isBlank(md5) || !md5.equals(getMd5(seckillId))) { 99 | // md5不同,秒杀的数据被改写 100 | throw new SeckillException("seckill data rewrite"); 101 | } 102 | 103 | try { 104 | // 执行秒杀逻辑:减库存 + 记录购买行为 105 | // 减库存 106 | int updateCount = seckillMapper.reduceNumber(seckillId, new Date()); 107 | if (updateCount <= 0) { 108 | // 没有更新到记录,秒杀结束 109 | throw new SeckillCloseException("seckill is closed"); 110 | } 111 | 112 | // 记录用户购买行为,唯一:seckillId,userPhone 113 | int insertCount = successKilledMapper.insertSuccessKilled(seckillId, userPhone); 114 | if (insertCount <= 0) { 115 | // 重复秒杀 116 | throw new RepeatKillException("seckill repeated"); 117 | } 118 | // 秒杀成功 119 | SuccessKilled successKilled = successKilledMapper.findBySeckillId(seckillId, userPhone); 120 | // 商品秒杀状态标识(-1:无效,0:成功,1:已付款) 121 | return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled); 122 | } catch (SeckillCloseException e1) { 123 | throw e1; 124 | } catch (RepeatKillException e2) { 125 | throw e2; 126 | } catch (SeckillException e3) { 127 | throw e3; 128 | } catch (Exception e) { 129 | Log.error(e.getMessage(), e); 130 | // 所有编译器异常,转换为运行期异常 131 | throw new SeckillException("seckill inner error: " + e.getMessage()); 132 | } 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/service/impl/SuccessKilledServiceImpl.java: -------------------------------------------------------------------------------- 1 | package cn.colg.service.impl; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | 6 | import cn.colg.dao.SuccessKilledMapper; 7 | import cn.colg.entity.SuccessKilled; 8 | import cn.colg.service.SuccessKilledService; 9 | 10 | @Service 11 | public class SuccessKilledServiceImpl implements SuccessKilledService { 12 | 13 | @Autowired 14 | private SuccessKilledMapper successKilledMapper; 15 | 16 | @Override 17 | public int insertSuccessKilled(String seckillId, String userPhone) { 18 | return successKilledMapper.insertSuccessKilled(seckillId, userPhone); 19 | } 20 | 21 | 22 | @Override 23 | public SuccessKilled findBySeckillId(String seckillId, String userPhone) { 24 | return successKilledMapper.findBySeckillId(seckillId, userPhone); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/cn/colg/web/SeckillController.java: -------------------------------------------------------------------------------- 1 | package cn.colg.web; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.CookieValue; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.ResponseBody; 13 | 14 | import cn.colg.core.BaseController; 15 | import cn.colg.dto.Exposer; 16 | import cn.colg.dto.SeckillExecution; 17 | import cn.colg.dto.SeckillResult; 18 | import cn.colg.entity.Seckill; 19 | import cn.colg.enums.SeckillStatEnum; 20 | import cn.colg.exception.RepeatKillException; 21 | import cn.colg.exception.SeckillCloseException; 22 | import cn.colg.exception.SeckillException; 23 | import cn.hutool.core.util.StrUtil; 24 | import cn.hutool.log.Log; 25 | import cn.hutool.log.LogFactory; 26 | 27 | /** 28 | * // url:模块/资源/{id}/细分 /seckill/list 29 | * 30 | * @author colg 31 | */ 32 | @Controller 33 | @RequestMapping("/seckill") 34 | public class SeckillController extends BaseController { 35 | 36 | private static final Log log = LogFactory.get(); 37 | 38 | /** 39 | * 商品列表页面 40 | * 41 | * @param model 42 | * @return 43 | */ 44 | @GetMapping("/list") 45 | public String list(Model model) { 46 | // 获取列表页 47 | List list = seckillService.querySeckill(); 48 | model.addAttribute("list", list); 49 | // list.jsp + model = ModelAndView 50 | // WEB-INF/jsp/"list".jsp 51 | return "list"; 52 | } 53 | 54 | /** 55 | * 商品详情页面 56 | * 57 | * @param model 58 | * @return 59 | */ 60 | @GetMapping("/{seckillId}/detail") 61 | public String detail(@PathVariable(required = false) String seckillId, Model model) { 62 | if (StrUtil.isBlank(seckillId)) { 63 | // 重定向到 list 64 | return "redirect:/seckill/list"; 65 | } 66 | 67 | Seckill seckill = seckillService.findById(seckillId); 68 | if (seckill == null) { 69 | return "forward:/seckill/list"; 70 | } 71 | model.addAttribute("seckill", seckill); 72 | return "detail"; 73 | } 74 | 75 | /** 76 | * 暴露秒杀地址 77 | * 78 | * ajax json 79 | * 80 | * @param seckillId 81 | */ 82 | @PostMapping("/{seckillId}/exposer") 83 | @ResponseBody 84 | public SeckillResult exposer(@PathVariable String seckillId) { 85 | SeckillResult result; 86 | try { 87 | Exposer exposer = seckillService.exportSeckillUrl(seckillId); 88 | result = new SeckillResult<>(true, exposer); 89 | } catch (Exception e) { 90 | log.error(e); 91 | result = new SeckillResult<>(false, e.getMessage()); 92 | } 93 | return result; 94 | } 95 | 96 | /** 97 | * 执行秒杀 98 | * 99 | * @param seckillId 100 | * @param md5 101 | * @param phone 102 | * @return 103 | */ 104 | @PostMapping("/{seckillId}/{md5}/execution") 105 | @ResponseBody 106 | public SeckillResult execute(@PathVariable("seckillId") String seckillId, @PathVariable("md5") String md5, 107 | @CookieValue(value = "killPhone", required = false) String phone) { 108 | // springmvc valid 109 | if (StrUtil.isBlank(phone)) { 110 | return new SeckillResult<>(false, "未注册"); 111 | } 112 | 113 | SeckillResult result; 114 | try { 115 | SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, phone, md5); 116 | result = new SeckillResult(true, seckillExecution); 117 | } catch (RepeatKillException e) { 118 | SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL); 119 | result = new SeckillResult<>(true, seckillExecution); 120 | } catch (SeckillCloseException e) { 121 | SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.END); 122 | result = new SeckillResult<>(true, seckillExecution); 123 | } catch (SeckillException e) { 124 | SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR); 125 | result = new SeckillResult<>(true, seckillExecution); 126 | } 127 | return result; 128 | } 129 | 130 | /** 131 | * 获取系统时间 132 | * 133 | * @return 134 | */ 135 | @GetMapping("/time/now") 136 | @ResponseBody 137 | public SeckillResult time() { 138 | long time = System.currentTimeMillis(); 139 | return new SeckillResult(true, time); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/main/resources/conf/jdbc.properties: -------------------------------------------------------------------------------- 1 | validationQuery=SELECT 1 2 | jdbc.url=jdbc:mysql://192.168.1.55:3306/seckill?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false 3 | jdbc.username=root 4 | jdbc.password=root 5 | jdbc.driver=com.mysql.jdbc.Driver -------------------------------------------------------------------------------- /src/main/resources/conf/resource.properties: -------------------------------------------------------------------------------- 1 | # md5\u76d0\u503c\u5b57\u7b26\u4e32\uff0c\u7528\u4e8e\u6df7\u6dc6md5 2 | SLAT=https://github.com/colg-cloud/seckill -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/resources/mybatis/mapper/SeckillMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | UPDATE seckill 15 | SET number = number - 1 16 | WHERE seckill_id = #{seckillId} 17 | AND start_time <= #{killTime} 18 | AND end_time >= #{killTime} 19 | AND number > 0 20 | 21 | 22 | 27 | 28 | 34 | 35 | 40 | -------------------------------------------------------------------------------- /src/main/resources/mybatis/mapper/SuccessKilledMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | INSERT IGNORE INTO success_killed(seckill_id, user_phone, state) 15 | VALUES(#{seckillId}, #{userPhone}, 0) 16 | 17 | 18 | 30 | -------------------------------------------------------------------------------- /src/main/resources/mybatis/mybatis-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-dao.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-service.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-trans.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | application/json;charset=UTF-8 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | WriteDateUseDateFormat 34 | WriteMapNullValue 35 | WriteNullListAsEmpty 36 | WriteNullStringAsEmpty 37 | DisableCircularReferenceDetect 38 | 39 | 40 | 41 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/common/head.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/common/tag.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib uri="http://java.sun.com/jstl/core_rt" prefix="c"%> 2 | <%@ taglib uri="http://java.sun.com/jstl/fmt_rt" prefix="fmt"%> 3 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/detail.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 | <%@ include file="common/tag.jsp"%> 3 | 4 | 5 | 6 | 秒杀详情页 7 | <%@ include file="common/head.jsp"%> 8 | 9 | 10 |
11 |
12 |
13 |

${seckill.name }

14 |
15 |
16 |

17 | 18 | 19 | 20 | 21 |

22 |
23 | 24 |
25 |
26 | 27 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 70 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/list.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 | 3 | <%@ include file="common/tag.jsp"%> 4 | 5 | 6 | 7 | 秒杀列表页 8 | <%@ include file="common/head.jsp"%> 9 | 10 | 11 | 12 |
13 |
14 |
15 |

秒杀列表

16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
序号名称库存开始时间结束时间创建时间详情页
${status.count }${sk.name }${sk.number }商品详情
44 |
45 | 46 |
47 |
48 | 49 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | seckill 9 | 10 | index.html 11 | index.htm 12 | index.jsp 13 | default.html 14 | default.htm 15 | default.jsp 16 | 17 | 18 | 19 | 20 | springDispatcherServlet 21 | org.springframework.web.servlet.DispatcherServlet 22 | 26 | 27 | contextConfigLocation 28 | classpath:spring/spring-*.xml 29 | 30 | 1 31 | 32 | 33 | 34 | springDispatcherServlet 35 | / 36 | 37 | -------------------------------------------------------------------------------- /src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 2 | 3 | 4 | 5 | 6 | Vue 测试实例 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
序号名称
{{index + 1}}{{key}}{{value}}
27 |
28 | 29 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/webapp/resources/script/seckill.js: -------------------------------------------------------------------------------- 1 | /* 2 | jquery.cookie.js插件: 3 | 4 | 5 | 6 | 7 | 新增cookie: 8 | $.cookie('cookieName', 'cookieValue'); 9 | 注:如果没有设置cookie的有效期,则cookie默认在浏览器关闭前都有效,故被称为"会话cookie"。 10 | 11 | // 创建一个cookie并设置有效时间为7天: 12 | $.cookie('cookieName', 'cookieValue', { expires: 7 }); 13 | 14 | // 创建一个cookie并设置cookie的有效路径: 15 | $.cookie('cookieName', 'cookieValue', { expires: 7, path: '/' }); 16 | 17 | 读取cookie: 18 | $.cookie('cookieName'); // 若cookie存在则返回'cookieValue';若cookie不存在则返回null 19 | 20 | 删除cookie:把ncookie的值设为null即可 21 | $.cookie('the_cookie', null); 22 | */ 23 | 24 | // 存放主要交互逻辑js代码 25 | // javascript 模块化(package.class.method) 26 | // seckill.detail.init(params); 27 | /** 28 | * seckill 模块逻辑 29 | * 30 | * seckill.detail.init(params); 31 | */ 32 | var seckill = { 33 | // 封装秒杀相关ajax的url 34 | URL : { 35 | now : function() { 36 | return "/seckill/time/now"; 37 | }, 38 | exposer : function(seckillId) { 39 | return "/seckill/" + seckillId + "/exposer"; 40 | }, 41 | execution : function(seckillId, md5) { 42 | return "/seckill/" + seckillId + "/" + md5 + "/execution"; 43 | } 44 | }, 45 | // 验证手机号 46 | validatePhone : function(phone) { 47 | if (phone && phone.length == 11 && !isNaN(phone)) { 48 | return true; 49 | } else { 50 | return false; 51 | } 52 | }, 53 | // 处理秒杀逻辑 54 | handleSeckillKill : function(seckillId, node) { 55 | // 获取秒杀地址,控制显示逻辑,执行秒杀 56 | node.hide() 57 | .html('');// 按钮 58 | $.post(seckill.URL.exposer(seckillId), {}, function(result) { 59 | // 在回调函数中,执行交互流程 60 | if(result && result["success"]) { 61 | var exposer = result["data"]; 62 | if(exposer["exposed"]) { 63 | // 开启秒杀 64 | // 获取秒杀地址 65 | var md5 = exposer["md5"]; 66 | var killUrl = seckill.URL.execution(seckillId, md5); 67 | // click 事件只能点击一次,防止用户重复点击 68 | $("#killBtn").one("click", function() { 69 | // 执行秒杀请求 70 | // 1:先禁用按钮 71 | $(this).addClass("disabled"); 72 | // 2:发送秒杀请求 73 | $.post(killUrl, {}, function(result) { 74 | if(result && result["success"]) { 75 | var killResult = result["data"]; 76 | var state = killResult["state"]; 77 | var stateInfo = killResult["stateInfo"]; 78 | console.log(stateInfo); 79 | // 3:显示秒杀结果 80 | node.html('' + stateInfo + ''); 81 | } 82 | }); 83 | }); 84 | node.show(); 85 | } else { 86 | // 未开启秒杀 87 | var now = exposer["now"]; 88 | var start = exposer["start"]; 89 | var end = exposer["end"]; 90 | // 重新计算计时逻辑 91 | seckill.countdown(seckillId, now, startTime, endTime); 92 | } 93 | } 94 | }); 95 | }, 96 | // 秒杀倒计时 97 | countdown: function(seckillId, nowTime, startTime, endTime) { 98 | var seckillBox = $("#seckill-box"); 99 | // 时间判断 100 | if(nowTime > endTime) { 101 | // 秒杀结束 102 | seckillBox.html("秒杀结束"); 103 | } else if(nowTime < startTime) { 104 | // 秒杀未开始,计时事件绑定 105 | /* 106 | 关于显示NaN天 NaN时 NaN分 NaN秒 的问题,原因是new Date(startTime + 1000),startTime 被解释成一个字符串了。 107 | 解决办法: 1.new Date(startTime-0 + 1000); 108 | 2.new Date(Number(startTime) + 1000); 109 | */ 110 | var killTime = new Date(Number(startTime) + 1000); // 加上一秒,防止时间偏移 111 | seckillBox.countdown(killTime, function(event) { 112 | // 控制时间格式 113 | var format = event.strftime("秒杀倒计时: %D天 %H时 %M分 %S秒"); 114 | seckillBox.html(format); 115 | // 时间完成后回调事件 116 | }).on("finish.countdown", function() { 117 | seckill.handleSeckillKill(seckillId, seckillBox); 118 | }); 119 | } else { 120 | // 秒杀开始 121 | seckill.handleSeckillKill(seckillId, seckillBox); 122 | } 123 | }, 124 | // 详情页秒杀逻辑 125 | detail : { 126 | // 详情页初始化 127 | init : function(params) { 128 | // 手机验证和登录,计时交互 129 | // 规划我们的交互流程 130 | // 在cookie中查找手机号 131 | var killPhone = $.cookie("killPhone"); 132 | var seckillId = params["seckillId"]; 133 | var startTime = params["startTime"]; 134 | var endTime = params["endTime"]; 135 | 136 | /* 137 | 这种方式也可以 138 | console.log(params.seckillId); 139 | console.log(params.startTime); 140 | console.log(params.endTime); 141 | */ 142 | 143 | // 验证手机号 144 | if (!seckill.validatePhone(killPhone)) { 145 | // 绑定phone 146 | // 控制输出 147 | var killPhoneModal = $("#killPhoneModal"); 148 | killPhoneModal.modal({ 149 | show : true, // 当初始化时显示模态框。 150 | backdrop : "static", // 指定一个静态的背景,当用户点击模态框外部时不会关闭模态框。 151 | keyboard : false // 当按下 escape 键时关闭模态框,设置为 false 时则按键无效。 152 | }); 153 | $("#killPhoneBtn").click(function() { 154 | var inputPhone = $("#killphoneKey").val(); 155 | if (seckill.validatePhone(inputPhone)) { 156 | // 手机号码写入cookie,7天,有效路径 157 | $.cookie("killPhone", inputPhone, { 158 | expires : 7, 159 | path : "/seckill" 160 | }); 161 | // 刷新页面 162 | window.location.reload(); 163 | } else { 164 | var label = ''; 165 | $("#killPhoneMessage").hide().html(label).show(300); 166 | } 167 | }); 168 | } 169 | // 已经登录 170 | // 计时交互 171 | $.get(seckill.URL.now(), {}, function(result) { 172 | if(result && result["success"]) { 173 | var nowTime = result["data"]; 174 | // 时间判断 175 | seckill.countdown(seckillId, nowTime, startTime, endTime); 176 | } 177 | }); 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /src/test/java/cn/colg/core/BaseTester.java: -------------------------------------------------------------------------------- 1 | package cn.colg.core; 2 | 3 | import org.junit.runner.RunWith; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.test.context.ContextConfiguration; 6 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 7 | 8 | import cn.colg.dao.SeckillMapper; 9 | import cn.colg.dao.SuccessKilledMapper; 10 | import cn.colg.service.SeckillService; 11 | import cn.colg.service.SuccessKilledService; 12 | 13 | /** 14 | * 基础测试类,单元测试继承该类即可 15 | * 16 | *
17 |  * spring-test, junit
18 |  * 配置 Spring 和 Junit 整合
19 |  * 
20 | * 21 | * @author colg 22 | */ 23 | @RunWith(SpringJUnit4ClassRunner.class) 24 | @ContextConfiguration(locations = { "classpath:spring/spring-*.xml" }) 25 | public abstract class BaseTester { 26 | 27 | /** 28 | * 注入service 29 | */ 30 | @Autowired 31 | protected SeckillService seckillService; 32 | @Autowired 33 | protected SuccessKilledService successKilledService; 34 | 35 | /** 36 | * 注入mapper 37 | */ 38 | @Autowired 39 | protected SeckillMapper seckillMapper; 40 | @Autowired 41 | protected SuccessKilledMapper successKilledMapper; 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/cn/colg/dao/SeckillMapperTest.java: -------------------------------------------------------------------------------- 1 | package cn.colg.dao; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | 6 | import org.junit.Test; 7 | 8 | import cn.colg.core.BaseTester; 9 | import cn.colg.entity.Seckill; 10 | import cn.hutool.log.Log; 11 | import cn.hutool.log.LogFactory; 12 | 13 | /** 14 | * 配置spring和junit整合 15 | * 16 | * spring-test, junit 17 | * 18 | * @author colg 19 | */ 20 | public class SeckillMapperTest extends BaseTester { 21 | 22 | private static final Log log = LogFactory.get(); 23 | 24 | @Test 25 | public void testReduceNumber() { 26 | String id = "c18c12a838c311e89fa754ee75c6aeb0"; 27 | int updateCount = seckillMapper.reduceNumber(id, new Date()); 28 | log.info("updateCount:{}", updateCount); 29 | } 30 | 31 | @Test 32 | public void testFindById() { 33 | String id = "c18c12a838c311e89fa754ee75c6aeb0"; 34 | Seckill seckill = seckillMapper.findById(id); 35 | log.info("seckill: {}", seckill); 36 | } 37 | 38 | @Test 39 | public void testQuerySeckill() { 40 | List list = seckillMapper.querySeckill(0, 100); 41 | log.info("list:{}", list); 42 | } 43 | 44 | @Test 45 | public void testFindNameById() { 46 | String id = "c18c12a838c311e89fa754ee75c6aeb0"; 47 | String name = seckillMapper.findNameById(id); 48 | log.info("name:{}", name); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/cn/colg/dao/SuccessKilledMapperTest.java: -------------------------------------------------------------------------------- 1 | package cn.colg.dao; 2 | 3 | import org.junit.Test; 4 | 5 | import cn.colg.core.BaseTester; 6 | import cn.colg.entity.SuccessKilled; 7 | import cn.hutool.log.Log; 8 | import cn.hutool.log.LogFactory; 9 | 10 | /** 11 | * 配置spring和junit整合 12 | * 13 | * spring-test, junit 14 | * 15 | * @author colg 16 | */ 17 | public class SuccessKilledMapperTest extends BaseTester { 18 | 19 | private static final Log log = LogFactory.get(); 20 | 21 | @Test 22 | public void testInsertSuccessKilled() { 23 | String seckillId = "c18c169938c311e89fa754ee75c6aeb0"; 24 | String userPhone = "18701012345"; 25 | int insertCount = successKilledMapper.insertSuccessKilled(seckillId, userPhone); 26 | log.info("insertCount: {}", insertCount); 27 | } 28 | 29 | @Test 30 | public void testFindBySeckillId() { 31 | String seckillId = "c18c169938c311e89fa754ee75c6aeb0"; 32 | String userPhone = "18701012345"; 33 | SuccessKilled successKilled = successKilledMapper.findBySeckillId(seckillId, userPhone); 34 | log.info("successKilled:{}", successKilled); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/cn/colg/service/SeckillServiceTest.java: -------------------------------------------------------------------------------- 1 | package cn.colg.service; 2 | 3 | import java.util.List; 4 | 5 | import org.junit.Test; 6 | 7 | import cn.colg.core.BaseTester; 8 | import cn.colg.dto.Exposer; 9 | import cn.colg.dto.SeckillExecution; 10 | import cn.colg.entity.Seckill; 11 | import cn.colg.exception.RepeatKillException; 12 | import cn.colg.exception.SeckillCloseException; 13 | import cn.colg.exception.SeckillException; 14 | import cn.hutool.log.Log; 15 | import cn.hutool.log.LogFactory; 16 | 17 | /** 18 | * 秒杀Service 测试 19 | * 20 | * @author colg 21 | */ 22 | public class SeckillServiceTest extends BaseTester { 23 | 24 | private static final Log log = LogFactory.get(); 25 | 26 | @Test 27 | public void testQuerySeckill() { 28 | List list = seckillService.querySeckill(); 29 | log.info("list:{}", list); 30 | } 31 | 32 | @Test 33 | public void testFindById() { 34 | String seckillId = "c18c12a838c311e89fa754ee75c6aeb0"; 35 | Seckill seckill = seckillService.findById(seckillId); 36 | log.info("seckill:{}", seckill); 37 | } 38 | 39 | @Test 40 | public void testExportSeckillUrl() { 41 | String seckillId = "c18c12a838c311e89fa754ee75c6aeb0"; 42 | Exposer exposer = seckillService.exportSeckillUrl(seckillId); 43 | log.info("exposer:{}", exposer); 44 | /* 45 | exposer: 46 | { 47 | "end": 1446393600000, 48 | "exposed": false, 49 | "md5": null, 50 | "now": 1522959531529, 51 | "seckillId": "c18c12a838c311e89fa754ee75c6aeb0", 52 | "start": 1446307200000 53 | } 54 | */ 55 | } 56 | 57 | @Test 58 | public void testExecuteSeckill() { 59 | String seckillId = "c18c12a838c311e89fa754ee75c6aeb0"; 60 | String userPhone = "95205246715"; 61 | String md5 = "76be59afc9b970b7c3fa41f6f1539865"; 62 | SeckillExecution seckillExecution = null; 63 | try { 64 | seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5); 65 | log.info("seckillExecution:{}", seckillExecution); 66 | } catch (RepeatKillException e) { 67 | log.error("e: {}", e.getMessage()); 68 | } catch (SeckillCloseException e) { 69 | log.error("e: {}", e.getMessage()); 70 | } catch (SeckillException e) { 71 | log.error("e: {}", e.getMessage()); 72 | } 73 | 74 | /* 75 | { 76 | "seckillId": "c18c12a838c311e89fa754ee75c6aeb0", 77 | "state": "1", 78 | "stateInfo": "秒杀成功", 79 | "successKilled": { 80 | "createTime": "2018-04-06 04:31:22", 81 | "seckill": { 82 | "createTime": "2018-04-05 19:23:25", 83 | "endTime": "2015-11-02 00:00:00", 84 | "name": "1000元秒杀iphone6", 85 | "number": 99, 86 | "seckillId": "c18c12a838c311e89fa754ee75c6aeb0", 87 | "startTime": "2015-11-01 00:00:00", 88 | "successKilleds": null 89 | }, 90 | "seckillId": "c18c12a838c311e89fa754ee75c6aeb0", 91 | "state": "0", 92 | "userPhone": "95205246715" 93 | } 94 | } 95 | */ 96 | } 97 | 98 | /** 99 | * 集成测试代码完整逻辑,注意可重复执行。 100 | * 101 | * 如果秒杀开启,执行秒杀操作,应该一起运行 102 | */ 103 | @Test 104 | public void testSeckillLogic() { 105 | String seckillId = "c18c12a838c311e89fa754ee75c6aeb0"; 106 | Exposer exposer = seckillService.exportSeckillUrl(seckillId); 107 | if (exposer.getExposed()) { 108 | log.info("exposer:{}", exposer); 109 | 110 | // 执行秒杀操作 111 | String userPhone = "95205246715"; 112 | String md5 = exposer.getMd5(); 113 | SeckillExecution seckillExecution = null; 114 | try { 115 | seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5); 116 | log.info("seckillExecution:{}", seckillExecution); 117 | } catch (RepeatKillException e) { 118 | log.error("e: {}", e.getMessage()); 119 | } catch (SeckillCloseException e) { 120 | log.error("e: {}", e.getMessage()); 121 | } catch (SeckillException e) { 122 | log.error("e: {}", e.getMessage()); 123 | } 124 | } else { 125 | log.warn("expose: {}", exposer); 126 | } 127 | } 128 | 129 | } 130 | --------------------------------------------------------------------------------