├── README.md ├── pom.xml ├── seckilled.iml └── src ├── main ├── java │ └── org │ │ └── seckill │ │ ├── dao │ │ ├── SeckillDao.java │ │ ├── SuccessKilledDao.java │ │ └── cache │ │ │ └── RedisDao.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 │ │ └── impl │ │ │ └── SeckillServiceImpl.java │ │ └── web │ │ └── SeckillController.java ├── resources │ ├── jdbc.properties │ ├── log4j2.xml │ ├── logback.xml2 │ ├── mapper │ │ ├── SeckillDao.xml │ │ └── SuccessKilledDao.xml │ ├── mybatis-config.xml │ └── spring │ │ ├── spring-dao.xml │ │ ├── spring-service.xml │ │ └── spring-web.xml ├── sql │ ├── schema.sql │ └── seckill.sql └── webapp │ ├── WEB-INF │ ├── jsp │ │ ├── common │ │ │ ├── head.jsp │ │ │ └── tag.jsp │ │ ├── detail.jsp │ │ └── list.jsp │ └── web.xml │ ├── index.jsp │ └── resources │ └── script │ └── seckill.js └── test └── java └── org └── seckill ├── dao ├── SeckillDaoTest.java └── SuccessKilledDaoTest.java └── service └── SeckillServiceTest.java /README.md: -------------------------------------------------------------------------------- 1 | # test 2 | java 高并发处理项目 3 | SpringMVC+Spring+mybatis 4 | 5 | 项目笔记: 6 | 7 | 8 | 优化方案分析: 原子计数器--(技术实现)--->redis/NoSQL 9 | | 10 | | 11 | 记录行为消息---(技术实现)-->分布式MQ(阿里RocketMQ,Apache ActiveMQ,LinkIn Kafka) 12 | | 13 | | 14 | 消费消息并落地实现--(实现)-->Mysql 15 | 成本分析: 16 | 运维成本:稳定性,NoSQL ,MQ 17 | 开发成本:数据一致性,回滚方案 18 | 幂等性难保证:重复秒杀 19 | 不适合新手 20 | 21 | 22 | 23 | 24 | 瓶颈分析: update 减库存 25 | | 26 | GC|网络延迟 27 | insert 28 | | 29 | GC|网络延迟 30 | commit/rollback 31 | 32 | 33 | 34 | 网络延迟分析:同城机房(0.5~2ms) max(1000qbs) 35 | -update后JVM-GC(50ms) max(20qps) 36 | 异地机房(北京到上海为例,距离1300公里)1300*2/300000*(2/3)=13ms 37 | 实际情况20ms左右 38 | 优化方案: 把客户端放到Mysql服务器,避免GC和网络延迟 39 | 如何把客户端放到MySQL服务器: 40 | 1,天猫/阿里解决方案: 41 | 定制SQL方案: update /*+[auto_commit]*/ ,需要修改Mysql源码,成本很高,一般公司没有技术能力、 42 | 2,使用存储过程,整个事务放在MySQL完成(银行经常使用) 43 | 44 | 45 | 46 | 47 | 优化总结: 48 | 前端控制:暴露接口,按钮防止重复点击 49 | 动态数据分离:CDN缓存,后端缓存(memcached,redis) 50 | 事务竞争优化:减少事务锁时间 51 | 事务的优化: 52 | 存储过程 53 | 1,存储过程优化,优化事务行级锁持有时间 54 | 2,不要过度依赖存储过程 55 | 3,简单逻辑,可以应用存储过程 56 | 4, qps 一个秒杀6000/qps 57 | 58 | 59 | 60 | 61 | 智能DNS解析<-----------流量 62 | | | 63 | Nginx | Nginx | 64 | | CDN 缓存 65 | | 66 | | 67 | 逻辑集群----------->缓存集群(redis redis) 68 | | 69 | | 70 | | 71 | 分库分表------------>统计分析 72 | (tddl淘宝内部技术) 73 | 74 | 75 | 数据层技术: 76 | 数据库设计和实现 77 | MyBatis理解和使用技巧 78 | MyBatis整合Spring技巧 79 | 业务层: 80 | 业务接口设计和封装 81 | SpringIoc配置技巧 82 | Spring声明式事务使用和理解 83 | WEB技术回顾: 84 | Restful接口运用 85 | SpringMvc使用技巧 86 | 前端交互过程 87 | Bootstrap的使用和Js的书写 88 | 89 | 90 | 并发优化: 91 | 系统瓶颈优化 92 | 事务锁,网络延迟的理解 93 | 前端,CDN,缓存的使用 94 | 集群化部署 95 | 96 | 97 | TestLog42使用将logback替换成log4j2,并测试并发性能 -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | org.seckilled 5 | seckilled 6 | war 7 | 1.0-SNAPSHOT 8 | seckilled Maven Webapp 9 | http://maven.apache.org 10 | 11 | 3.3.4 12 | 13 | 14 | 15 | junit 16 | junit 17 | 4.11 18 | test 19 | 20 | 21 | 26 | 27 | 28 | org.slf4j 29 | slf4j-api 30 | 1.7.12 31 | 32 | 33 | ch.qos.logback 34 | logback-core 35 | 1.1.1 36 | 37 | 38 | 39 | ch.qos.logback 40 | logback-classic 41 | 1.1.1 42 | 43 | 44 | 45 | org.apache.logging.log4j 46 | log4j-api 47 | 2.8.2 48 | 49 | 50 | org.apache.logging.log4j 51 | log4j-core 52 | 2.8.2 53 | 54 | 55 | 56 | com.lmax 57 | disruptor 58 | ${disruptor.version} 59 | 60 | 61 | 62 | mysql 63 | mysql-connector-java 64 | 5.1.35 65 | runtime 66 | 67 | 68 | c3p0 69 | c3p0 70 | 0.9.1.2 71 | 72 | 73 | 74 | 75 | org.mybatis 76 | mybatis 77 | 3.3.0 78 | 79 | 80 | 81 | org.mybatis 82 | mybatis-spring 83 | 1.2.3 84 | 85 | 86 | 87 | taglibs 88 | standard 89 | 1.1.2 90 | 91 | 92 | jstl 93 | jstl 94 | 1.2 95 | 96 | 97 | com.fasterxml.jackson.core 98 | jackson-databind 99 | 2.5.4 100 | 101 | 102 | javax.servlet 103 | javax.servlet-api 104 | 3.1.0 105 | 106 | 107 | 108 | 109 | org.springframework 110 | spring-core 111 | 4.1.7.RELEASE 112 | 113 | 114 | org.springframework 115 | spring-beans 116 | 4.1.7.RELEASE 117 | 118 | 119 | org.springframework 120 | spring-context 121 | 4.1.7.RELEASE 122 | 123 | 124 | 125 | org.springframework 126 | spring-jdbc 127 | 4.1.7.RELEASE 128 | 129 | 130 | org.springframework 131 | spring-tx 132 | 4.1.7.RELEASE 133 | 134 | 135 | 136 | org.springframework 137 | spring-web 138 | 4.1.7.RELEASE 139 | 140 | 141 | org.springframework 142 | spring-webmvc 143 | 4.1.7.RELEASE 144 | 145 | 146 | 147 | 148 | org.springframework 149 | spring-test 150 | 4.1.7.RELEASE 151 | 152 | 153 | 154 | 155 | redis.clients 156 | jedis 157 | 2.7.3 158 | 159 | 160 | 161 | com.dyuproject.protostuff 162 | protostuff-core 163 | 1.0.8 164 | 165 | 166 | com.dyuproject.protostuff 167 | protostuff-runtime 168 | 1.0.8 169 | 170 | 171 | commons-collections 172 | commons-collections 173 | 3.2 174 | 175 | 176 | 177 | 178 | 179 | seckilled 180 | 181 | 182 | -------------------------------------------------------------------------------- /seckilled.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | file://$MODULE_DIR$/src/main/resources/spring/spring-dao.xml 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 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/dao/SeckillDao.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dao; 2 | 3 | import org.apache.ibatis.annotations.Param; 4 | import org.seckill.entity.Seckill; 5 | 6 | import java.util.Date; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | /** 11 | * Created by lisa on 2016/8/27. 12 | */ 13 | public interface SeckillDao { 14 | 15 | /** 16 | *减库存, 17 | * @param seckillId 18 | * @param killTime 19 | * @return 如果影响行数>1,表示更新的记录行数 20 | */ 21 | int reduceNumber(@Param("seckillId")long seckillId,@Param("killTime")Date killTime); 22 | 23 | /** 24 | * 根据Id查询秒杀对象 25 | * @param seckillId 26 | * @return 27 | */ 28 | Seckill queryById(long seckillId); 29 | 30 | /** 31 | * 根据偏移量查询秒杀商品列表 32 | * @param offet 33 | * @param limit 34 | * @return 35 | */ 36 | List queryAll(@Param("offet")int offet,@Param("limit")int limit); 37 | 38 | /** 39 | * 使用存储过程执行秒杀 40 | * @param paraMap 41 | */ 42 | void killByProcedure(Map paraMap); 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/dao/SuccessKilledDao.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dao; 2 | 3 | import org.apache.ibatis.annotations.Param; 4 | import org.seckill.entity.SuccessKilled; 5 | 6 | /** 7 | * Created by lisa on 2016/8/27. 8 | */ 9 | public interface SuccessKilledDao { 10 | /** 11 | * 插入购买明细,可过滤重复 12 | * @param seckillId 13 | * @param userPhone 14 | * @return 15 | */ 16 | int insertSuccessKilled(@Param("seckillId")long seckillId,@Param("userPhone")long userPhone); 17 | 18 | /** 19 | *根据id查询SuccessKilled,并携带秒杀产品对象实体 20 | * @param seckillId 21 | * @return 22 | */ 23 | SuccessKilled queryByIdWithSeckill(@Param("seckillId")long seckillId,@Param("phone")long phone); 24 | 25 | 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/dao/cache/RedisDao.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dao.cache; 2 | 3 | import com.dyuproject.protostuff.LinkedBuffer; 4 | import com.dyuproject.protostuff.ProtostuffIOUtil; 5 | import com.dyuproject.protostuff.runtime.RuntimeSchema; 6 | import org.seckill.entity.Seckill; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import redis.clients.jedis.Jedis; 10 | import redis.clients.jedis.JedisPool; 11 | 12 | /** 13 | * Created by typ on 2017/5/14. 14 | */ 15 | public class RedisDao { 16 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 17 | 18 | private final JedisPool jedisPool; 19 | 20 | // RuntimeSchema相当于一个类的描述,从class文件中进行获取. 21 | private RuntimeSchema schema = RuntimeSchema.createFrom(Seckill.class); 22 | 23 | public RedisDao(String ip, int port) { 24 | // 用于初始化Redis连接池. 25 | this.jedisPool = new JedisPool(ip, port); 26 | } 27 | 28 | public Seckill getSeckill(long seckillId) { 29 | // redis操作的逻辑 30 | try { 31 | // 获取一个jedis的连接对象. 32 | Jedis jedis = jedisPool.getResource(); 33 | try { 34 | // 在Redis内部并没有实现序列化操作. 35 | String key = "seckill:" + seckillId; 36 | // get -> byte[] -> 反序列化 -> Object(seckill) 37 | // 采用自定义的序列化, protostuff.pojo; 38 | byte[] bytes = jedis.get(key.getBytes()); 39 | // 缓存重新获取得到 40 | if(null != bytes) { 41 | // 创建一个空对象 42 | Seckill secKill = schema.newMessage(); 43 | // 使用ProtoStuff从缓存中反序列化对象. 44 | ProtostuffIOUtil.mergeFrom(bytes, secKill, schema); 45 | return secKill; 46 | } 47 | } finally { 48 | jedis.close(); // 当执行缓存操作结束之后,释放连接的对象. 49 | } 50 | } catch (Exception e) { 51 | logger.error(e.getMessage(), e); 52 | } 53 | return null; 54 | } 55 | 56 | /** 57 | * 将一个Seckill对象放入到Redis缓存中 58 | * */ 59 | public String putSeckill(Seckill secKill) { 60 | // set Object(SecKill) --> 序列化 --> byte[] 61 | try { 62 | Jedis jedis = jedisPool.getResource(); 63 | try { 64 | String key = "seckill:" + secKill.getSeckillId(); 65 | // 将seckill对象进行序列化 66 | byte[] bytes = ProtostuffIOUtil.toByteArray(secKill, schema, 67 | LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE)); 68 | // 超时缓存 69 | int timeout = 60 * 60; // 1小时 70 | // 当缓存成功之后,会返回“ok”的String字符串. 71 | String result = jedis.setex(key.getBytes(), timeout, bytes); 72 | return result; 73 | } finally { 74 | jedis.close(); // 当执行完缓存操作之后,关闭jedis. 75 | } 76 | 77 | } catch (Exception e) { 78 | logger.error(e.getMessage(), e); 79 | } 80 | return null; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/dto/Exposer.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dto; 2 | 3 | /** 4 | * 暴露秒杀地址DTO 5 | * Created by lisa on 2016/8/29. 6 | */ 7 | public class Exposer { 8 | 9 | //是否开启秒杀 10 | private boolean exposed; 11 | 12 | //一种加密措施 13 | private String md5; 14 | 15 | //id 16 | private long seckillId; 17 | 18 | //系统当前时间 19 | private long now; 20 | 21 | //开启时间 22 | private long start; 23 | 24 | //结束时间 25 | private long end; 26 | 27 | public Exposer(boolean exposed, String md5, long seckillId) { 28 | this.exposed = exposed; 29 | this.md5 = md5; 30 | this.seckillId = seckillId; 31 | } 32 | 33 | public Exposer(boolean exposed, long seckillId, long now, long start, long end) { 34 | this.exposed = exposed; 35 | this.seckillId = seckillId; 36 | this.now = now; 37 | this.start = start; 38 | this.end = end; 39 | } 40 | 41 | public Exposer(boolean exposed, long seckillId) { 42 | this.exposed = exposed; 43 | this.seckillId = seckillId; 44 | } 45 | 46 | public boolean isExposed() { 47 | return exposed; 48 | } 49 | 50 | public void setExposed(boolean exposed) { 51 | this.exposed = exposed; 52 | } 53 | 54 | public String getMd5() { 55 | return md5; 56 | } 57 | 58 | public void setMd5(String md5) { 59 | this.md5 = md5; 60 | } 61 | 62 | public long getSeckillId() { 63 | return seckillId; 64 | } 65 | 66 | public void setSeckillId(long seckillId) { 67 | this.seckillId = seckillId; 68 | } 69 | 70 | public long getNow() { 71 | return now; 72 | } 73 | 74 | public void setNow(long now) { 75 | this.now = now; 76 | } 77 | 78 | public long getStart() { 79 | return start; 80 | } 81 | 82 | public void setStart(long start) { 83 | this.start = start; 84 | } 85 | 86 | public long getEnd() { 87 | return end; 88 | } 89 | 90 | public void setEnd(long end) { 91 | this.end = end; 92 | } 93 | 94 | @Override 95 | public String toString() { 96 | return "Exposer{" + 97 | "exposed=" + exposed + 98 | ", md5='" + md5 + '\'' + 99 | ", seckillId=" + seckillId + 100 | ", now=" + now + 101 | ", start=" + start + 102 | ", end=" + end + 103 | '}'; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/dto/SeckillExecution.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dto; 2 | 3 | import org.seckill.entity.SuccessKilled; 4 | import org.seckill.enums.SeckillStatEnum; 5 | 6 | /** 7 | * 封装秒杀执行后的结果 8 | * Created by lisa on 2016/8/29. 9 | */ 10 | public class SeckillExecution { 11 | 12 | private long seckillId; 13 | 14 | //秒杀执行结果状态 15 | private int state; 16 | 17 | //状态的标识 18 | private String stateInfo; 19 | 20 | private SuccessKilled successKilled; 21 | 22 | public SeckillExecution(long seckillId, SeckillStatEnum stateEnum, SuccessKilled successKilled) { 23 | this.seckillId = seckillId; 24 | this.state = stateEnum.getState(); 25 | this.stateInfo = stateEnum.getStateInfo(); 26 | this.successKilled = successKilled; 27 | } 28 | 29 | public SeckillExecution(long seckillId,SeckillStatEnum stateEnum) { 30 | this.seckillId = seckillId; 31 | this.state = stateEnum.getState(); 32 | this.stateInfo = stateEnum.getStateInfo(); 33 | } 34 | 35 | public long getSeckillId() { 36 | return seckillId; 37 | } 38 | 39 | public void setSeckillId(long seckillId) { 40 | this.seckillId = seckillId; 41 | } 42 | 43 | public int getState() { 44 | return state; 45 | } 46 | 47 | public void setState(int state) { 48 | this.state = state; 49 | } 50 | 51 | public String getStateInfo() { 52 | return stateInfo; 53 | } 54 | 55 | public void setStateInfo(String stateInfo) { 56 | this.stateInfo = stateInfo; 57 | } 58 | 59 | public SuccessKilled getSuccessKilled() { 60 | return successKilled; 61 | } 62 | 63 | public void setSuccessKilled(SuccessKilled successKilled) { 64 | this.successKilled = successKilled; 65 | } 66 | 67 | @Override 68 | public String toString() { 69 | return "SeckillExecution{" + 70 | "seckillId=" + seckillId + 71 | ", state=" + state + 72 | ", stateInfo='" + stateInfo + '\'' + 73 | ", successKilled=" + successKilled + 74 | '}'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/dto/SeckillResult.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dto; 2 | 3 | /** 4 | * Created by lisa on 2016/8/30. 5 | */ 6 | //所有的ajax请求返回类型,封装json结果 7 | public class SeckillResult { 8 | private boolean success; 9 | 10 | private T data; 11 | 12 | private String error; 13 | 14 | public SeckillResult(boolean success, T data) { 15 | this.success = success; 16 | this.data = data; 17 | } 18 | 19 | public SeckillResult(boolean success, String error) { 20 | this.success = success; 21 | this.error = error; 22 | } 23 | 24 | public boolean isSuccess() { 25 | return success; 26 | } 27 | 28 | public void setSuccess(boolean success) { 29 | this.success = success; 30 | } 31 | 32 | public T getData() { 33 | return data; 34 | } 35 | 36 | public void setData(T data) { 37 | this.data = data; 38 | } 39 | 40 | public String getError() { 41 | return error; 42 | } 43 | 44 | public void setError(String error) { 45 | this.error = error; 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return "SeckillResult{" + 51 | "success=" + success + 52 | ", data=" + data + 53 | ", error='" + error + '\'' + 54 | '}'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/entity/Seckill.java: -------------------------------------------------------------------------------- 1 | package org.seckill.entity; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * Created by lisa on 2016/8/27. 7 | */ 8 | public class Seckill { 9 | private long seckillId; 10 | 11 | private String name; 12 | 13 | private int number; 14 | 15 | private Date startTime; 16 | 17 | private Date endTime; 18 | 19 | private Date createTime; 20 | 21 | public long getSeckillId() { 22 | return seckillId; 23 | } 24 | 25 | public void setSeckillId(long seckillId) { 26 | this.seckillId = seckillId; 27 | } 28 | 29 | public String getName() { 30 | return name; 31 | } 32 | 33 | public void setName(String name) { 34 | this.name = name; 35 | } 36 | 37 | public int getNumber() { 38 | return number; 39 | } 40 | 41 | public void setNumber(int number) { 42 | this.number = number; 43 | } 44 | 45 | public Date getStartTime() { 46 | return startTime; 47 | } 48 | 49 | public void setStartTime(Date startTime) { 50 | this.startTime = startTime; 51 | } 52 | 53 | public Date getCreateTime() { 54 | return createTime; 55 | } 56 | 57 | public void setCreateTime(Date createTime) { 58 | this.createTime = createTime; 59 | } 60 | 61 | public Date getEndTime() { 62 | return endTime; 63 | } 64 | 65 | public void setEndTime(Date endTime) { 66 | this.endTime = endTime; 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | return "Seckill{" + 72 | "seckillId=" + seckillId + 73 | ", name='" + name + '\'' + 74 | ", number=" + number + 75 | ", startTime=" + startTime + 76 | ", endTime=" + endTime + 77 | ", createTime=" + createTime + 78 | '}'; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/entity/SuccessKilled.java: -------------------------------------------------------------------------------- 1 | package org.seckill.entity; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * Created by lisa on 2016/8/27. 7 | */ 8 | public class SuccessKilled { 9 | 10 | private long seckillId; 11 | 12 | private long userPhone; 13 | 14 | private short state; 15 | 16 | private Date createTime; 17 | 18 | //变通, 19 | //多对一 20 | private Seckill seckill; 21 | 22 | public long getSeckillId() { 23 | return seckillId; 24 | } 25 | 26 | public void setSeckillId(long seckillId) { 27 | this.seckillId = seckillId; 28 | } 29 | 30 | public long getUserPhone() { 31 | return userPhone; 32 | } 33 | 34 | public void setUserPhone(long userPhone) { 35 | this.userPhone = userPhone; 36 | } 37 | 38 | public short getState() { 39 | return state; 40 | } 41 | 42 | public void setState(short state) { 43 | this.state = state; 44 | } 45 | 46 | public Date getCreateTime() { 47 | return createTime; 48 | } 49 | 50 | public void setCreateTime(Date createTime) { 51 | this.createTime = createTime; 52 | } 53 | 54 | public Seckill getSeckill() { 55 | return seckill; 56 | } 57 | 58 | public void setSeckill(Seckill seckill) { 59 | this.seckill = seckill; 60 | } 61 | 62 | @Override 63 | public String toString() { 64 | return "SuccessKilled{" + 65 | "seckillId=" + seckillId + 66 | ", userPhone=" + userPhone + 67 | ", state=" + state + 68 | ", createTime=" + createTime + 69 | '}'; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/enums/SeckillStatEnum.java: -------------------------------------------------------------------------------- 1 | package org.seckill.enums; 2 | 3 | /** 4 | * 使用枚举标识常量数据 5 | * Created by lisa on 2016/8/29. 6 | */ 7 | public enum SeckillStatEnum { 8 | SUCCESS(1,"秒杀成功"), 9 | END(0,"秒杀结束"), 10 | REPEAT_KILL(-1,"重复秒杀"), 11 | INNER_ORROR(-2,"系统异常"), 12 | DATA_REWRITE(-3,"数据篡改"); 13 | 14 | 15 | private int state; 16 | private String stateInfo; 17 | 18 | SeckillStatEnum(int state, String stateInfo) { 19 | this.state = state; 20 | this.stateInfo = stateInfo; 21 | } 22 | 23 | public int getState() { 24 | return state; 25 | } 26 | 27 | public void setState(int state) { 28 | this.state = state; 29 | } 30 | 31 | public String getStateInfo() { 32 | return stateInfo; 33 | } 34 | 35 | public void setStateInfo(String stateInfo) { 36 | this.stateInfo = stateInfo; 37 | } 38 | 39 | public static SeckillStatEnum stateOf(int index){ 40 | for(SeckillStatEnum state: values()){ 41 | if(state.getState()==index){ 42 | return state; 43 | } 44 | } 45 | return null; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/exception/RepeatKillException.java: -------------------------------------------------------------------------------- 1 | package org.seckill.exception; 2 | 3 | /** 4 | * 重复秒杀异常(运行期异常) 5 | * Created by lisa on 2016/8/29. 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 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/exception/SeckillCloseException.java: -------------------------------------------------------------------------------- 1 | package org.seckill.exception; 2 | 3 | /** 4 | * 秒杀关闭异常 5 | * Created by lisa on 2016/8/29. 6 | */ 7 | public class SeckillCloseException extends SeckillException{ 8 | 9 | public SeckillCloseException(String message) { 10 | super(message); 11 | } 12 | 13 | public SeckillCloseException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/exception/SeckillException.java: -------------------------------------------------------------------------------- 1 | package org.seckill.exception; 2 | 3 | /** 4 | * 秒杀相关业务异常 5 | * Created by lisa on 2016/8/29. 6 | */ 7 | public class SeckillException extends RuntimeException { 8 | 9 | public SeckillException(String message) { 10 | super(message); 11 | } 12 | 13 | public SeckillException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/service/SeckillService.java: -------------------------------------------------------------------------------- 1 | package org.seckill.service; 2 | 3 | import org.seckill.dto.Exposer; 4 | import org.seckill.dto.SeckillExecution; 5 | import org.seckill.entity.Seckill; 6 | import org.seckill.exception.RepeatKillException; 7 | import org.seckill.exception.SeckillCloseException; 8 | import org.seckill.exception.SeckillException; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * 业务接口:站在“使用者”角度设计接口 14 | * 三个方面:方法定义粒度,参数,返回类型(return) 15 | * 16 | * Created by lisa on 2016/8/29. 17 | */ 18 | public interface SeckillService { 19 | 20 | /** 21 | * 查询所有秒杀记录 22 | * @return 23 | */ 24 | List getSeckillList(); 25 | 26 | /** 27 | * 查询单个秒杀记录 28 | * @param seckillId 29 | * @return 30 | */ 31 | Seckill getById(long seckillId); 32 | 33 | 34 | /** 35 | * 秒杀开启时,输出秒杀接口的地址, 36 | * 否则输出系统时间和秒杀时间 37 | * @param seckillId 38 | */ 39 | Exposer exportSeckillUrl(long seckillId); 40 | 41 | 42 | /** 43 | * 执行秒杀操作 44 | * @param seckillId 45 | * @param userPhone 46 | * @param md5 47 | */ 48 | SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) 49 | throws SeckillException,RepeatKillException,SeckillCloseException; 50 | 51 | /** 52 | * 执行秒杀操作 53 | * @param seckillId 54 | * @param userPhone 55 | * @param md5 56 | */ 57 | SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5); 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/service/impl/SeckillServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.seckill.service.impl; 2 | 3 | import org.apache.commons.collections.MapUtils; 4 | import org.seckill.dao.SeckillDao; 5 | import org.seckill.dao.SuccessKilledDao; 6 | import org.seckill.dao.cache.RedisDao; 7 | import org.seckill.dto.Exposer; 8 | import org.seckill.dto.SeckillExecution; 9 | import org.seckill.entity.Seckill; 10 | import org.seckill.entity.SuccessKilled; 11 | import org.seckill.enums.SeckillStatEnum; 12 | import org.seckill.exception.RepeatKillException; 13 | import org.seckill.exception.SeckillCloseException; 14 | import org.seckill.exception.SeckillException; 15 | import org.seckill.service.SeckillService; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.stereotype.Service; 20 | import org.springframework.transaction.annotation.Transactional; 21 | import org.springframework.util.DigestUtils; 22 | 23 | import java.util.Date; 24 | import java.util.HashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | 28 | /** 29 | * Created by lisa on 2016/8/29. 30 | */ 31 | //@Compent @Service @Dao @Controller 32 | @Service 33 | public class SeckillServiceImpl implements SeckillService { 34 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 35 | 36 | //注入Service依赖 37 | @Autowired 38 | private SeckillDao seckillDao; 39 | @Autowired 40 | private SuccessKilledDao successKilledDao; 41 | @Autowired 42 | private RedisDao redisDao; 43 | 44 | //md5字符串,用于混淆md5 45 | private final String slat = "341g354grtDSAasd#W$T^GVBGZRFD125"; 46 | 47 | public List getSeckillList() { 48 | return seckillDao.queryAll(0, 4); 49 | } 50 | 51 | public Seckill getById(long seckillId) { 52 | return seckillDao.queryById(seckillId); 53 | } 54 | 55 | public Exposer exportSeckillUrl(long seckillId) { 56 | //优化点:缓存优化,一致性维护:建立在超时的基础上 57 | /** 58 | * get from cache 59 | * if null 60 | * get db 61 | * else 62 | * put cache 63 | */ 64 | //1,访问redis 65 | Seckill seckill = redisDao.getSeckill(seckillId); 66 | if (seckill == null) { 67 | //2:缓存中没有,访问数据库 68 | seckill = seckillDao.queryById(seckillId); 69 | if (seckill == null) { 70 | return new Exposer(false, seckillId); 71 | } else { 72 | //3:放入redis 73 | redisDao.putSeckill(seckill); 74 | } 75 | } 76 | 77 | 78 | Date startTime = seckill.getStartTime(); 79 | Date endTime = seckill.getEndTime(); 80 | //获得系统时间 81 | Date date = new Date(); 82 | 83 | if (date.getTime() < startTime.getTime() || 84 | date.getTime() > endTime.getTime()) { 85 | return new Exposer(false, seckillId, date.getTime(), startTime.getTime(), endTime.getTime()); 86 | 87 | } 88 | // 转化特定字符串的过程,不可逆 89 | String md5 = getMD5(seckillId); 90 | return new Exposer(true, md5, seckillId); 91 | } 92 | 93 | private String getMD5(long seckillId) { 94 | String base = seckillId + "/" + slat; 95 | String md5 = DigestUtils.md5DigestAsHex(base.getBytes()); 96 | return md5; 97 | 98 | } 99 | 100 | /** 101 | * 使用注解控制事务方法的优点 102 | * 1:开发团队达成一致,明确标注事务方法的编程风格 103 | * 2:保证事务方法的执行时间尽可能短,不要穿插其他的网络操作,RPC/HTTP请求 104 | * 或者剥离到事务方法外部 105 | * 3:不是所有的方法都需要事务,如:只有一条修改操作,只读操作不需要事务。 106 | * 两条以上需要同时去完成添加事务 107 | * 4: 108 | * 109 | * @param seckillId 110 | * @param userPhone 111 | * @param md5 112 | * @return 113 | * @throws SeckillException 114 | * @throws RepeatKillException 115 | * @throws SeckillCloseException 116 | */ 117 | @Transactional 118 | public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException { 119 | if (md5 == null || !md5.equals(getMD5(seckillId))) { 120 | throw new SeckillException("seckill data rewrite"); 121 | } 122 | 123 | try { 124 | //减库存成功,记录购买行为 125 | int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone); 126 | //唯一seckillId,userPhone 127 | if (insertCount <= 0) { 128 | //重复秒杀 129 | throw new RepeatKillException("seckill repeated"); 130 | } else { 131 | //减库存,热点商品竞争 132 | int count = seckillDao.reduceNumber(seckillId, new Date()); 133 | if (count <= 0) { 134 | //没有更新到记录,秒杀结束,rollback 135 | throw new SeckillCloseException("seckill is closed"); 136 | } else { 137 | //秒杀成功 commit 138 | SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); 139 | return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled); 140 | } 141 | 142 | 143 | } 144 | //执行秒杀逻辑:减库存,记录购买行为 145 | 146 | 147 | } catch (SeckillCloseException e1) { 148 | throw e1; 149 | } catch (RepeatKillException e2) { 150 | throw e2; 151 | } catch (Exception e) { 152 | logger.error(e.getMessage(), e); 153 | //所有编译期异常转化为运行期异常 154 | throw new SeckillException("seckill innor error:" + e.getMessage()); 155 | } 156 | 157 | } 158 | 159 | public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) { 160 | if (md5 == null || !md5.equals(getMD5(seckillId))) { 161 | return new SeckillExecution(seckillId, 162 | SeckillStatEnum.DATA_REWRITE); 163 | } 164 | Date killTime = new Date(); 165 | Map map = new HashMap(); 166 | map.put("seckillId", seckillId); 167 | map.put("phone", userPhone); 168 | map.put("killTime", killTime); 169 | map.put("result", null); 170 | try { 171 | seckillDao.killByProcedure(map); 172 | //获取result 173 | int result = MapUtils.getInteger(map, "result", -2); 174 | if (result == 1) { 175 | SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); 176 | 177 | return new SeckillExecution(seckillId, 178 | SeckillStatEnum.SUCCESS, sk); 179 | 180 | } else { 181 | return new SeckillExecution(seckillId, 182 | SeckillStatEnum.stateOf(result)); 183 | } 184 | } catch (Exception e) { 185 | e.printStackTrace(); 186 | logger.error(e.getMessage(), e); 187 | return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ORROR); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/web/SeckillController.java: -------------------------------------------------------------------------------- 1 | package org.seckill.web; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | import org.seckill.dto.Exposer; 6 | import org.seckill.dto.SeckillExecution; 7 | import org.seckill.dto.SeckillResult; 8 | import org.seckill.entity.Seckill; 9 | import org.seckill.enums.SeckillStatEnum; 10 | import org.seckill.exception.RepeatKillException; 11 | import org.seckill.exception.SeckillCloseException; 12 | import org.seckill.exception.SeckillException; 13 | import org.seckill.service.SeckillService; 14 | import org.slf4j.LoggerFactory; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.stereotype.Controller; 17 | import org.springframework.ui.Model; 18 | import org.springframework.web.bind.annotation.*; 19 | 20 | import java.util.Date; 21 | import java.util.List; 22 | 23 | 24 | /** 25 | * Created by lisa on 2016/8/30. 26 | */ 27 | @Controller//@Service @Compent 28 | @RequestMapping("/seckill")//url:/模块/资源/{id}/细分 29 | public class SeckillController { 30 | private final org.slf4j.Logger logger = LoggerFactory.getLogger(this.getClass()); 31 | Logger loggerDis = LogManager.getLogger("distance"); 32 | @Autowired 33 | private SeckillService seckillService; 34 | 35 | @RequestMapping(value = "/list", method = RequestMethod.GET) 36 | public String list(Model model) { 37 | //list.jsp+model=ModelAndView 38 | List seckills = seckillService.getSeckillList(); 39 | model.addAttribute("list", seckills); 40 | return "list"; 41 | } 42 | 43 | @RequestMapping(value = "/{seckillId}/detail", method = RequestMethod.GET) 44 | public String datail(@PathVariable("seckillId") Long seckillId, Model model) { 45 | 46 | if (seckillId == null) { 47 | return "redirect:/seckill/list"; 48 | } 49 | Seckill seckill = seckillService.getById(seckillId); 50 | if (seckill == null) { 51 | return "forward:/seckill/list"; 52 | } 53 | model.addAttribute("seckill", seckill); 54 | return "detail"; 55 | } 56 | 57 | 58 | //ajax json 59 | @RequestMapping(value = "/{seckillId}/exposer", 60 | method = RequestMethod.POST, 61 | produces = {"application/json;charset=UTF-8"} 62 | ) 63 | @ResponseBody 64 | public SeckillResult exposer(@PathVariable Long seckillId) { 65 | SeckillResult result; 66 | 67 | try { 68 | Exposer exposer = seckillService.exportSeckillUrl(seckillId); 69 | result = new SeckillResult(true, exposer); 70 | } catch (Exception e) { 71 | logger.error(e.getMessage(), e); 72 | result = new SeckillResult(false, e.getMessage()); 73 | } 74 | 75 | return result; 76 | } 77 | 78 | 79 | @RequestMapping(value = "/{seckillId}/{md5}/execution", 80 | method = RequestMethod.POST, 81 | produces = {"application/json;charser=UTF-8 "} 82 | ) 83 | @ResponseBody 84 | public SeckillResult execute( 85 | @PathVariable("seckillId") Long seckillId, 86 | @PathVariable("md5") String md5, 87 | @CookieValue(value = "killPhone", required = false) Long phone) { 88 | //springmvc valid 89 | if(phone==null){ 90 | return new SeckillResult(false,"未注册"); 91 | } 92 | 93 | SeckillResult result=null; 94 | try { 95 | SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, 96 | phone, md5); 97 | return new SeckillResult(true,seckillExecution); 98 | } catch (RepeatKillException e) { 99 | SeckillExecution execution=new SeckillExecution(seckillId, 100 | SeckillStatEnum.REPEAT_KILL); 101 | return new SeckillResult(true,execution); 102 | }catch (SeckillCloseException e) { 103 | SeckillExecution execution=new SeckillExecution(seckillId, 104 | SeckillStatEnum.END); 105 | return new SeckillResult(true,execution); 106 | } 107 | catch (Exception e) { 108 | logger.error(e.getMessage(), e); 109 | SeckillExecution execution=new SeckillExecution(seckillId, 110 | SeckillStatEnum.INNER_ORROR); 111 | return new SeckillResult(true,execution); 112 | } 113 | 114 | } 115 | 116 | @RequestMapping(value = "/time/now",method = RequestMethod.GET) 117 | @ResponseBody 118 | public SeckillResult time(){ 119 | Date date=new Date(); 120 | return new SeckillResult(true,date.getTime()); 121 | 122 | } 123 | 124 | @RequestMapping(value = "/logger",method = RequestMethod.GET) 125 | @ResponseBody 126 | public SeckillResult log(){ 127 | long aa=System.currentTimeMillis(); 128 | loggerDis.info("我是info信息"); 129 | return new SeckillResult(true,aa); 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/main/resources/jdbc.properties: -------------------------------------------------------------------------------- 1 | driver=com.mysql.jdbc.Driver 2 | jdbcUrl=jdbc:mysql://127.0.0.1:3306/seckill?useUnicode=true&characterEncoding=utf8 3 | usename=root 4 | password=root 5 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | D:/typ/logs 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 94 | 95 | 96 | 97 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/mapper/SeckillDao.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | update 11 | seckill 12 | set 13 | number=number-1 14 | where seckill_id=#{seckillId} 15 | and start_time #{killTime} 16 | and end_time>=#{killTime} 17 | and number>0; 18 | 19 | 20 | 21 | 28 | 29 | 36 | 37 | 38 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/resources/mapper/SuccessKilledDao.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | insert ignore into success_killed(seckill_id,user_phone) 9 | VALUES (#{seckillId},#{userPhone}) 10 | 11 | 12 | 35 | 36 | -------------------------------------------------------------------------------- /src/main/resources/mybatis-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-dao.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 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 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-service.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/sql/schema.sql: -------------------------------------------------------------------------------- 1 | --数据库初始化脚本 2 | --创建数据库 3 | CREATE DATABASE seckill; 4 | --使用数据库 5 | use seckill; 6 | --创建表 秒杀库存表 7 | CREATE TABLE seckill( 8 | `seckill_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品库存id', 9 | `name` varchar(120) NOT NULL COMMENT '商品名称', 10 | `number` int NOT NULL COMMENT '库存数量', 11 | `start_time` TIMESTAMP not null comment '秒杀开始时间', 12 | `end_time` TIMESTAMP not null comment '秒杀结束时间', 13 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP 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 | CREATE TABLE seckill ( 21 | `seckill_id` bigint NOT NULL AUTO_INCREMENT , 22 | `name` varchar(120) NOT NULL , 23 | `number` int NOT NULL , 24 | `start_time` timestamp NOT NULL , 25 | `end_time` timestamp NOT NULL , 26 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , 27 | PRIMARY KEY (`seckill_id`), 28 | INDEX `idx_start_time` (`start_time`) , 29 | INDEX `idx_end_time` (`end_time`) , 30 | INDEX `idx_create_time` (`create_time`) 31 | )ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARACTER SET=utf8; 32 | 33 | 34 | CREATE TABLE `seckill` ( 35 | `seckill_id` bigint(20) NOT NULL AUTO_INCREMENT, 36 | PRIMARY KEY (`seckill_id`) 37 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 38 | 39 | -- 初始化数据 40 | insert into seckill(name,number,start_time,end_time) values ('1000元秒杀ipone6',100,'2016-11-01 00:00:00','2016-11-02 00:00:00'), 41 | ('500元秒杀ipad2',200,'2016-11-01 00:00:00','2016-11-02 00:00:00'), 42 | ('300元秒杀小米4',300,'2016-11-01 00:00:00','2016-11-02 00:00:00'), 43 | ('200元秒杀红米note',400,'2016-11-01 00:00:00','2016-11-02 00:00:00') 44 | 45 | 46 | 47 | --秒杀成功明细表 48 | --用户登录认证的相关信息 49 | create table success_killed( 50 | `seckill_id` bigint not null comment '秒杀商品id', 51 | `user_phone` bigint not null comment '用户手机号', 52 | `state` tinyint not null default -1 comment '状态标识:-1 无效 0:成功 1: 已付款', 53 | `create_time` timestamp not null comment '创建时间', 54 | primary key (seckill_id,user_phone),/*联合主键*/ 55 | key idx_create_time(create_time) 56 | )ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表'; 57 | 58 | 59 | --连接数据库控制台 60 | -------------------------------------------------------------------------------- /src/main/sql/seckill.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat MySQL Data Transfer 3 | 4 | Source Server : localhost 5 | Source Server Version : 50717 6 | Source Host : localhost:3306 7 | Source Database : seckill 8 | 9 | Target Server Type : MYSQL 10 | Target Server Version : 50717 11 | File Encoding : 65001 12 | 13 | Date: 2017-09-07 20:45:24 14 | */ 15 | 16 | SET FOREIGN_KEY_CHECKS=0; 17 | 18 | -- ---------------------------- 19 | -- Table structure for seckill 20 | -- ---------------------------- 21 | DROP TABLE IF EXISTS `seckill`; 22 | CREATE TABLE `seckill` ( 23 | `seckill_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品库存id', 24 | `name` varchar(120) NOT NULL COMMENT '商品名称', 25 | `number` int(11) NOT NULL COMMENT '库存数量', 26 | `start_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '秒杀开始时间', 27 | `end_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '秒杀结束时间', 28 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 29 | PRIMARY KEY (`seckill_id`), 30 | KEY `idx_start_time` (`start_time`), 31 | KEY `idx_end_time` (`end_time`), 32 | KEY `idx_create_time` (`create_time`) 33 | ) ENGINE=InnoDB AUTO_INCREMENT=1004 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表'; 34 | 35 | -- ---------------------------- 36 | -- Records of seckill 37 | -- ---------------------------- 38 | INSERT INTO `seckill` VALUES ('1000', '1000元秒杀ipone6', '100', '2016-11-01 00:00:00', '2016-11-02 00:00:00', '2017-09-07 20:44:05'); 39 | INSERT INTO `seckill` VALUES ('1001', '500元秒杀ipad2', '200', '2016-11-01 00:00:00', '2016-11-02 00:00:00', '2017-09-07 20:44:05'); 40 | INSERT INTO `seckill` VALUES ('1002', '300元秒杀小米4', '300', '2016-11-01 00:00:00', '2016-11-02 00:00:00', '2017-09-07 20:44:05'); 41 | INSERT INTO `seckill` VALUES ('1003', '200元秒杀红米note', '400', '2016-11-01 00:00:00', '2016-11-02 00:00:00', '2017-09-07 20:44:05'); 42 | 43 | -- ---------------------------- 44 | -- Table structure for success_killed 45 | -- ---------------------------- 46 | DROP TABLE IF EXISTS `success_killed`; 47 | CREATE TABLE `success_killed` ( 48 | `seckill_id` bigint(20) NOT NULL COMMENT '秒杀商品id', 49 | `user_phone` bigint(20) NOT NULL COMMENT '用户手机号', 50 | `state` tinyint(4) NOT NULL DEFAULT '-1' COMMENT '状态标识:-1 无效 0:成功 1: 已付款', 51 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', 52 | PRIMARY KEY (`seckill_id`,`user_phone`), 53 | KEY `idx_create_time` (`create_time`) 54 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表'; 55 | 56 | -- ---------------------------- 57 | -- Records of success_killed 58 | -- ---------------------------- 59 | SET FOREIGN_KEY_CHECKS=1; 60 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/common/head.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 19 | 20 | 21 |

