├── src ├── main │ ├── webapp │ │ ├── META-INF │ │ │ └── MANIFEST.MF │ │ ├── index.jsp │ │ ├── WEB-INF │ │ │ ├── jsp │ │ │ │ ├── common │ │ │ │ │ ├── tag.jsp │ │ │ │ │ └── head.jsp │ │ │ │ ├── list.jsp │ │ │ │ └── detail.jsp │ │ │ └── web.xml │ │ └── resources │ │ │ └── script │ │ │ └── seckill.js │ ├── resources │ │ ├── jdbc.properties │ │ ├── logback.xml │ │ ├── mybatis-config.xml │ │ └── spring │ │ │ ├── spring-service.xml │ │ │ ├── spring-web.xml │ │ │ └── spring-dao.xml │ └── java │ │ └── com │ │ └── lewis │ │ ├── exception │ │ ├── SeckillException.java │ │ ├── SeckillCloseException.java │ │ └── RepeatKillException.java │ │ ├── dao │ │ ├── SuccessKilledDao.java │ │ ├── SeckillDao.java │ │ └── cache │ │ │ └── RedisDao.java │ │ ├── enums │ │ └── SeckillStatEnum.java │ │ ├── dto │ │ ├── SeckillResult.java │ │ ├── Exposer.java │ │ └── SeckillExecution.java │ │ ├── mapper │ │ ├── SuccessKilledDao.xml │ │ └── SeckillDao.xml │ │ ├── entity │ │ ├── SuccessKilled.java │ │ └── Seckill.java │ │ ├── service │ │ ├── SeckillService.java │ │ └── impl │ │ │ └── SeckillServiceImpl.java │ │ └── web │ │ └── SeckillController.java └── test │ └── java │ └── com │ └── lewis │ ├── dao │ ├── SuccessKilledDaoTest.java │ ├── cache │ │ └── RedisDaoTest.java │ └── SeckillDaoTest.java │ └── service │ └── SeckillServiceTest.java ├── .gitignore ├── sql ├── execute_seckill.sql └── schema.sql ├── pom.xml └── README.md /src/main/webapp/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Class-Path: 3 | 4 | -------------------------------------------------------------------------------- /src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/jdbc.properties: -------------------------------------------------------------------------------- 1 | driver=com.mysql.jdbc.Driver 2 | url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8 3 | jdbc.username=root 4 | password=123 5 | 6 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/common/tag.jsp: -------------------------------------------------------------------------------- 1 | <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 2 | <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> 3 | <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> 4 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/exception/SeckillException.java: -------------------------------------------------------------------------------- 1 | package com.lewis.exception; 2 | 3 | /** 4 | * 秒杀相关的所有业务异常 5 | */ 6 | public class SeckillException extends RuntimeException { 7 | public SeckillException(String message) { 8 | super(message); 9 | } 10 | 11 | public SeckillException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/exception/SeckillCloseException.java: -------------------------------------------------------------------------------- 1 | package com.lewis.exception; 2 | 3 | /** 4 | * 秒杀关闭异常,当秒杀结束时用户还要进行秒杀就会出现这个异常 5 | */ 6 | public class SeckillCloseException extends SeckillException{ 7 | public SeckillCloseException(String message) { 8 | super(message); 9 | } 10 | 11 | public SeckillCloseException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/exception/RepeatKillException.java: -------------------------------------------------------------------------------- 1 | package com.lewis.exception; 2 | 3 | /** 4 | * 重复秒杀异常,是一个运行期异常,不需要我们手动try catch 5 | * Mysql只支持运行期异常的回滚操作 6 | */ 7 | public class RepeatKillException extends SeckillException { 8 | 9 | public RepeatKillException(String message) { 10 | super(message); 11 | } 12 | 13 | public RepeatKillException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | 24 | # Maven 25 | target/ 26 | 27 | # Eclipse 28 | .settings/ 29 | build/ 30 | .classpath 31 | .project 32 | 33 | # IDEA 34 | *.iml 35 | .idea/ 36 | *.ipr 37 | *.iws 38 | 39 | # Java File 40 | Test.java 41 | MyTest.java -------------------------------------------------------------------------------- /src/main/java/com/lewis/dao/SuccessKilledDao.java: -------------------------------------------------------------------------------- 1 | package com.lewis.dao; 2 | 3 | import org.apache.ibatis.annotations.Param; 4 | 5 | import com.lewis.entity.SuccessKilled; 6 | 7 | public interface SuccessKilledDao { 8 | 9 | /** 10 | * 插入购买明细,可过滤重复 11 | * 12 | * @param seckillId 13 | * @param userphone 14 | * @return 插入的行数,如果返回值<1则表示插入失败 15 | */ 16 | int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone); 17 | 18 | /** 19 | * 根据id查询SuccessKilled并携带秒杀商品对象实体 20 | * 21 | * @param seckillId 22 | * @return 23 | */ 24 | SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/common/head.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | -------------------------------------------------------------------------------- /src/main/resources/mybatis-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/enums/SeckillStatEnum.java: -------------------------------------------------------------------------------- 1 | package com.lewis.enums; 2 | 3 | public enum SeckillStatEnum { 4 | 5 | SUCCESS(1,"秒杀成功"), 6 | END(0,"秒杀结束"), 7 | REPEAT_KILL(-1,"重复秒杀"), 8 | INNER_ERROR(-2,"系统异常"), 9 | DATE_REWRITE(-3,"数据篡改"); 10 | 11 | private int state; 12 | private String info; 13 | 14 | SeckillStatEnum(int state, String info) { 15 | this.state = state; 16 | this.info = info; 17 | } 18 | 19 | public int getState() { 20 | return state; 21 | } 22 | 23 | public String getInfo() { 24 | return info; 25 | } 26 | 27 | public static SeckillStatEnum stateOf(int index) { 28 | for (SeckillStatEnum state : values()) { 29 | if (state.getState() == index) { 30 | return state; 31 | } 32 | } 33 | return null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/dao/SeckillDao.java: -------------------------------------------------------------------------------- 1 | package com.lewis.dao; 2 | 3 | import java.util.Date; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | import org.apache.ibatis.annotations.Param; 8 | 9 | import com.lewis.entity.Seckill; 10 | 11 | public interface SeckillDao { 12 | 13 | /** 14 | * 减库存 15 | * 16 | * @param seckillId 17 | * @param killTime 18 | * @return 更新的记录行数,如果返回值<1则表示更新失败 19 | */ 20 | int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime); 21 | 22 | /** 23 | * 根据id查询秒杀商品 24 | * 25 | * @param seckillId 26 | * @return 27 | */ 28 | Seckill queryById(long seckillId); 29 | 30 | /** 31 | * 根据偏移量查询秒杀商品列表 32 | * 33 | * @param offset 34 | * @param limit 35 | * @return 36 | */ 37 | List queryAll(@Param("offset") int offset, @Param("limit") int limit); 38 | 39 | /** 40 | * 使用储存过程执行秒杀 41 | * @param paramMap 42 | */ 43 | void killByProcedure(Map paramMap); 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/dto/SeckillResult.java: -------------------------------------------------------------------------------- 1 | package com.lewis.dto; 2 | 3 | //将所有的ajax请求返回类型,全部封装成json数据 4 | public class SeckillResult { 5 | 6 | //请求是否成功 7 | private boolean success; 8 | private T data; 9 | private String error; 10 | 11 | public SeckillResult(boolean success, T data) { 12 | this.success = success; 13 | this.data = data; 14 | } 15 | 16 | public SeckillResult(boolean success, String error) { 17 | this.success = success; 18 | this.error = error; 19 | } 20 | 21 | public boolean isSuccess() { 22 | return success; 23 | } 24 | 25 | public void setSuccess(boolean success) { 26 | this.success = success; 27 | } 28 | 29 | public T getData() { 30 | return data; 31 | } 32 | 33 | public void setData(T data) { 34 | this.data = data; 35 | } 36 | 37 | public String getError() { 38 | return error; 39 | } 40 | 41 | public void setError(String error) { 42 | this.error = error; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | seckill-dispatcher 11 | org.springframework.web.servlet.DispatcherServlet 12 | 14 | 15 | contextConfigLocation 16 | classpath:spring/spring-*.xml 17 | 18 | 19 | 20 | seckill-dispatcher 21 | 22 | / 23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-service.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/test/java/com/lewis/dao/SuccessKilledDaoTest.java: -------------------------------------------------------------------------------- 1 | package com.lewis.dao; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import javax.annotation.Resource; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.test.context.ContextConfiguration; 10 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 11 | 12 | import com.lewis.entity.SuccessKilled; 13 | 14 | @RunWith(SpringJUnit4ClassRunner.class) 15 | // 告诉junit spring的配置文件 16 | @ContextConfiguration({ "classpath:spring/spring-dao.xml" }) 17 | public class SuccessKilledDaoTest { 18 | 19 | @Resource 20 | private SuccessKilledDao successKilledDao; 21 | 22 | @Test 23 | public void testInsertSuccessKilled() { 24 | 25 | long seckillId = 1000L; 26 | long userPhone = 13476191877L; 27 | int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone); 28 | System.out.println("insertCount=" + insertCount); 29 | } 30 | 31 | @Test 32 | public void testQueryByIdWithSeckill() { 33 | 34 | long seckillId = 1000L; 35 | long userPhone = 13476191877L; 36 | SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); 37 | System.out.println(successKilled); 38 | System.out.println(successKilled.getSeckill()); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/mapper/SuccessKilledDao.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | INSERT ignore INTO success_killed(seckill_id,user_phone,state) 9 | VALUES (#{seckillId},#{userPhone},0) 10 | 11 | 12 | 33 | 34 | -------------------------------------------------------------------------------- /src/test/java/com/lewis/dao/cache/RedisDaoTest.java: -------------------------------------------------------------------------------- 1 | package com.lewis.dao.cache; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.test.context.ContextConfiguration; 11 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 12 | 13 | import com.lewis.dao.SeckillDao; 14 | import com.lewis.entity.Seckill; 15 | 16 | @RunWith(SpringJUnit4ClassRunner.class) 17 | // 告诉junit spring的配置文件 18 | @ContextConfiguration({ "classpath:spring/spring-dao.xml" }) 19 | public class RedisDaoTest { 20 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 21 | 22 | private long id = 1001; 23 | 24 | @Autowired 25 | private RedisDao redisDao; 26 | 27 | @Autowired 28 | private SeckillDao seckillDao; 29 | 30 | @Test 31 | public void testSeckill() { 32 | 33 | Seckill seckill = redisDao.getSeckill(id); 34 | if (seckill == null) { 35 | seckill = seckillDao.queryById(id); 36 | if (seckill != null) { 37 | String result = redisDao.putSeckill(seckill); 38 | logger.info("result={}", result); 39 | seckill = redisDao.getSeckill(id); 40 | logger.info("seckill={}", seckill); 41 | } 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/entity/SuccessKilled.java: -------------------------------------------------------------------------------- 1 | package com.lewis.entity; 2 | 3 | import java.util.Date; 4 | 5 | public class SuccessKilled { 6 | private Byte state; 7 | 8 | private Date createTime; 9 | 10 | private Long seckillId; 11 | 12 | private Long userPhone; 13 | 14 | // 多对一,因为一件商品在库存中有很多数量,对应的购买明细也有很多。 15 | private Seckill seckill; 16 | 17 | public Seckill getSeckill() { 18 | return seckill; 19 | } 20 | 21 | public void setSeckill(Seckill seckill) { 22 | this.seckill = seckill; 23 | } 24 | 25 | public Long getSeckillId() { 26 | return seckillId; 27 | } 28 | 29 | public void setSeckillId(Long seckillId) { 30 | this.seckillId = seckillId; 31 | } 32 | 33 | public Long getUserPhone() { 34 | return userPhone; 35 | } 36 | 37 | public void setUserPhone(Long userPhone) { 38 | this.userPhone = userPhone; 39 | } 40 | 41 | public Byte getState() { 42 | return state; 43 | } 44 | 45 | public void setState(Byte state) { 46 | this.state = state; 47 | } 48 | 49 | public Date getCreateTime() { 50 | return createTime; 51 | } 52 | 53 | public void setCreateTime(Date createTime) { 54 | this.createTime = createTime; 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | return "SuccessKilled [state=" + state + ", createTime=" + createTime + ", seckillId=" + seckillId 60 | + ", userPhone=" + userPhone + "]"; 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/test/java/com/lewis/dao/SeckillDaoTest.java: -------------------------------------------------------------------------------- 1 | package com.lewis.dao; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.util.Date; 6 | import java.util.List; 7 | 8 | import javax.annotation.Resource; 9 | 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.springframework.test.context.ContextConfiguration; 13 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 14 | 15 | import com.lewis.entity.Seckill; 16 | 17 | /** 18 | * 配置Spring和Junit整合,junit启动时加载springIOC容器 spring-test,junit 19 | */ 20 | @RunWith(SpringJUnit4ClassRunner.class) 21 | // 告诉junit spring的配置文件 22 | @ContextConfiguration({ "classpath:spring/spring-dao.xml" }) 23 | public class SeckillDaoTest { 24 | 25 | // 注入Dao实现类依赖 26 | @Resource 27 | private SeckillDao seckillDao; 28 | 29 | @Test 30 | public void testQueryById() { 31 | 32 | long seckillId = 1000; 33 | Seckill seckill = seckillDao.queryById(seckillId); 34 | System.out.println(seckill.getName()); 35 | System.out.println(seckill); 36 | } 37 | 38 | @Test 39 | public void testQueryAll() { 40 | 41 | List seckills = seckillDao.queryAll(0, 100); 42 | for (Seckill seckill : seckills) { 43 | System.out.println(seckill); 44 | } 45 | } 46 | 47 | @Test 48 | public void testReduceNumber() { 49 | 50 | long seckillId = 1000; 51 | Date date = new Date(); 52 | int updateCount = seckillDao.reduceNumber(seckillId, date); 53 | System.out.println(updateCount); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /sql/execute_seckill.sql: -------------------------------------------------------------------------------- 1 | -- 秒杀执行储存过程 2 | DELIMITER $$ -- 将定界符从;转换为$$ 3 | -- 定义储存过程 4 | -- 参数: in输入参数 out输出参数 5 | -- row_count() 返回上一条修改类型sql(delete,insert,update)的影响行数 6 | -- row_count:0:未修改数据 ; >0:表示修改的行数; <0:sql错误 7 | CREATE PROCEDURE `seckill`.`execute_seckill` 8 | (IN v_seckill_id BIGINT, IN v_phone BIGINT, 9 | IN v_kill_time TIMESTAMP, OUT r_result INT) 10 | BEGIN 11 | DECLARE insert_count INT DEFAULT 0; 12 | START TRANSACTION; 13 | INSERT IGNORE INTO success_killed 14 | (seckill_id, user_phone, state) 15 | VALUES (v_seckill_id, v_phone, 0); 16 | SELECT row_count() INTO insert_count; 17 | IF (insert_count = 0) THEN 18 | ROLLBACK; 19 | SET r_result = -1; 20 | ELSEIF (insert_count < 0) THEN 21 | ROLLBACK; 22 | SET r_result = -2; 23 | ELSE 24 | UPDATE seckill 25 | SET number = number - 1 26 | WHERE seckill_id = v_seckill_id 27 | AND end_time > v_kill_time 28 | AND start_time < v_kill_time 29 | AND number > 0; 30 | SELECT row_count() INTO insert_count; 31 | IF (insert_count = 0) THEN 32 | ROLLBACK; 33 | SET r_result = 0; 34 | ELSEIF (insert_count < 0) THEN 35 | ROLLBACK; 36 | SET r_result = -2; 37 | ELSE 38 | COMMIT; 39 | SET r_result = 1; 40 | END IF; 41 | END IF; 42 | END; 43 | $$ 44 | -- 储存过程定义结束 45 | -- 将定界符重新改为; 46 | DELIMITER ; 47 | 48 | -- 定义一个用户变量r_result 49 | SET @r_result = -3; 50 | -- 执行储存过程 51 | CALL execute_seckill(1003, 13502178891, now(), @r_result); 52 | -- 获取结果 53 | SELECT @r_result; -------------------------------------------------------------------------------- /src/main/java/com/lewis/mapper/SeckillDao.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | UPDATE seckill 12 | SET number = number-1 13 | WHERE seckill_id=#{seckillId} 14 | AND start_time 15 | #{killTime} 16 | AND end_time >= #{killTime} 17 | AND number > 0; 18 | 19 | 20 | 22 | 30 | 31 | 37 | 38 | 39 | 47 | 48 | -------------------------------------------------------------------------------- /sql/schema.sql: -------------------------------------------------------------------------------- 1 | -- 数据库初始化脚本 2 | 3 | -- 创建数据库 4 | CREATE DATABASE seckill; 5 | -- 使用数据库 6 | use seckill; 7 | CREATE TABLE seckill( 8 | `seckill_id` BIGINT NOT NUll AUTO_INCREMENT COMMENT '商品库存ID', 9 | `name` VARCHAR(120) NOT NULL COMMENT '商品名称', 10 | `number` int NOT NULL COMMENT '库存数量', 11 | `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 12 | `start_time` TIMESTAMP NOT NULL COMMENT '秒杀开始时间', 13 | `end_time` TIMESTAMP NOT NULL COMMENT '秒杀结束时间', 14 | PRIMARY KEY (seckill_id), 15 | key idx_start_time(start_time), 16 | key idx_end_time(end_time), 17 | key idx_create_time(create_time) 18 | )ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表'; 19 | 20 | -- 初始化数据 21 | INSERT into seckill(name,number,start_time,end_time) 22 | VALUES 23 | ('1000元秒杀iphone6',100,'2016-01-01 00:00:00','2016-01-02 00:00:00'), 24 | ('800元秒杀ipad',200,'2016-01-01 00:00:00','2016-01-02 00:00:00'), 25 | ('6600元秒杀mac book pro',300,'2016-01-01 00:00:00','2016-01-02 00:00:00'), 26 | ('7000元秒杀iMac',400,'2016-01-01 00:00:00','2016-01-02 00:00:00'); 27 | 28 | -- 秒杀成功明细表 29 | -- 用户登录认证相关信息(简化为手机号) 30 | CREATE TABLE success_killed( 31 | `seckill_id` BIGINT NOT NULL COMMENT '秒杀商品ID', 32 | `user_phone` BIGINT NOT NULL COMMENT '用户手机号', 33 | `state` TINYINT NOT NULL DEFAULT -1 COMMENT '状态标识:-1:无效 0:成功 1:已付款 2:已发货', 34 | `create_time` TIMESTAMP NOT NULL COMMENT '创建时间', 35 | PRIMARY KEY(seckill_id,user_phone),/*联合主键*/ 36 | KEY idx_create_time(create_time) 37 | )ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表'; 38 | 39 | -- SHOW CREATE TABLE seckill;#显示表的创建信息 -------------------------------------------------------------------------------- /src/main/resources/spring/spring-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/service/SeckillService.java: -------------------------------------------------------------------------------- 1 | package com.lewis.service; 2 | 3 | import java.util.List; 4 | 5 | import com.lewis.dto.Exposer; 6 | import com.lewis.dto.SeckillExecution; 7 | import com.lewis.entity.Seckill; 8 | import com.lewis.exception.RepeatKillException; 9 | import com.lewis.exception.SeckillCloseException; 10 | import com.lewis.exception.SeckillException; 11 | 12 | /** 13 | * 业务接口:站在使用者(程序员)的角度设计接口 三个方面:1.方法定义粒度,方法定义的要非常清楚2.参数,要越简练越好 3.返回类型(return 14 | * 类型一定要友好/或者return异常,我们允许的异常) 15 | */ 16 | public interface SeckillService { 17 | 18 | /** 19 | * 查询全部的秒杀记录 20 | * 21 | * @return 22 | */ 23 | List getSeckillList(); 24 | 25 | /** 26 | * 查询单个秒杀记录 27 | * 28 | * @param seckillId 29 | * @return 30 | */ 31 | Seckill getById(long seckillId); 32 | 33 | // 再往下,是我们最重要的行为的一些接口 34 | 35 | /** 36 | * 在秒杀开启时输出秒杀接口的地址,否则输出系统时间和秒杀时间 37 | * 38 | * @param seckillId 秒杀商品Id 39 | * @return 根据对应的状态返回对应的状态实体 40 | */ 41 | Exposer exportSeckillUrl(long seckillId); 42 | 43 | /** 44 | * 执行秒杀操作,有可能失败,有可能成功,所以要抛出我们允许的异常 45 | * 46 | * @param seckillId 秒杀的商品ID 47 | * @param userPhone 手机号码 48 | * @param md5 md5加密值 49 | * @return 根据不同的结果返回不同的实体信息 50 | */ 51 | SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, 52 | RepeatKillException, SeckillCloseException; 53 | 54 | /** 55 | * 调用存储过程来执行秒杀操作,不需要抛出异常 56 | * 57 | * @param seckillId 秒杀的商品ID 58 | * @param userPhone 手机号码 59 | * @param md5 md5加密值 60 | * @return 根据不同的结果返回不同的实体信息 61 | */ 62 | SeckillExecution executeSeckillProcedure(long seckillId,long userPhone,String md5); 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/entity/Seckill.java: -------------------------------------------------------------------------------- 1 | package com.lewis.entity; 2 | 3 | import java.util.Date; 4 | 5 | public class Seckill { 6 | private Long seckillId; 7 | 8 | private String name; 9 | 10 | private Integer number; 11 | 12 | private Date createTime; 13 | 14 | private Date startTime; 15 | 16 | private Date endTime; 17 | 18 | public Long getSeckillId() { 19 | return seckillId; 20 | } 21 | 22 | public void setSeckillId(Long seckillId) { 23 | this.seckillId = seckillId; 24 | } 25 | 26 | public String getName() { 27 | return name; 28 | } 29 | 30 | public void setName(String name) { 31 | this.name = name == null ? null : name.trim(); 32 | } 33 | 34 | public Integer getNumber() { 35 | return number; 36 | } 37 | 38 | public void setNumber(Integer number) { 39 | this.number = number; 40 | } 41 | 42 | public Date getCreateTime() { 43 | return createTime; 44 | } 45 | 46 | public void setCreateTime(Date createTime) { 47 | this.createTime = createTime; 48 | } 49 | 50 | public Date getStartTime() { 51 | return startTime; 52 | } 53 | 54 | public void setStartTime(Date startTime) { 55 | this.startTime = startTime; 56 | } 57 | 58 | public Date getEndTime() { 59 | return endTime; 60 | } 61 | 62 | public void setEndTime(Date endTime) { 63 | this.endTime = endTime; 64 | } 65 | 66 | @Override 67 | public String toString() { 68 | return "Seckill [seckillId=" + seckillId + ", name=" + name + ", number=" + number + ", createTime=" + createTime + ", startTime=" 69 | + startTime + ", endTime=" + endTime + "]"; 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/java/com/lewis/dao/cache/RedisDao.java: -------------------------------------------------------------------------------- 1 | package com.lewis.dao.cache; 2 | 3 | import com.dyuproject.protostuff.LinkedBuffer; 4 | import com.dyuproject.protostuff.ProtostuffIOUtil; 5 | import com.dyuproject.protostuff.runtime.RuntimeSchema; 6 | import com.lewis.entity.Seckill; 7 | 8 | import redis.clients.jedis.Jedis; 9 | import redis.clients.jedis.JedisPool; 10 | 11 | public class RedisDao { 12 | private final JedisPool jedisPool; 13 | 14 | public RedisDao(String ip, int port) { 15 | jedisPool = new JedisPool(ip, port); 16 | } 17 | 18 | private RuntimeSchema schema = RuntimeSchema.createFrom(Seckill.class); 19 | 20 | public Seckill getSeckill(long seckillId) { 21 | // redis操作逻辑 22 | try { 23 | Jedis jedis = jedisPool.getResource(); 24 | try { 25 | String key = "seckill:" + seckillId; 26 | // 并没有实现哪部序列化操作 27 | // 采用自定义序列化 28 | // protostuff: pojo. 29 | byte[] bytes = jedis.get(key.getBytes()); 30 | // 缓存重获取到 31 | if (bytes != null) { 32 | Seckill seckill = schema.newMessage(); 33 | ProtostuffIOUtil.mergeFrom(bytes, seckill, schema); 34 | // seckill被反序列化 35 | 36 | return seckill; 37 | } 38 | } finally { 39 | jedis.close(); 40 | } 41 | } catch (Exception e) { 42 | 43 | } 44 | return null; 45 | } 46 | 47 | public String putSeckill(Seckill seckill) { 48 | try { 49 | Jedis jedis = jedisPool.getResource(); 50 | try { 51 | String key = "seckill:" + seckill.getSeckillId(); 52 | byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, 53 | LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE)); 54 | // 超时缓存 55 | int timeout = 60 * 60;// 1小时 56 | String result = jedis.setex(key.getBytes(), timeout, bytes); 57 | 58 | return result; 59 | } finally { 60 | jedis.close(); 61 | } 62 | } catch (Exception e) { 63 | 64 | } 65 | 66 | return null; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/dto/Exposer.java: -------------------------------------------------------------------------------- 1 | package com.lewis.dto; 2 | 3 | /** 4 | * 暴露秒杀地址(接口)DTO 5 | */ 6 | public class Exposer { 7 | 8 | // 是否开启秒杀 9 | private boolean exposed; 10 | 11 | // 加密措施 12 | private String md5; 13 | 14 | //id为seckillId的商品的秒杀地址 15 | private long seckillId; 16 | 17 | // 系统当前时间(毫秒) 18 | private long now; 19 | 20 | // 秒杀的开启时间 21 | private long start; 22 | 23 | // 秒杀的结束时间 24 | private long end; 25 | 26 | public Exposer(boolean exposed, String md5, long seckillId) { 27 | this.exposed = exposed; 28 | this.md5 = md5; 29 | this.seckillId = seckillId; 30 | } 31 | 32 | public Exposer(boolean exposed, long seckillId, long now, long start, long end) { 33 | this.exposed = exposed; 34 | this.seckillId = seckillId; 35 | this.now = now; 36 | this.start = start; 37 | this.end = end; 38 | } 39 | 40 | public Exposer(boolean exposed, long seckillId) { 41 | this.exposed = exposed; 42 | this.seckillId = seckillId; 43 | } 44 | 45 | public boolean isExposed() { 46 | return exposed; 47 | } 48 | 49 | public void setExposed(boolean exposed) { 50 | this.exposed = exposed; 51 | } 52 | 53 | public String getMd5() { 54 | return md5; 55 | } 56 | 57 | public void setMd5(String md5) { 58 | this.md5 = md5; 59 | } 60 | 61 | public long getSeckillId() { 62 | return seckillId; 63 | } 64 | 65 | public void setSeckillId(long seckillId) { 66 | this.seckillId = seckillId; 67 | } 68 | 69 | public long getNow() { 70 | return now; 71 | } 72 | 73 | public void setNow(long now) { 74 | this.now = now; 75 | } 76 | 77 | public long getStart() { 78 | return start; 79 | } 80 | 81 | public void setStart(long start) { 82 | this.start = start; 83 | } 84 | 85 | public long getEnd() { 86 | return end; 87 | } 88 | 89 | public void setEnd(long end) { 90 | this.end = end; 91 | } 92 | 93 | @Override 94 | public String toString() { 95 | return "Exposer{" + "exposed=" + exposed + ", md5='" + md5 + '\'' + ", seckillId=" + seckillId + ", now=" + now 96 | + ", start=" + start + ", end=" + end + '}'; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/dto/SeckillExecution.java: -------------------------------------------------------------------------------- 1 | package com.lewis.dto; 2 | 3 | import com.lewis.entity.SuccessKilled; 4 | import com.lewis.enums.SeckillStatEnum; 5 | 6 | 7 | /** 8 | * 封装执行秒杀后的结果:是否秒杀成功 9 | */ 10 | public class SeckillExecution { 11 | 12 | private long seckillId; 13 | 14 | //秒杀执行结果的状态 15 | private int state; 16 | 17 | //状态的明文标识 18 | private String stateInfo; 19 | 20 | //当秒杀成功时,需要传递秒杀成功的对象回去 21 | private SuccessKilled successKilled; 22 | 23 | //秒杀成功返回所有信息 24 | public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) { 25 | this.seckillId = seckillId; 26 | this.state = statEnum.getState(); 27 | this.stateInfo = statEnum.getInfo(); 28 | this.successKilled = successKilled; 29 | } 30 | 31 | //秒杀失败 32 | public SeckillExecution(long seckillId, SeckillStatEnum statEnum) { 33 | this.seckillId = seckillId; 34 | this.state = statEnum.getState(); 35 | this.stateInfo = statEnum.getInfo(); 36 | } 37 | 38 | public long getSeckillId() { 39 | return seckillId; 40 | } 41 | 42 | public void setSeckillId(long seckillId) { 43 | this.seckillId = seckillId; 44 | } 45 | 46 | public int getState() { 47 | return state; 48 | } 49 | 50 | public void setState(int state) { 51 | this.state = state; 52 | } 53 | 54 | public String getStateInfo() { 55 | return stateInfo; 56 | } 57 | 58 | public void setStateInfo(String stateInfo) { 59 | this.stateInfo = stateInfo; 60 | } 61 | 62 | public SuccessKilled getSuccessKilled() { 63 | return successKilled; 64 | } 65 | 66 | public void setSuccessKilled(SuccessKilled successKilled) { 67 | 68 | this.successKilled = successKilled; 69 | } 70 | 71 | @Override 72 | public String toString() { 73 | return "SeckillExecution{" + 74 | "seckillId=" + seckillId + 75 | ", state=" + state + 76 | ", stateInfo='" + stateInfo + '\'' + 77 | ", successKilled=" + successKilled + 78 | '}'; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/list.jsp: -------------------------------------------------------------------------------- 1 | <%@page contentType="text/html; charset=UTF-8" language="java" %> 2 | <%@include file="common/tag.jsp"%> 3 | 4 | 5 | 6 | 秒杀商品列表 7 | <%@include file="common/head.jsp" %> 8 | 9 | 10 |
11 |
12 |
13 |

秒杀列表

14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 38 | 41 | 42 | 43 | 44 | 45 |
名称库存开始时间结束时间创建时间详情页
${sk.name}${sk.number} 33 | 34 | 36 | 37 | 39 | 40 | 详情
46 | 47 |
48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-dao.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 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 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/detail.jsp: -------------------------------------------------------------------------------- 1 | <%@page contentType="text/html; charset=UTF-8" language="java" %> 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 | <%--显示time图标--%> 19 | 20 | <%--展示倒计时--%> 21 | 22 |

23 |
24 |
25 |
26 | <%--登录弹出层 输入电话--%> 27 | 60 | 61 | 62 | <%--jQery文件,务必在bootstrap.min.js之前引入--%> 63 | 64 | 65 | <%--使用CDN 获取公共js http://www.bootcdn.cn/--%> 66 | <%--jQuery Cookie操作插件--%> 67 | 68 | <%--jQuery countDown倒计时插件--%> 69 | 70 | 71 | 72 | 73 | 83 | -------------------------------------------------------------------------------- /src/test/java/com/lewis/service/SeckillServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.lewis.service; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import java.util.List; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.test.context.ContextConfiguration; 13 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | import com.lewis.dto.Exposer; 17 | import com.lewis.dto.SeckillExecution; 18 | import com.lewis.entity.Seckill; 19 | import com.lewis.exception.RepeatKillException; 20 | import com.lewis.exception.SeckillCloseException; 21 | 22 | @RunWith(SpringJUnit4ClassRunner.class) 23 | // 告诉junit spring的配置文件 24 | @ContextConfiguration({ "classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml" }) 25 | public class SeckillServiceTest { 26 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 27 | 28 | // 注入Service实现类依赖 29 | @Autowired 30 | private SeckillService seckillService; 31 | 32 | @Test 33 | public void testGetSeckillList() { 34 | List list = seckillService.getSeckillList(); 35 | logger.info("list={}", list); 36 | } 37 | 38 | @Test 39 | public void testGetById() { 40 | long seckillId = 1000; 41 | Seckill seckill = seckillService.getById(seckillId); 42 | logger.info("seckill={}", seckill); 43 | } 44 | 45 | @Test 46 | public void testExportSeckillUrl() { 47 | long seckillId = 1000; 48 | Exposer exposer = seckillService.exportSeckillUrl(seckillId); 49 | logger.info("exposer={}", exposer); 50 | } 51 | 52 | @Transactional 53 | @Test 54 | public void testExecuteSeckill() { 55 | long seckillId = 1000; 56 | long userPhone = 13476191876L; 57 | String md5 = "70b9564762568e9ff29a4a949f8f6de4"; 58 | 59 | try { 60 | SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5); 61 | logger.info("result={}", execution); 62 | } catch (RepeatKillException e) { 63 | logger.error(e.getMessage()); 64 | } catch (SeckillCloseException e1) { 65 | logger.error(e1.getMessage()); 66 | } 67 | } 68 | 69 | // 集成测试代码完整逻辑,注意可重复执行 70 | @Transactional 71 | @Test 72 | public void testSeckillLogic() throws Exception { 73 | long seckillId = 1000; 74 | Exposer exposer = seckillService.exportSeckillUrl(seckillId); 75 | if (exposer.isExposed()) { 76 | logger.info("exposer={}", exposer); 77 | long userPhone = 13476191576L; 78 | String md5 = exposer.getMd5(); 79 | 80 | try { 81 | SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5); 82 | logger.info("result={}", execution); 83 | } catch (RepeatKillException e) { 84 | logger.error(e.getMessage()); 85 | } catch (SeckillCloseException e1) { 86 | logger.error(e1.getMessage()); 87 | } 88 | } else { 89 | // 秒杀未开启 90 | logger.warn("exposer={}", exposer); 91 | } 92 | } 93 | 94 | @Test 95 | public void executeSeckillProcedure(){ 96 | long seckillId = 1001; 97 | long phone = 13680115101L; 98 | Exposer exposer = seckillService.exportSeckillUrl(seckillId); 99 | if (exposer.isExposed()) { 100 | String md5 = exposer.getMd5(); 101 | SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5); 102 | logger.info("execution={}", execution); 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/web/SeckillController.java: -------------------------------------------------------------------------------- 1 | package com.lewis.web; 2 | 3 | import com.lewis.dto.Exposer; 4 | import com.lewis.dto.SeckillExecution; 5 | import com.lewis.dto.SeckillResult; 6 | import com.lewis.entity.Seckill; 7 | import com.lewis.enums.SeckillStatEnum; 8 | import com.lewis.exception.RepeatKillException; 9 | import com.lewis.exception.SeckillCloseException; 10 | import com.lewis.service.SeckillService; 11 | import com.sun.org.apache.xpath.internal.operations.Mod; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Controller; 14 | import org.springframework.ui.Model; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import java.util.Date; 18 | import java.util.List; 19 | 20 | @Controller 21 | @RequestMapping("/seckill")//url:模块/资源/{}/细分 22 | public class SeckillController 23 | { 24 | @Autowired 25 | private SeckillService seckillService; 26 | 27 | @RequestMapping(value = "/list",method = RequestMethod.GET) 28 | public String list(Model model) 29 | { 30 | //list.jsp+mode=ModelAndView 31 | //获取列表页 32 | List list=seckillService.getSeckillList(); 33 | model.addAttribute("list",list); 34 | return "list"; 35 | } 36 | 37 | @RequestMapping(value = "/{seckillId}/detail",method = RequestMethod.GET) 38 | public String detail(@PathVariable("seckillId") Long seckillId, Model model) 39 | { 40 | if (seckillId == null) 41 | { 42 | return "redirect:/seckill/list"; 43 | } 44 | 45 | Seckill seckill=seckillService.getById(seckillId); 46 | if (seckill == null) 47 | { 48 | return "forward:/seckill/list"; 49 | } 50 | 51 | model.addAttribute("seckill",seckill); 52 | 53 | return "detail"; 54 | } 55 | 56 | //ajax ,json暴露秒杀接口的方法 57 | @RequestMapping(value = "/{seckillId}/exposer", 58 | method = RequestMethod.GET, 59 | produces = {"application/json;charset=UTF-8"}) 60 | @ResponseBody 61 | public SeckillResult exposer(@PathVariable("seckillId") Long seckillId) 62 | { 63 | SeckillResult result; 64 | try{ 65 | Exposer exposer=seckillService.exportSeckillUrl(seckillId); 66 | result=new SeckillResult(true,exposer); 67 | }catch (Exception e) 68 | { 69 | e.printStackTrace(); 70 | result=new SeckillResult(false,e.getMessage()); 71 | } 72 | 73 | return result; 74 | } 75 | 76 | @RequestMapping(value = "/{seckillId}/{md5}/execution", 77 | method = RequestMethod.POST, 78 | produces = {"application/json;charset=UTF-8"}) 79 | @ResponseBody 80 | public SeckillResult execute(@PathVariable("seckillId") Long seckillId, 81 | @PathVariable("md5") String md5, 82 | @CookieValue(value = "userPhone",required = false) Long userPhone) 83 | { 84 | if (userPhone == null) 85 | { 86 | return new SeckillResult(false,"未注册"); 87 | } 88 | 89 | try { 90 | //这里改为调用存储过程 91 | // SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5); 92 | SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, userPhone, md5); 93 | return new SeckillResult(true, execution); 94 | }catch (RepeatKillException e1) 95 | { 96 | SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL); 97 | return new SeckillResult(true,execution); 98 | }catch (SeckillCloseException e2) 99 | { 100 | SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.END); 101 | return new SeckillResult(true,execution); 102 | } 103 | catch (Exception e) 104 | { 105 | SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR); 106 | return new SeckillResult(true,execution); 107 | } 108 | } 109 | 110 | //获取系统时间 111 | @RequestMapping(value = "/time/now",method = RequestMethod.GET) 112 | @ResponseBody 113 | public SeckillResult time() 114 | { 115 | Date now=new Date(); 116 | return new SeckillResult(true,now.getTime()); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.lewis 5 | seckill 6 | war 7 | 0.0.1-SNAPSHOT 8 | seckill Maven Webapp 9 | http://maven.apache.org 10 | 11 | 12 | junit 13 | junit 14 | 4.11 15 | test 16 | 17 | 18 | 20 | 21 | org.slf4j 22 | slf4j-api 23 | 1.7.12 24 | 25 | 26 | ch.qos.logback 27 | logback-core 28 | 1.1.1 29 | 30 | 31 | 32 | ch.qos.logback 33 | logback-classic 34 | 1.1.1 35 | 36 | 37 | 38 | 39 | mysql 40 | mysql-connector-java 41 | 5.1.35 42 | runtime 43 | 44 | 45 | c3p0 46 | c3p0 47 | 0.9.1.1 48 | 49 | 50 | 51 | 52 | org.mybatis 53 | mybatis 54 | 3.3.0 55 | 56 | 57 | 58 | org.mybatis 59 | mybatis-spring 60 | 1.2.3 61 | 62 | 63 | 64 | 65 | taglibs 66 | standard 67 | 1.1.2 68 | 69 | 70 | jstl 71 | jstl 72 | 1.2 73 | 74 | 75 | com.fasterxml.jackson.core 76 | jackson-databind 77 | 2.5.4 78 | 79 | 80 | javax.servlet 81 | javax.servlet-api 82 | 3.1.0 83 | provided 84 | 85 | 86 | 87 | 88 | 89 | org.springframework 90 | spring-core 91 | 4.1.7.RELEASE 92 | 93 | 94 | org.springframework 95 | spring-beans 96 | 4.1.7.RELEASE 97 | 98 | 99 | org.springframework 100 | spring-context 101 | 4.1.7.RELEASE 102 | 103 | 104 | 105 | org.springframework 106 | spring-jdbc 107 | 4.1.7.RELEASE 108 | 109 | 110 | org.springframework 111 | spring-tx 112 | 4.1.7.RELEASE 113 | 114 | 115 | 116 | org.springframework 117 | spring-web 118 | 4.1.7.RELEASE 119 | 120 | 121 | org.springframework 122 | spring-webmvc 123 | 4.1.7.RELEASE 124 | 125 | 126 | 127 | org.springframework 128 | spring-test 129 | 4.1.7.RELEASE 130 | 131 | 132 | 133 | 134 | redis.clients 135 | jedis 136 | 2.7.3 137 | 138 | 139 | 140 | 141 | com.dyuproject.protostuff 142 | protostuff-core 143 | 1.0.8 144 | 145 | 146 | com.dyuproject.protostuff 147 | protostuff-runtime 148 | 1.0.8 149 | 150 | 151 | 152 | commons-collections 153 | commons-collections 154 | 3.2.2 155 | 156 | 157 | 158 | seckill 159 | 160 | 161 | 162 | src/main/java 163 | 164 | **/*.xml 165 | 166 | false 167 | 168 | 169 | src/main/resources 170 | 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /src/main/java/com/lewis/service/impl/SeckillServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.lewis.service.impl; 2 | 3 | import org.apache.commons.collections.MapUtils; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.transaction.annotation.Transactional; 9 | import org.springframework.util.DigestUtils; 10 | 11 | import com.lewis.dao.SeckillDao; 12 | import com.lewis.dao.SuccessKilledDao; 13 | import com.lewis.dao.cache.RedisDao; 14 | import com.lewis.dto.Exposer; 15 | import com.lewis.dto.SeckillExecution; 16 | import com.lewis.entity.Seckill; 17 | import com.lewis.entity.SuccessKilled; 18 | import com.lewis.enums.SeckillStatEnum; 19 | import com.lewis.exception.RepeatKillException; 20 | import com.lewis.exception.SeckillCloseException; 21 | import com.lewis.exception.SeckillException; 22 | import com.lewis.service.SeckillService; 23 | 24 | import javax.annotation.Resource; 25 | import java.util.Date; 26 | import java.util.HashMap; 27 | import java.util.List; 28 | import java.util.Map; 29 | 30 | //@Component @Service @Repository @Controller 31 | @Service 32 | public class SeckillServiceImpl implements SeckillService { 33 | // 日志对象 34 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 35 | 36 | // 加入一个混淆字符串(秒杀接口)的salt,为了避免用户猜出我们的md5值,值任意给,越复杂越好 37 | private final String salt = "aksehiucka24sf*&%&^^#^%$"; 38 | 39 | // 注入Service依赖 40 | @Autowired 41 | // @Resource 42 | private SeckillDao seckillDao; 43 | 44 | @Autowired 45 | // @Resource 46 | private SuccessKilledDao successKilledDao; 47 | 48 | @Autowired 49 | private RedisDao redisDao; 50 | 51 | public List getSeckillList() { 52 | return seckillDao.queryAll(0, 4); 53 | } 54 | 55 | public Seckill getById(long seckillId) { 56 | return seckillDao.queryById(seckillId); 57 | } 58 | 59 | public Exposer exportSeckillUrl(long seckillId) { 60 | // 优化点:缓存优化:超时的基础上维护一致性 61 | // 1.访问redi 62 | 63 | Seckill seckill = redisDao.getSeckill(seckillId); 64 | if (seckill == null) { 65 | // 2.访问数据库 66 | seckill = seckillDao.queryById(seckillId); 67 | if (seckill == null) {// 说明查不到这个秒杀产品的记录 68 | return new Exposer(false, seckillId); 69 | } else { 70 | // 3.放入redis 71 | redisDao.putSeckill(seckill); 72 | } 73 | } 74 | 75 | // 若是秒杀未开启 76 | Date startTime = seckill.getStartTime(); 77 | Date endTime = seckill.getEndTime(); 78 | // 系统当前时间 79 | Date nowTime = new Date(); 80 | if (startTime.getTime() > nowTime.getTime() || endTime.getTime() < nowTime.getTime()) { 81 | return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime()); 82 | } 83 | 84 | // 秒杀开启,返回秒杀商品的id、用给接口加密的md5 85 | String md5 = getMD5(seckillId); 86 | return new Exposer(true, md5, seckillId); 87 | } 88 | 89 | private String getMD5(long seckillId) { 90 | String base = seckillId + "/" + salt; 91 | String md5 = DigestUtils.md5DigestAsHex(base.getBytes()); 92 | return md5; 93 | } 94 | 95 | // 秒杀是否成功,成功:减库存,增加明细;失败:抛出异常,事务回滚 96 | @Transactional 97 | /** 98 | * 使用注解控制事务方法的优点: 99 | * 1.开发团队达成一致约定,明确标注事务方法的编程风格 100 | * 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部 101 | * 3.不是所有的方法都需要事务,如只有一条修改操作、只读操作不要事务控制 102 | */ 103 | public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, 104 | RepeatKillException, SeckillCloseException { 105 | 106 | if (md5 == null || !md5.equals(getMD5(seckillId))) { 107 | throw new SeckillException("seckill data rewrite");// 秒杀数据被重写了 108 | } 109 | // 执行秒杀逻辑:减库存+增加购买明细 110 | Date nowTime = new Date(); 111 | 112 | try { 113 | 114 | // 否则更新了库存,秒杀成功,增加明细 115 | int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone); 116 | // 看是否该明细被重复插入,即用户是否重复秒杀 117 | if (insertCount <= 0) { 118 | throw new RepeatKillException("seckill repeated"); 119 | } else { 120 | 121 | // 减库存,热点商品竞争 122 | int updateCount = seckillDao.reduceNumber(seckillId, nowTime); 123 | if (updateCount <= 0) { 124 | // 没有更新库存记录,说明秒杀结束 rollback 125 | throw new SeckillCloseException("seckill is closed"); 126 | } else { 127 | // 秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息 commit 128 | SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); 129 | return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled); 130 | } 131 | } 132 | } catch (SeckillCloseException e1) { 133 | throw e1; 134 | } catch (RepeatKillException e2) { 135 | throw e2; 136 | } catch (Exception e) { 137 | logger.error(e.getMessage(), e); 138 | // 将编译期异常转化为运行期异常 139 | throw new SeckillException("seckill inner error :" + e.getMessage()); 140 | } 141 | 142 | } 143 | 144 | @Override 145 | public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) { 146 | if (md5 == null || !md5.equals(getMD5(seckillId))) { 147 | return new SeckillExecution(seckillId, SeckillStatEnum.DATE_REWRITE); 148 | } 149 | Date killTime = new Date(); 150 | Map map = new HashMap<>(); 151 | map.put("seckillId", seckillId); 152 | map.put("phone", userPhone); 153 | map.put("killTime", killTime); 154 | map.put("result", null); 155 | // 执行储存过程,result被复制 156 | seckillDao.killByProcedure(map); 157 | // 获取result 158 | int result = MapUtils.getInteger(map, "result", -2); 159 | if (result == 1) { 160 | SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); 161 | return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled); 162 | } else { 163 | return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result)); 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/webapp/resources/script/seckill.js: -------------------------------------------------------------------------------- 1 | //存放主要交互逻辑的js代码 2 | // javascript 模块化(package.类.方法) 3 | 4 | var seckill = { 5 | 6 | //封装秒杀相关ajax的url 7 | URL: { 8 | now: function () { 9 | return '/seckill/seckill/time/now'; 10 | }, 11 | exposer: function (seckillId) { 12 | return '/seckill/seckill/' + seckillId + '/exposer'; 13 | }, 14 | execution: function (seckillId, md5) { 15 | return '/seckill/seckill/' + seckillId + '/' + md5 + '/execution'; 16 | } 17 | }, 18 | 19 | //验证手机号 20 | validatePhone: function (phone) { 21 | if (phone && phone.length == 11 && !isNaN(phone)) { 22 | return true;//直接判断对象会看对象是否为空,空就是undefine就是false; isNaN 非数字返回true 23 | } else { 24 | return false; 25 | } 26 | }, 27 | 28 | //详情页秒杀逻辑 29 | detail: { 30 | //详情页初始化 31 | init: function (params) { 32 | //手机验证和登录,计时交互 33 | //规划我们的交互流程 34 | //在cookie中查找手机号 35 | var userPhone = $.cookie('userPhone'); 36 | //验证手机号 37 | if (!seckill.validatePhone(userPhone)) { 38 | //绑定手机 控制输出 39 | var killPhoneModal = $('#killPhoneModal'); 40 | killPhoneModal.modal({ 41 | show: true,//显示弹出层 42 | backdrop: 'static',//禁止位置关闭 43 | keyboard: false//关闭键盘事件 44 | }); 45 | 46 | $('#killPhoneBtn').click(function () { 47 | var inputPhone = $('#killPhoneKey').val(); 48 | // console.log("inputPhone: " + inputPhone); 49 | if (seckill.validatePhone(inputPhone)) { 50 | //电话写入cookie(7天过期) 51 | $.cookie('userPhone', inputPhone, {expires: 7, path: '/seckill'}); 52 | //验证通过  刷新页面 53 | window.location.reload(); 54 | } else { 55 | //todo 错误文案信息抽取到前端字典里 56 | $('#killPhoneMessage').hide().html('').show(300); 57 | } 58 | }); 59 | } 60 | 61 | //已经登录 62 | //计时交互 63 | var startTime = params['startTime']; 64 | var endTime = params['endTime']; 65 | var seckillId = params['seckillId']; 66 | // console.log("开始秒杀时间=======" + startTime); 67 | // console.log("结束秒杀时间========" + endTime); 68 | $.get(seckill.URL.now(), {}, function (result) { 69 | if (result && result['success']) { 70 | var nowTime = result['data']; 71 | //时间判断 计时交互 72 | seckill.countDown(seckillId, nowTime, startTime, endTime); 73 | } else { 74 | console.log('result: ' + result); 75 | alert('result: ' + result); 76 | } 77 | }); 78 | } 79 | }, 80 | 81 | handlerSeckill: function (seckillId, node) { 82 | //获取秒杀地址,控制显示器,执行秒杀 83 | node.hide().html(''); 84 | 85 | $.get(seckill.URL.exposer(seckillId), {}, function (result) { 86 | //在回调函数种执行交互流程 87 | if (result && result['success']) { 88 | var exposer = result['data']; 89 | if (exposer['exposed']) { 90 | //开启秒杀 91 | //获取秒杀地址 92 | var md5 = exposer['md5']; 93 | var killUrl = seckill.URL.execution(seckillId, md5); 94 | console.log("killUrl: " + killUrl); 95 | //绑定一次点击事件 96 | $('#killBtn').one('click', function () { 97 | //执行秒杀请求 98 | //1.先禁用按钮 99 | $(this).addClass('disabled');//,<-$(this)===('#killBtn')-> 100 | //2.发送秒杀请求执行秒杀 101 | $.post(killUrl, {}, function (result) { 102 | if (result && result['success']) { 103 | var killResult = result['data']; 104 | var state = killResult['state']; 105 | var stateInfo = killResult['stateInfo']; 106 | //显示秒杀结果 107 | node.html('' + stateInfo + ''); 108 | } 109 | }); 110 | }); 111 | node.show(); 112 | } else { 113 | //未开启秒杀(浏览器计时偏差) 114 | var now = exposer['now']; 115 | var start = exposer['start']; 116 | var end = exposer['end']; 117 | seckill.countDown(seckillId, now, start, end); 118 | } 119 | } else { 120 | console.log('result: ' + result); 121 | } 122 | }); 123 | 124 | }, 125 | 126 | countDown: function (seckillId, nowTime, startTime, endTime) { 127 | console.log(seckillId + '_' + nowTime + '_' + startTime + '_' + endTime); 128 | var seckillBox = $('#seckill-box'); 129 | if (nowTime > endTime) { 130 | //秒杀结束 131 | seckillBox.html('秒杀结束!'); 132 | } else if (nowTime < startTime) { 133 | //秒杀未开始,计时事件绑定 134 | var killTime = new Date(startTime + 1000);//todo 防止时间偏移 135 | seckillBox.countdown(killTime, function (event) { 136 | //时间格式 137 | var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒 '); 138 | seckillBox.html(format); 139 | }).on('finish.countdown', function () { 140 | //时间完成后回调事件 141 | //获取秒杀地址,控制现实逻辑,执行秒杀 142 | console.log('______fininsh.countdown'); 143 | seckill.handlerSeckill(seckillId, seckillBox); 144 | }); 145 | } else { 146 | //秒杀开始 147 | seckill.handlerSeckill(seckillId, seckillBox); 148 | } 149 | } 150 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SSM实战项目——Java高并发秒杀API 2 | 3 | ### 本文包括了项目的完整流程+开发过程中遇到的各种坑的总结+学习笔记和问题扩展,如果觉得README太长,我在blog里进行了分章,[点击前往](https://lewky.cn/posts/65067.html/) 4 | 5 | ### ~~这是完成后的项目,点我看看实际效果(阿里云到期了orz)~~ 6 | --- 7 | ## 项目截图 8 | 9 | ### 秒杀列表 10 | 11 | ![秒杀列表](https://raw.githubusercontent.com/lewky/markdownImages/master/resource/seckill/%E7%A7%92%E6%9D%80%E5%88%97%E8%A1%A8.jpg) 12 | 13 | ### 秒杀详情页 14 | 15 | ![秒杀详情页](https://raw.githubusercontent.com/lewky/markdownImages/master/resource/seckill/%E7%A7%92%E6%9D%80%E8%AF%A6%E6%83%85%E9%A1%B5.jpg) 16 | 17 | ### 错误提示 18 | 19 | ![错误提示](https://raw.githubusercontent.com/lewky/markdownImages/master/resource/seckill/%E9%94%99%E8%AF%AF%E6%8F%90%E7%A4%BA.jpg) 20 | 21 | ### 开始秒杀 22 | 23 | ![开始秒杀](https://raw.githubusercontent.com/lewky/markdownImages/master/resource/seckill/%E5%BC%80%E5%A7%8B%E7%A7%92%E6%9D%80.jpg) 24 | 25 | ### 秒杀成功 26 | 27 | ![秒杀成功](https://raw.githubusercontent.com/lewky/markdownImages/master/resource/seckill/%E7%A7%92%E6%9D%80%E6%88%90%E5%8A%9F.jpg) 28 | 29 | ### 重复秒杀 30 | 31 | ![重复秒杀](https://raw.githubusercontent.com/lewky/markdownImages/master/resource/seckill/%E9%87%8D%E5%A4%8D%E7%A7%92%E6%9D%80.jpg) 32 | 33 | ### 秒杀倒计时 34 | 35 | ![秒杀倒计时](https://raw.githubusercontent.com/lewky/markdownImages/master/resource/seckill/%E7%A7%92%E6%9D%80%E5%80%92%E8%AE%A1%E6%97%B6.jpg) 36 | 37 | ### 秒杀结束 38 | 39 | ![秒杀结束](https://raw.githubusercontent.com/lewky/markdownImages/master/resource/seckill/%E7%A7%92%E6%9D%80%E7%BB%93%E6%9D%9F.jpg) 40 | 41 | ## 项目介绍 42 | 43 | >何为秒杀? 44 | 45 | 所谓“秒杀”,就是网络卖家发布一些超低价格的商品,所有买家在同一时间网上抢购的一种销售方式。由于商品价格低廉,往往一上架就被抢购一空,有时只用一秒钟。 46 | 47 | >为何选择Java高并发秒杀作为实战项目? 48 | 49 | * 秒杀业务场景具有典型事务特性 50 | * 秒杀/红包类需求越来越常见 51 | 52 | >为何使用SpringMVC+Spring+MyBatis框架 53 | 54 | * 框架易于使用和轻量级 55 | * 低代码侵入性 56 | * 成熟的社区和用户群 57 | 58 | >能从该项目得到什么收获? 59 | 60 | * 框架的使用和整合技巧 61 | * 秒杀分析过程与优化思路 62 | 63 | >项目来源 64 | 65 | 这是慕课网上的一个免费项目教学视频,名为Java高并发秒杀API,一共有如下四节课程,附带视频传送门(在视频中老师是用IDEA,本文用的是Eclipse) 66 | 67 | * [Java高并发秒杀API之业务分析与DAO层](http://www.imooc.com/learn/587) 68 | * [Java高并发秒杀API之Service层](http://www.imooc.com/learn/631) 69 | * [Java高并发秒杀API之Web层](http://www.imooc.com/learn/630) 70 | * [Java高并发秒杀API之高并发优化](http://www.imooc.com/learn/632) 71 | 72 | ## 相关技术介绍 73 | 74 | >MySQL 75 | 76 | * 表设计 77 | * SQL技巧 78 | * 事务和行级锁 79 | 80 | >MyBatis 81 | 82 | * DAO层设计与开发 83 | * MyBatis合理使用 84 | * 与Spring整合 85 | 86 | >Spring 87 | 88 | * Spring IOC整合Service 89 | * 声明式事务运用 90 | 91 | >SpringMVC 92 | 93 | * Restful借口设计和使用 94 | * 框架运作流程 95 | * Controller开发技巧 96 | 97 | >前端 98 | 99 | * 交互设计 100 | * Bootstrap 101 | * jQuery 102 | 103 | >高并发 104 | 105 | * 高并发点和高并发分析 106 | * 优化思路并实现 107 | 108 | ## 开发环境 109 | 110 | * **操作系统**:Windows 8 111 | * **IDE工具**:Eclipse 112 | * **JDK**:JDK1.7 113 | * **中间件**:Tomcat 7.0 114 | * **数据库**:MySQL 5.0 115 | * **构建工具**:Maven 116 | * **框架**:SSM 117 | 118 | ## 项目总结 119 | 120 | >本文根据慕课网的视频教学进行了相应的学习总结,全文较长,分为四节,附带CSDN传送门 121 | 122 | * [**Java高并发秒杀API(一)之业务分析与DAO层**](http://blog.csdn.net/lewky_liu/article/details/78159983) 123 | * [**Java高并发秒杀API(二)之Service层**](http://blog.csdn.net/lewky_liu/article/details/78162149) 124 | * [**Java高并发秒杀API(三)之Web层**](http://blog.csdn.net/lewky_liu/article/details/78162153) 125 | * [**Java高并发秒杀API(四)之高并发优化**](http://blog.csdn.net/lewky_liu/article/details/78166080) 126 | 127 | >项目源码 128 | 129 | * [**源码下载**](http://download.csdn.net/download/lewky_liu/10013556) 130 | * [**GitHub地址**](https://github.com/lewky/Seckill) 131 | 132 | >How to play 133 | 134 | * 将下载的源码解压后作为Maven项目导入到IDE工具中;或者将从GitHub克隆下来的项目作为Maven项目导入到IDE工具中 135 | * 打开项目中的jdbc.properties文件,修改里边的url,username和password 136 | * 将项目部署到Tomcat上并启动 137 | * 可以直接用IDE内嵌的Tomcat启动项目 138 | * 或者将本项目通过**mvn clean package**命令打成war包并丢到本地安装的Tomcat的webapps目录下,接着启动Tomcat即可 139 | * 在浏览器上访问:`http://localhost:8080/seckill` 140 | 141 | --- 142 | 143 | # [Java高并发秒杀API(一)之业务分析与DAO层](http://blog.csdn.net/lewky_liu/article/details/78159983) 144 | 145 | >本SSM实战项目使用了Maven进行依赖管理,如果有不清楚Maven是什么的可以参考[这篇文章](http://blog.csdn.net/lewky_liu/article/details/78138381) 146 | 147 | ## 1. 创建Maven项目和依赖 148 | 149 | ### 1.1 创建项目前需要先安装Maven,并设置好环境变量 150 | 151 | * [Maven下载](http://download.csdn.net/download/lewky_liu/10000144) 152 | * 设置环境变量 153 | * 新建变量`MAVEN_HOME`,值为Maven的目录`X:\XXX\apache-maven-XXX` 154 | * 将`%MAVEN_HOME%\bin`添加到`Path`变量下 155 | * 运行CMD,输入`mvn -v`后可以看到Maven的版本信息等则表示安装成功 156 | 157 | ### 1.2 创建Maven项目有两种方式,如下 158 | 159 | **第一种创建方式:使用命令行手动创建** 160 | 161 | mvn archetype:generate -DgroupId=com.lewis.seckill -DartifactId=seckill -Dpackage=com.lewis.seckill -Dversion=1.0-SNAPSHOT -DarchetypeArtifactId=maven-archetype-webapp 162 | 163 | 在视频中使用的是`archetype:create`,该方法已被废弃,请使用`archetype:generate`来创建。命令行执行后会创建一个`maven-archetype-webapp`骨架的Maven项目,其中`groupId`是项目组织唯一的标识符,实际对应JAVA的包的结构;`artifactId`是项目的唯一的标识符,实际对应项目的名称;`package`一般是`groupId`+`artifactId`,是自动生成的,可以修改 164 | 165 | **第二种创建方式:借助IDE工具的Maven插件来创建项目** 166 | 167 | >Eclipse安装Maven插件 168 | 169 | * 不知道怎么Maven插件的请参考[该博文](http://blog.csdn.net/wode_dream/article/details/38052639),推荐使用link方式手工安装的方式 170 | * 如果是手工安装Maven插件的,可能会缺少pom.xml 图形化编辑工具,请另外添加进去,具体情况请参考[该博文](http://www.micmiu.com/software/build/eclipse-m2e-plugins/) 171 | * 已经安装了Maven插件的请走下一个步骤 172 | * `File`→`New`→`Other...`→`Maven Project`→`Next`,进入如下界面 173 | 174 | ![Maven1](https://github.com/lewky/MarkdownImages/blob/master/resource/CSDN/Maven1.jpg?raw=true) 175 | 176 | * 点击`Next`,选择要构建的骨架`maven-archetype-webapp`,如下图 177 | 178 | ![Maven2](https://github.com/lewky/MarkdownImages/blob/master/resource/CSDN/Maven2.jpg?raw=true) 179 | 180 | * 点击`Next`,填写`groupId=com.lewis.seckill`,`DartifactId=seckill`,`package=com.lewis.seckill`(根据实际情况填写),然后`Finish` 181 | 182 | >如果是第一次使用Eclipse的Maven插件来创建Maven项目的可能会遇到一些问题,可以参考[该博文](http://blog.csdn.net/lewky_liu/article/details/78138381) 183 | 184 | ### 1.3 修改pom.xml文件 185 | 186 | 当创建完Maven项目后会在根目录下有一个pom.xml文件,Maven项目通过pom.xml进行项目依赖的管理,如果没有该xml文件,Eclipse不会将该项目当作一个Maven项目 187 | 188 | >添加项目需要的jar包依赖 189 | 190 | 192 | 4.0.0 193 | com.lewis 194 | seckill 195 | war 196 | 0.0.1-SNAPSHOT 197 | seckill Maven Webapp 198 | http://maven.apache.org 199 | 200 | 201 | junit 202 | junit 203 | 4.11 204 | test 205 | 206 | 207 | 210 | 211 | org.slf4j 212 | slf4j-api 213 | 1.7.12 214 | 215 | 216 | ch.qos.logback 217 | logback-core 218 | 1.1.1 219 | 220 | 221 | 222 | ch.qos.logback 223 | logback-classic 224 | 1.1.1 225 | 226 | 227 | 228 | 229 | mysql 230 | mysql-connector-java 231 | 5.1.35 232 | runtime 233 | 234 | 235 | c3p0 236 | c3p0 237 | 0.9.1.1 238 | 239 | 240 | 241 | 242 | org.mybatis 243 | mybatis 244 | 3.3.0 245 | 246 | 247 | 248 | org.mybatis 249 | mybatis-spring 250 | 1.2.3 251 | 252 | 253 | 254 | 255 | taglibs 256 | standard 257 | 1.1.2 258 | 259 | 260 | jstl 261 | jstl 262 | 1.2 263 | 264 | 265 | com.fasterxml.jackson.core 266 | jackson-databind 267 | 2.5.4 268 | 269 | 270 | javax.servlet 271 | javax.servlet-api 272 | 3.1.0 273 | 274 | 275 | 276 | 277 | 278 | org.springframework 279 | spring-core 280 | 4.1.7.RELEASE 281 | 282 | 283 | org.springframework 284 | spring-beans 285 | 4.1.7.RELEASE 286 | 287 | 288 | org.springframework 289 | spring-context 290 | 4.1.7.RELEASE 291 | 292 | 293 | 294 | org.springframework 295 | spring-jdbc 296 | 4.1.7.RELEASE 297 | 298 | 299 | org.springframework 300 | spring-tx 301 | 4.1.7.RELEASE 302 | 303 | 304 | 305 | org.springframework 306 | spring-web 307 | 4.1.7.RELEASE 308 | 309 | 310 | org.springframework 311 | spring-webmvc 312 | 4.1.7.RELEASE 313 | 314 | 315 | 316 | org.springframework 317 | spring-test 318 | 4.1.7.RELEASE 319 | 320 | 321 | 322 | 323 | redis.clients 324 | jedis 325 | 2.7.3 326 | 327 | 328 | 329 | 330 | com.dyuproject.protostuff 331 | protostuff-core 332 | 1.0.8 333 | 334 | 335 | com.dyuproject.protostuff 336 | protostuff-runtime 337 | 1.0.8 338 | 339 | 340 | 341 | seckill 342 | 343 | 345 | 346 | src/main/java 347 | 348 | **/*.xml 349 | 350 | false 351 | 352 | 353 | src/main/resources 354 | 355 | 356 | 357 | 358 | 359 | >关于maven依赖的简化写法 360 | 361 | 教学视频中老师写了很多的依赖,但其实这里面有一些是可以省略不写的,因为有些包会自动依赖其它的包(Maven的传递性依赖)。这里面可以省略的依赖有:spring-core;spring-beans(上面这两个spring-context会自动依赖);spring-context,spring-jdbc(mybatis-spring会依赖);spring-web(spring-webmvc会依赖);logback-core(logback-classic会依赖) 362 | 363 | >有想要了解Maven的依赖范围与传递性依赖的请参考[该博文](http://blog.csdn.net/lewky_liu/article/details/78145211) 364 | 365 | ## 2. 秒杀业务分析 366 | 367 | ### 2.1 业务分析 368 | 369 | >秒杀业务的核心是对库存的处理,其业务流程如下图 370 | 371 | ![1.png](https://github.com/lewky/MarkdownImages/blob/master/resource/seckill/1.png?raw=true) 372 | 373 | > 用户针对库存业务分析 374 | 375 | 当用户执行秒杀成功时,应该发生以下两个操作: 376 | 377 | * 减库存 378 | * 记录购买明细 379 | 380 | 这两个操作属于一个完整事务,通过事务来实现数据落地 381 | >为什么需要事务? 382 | 383 | * 减库存却没有记录购买明细,会导致商品少卖 384 | * 记录购买明细却没有减库存,会导致商品超卖 385 | 386 | 在实际中,以上都是很严重的事故,会给商家或买家带来损失,这是不能被允许的。一旦发生这种事故,事故责任很自然的就会去找设计实现业务的程序员 387 | 388 | >如何实现数据落地? 389 | 390 | 有**MySQL与NoSQL**两种数据落地的方案 391 | 392 | * MySQL属于关系型数据库,而MySQL内置的事务机制来可以准确的帮我们完成减库存和记录购买明细的过程。MySQL有多种存储引擎,但只有InnoDB存储引擎支持事务。InnoDB支持行级锁和表级锁,默认使用行级锁 393 | * NoSQL属于非关系型数据库,近些年来在数据存储方面承担了很大的职责,但是对于事务的支持做的并不是很好,更多追求的是性能、高复用、分布式。 394 | 395 | 事务机制依然是目前最可靠的数据落地方案。 396 | 397 | > 数据落地与不数据落地 398 | 399 | * **落地数据**:就是被持久化的数据,这种数据一般放在硬盘或是其他的持久化存储设备里,例如:图片、系统日志、在页面上显示的数据以及保存在关系数据库里的数据等等,落地数据一定会有一个固定的载体,他们不会瞬时消失的。 400 | * **不落地数据**:一般指存储在内存或者是网络传输里的数据,这些数据是瞬时,使用完毕就会消失,例如:我们在浏览器发送给服务器的请求;从数据库读取出来的一直到页面展示前的数据等等。 401 | * “不落地”传输能够满足用户在性能上的要求。 402 | 403 | ### 2.2 使用MySQL实现秒杀的难点分析 404 | 405 | >难点问题:如何高效地处理竞争? 406 | 407 | 当一个用户在执行秒杀某件商品时,其他也想要秒杀该商品的用户就只能等待,直到上一个用户提交或回滚了事务,他才能够得到该商品的锁执行秒杀操作。这里就涉及到了锁的竞争。 408 | 409 | ![2.jpg](https://github.com/lewky/MarkdownImages/blob/master/resource/seckill/2.jpg?raw=true) 410 | 411 | 对于MySQL来说,竞争反应到背后的技术是就是事务+行级锁: 412 | 413 | start transaction(开启事务)→ update库存数量 → insert购买明细 → commit(提交事务) 414 | 415 | 在秒杀系统中,在同一时刻会有很多用户在秒杀同一件商品,那么如何高效低处理这些竞争?如何高效地提交事务?这些将在[Java高并发秒杀API(四)之高并发优化](http://blog.csdn.net/lewky_liu/article/details/78166080)进行分析总结。 416 | 417 | >实现哪些秒杀功能? 418 | 419 | 下面先以天猫的秒杀库存系统为例,如下图 420 | 421 | ![3.jpg](https://github.com/lewky/MarkdownImages/blob/master/resource/seckill/3.jpg?raw=true) 422 | 423 | 可以看到,天猫的秒杀库存系统是很复杂的,需要很多工程师共同开发。在这里,我们只实现秒杀相关的功能 424 | 425 | * 秒杀接口暴露 426 | * 执行秒杀 427 | * 相关查询 428 | 429 | >为什么要进行秒杀接口暴露的操作? 430 | 431 | 现实中有的用户回通过浏览器插件提前知道秒杀接口,填入参数和地址来实现自动秒杀,这对于其他用户来说是不公平的,我们也不希望看到这种情况 432 | 433 | ## 3. DAO层设计 434 | 435 | ### 3.1 创建数据库 436 | 437 | 源码里有个sql文件夹,可以给出了sql语句;也可以选择自己手写。数据库一共就两个表:秒杀库存表、秒杀成功明细表。 438 | 439 | -- 数据库初始化脚本 440 | 441 | -- 创建数据库 442 | CREATE DATABASE seckill; 443 | -- 使用数据库 444 | use seckill; 445 | CREATE TABLE seckill( 446 | `seckill_id` BIGINT NOT NUll AUTO_INCREMENT COMMENT '商品库存ID', 447 | `name` VARCHAR(120) NOT NULL COMMENT '商品名称', 448 | `number` int NOT NULL COMMENT '库存数量', 449 | `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 450 | `start_time` TIMESTAMP NOT NULL COMMENT '秒杀开始时间', 451 | `end_time` TIMESTAMP NOT NULL COMMENT '秒杀结束时间', 452 | PRIMARY KEY (seckill_id), 453 | key idx_start_time(start_time), 454 | key idx_end_time(end_time), 455 | key idx_create_time(create_time) 456 | )ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表'; 457 | 458 | -- 初始化数据 459 | INSERT into seckill(name,number,start_time,end_time) 460 | VALUES 461 | ('1000元秒杀iphone6',100,'2016-01-01 00:00:00','2016-01-02 00:00:00'), 462 | ('800元秒杀ipad',200,'2016-01-01 00:00:00','2016-01-02 00:00:00'), 463 | ('6600元秒杀mac book pro',300,'2016-01-01 00:00:00','2016-01-02 00:00:00'), 464 | ('7000元秒杀iMac',400,'2016-01-01 00:00:00','2016-01-02 00:00:00'); 465 | 466 | -- 秒杀成功明细表 467 | -- 用户登录认证相关信息(简化为手机号) 468 | CREATE TABLE success_killed( 469 | `seckill_id` BIGINT NOT NULL COMMENT '秒杀商品ID', 470 | `user_phone` BIGINT NOT NULL COMMENT '用户手机号', 471 | `state` TINYINT NOT NULL DEFAULT -1 COMMENT '状态标识:-1:无效 0:成功 1:已付款 2:已发货', 472 | `create_time` TIMESTAMP NOT NULL COMMENT '创建时间', 473 | PRIMARY KEY(seckill_id,user_phone),/*联合主键*/ 474 | KEY idx_create_time(create_time) 475 | )ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表'; 476 | 477 | >秒杀成功明细表为何使用联合主键 478 | 479 | 之所以使用联合主键,是为了能够过滤重复插入,可以通过`insert ignore into`语句来避免用户重复秒杀同一件商品。这样当有重复记录就会忽略,语句执行后返回数字0。 480 | 481 | >可能存在的问题 482 | 483 | 安装视频里的建表过程,可能会出现建表失败的情况。原因是当你给一个timestamp设置为on update current_timestamp的时候,其他的timestamp字段需要显式设定default值。 484 | 485 | 但是如果你有两个timestamp字段,但是只把第一个设定为current_timestamp而第二个没有设定默认值,MySQL也能成功建表,但是反过来就不行。这是mysql5.5版本对timestamp的处理。 486 | 487 | 为了解决这个问题,将create_time放到start_time和end_time的前面,还有的mysql版本需要将三个时间戳都设置默认值。 488 | 489 | ### 3.2 创建数据表对应的实体类 490 | 491 | >在`src/main/java`包下创建com.lewis.entity包,接着建立`Seckill`实体类 492 | 493 | 494 | public class Seckill { 495 | private Long seckillId; 496 | 497 | private String name; 498 | 499 | private Integer number; 500 | 501 | private Date createTime; 502 | 503 | private Date startTime; 504 | 505 | private Date endTime; 506 | 507 | public Long getSeckillId() { 508 | return seckillId; 509 | } 510 | 511 | public void setSeckillId(Long seckillId) { 512 | this.seckillId = seckillId; 513 | } 514 | 515 | public String getName() { 516 | return name; 517 | } 518 | 519 | public void setName(String name) { 520 | this.name = name == null ? null : name.trim(); 521 | } 522 | 523 | public Integer getNumber() { 524 | return number; 525 | } 526 | 527 | public void setNumber(Integer number) { 528 | this.number = number; 529 | } 530 | 531 | public Date getCreateTime() { 532 | return createTime; 533 | } 534 | 535 | public void setCreateTime(Date createTime) { 536 | this.createTime = createTime; 537 | } 538 | 539 | public Date getStartTime() { 540 | return startTime; 541 | } 542 | 543 | public void setStartTime(Date startTime) { 544 | this.startTime = startTime; 545 | } 546 | 547 | public Date getEndTime() { 548 | return endTime; 549 | } 550 | 551 | public void setEndTime(Date endTime) { 552 | this.endTime = endTime; 553 | } 554 | 555 | @Override 556 | public String toString() { 557 | return "Seckill [seckillId=" + seckillId + ", name=" + name + ", number=" + number + ", createTime=" + createTime + ", startTime=" 558 | + startTime + ", endTime=" + endTime + "]"; 559 | } 560 | } 561 | 562 | >在com.lewis.entity包下,接着建立`SuccessKilled`实体类 563 | 564 | public class SuccessKilled { 565 | private Byte state; 566 | 567 | private Date createTime; 568 | 569 | private Long seckillId; 570 | 571 | private Long userPhone; 572 | 573 | // 多对一,因为一件商品在库存中有很多数量,对应的购买明细也有很多。 574 | private Seckill seckill; 575 | 576 | public Seckill getSeckill() { 577 | return seckill; 578 | } 579 | 580 | public void setSeckill(Seckill seckill) { 581 | this.seckill = seckill; 582 | } 583 | 584 | public Long getSeckillId() { 585 | return seckillId; 586 | } 587 | 588 | public void setSeckillId(Long seckillId) { 589 | this.seckillId = seckillId; 590 | } 591 | 592 | public Long getUserPhone() { 593 | return userPhone; 594 | } 595 | 596 | public void setUserPhone(Long userPhone) { 597 | this.userPhone = userPhone; 598 | } 599 | 600 | public Byte getState() { 601 | return state; 602 | } 603 | 604 | public void setState(Byte state) { 605 | this.state = state; 606 | } 607 | 608 | public Date getCreateTime() { 609 | return createTime; 610 | } 611 | 612 | public void setCreateTime(Date createTime) { 613 | this.createTime = createTime; 614 | } 615 | 616 | @Override 617 | public String toString() { 618 | return "SuccessKilled [state=" + state + ", createTime=" + createTime + ", seckillId=" + seckillId 619 | + ", userPhone=" + userPhone + "]"; 620 | } 621 | 622 | } 623 | 624 | ### 3.3 创建实体类对应的DAO层接口(也就是Mapper接口,DAO针对的是具体实体来操作的“实体的增删改查”) 625 | 626 | >在`src/main/java`下建立`com.lewis.dao`包,在包下建立`SeckillDao`接口 627 | 628 | public interface SeckillDao { 629 | 630 | /** 631 | * 减库存 632 | * 633 | * @param seckillId 634 | * @param killTime 635 | * @return 更新的记录行数,如果返回值<1则表示更新失败 636 | */ 637 | int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime); 638 | 639 | /** 640 | * 根据id查询秒杀商品 641 | * 642 | * @param seckillId 643 | * @return 644 | */ 645 | Seckill queryById(long seckillId); 646 | 647 | /** 648 | * 根据偏移量查询秒杀商品列表 649 | * 650 | * @param offset 651 | * @param limit 652 | * @return 653 | */ 654 | List queryAll(@Param("offset") int offset, @Param("limit") int limit); 655 | } 656 | 657 | >在`com.lewis.dao`包下建立`SuccessKilledDao`接口 658 | 659 | public interface SuccessKilledDao { 660 | 661 | /** 662 | * 插入购买明细,可过滤重复 663 | * 664 | * @param seckillId 665 | * @param userphone 666 | * @return 插入的行数,如果返回值<1则表示插入失败 667 | */ 668 | int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone); 669 | 670 | /** 671 | * 根据id查询SuccessKilled并携带秒杀商品对象实体 672 | * 673 | * @param seckillId 674 | * @return 675 | */ 676 | SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone); 677 | } 678 | 679 | >为什么有的方法形参前有@Param,有的却没有? 680 | 681 | 从上面的代码可以发现,当方法的形参在两个及两个以上时,需要在参数前加上@Param,如果不加上该注解会在之后的测试运行时报错。这是Sun提供的默认编译器(javac)在编译后的Class文件中会丢失参数的实际名称,方法中的形参会变成无意义的arg0、arg1等,在只有一个参数时就无所谓,但当参数在两个和两个以上时,传入方法的参数就会找不到对应的形参。因为Java形参的问题,所以在多个基本类型参数时需要用@Param注解区分开来。 682 | 683 | ### 3.4 基于MyBatis实现DAO接口 684 | 685 | >MyBatis怎么用?SQL写在哪里? 686 | 687 | Mybatis有两种提供SQL的方式:XML提供SQL、注解提供SQL(注解是java5.0之后提供的一个新特性)。 688 | 689 | 对于实际的使用中建议使用XML文件的方式提供SQL。如果通过注解的方式提供SQL,由于注解本身还是java源码,这对于修改和调整SQL其实是非常不方便的,一样需要重新编译类,当我们写复杂的SQL尤其拼接逻辑时,注解处理起来就会非常繁琐。而XML提供了很多的SQL拼接和处理逻辑的标签,可以非常方便的帮我们去做封装。 690 | 691 | >如何去实现DAO接口? 692 | 693 | Mapper自动实现DAO(也就是DAO只需要设计接口,不需要去写实现类,MyBatis知道我们的参数、返回类型是什么,同时也有SQL文件,它可以自动帮我们生成接口的实现类来帮我们执行参数的封装,执行SQL,把我们的返回结果集封装成我们想要的类型) 。 694 | 695 | 第二种是通过API编程方式实现DAO接口(MyBatis通过给我们提供了非常多的API,跟其他的ORM和JDBC很像) 696 | 697 | 在实际开发中建议使用Mapper自动实现DAO,这样可以直接只关注SQL如何编写,如何去设计DAO接口,帮我们节省了很多的维护程序,所有的实现都是MyBatis自动完成。 698 | 699 | >创建一个目录存放Mybatis的SQL映射 700 | 701 | 按照Maven的规范,SQL映射文件应该放在`src/main/resources`包下,在该包下建立`mapper`目录,用来存放映射DAO接口的XML文件。这样Maven在编译时就会自动将`src/main/resources`下的这些配置文件编译进来。 702 | 703 | 我们也可以按照原本的习惯,在`src/main/java`下建立`com.lewis.mapper`包,将这些SQL映射存放到这里。由于Maven默认不会编译`src/main/java`下除源码以外的文件,所以需要在pom.xml中进行额外的配置。 704 | 705 | 706 | seckill 707 | 708 | 709 | 710 | src/main/java 711 | 712 | **/*.xml 713 | 714 | false 715 | 716 | 717 | src/main/resources 718 | 719 | 720 | 721 | 722 | 在本项目中,我是采用的第二种方式存放Mybatis的SQL映射。(只是将映射DAO的mapper文件放在java包下,其他的关于Spring、MyBatis等的配置文件还是放在resources包下) 723 | 724 | >在`src/main/resources`目录下配置mybatis-config.xml(配置MyBatis的全局属性) 725 | 726 | 打开MyBatis的[官方文档](http://www.mybatis.org/mybatis-3/zh/index.html)(MyBatis的官方文档做的非常友好,提供了非常多版本的国际化支持),选择` 727 | 入门`,找到MyBatis全局配置,里面有XML的规范(XML的标签约束dtd文件),拷入到项目的MyBatis全局配置文件中,开始配置MyBatis,如下: 728 | 729 | 730 | 733 | 734 | 735 | 736 | 739 | 740 | 741 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | >在`src/main/java`目录下的`com.lewis.mapper`包里创建SeckillDao.xml 753 | 754 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | UPDATE seckill 765 | SET number = number-1 766 | WHERE seckill_id=#{seckillId} 767 | AND start_time 768 | #{killTime} 769 | AND end_time >= #{killTime} 770 | AND number > 0; 771 | 772 | 773 | 775 | 783 | 784 | 790 | 791 | 792 | 793 | >在`src/main/java`目录下的`com.lewis.mapper`包里创建SuccessKilledDao.xml 794 | 795 | 798 | 799 | 800 | 801 | 802 | INSERT ignore INTO success_killed(seckill_id,user_phone,state) 803 | VALUES (#{seckillId},#{userPhone},0) 804 | 805 | 806 | 827 | 828 | 829 | 830 | 注:上面的s.seckill_id “seckill.seckill_id”表示s.seckill_id这一列的数据是Success_killed实体类里的seckill属性里的seckill_id属性,是一个级联的过程,使用的就是别名只是忽略了as关键字,别名要加上双引号。 831 | 832 | >为什么要用``把`<=`给包起来 833 | 834 | CDATA指的是不应由 XML 解析器进行解析的文本数据,在XML元素中,`<`和`&`是非法的: 835 | 836 | * `<`会产生错误,因为解析器会把该字符解释为新元素的开始。 837 | * `&`也会产生错误,因为解析器会把该字符解释为字符实体的开始。(字符实体:比如` `表示一个空格) 838 | 839 | 所以在这里我们需要使用``来告诉XML`<=`不是XML的语言。 840 | 841 | ### 3.5 整合Spring和MyBatis 842 | 843 | 在`resources`目录下创建一个新的目录`spring`(存放所有Spring相关的配置) 844 | 845 | >在resources包下创建jdbc.properties,用于配置数据库的连接信息 846 | 847 | driver=com.mysql.jdbc.Driver 848 | url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8 849 | jdbc.username=root 850 | password=123 851 | 852 | >在`resources/spring`目录下创建Spring关于DAO层的配置文件spring-dao.xml 853 | 854 | 855 | 861 | 862 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 915 | 916 | 917 | >关于数据库连接池的配置可能出现的问题 918 | 919 | 在jdbc.properties里使用的是`jdbc.username`,而不是`username`或者`name`,这是因为后两个属性名可能会与全局变量冲突,导致连接的数据库用户名变成了电脑的用户名,所以使用了`jdbc.username`。 920 | 921 | >相关链接 922 | 923 | 关于Spring的XML配置文件的头部文件的说明可以参考[这篇文章](http://blog.csdn.net/lewky_liu/article/details/78157747) 924 | 925 | ### 3.6 DAO层单元测试 926 | 927 | 有不知道Eclipse如何直接进行生成快速的测试单元的,可以看看[这篇文章](http://blog.csdn.net/jj_nan/article/details/64134781) 928 | 929 | 使用Eclipse工具直接生成测试单元,这些测试代码按照Maven规范放到`src/test/java`包下。在生成的测试代码里测试我们的方法,测试的具体代码如下: 930 | 931 | >`SeckillDaoTest.java` 932 | 933 | package com.lewis.dao; 934 | 935 | import static org.junit.Assert.*; 936 | 937 | import java.util.Date; 938 | import java.util.List; 939 | 940 | import javax.annotation.Resource; 941 | 942 | import org.junit.Test; 943 | import org.junit.runner.RunWith; 944 | import org.springframework.test.context.ContextConfiguration; 945 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 946 | 947 | import com.lewis.entity.Seckill; 948 | 949 | /** 950 | * 配置Spring和Junit整合,junit启动时加载springIOC容器 spring-test,junit 951 | */ 952 | @RunWith(SpringJUnit4ClassRunner.class) 953 | // 告诉junit spring的配置文件 954 | @ContextConfiguration({ "classpath:spring/spring-dao.xml" }) 955 | public class SeckillDaoTest { 956 | 957 | // 注入Dao实现类依赖 958 | @Resource 959 | private SeckillDao seckillDao; 960 | 961 | @Test 962 | public void testQueryById() { 963 | 964 | long seckillId = 1000; 965 | Seckill seckill = seckillDao.queryById(seckillId); 966 | System.out.println(seckill.getName()); 967 | System.out.println(seckill); 968 | } 969 | 970 | @Test 971 | public void testQueryAll() { 972 | 973 | List seckills = seckillDao.queryAll(0, 100); 974 | for (Seckill seckill : seckills) { 975 | System.out.println(seckill); 976 | } 977 | } 978 | 979 | @Test 980 | public void testReduceNumber() { 981 | 982 | long seckillId = 1000; 983 | Date date = new Date(); 984 | int updateCount = seckillDao.reduceNumber(seckillId, date); 985 | System.out.println(updateCount); 986 | } 987 | 988 | } 989 | 990 | >测试说明 991 | 992 | 先左键单击要测试的那个方法名,再右键点击选择`Debug As`可以单独对该方法进行单元测试。三个方法都测试通过,但是对于最后一个方法会发现数据库中该商品数量并没有减少,这是因为我们设置了秒杀时间,当前时间不满足秒杀时间,所以不会秒杀成功减少数量。 993 | 994 | 如果之前没有在DAO接口的多参数方法里在形参前加上@Param注解,那么在这里进行单元测试时,MyBatis会报绑定参数失败的错误,因为无法找到参数。这是因为Java没有保存行参的记录,Java在运行的时候会把`queryAll(int offset,int limit)`中的参数变成这样`queryAll(int arg0,int arg1)`,导致MyBatis无法识别这两个参数。 995 | 996 | >`SuccessKilledDaoTest.java` 997 | 998 | package com.lewis.dao; 999 | 1000 | import static org.junit.Assert.*; 1001 | 1002 | import javax.annotation.Resource; 1003 | 1004 | import org.junit.Test; 1005 | import org.junit.runner.RunWith; 1006 | import org.springframework.test.context.ContextConfiguration; 1007 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 1008 | 1009 | import com.lewis.entity.SuccessKilled; 1010 | 1011 | @RunWith(SpringJUnit4ClassRunner.class) 1012 | // 告诉junit spring的配置文件 1013 | @ContextConfiguration({ "classpath:spring/spring-dao.xml" }) 1014 | public class SuccessKilledDaoTest { 1015 | 1016 | @Resource 1017 | private SuccessKilledDao successKilledDao; 1018 | 1019 | @Test 1020 | public void testInsertSuccessKilled() { 1021 | 1022 | long seckillId = 1000L; 1023 | long userPhone = 13476191877L; 1024 | int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone); 1025 | System.out.println("insertCount=" + insertCount); 1026 | } 1027 | 1028 | @Test 1029 | public void testQueryByIdWithSeckill() { 1030 | 1031 | long seckillId = 1000L; 1032 | long userPhone = 13476191877L; 1033 | SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); 1034 | System.out.println(successKilled); 1035 | System.out.println(successKilled.getSeckill()); 1036 | } 1037 | 1038 | } 1039 | 1040 | >测试说明 1041 | 1042 | 测试方法同上,测试结果通过,另外由于我们使用了联合主键,在insert时使用了ignore关键字,所以对于`testInsertSuccessKilled()`重复插入同一条数据是无效的,会被过滤掉,确保了一个用户不能重复秒杀同一件商品。 1043 | 1044 | >本节结语 1045 | 1046 | 至此,关于Java高并发秒杀API的DAO层的开发与测试已经完成,接下来进行Service层的开发、测试,详情可以参考[Java高并发秒杀API(二)之Service层](http://blog.csdn.net/lewky_liu/article/details/78162149)。 1047 | --------------------------------------------------------------------------------