22 |
23 |
24 |
25 | 26 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 77 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/list.jsp: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> 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 | 36 | 39 | 42 | 45 | 46 | 47 | 48 |
名称库存开始时间结束时间创建时间详情页
${sk.name}${sk.number} 34 | 35 | 37 | 38 | 40 | 41 | 43 | Link 44 |
49 |
50 |
51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | Archetype Created Web Application 9 | 10 | seckill-dispatcher 11 | org.springframework.web.servlet.DispatcherServlet 12 | 16 | 17 | contextConfigLocation 18 | classpath:spring/spring-*.xml 19 | 20 | 21 | 22 | 23 | seckill-dispatcher 24 | 25 | / 26 | 27 | -------------------------------------------------------------------------------- /src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Hello World!111111111

4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/webapp/resources/script/seckill.js: -------------------------------------------------------------------------------- 1 | //存放主要交互逻辑的代码 2 | //javascript 模块化 3 | //seckill.detail.init(params) 4 | var seckill={ 5 | //封装秒杀相关ajax的url 6 | URL:{ 7 | now :function(){ 8 | return '/seckill/time/now'; 9 | }, 10 | exposer: function(seckillId){ 11 | return '/seckill/'+seckillId+'/exposer'; 12 | }, 13 | execution : function(seckillId,md5){ 14 | return '/seckill/'+seckillId+'/'+md5+'/execution'; 15 | } 16 | }, 17 | handleSeckillkill :function(seckillId,node){ 18 | //处理秒杀逻辑 19 | //获取秒杀地址,控制显示逻辑,执行秒杀 20 | node.hide() 21 | .html(' '); 22 | $.post(seckill.URL.exposer(seckillId),{},function(result){ 23 | //在回调函数中执行交互流程 24 | console.log("处理秒杀逻辑:"+result); 25 | console.log(result); 26 | if(result&&result['success']){ 27 | var exposer=result['data']; 28 | if(exposer['exposed']){ 29 | //开启秒杀 30 | //获取秒杀地址 31 | var md5=exposer['md5']; 32 | var killUrl=seckill.URL.execution(seckillId,md5); 33 | console.log("killUrl:"+killUrl); 34 | //只绑定一次点击事件 35 | $('#killBtn').one('click',function(){ 36 | //绑定执行秒杀请求 37 | //1,先禁用按钮 38 | console.log("sssss"); 39 | $(this).addClass('disabled'); 40 | //2:发送秒杀请求 41 | $.post(killUrl,{},function(result){ 42 | if(result&&result['success']){ 43 | var killResult=result['data']; 44 | var state=killResult['state']; 45 | var stateInfo=killResult['stateInfo']; 46 | //显示秒杀结果 47 | node.html(''+stateInfo+''); 48 | } 49 | }) 50 | }); 51 | node.show(); 52 | }else{ 53 | //未开启秒杀 54 | var now =exposer['now']; 55 | var start=exposer['start']; 56 | var end=exposer['end']; 57 | //重新计算计时逻辑 58 | seckill.countdown(seckillId,now,start,end); 59 | } 60 | }else{ 61 | console.log('result'+result); 62 | } 63 | }); 64 | }, 65 | //详情页秒杀逻辑 66 | //验证手机号 67 | validatePhone:function(phone){ 68 | if(phone&&phone.length==11&&!isNaN(phone)){ 69 | return true; 70 | }else{ 71 | return false; 72 | } 73 | }, 74 | countdown:function(seckillId,nowTime,startTime,endTime){ 75 | var seckillBox=$('#seckill-box'); 76 | //时间判断 77 | if(nowTime>endTime){ 78 | //秒杀结束 79 | seckillBox.html("秒杀结束"); 80 | }else if(nowTime手机号错误!>').show(300); 136 | } 137 | }); 138 | } 139 | //已经登录 140 | //计时交互 141 | var startTime=params['startTime']; 142 | var endTime=params['endTime']; 143 | var seckillId=params['seckillId']; 144 | $.get(seckill.URL.now(),{},function(result){ 145 | if(result&&result['success']){ 146 | 147 | var nowTime=result['data']; 148 | //时间判断 149 | seckill.countdown(seckillId,nowTime,startTime,endTime); 150 | }else{ 151 | console.log("result:"+result); 152 | } 153 | 154 | }) 155 | 156 | } 157 | } 158 | 159 | } -------------------------------------------------------------------------------- /src/test/java/org/seckill/dao/SeckillDaoTest.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dao; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.seckill.entity.Seckill; 6 | import org.springframework.test.context.ContextConfiguration; 7 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 8 | 9 | import javax.annotation.Resource; 10 | 11 | import java.util.Date; 12 | import java.util.List; 13 | 14 | import static org.junit.Assert.*; 15 | 16 | /** 17 | * Created by lisa on 2016/8/27. 18 | */ 19 | 20 | /** 21 | * 配置spring 和junit整合,junit启动时加载springIoc容器 22 | * spring-test 23 | */ 24 | @RunWith(SpringJUnit4ClassRunner.class) 25 | //告诉junit spring配置文件 26 | @ContextConfiguration({"classpath:spring/spring-dao.xml"}) 27 | public class SeckillDaoTest { 28 | //注入dao实现类 29 | @Resource 30 | private SeckillDao seckillDao; 31 | 32 | @Test 33 | public void testQueryById() throws Exception { 34 | long id = 1000; 35 | Seckill seckill = seckillDao.queryById(id); 36 | System.out.println(seckill.toString()); 37 | } 38 | 39 | @Test 40 | public void testQueryAll() throws Exception { 41 | //java中没有保存形参的记录 42 | List seckills= seckillDao.queryAll(0, 100); 43 | for(Seckill s:seckills){ 44 | System.out.println(s); 45 | } 46 | 47 | } 48 | 49 | @Test 50 | public void testReduceNumber() throws Exception { 51 | Date killTime=new Date(); 52 | int updateCount=seckillDao.reduceNumber(1000L,killTime); 53 | System.out.println("**********updateCount"+updateCount); 54 | 55 | } 56 | 57 | 58 | } -------------------------------------------------------------------------------- /src/test/java/org/seckill/dao/SuccessKilledDaoTest.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dao; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.seckill.entity.SuccessKilled; 6 | import org.springframework.test.context.ContextConfiguration; 7 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 8 | 9 | import javax.annotation.Resource; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Created by lisa on 2016/8/27. 15 | */ 16 | @RunWith(SpringJUnit4ClassRunner.class) 17 | //告诉junit spring配置文件 18 | @ContextConfiguration({"classpath:spring/spring-dao.xml"}) 19 | public class SuccessKilledDaoTest { 20 | @Resource 21 | private SuccessKilledDao seckillDaoTest; 22 | 23 | @Test 24 | public void testInsertSuccessKilled() throws Exception { 25 | long id=1000L; 26 | long phone=13502181181L; 27 | int a= seckillDaoTest.insertSuccessKilled(id,phone); 28 | System.out.println("00000000000000000000 "+a); 29 | } 30 | 31 | @Test 32 | public void testQueryByIdWithSeckill() throws Exception { 33 | long id=1000L; 34 | long phone=13502181181L; 35 | SuccessKilled a= seckillDaoTest.queryByIdWithSeckill(id,phone); 36 | System.out.println("00000000000000000000 "+a); 37 | System.out.println("******************** "+a.getSeckill()); 38 | 39 | } 40 | } -------------------------------------------------------------------------------- /src/test/java/org/seckill/service/SeckillServiceTest.java: -------------------------------------------------------------------------------- 1 | package org.seckill.service; 2 | 3 | import org.junit.Test; 4 | import org.junit.internal.builders.JUnit4Builder; 5 | import org.junit.runner.RunWith; 6 | import org.seckill.dto.Exposer; 7 | import org.seckill.dto.SeckillExecution; 8 | import org.seckill.entity.Seckill; 9 | import org.seckill.exception.RepeatKillException; 10 | import org.seckill.exception.SeckillCloseException; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.test.context.ContextConfiguration; 15 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 16 | 17 | import java.util.List; 18 | 19 | import static org.junit.Assert.*; 20 | 21 | /** 22 | * Created by lisa on 2016/8/29. 23 | */ 24 | @RunWith(SpringJUnit4ClassRunner.class) 25 | @ContextConfiguration({"classpath:spring/spring-dao.xml", 26 | "classpath:spring/spring-service.xml"}) 27 | public class SeckillServiceTest { 28 | private final Logger logger= LoggerFactory.getLogger(this.getClass()); 29 | 30 | @Autowired 31 | private SeckillService seckillService; 32 | 33 | @Test 34 | public void testGetSeckillList() throws Exception { 35 | List list=seckillService.getSeckillList(); 36 | logger.info("list={}",list); 37 | 38 | } 39 | 40 | @Test 41 | public void testGetById() throws Exception { 42 | long id=1000; 43 | Seckill seckill=seckillService.getById(id); 44 | logger.info("seckill={}",seckill); 45 | } 46 | 47 | //测试代码完整逻辑,注意可重复执行 48 | @Test 49 | public void testExportSeckillUrl() throws Exception { 50 | long id=1002; 51 | Exposer exposer=seckillService.exportSeckillUrl(id); 52 | //exposer=Exposer{exposed=true, md5='45c49ca170b89875b6f08e8bcdcf4040', 53 | // seckillId=1000, now=0, start=0, end=0} 54 | if(exposer.isExposed()){ 55 | logger.info("exposer={}",exposer); 56 | long phone=13502171122L; 57 | String md5=exposer.getMd5(); 58 | try { 59 | SeckillExecution seckillExecution = seckillService.executeSeckill(id, phone, md5); 60 | logger.info("result={}", seckillExecution); 61 | }catch (SeckillCloseException e){ 62 | logger.error(e.getMessage()); 63 | }catch (RepeatKillException e){ 64 | logger.error(e.getMessage()); 65 | } 66 | }else{ 67 | //秒杀为开启 68 | logger.warn("exposer={}", exposer); 69 | } 70 | // result=SeckillExecution{seckillId=1002, state=1, 71 | // stateInfo='秒杀成功', successKilled=SuccessKilled{seckillId=1002, 72 | // userPhone=13502171125, state=-1, createTime=Mon Aug 29 17:46:20 CST 2016}} 73 | 74 | } 75 | 76 | @Test 77 | public void testExecuteSeckill() throws Exception { 78 | long id=1000; 79 | long phone=13502171125L; 80 | String md5="45c49ca170b89875b6f08e8bcdcf4040"; 81 | try { 82 | SeckillExecution seckillExecution = seckillService.executeSeckill(id, phone, md5); 83 | logger.info("result={}", seckillExecution); 84 | }catch (SeckillCloseException e){ 85 | logger.error(e.getMessage()); 86 | }catch (RepeatKillException e){ 87 | logger.error(e.getMessage()); 88 | } 89 | 90 | 91 | } 92 | 93 | @Test 94 | public void testExecuteSeckillProcedure() throws Exception { 95 | long seckillId=1001; 96 | long phone=136801110; 97 | Exposer exposer=seckillService.exportSeckillUrl(seckillId); 98 | if(exposer.isExposed()){ 99 | String md5=exposer.getMd5(); 100 | SeckillExecution seckillExecution= seckillService.executeSeckillProcedure(seckillId 101 | , phone, md5); 102 | logger.info(seckillExecution.getStateInfo()); 103 | } 104 | 105 | } 106 | } --------------------------------------------------------------------------------