├── .gitignore ├── README.md ├── pom.xml └── src ├── main ├── conf │ ├── dev │ │ ├── jdbc.properties │ │ └── logback_dev.xml │ └── product │ │ └── logback.xml ├── 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 │ ├── mapper │ │ ├── SeckillDao.xml │ │ └── SuccessKilledDao.xml │ ├── mybatis-config.xml │ └── spring │ │ ├── application-context.xml │ │ ├── spring-dao.xml │ │ ├── spring-service.xml │ │ └── spring-web.xml ├── sql │ └── seckill.sql └── webapp │ ├── WEB-INF │ ├── jsp │ │ ├── common │ │ │ ├── head.jsp │ │ │ └── tag.jsp │ │ ├── detail.jsp │ │ └── list.jsp │ └── web.xml │ ├── index.jsp │ └── resource │ └── script │ └── seckill.js └── test └── java └── org └── seckill ├── Test.java ├── dao ├── SeckillDaoTest.java ├── SuccessKilledDaoTest.java └── cache │ └── RedisDaoTest.java └── service └── SeckillServiceTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Package Files # 2 | .DS_Store 3 | *.iml 4 | *.class 5 | *.jar 6 | *.war 7 | *.ear 8 | # Mobile Tools for Java (J2ME) 9 | .mtj.tmp/ 10 | out/ 11 | .idea/ 12 | target/ 13 | 14 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 15 | hs_err_pid* 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Imooc Java High Concurrency Seckill Online Shopping System([Course](http://www.imooc.com/learn/587)) 2 | - Spring,SpringMVC,Mybatis,Maven,Bootstrap,jQuery,Mysql,Restful API. 3 | - Use Redis the improve the performance. 4 | 5 | #### SQL Script 6 | ```sql 7 | CREATE DATABASE seckill; 8 | USE seckill; 9 | 10 | -- todo:mysql Ver 5.7.12for Linux(x86_64)中一个表只能有一个TIMESTAMP 11 | CREATE TABLE seckill( 12 | `seckill_id` BIGINT NOT NUll AUTO_INCREMENT COMMENT '商品库存ID', 13 | `name` VARCHAR(120) NOT NULL COMMENT '商品名称', 14 | `number` int NOT NULL COMMENT '库存数量', 15 | `start_time` TIMESTAMP NOT NULL COMMENT '秒杀开始时间', 16 | `end_time` DATETIME NOT NULL COMMENT '秒杀结束时间', 17 | `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 18 | PRIMARY KEY (seckill_id), 19 | key idx_start_time(start_time), 20 | key idx_end_time(end_time), 21 | key idx_create_time(create_time) 22 | )ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表'; 23 | 24 | -- 初始化数据 25 | INSERT into seckill(name,number,start_time,end_time) 26 | VALUES 27 | ('3000元秒杀iphone6',100,'2016-01-01 00:00:00','2016-12-31 00:00:00'), 28 | ('2000元秒杀ipad',100,'2016-01-01 00:00:00','2016-05-01 00:00:00'), 29 | ('6000元秒杀mac book pro',100,'2016-07-01 00:00:00','2016-12-31 00:00:00'), 30 | ('7000元秒杀iMac',100,'2016-05-01 00:00:00','2016-12-31 00:00:00') 31 | 32 | -- 秒杀成功明细表 33 | -- 用户登录认证相关信息(简化为手机号) 34 | CREATE TABLE success_killed( 35 | `seckill_id` BIGINT NOT NULL COMMENT '秒杀商品ID', 36 | `user_phone` BIGINT NOT NULL COMMENT '用户手机号', 37 | `state` TINYINT NOT NULL DEFAULT -1 COMMENT '状态标识:-1:无效 0:成功 1:已付款 2:已发货', 38 | `create_time` TIMESTAMP NOT NULL COMMENT '创建时间', 39 | PRIMARY KEY(seckill_id,user_phone),/*联合主键*/ 40 | KEY idx_create_time(create_time) 41 | )ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表' 42 | 43 | SHOW CREATE TABLE seckill\G;#显示表的创建信息 44 | ``` 45 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | seckill_Gid 8 | seckill_Aid 9 | 1.0-SNAPSHOT 10 | war 11 | seckill 12 | 13 | 14 | 15 | chongbeiwang 16 | wchb20155@gmail.com 17 | 18 | 19 | 20 | 21 | UTF-8 22 | 4.2.0.RELEASE 23 | 1.8 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ch.qos.logback 32 | logback-classic 33 | 1.2.3 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | org.projectlombok 42 | lombok 43 | 1.16.20 44 | 45 | 46 | 47 | com.google.guava 48 | guava 49 | 18.0 50 | 51 | 52 | 53 | org.apache.commons 54 | commons-lang3 55 | 3.3.2 56 | 57 | 58 | 59 | commons-io 60 | commons-io 61 | 2.4 62 | 63 | 64 | 65 | junit 66 | junit 67 | 4.12 68 | 69 | 70 | 71 | commons-collections 72 | commons-collections 73 | 3.2.1 74 | 75 | 76 | 77 | 78 | 79 | 80 | org.springframework 81 | spring-core 82 | ${spring.version} 83 | 84 | 85 | 86 | org.springframework 87 | spring-beans 88 | ${spring.version} 89 | 90 | 91 | 92 | org.springframework 93 | spring-context 94 | ${spring.version} 95 | 96 | 97 | 98 | org.springframework 99 | spring-context-support 100 | ${spring.version} 101 | 102 | 103 | 104 | org.springframework 105 | spring-aop 106 | ${spring.version} 107 | 108 | 109 | 110 | org.springframework 111 | spring-aspects 112 | ${spring.version} 113 | 114 | 115 | 116 | org.springframework 117 | spring-expression 118 | ${spring.version} 119 | 120 | 121 | 122 | org.springframework 123 | spring-tx 124 | ${spring.version} 125 | 126 | 127 | 128 | org.springframework 129 | spring-test 130 | ${spring.version} 131 | 132 | 133 | 134 | 135 | 136 | jstl 137 | jstl 138 | 139 | 1.2 140 | 141 | 142 | 143 | javax.servlet 144 | javax.servlet-api 145 | 3.1.0 146 | 147 | 148 | 149 | taglibs 150 | standard 151 | 1.1.2 152 | 153 | 154 | 155 | org.springframework 156 | spring-web 157 | ${spring.version} 158 | 159 | 160 | 161 | org.springframework 162 | spring-webmvc 163 | ${spring.version} 164 | 165 | 166 | 167 | com.fasterxml.jackson.core 168 | jackson-databind 169 | 2.7.4 170 | 171 | 172 | 173 | 174 | 175 | mysql 176 | mysql-connector-java 177 | 5.1.38 178 | runtime 179 | 180 | 181 | 182 | c3p0 183 | c3p0 184 | 0.9.1.2 185 | 186 | 187 | 188 | commons-dbcp 189 | commons-dbcp 190 | 1.4 191 | 192 | 193 | 194 | org.springframework 195 | spring-jdbc 196 | ${spring.version} 197 | 198 | 199 | 200 | org.mybatis 201 | mybatis 202 | 3.4.0 203 | 204 | 205 | 206 | org.mybatis 207 | mybatis-spring 208 | 1.3.0 209 | 210 | 211 | 212 | redis.clients 213 | jedis 214 | 2.7.3 215 | 216 | 217 | 218 | 219 | com.dyuproject.protostuff 220 | protostuff-core 221 | 1.0.8 222 | 223 | 224 | 225 | com.dyuproject.protostuff 226 | protostuff-runtime 227 | 1.0.8 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | seckill_F 236 | 237 | 238 | 239 | src/main/resources 240 | 241 | true 242 | 243 | 244 | 245 | src/main/conf/${envconf.dir} 246 | true 247 | 248 | 249 | 250 | 251 | 252 | org.apache.maven.plugins 253 | maven-war-plugin 254 | 2.6 255 | 256 | 257 | 258 | src/main/webapp 259 | true 260 | 261 | 262 | 263 | 264 | 265 | 266 | org.apache.maven.plugins 267 | maven-compiler-plugin 268 | 2.5.1 269 | 270 | ${java.version} 271 | ${java.version} 272 | -Xlint:all 273 | true 274 | true 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | dev 283 | 284 | true 285 | 286 | 287 | dev 288 | 289 | 290 | 291 | 292 | product 293 | 294 | product 295 | 296 | 297 | 298 | -------------------------------------------------------------------------------- /src/main/conf/dev/jdbc.properties: -------------------------------------------------------------------------------- 1 | mysql.driver=com.mysql.jdbc.Driver 2 | jdbc.url=jdbc:mysql://IP:3306/test_mysql 3 | jdbc.username=111 4 | jdbc.password=222 -------------------------------------------------------------------------------- /src/main/conf/dev/logback_dev.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %d{HH:mm:ss.SSS} [%thread] %-5level %msg %n 9 | 10 | 11 | 12 | 13 | 14 | 15 | ERROR 16 | DENY 17 | ACCEPT 18 | 19 | 20 | 21 | %d{HH:mm:ss.SSS} [%thread] %-5level %msg %n 22 | 23 | 24 | 25 | 26 | 27 | /Users/wchb2018/Music/seckill/info.%d.log 28 | 29 | 30 | 31 | 32 | 33 | ERROR 34 | 35 | 36 | 37 | %d{HH:mm:ss.SSS} [%thread] %-5level %msg %n 38 | 39 | 40 | 41 | 42 | 43 | /Users/wchb2018/Music/seckill/error.%d.log 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/main/conf/product/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %d{HH:mm:ss.SSS} [%thread] %-5level %msg %n 9 | 10 | 11 | 12 | 13 | 14 | 15 | ERROR 16 | DENY 17 | ACCEPT 18 | 19 | 20 | 21 | %d{HH:mm:ss.SSS} [%thread] %-5level %msg %n 22 | 23 | 24 | 25 | 26 | 27 | ~/Music/seckill/info.%d.log 28 | 29 | 30 | 31 | 32 | 33 | ERROR 34 | 35 | 36 | 37 | %d{HH:mm:ss.SSS} [%thread] %-5level %msg %n 38 | 39 | 40 | 41 | 42 | 43 | ~/Music/seckill/error.%d.log 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /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 | public interface SeckillDao { 11 | 12 | /** 13 | * 减库存 14 | * 15 | * @param seckillId 16 | * @param killTime 17 | * @return 如果更新行数大于1,表示更新的行数 18 | */ 19 | Integer reduceNumber(@Param("seckillId") Long seckillId, @Param("killTime") Date killTime); 20 | 21 | 22 | /** 23 | * 根据ID查询秒杀对象 24 | * 25 | * @param seckillId 26 | * @return 27 | */ 28 | Seckill queryById(Long seckillId); 29 | 30 | 31 | /** 32 | * 根据偏移量查询秒杀商品列表 33 | * 34 | * @param offset 35 | * @param limit 36 | * @return 37 | */ 38 | List queryAll(@Param("offset") Integer offset, @Param("limit") Integer limit); 39 | 40 | /** 41 | * 使用存储过程执行秒杀 42 | * 43 | * @param paramMap 44 | */ 45 | void killByProcedure(Map paramMap); 46 | 47 | } 48 | -------------------------------------------------------------------------------- /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 wchb7 on 16-5-8. 8 | */ 9 | public interface SuccessKilledDao { 10 | 11 | /** 12 | * 插入购买明细,可过滤重复(数据库有联合主键) 13 | * 14 | * @param seckilledId 15 | * @param userPhone 16 | * @return 17 | */ 18 | Integer insertSuccessKilled(@Param("seckilledId") long seckilledId, @Param("userPhone") long userPhone); 19 | 20 | /** 21 | * 根据ID查询SuccessKilled并携带秒杀产品对象实体 22 | * 23 | * @param seckilledId 24 | * @param userPhone 25 | * @return 26 | */ 27 | SuccessKilled queryByIdWithSeckill(@Param("seckilledId") long seckilledId, @Param("userPhone") long userPhone); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /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 | public class RedisDao { 14 | 15 | private static final Logger LOG = LoggerFactory.getLogger(RedisDao.class); 16 | 17 | private final JedisPool jedisPool; 18 | 19 | private RuntimeSchema schema = RuntimeSchema.createFrom(Seckill.class); 20 | 21 | public RedisDao(String ip, int port) { 22 | jedisPool = new JedisPool(ip, port); 23 | 24 | } 25 | 26 | public Seckill getSeckill(long seckillId) { 27 | //redis逻辑操作 28 | try { 29 | Jedis jedis = jedisPool.getResource(); 30 | try { 31 | String key = "seckill:" + seckillId; 32 | //并没有实现内部序列化操作 33 | //get:byte[]->反序列化->Object(Seckill) 34 | //采用自定义序列化 35 | 36 | byte[] bytes = jedis.get(key.getBytes()); 37 | if (bytes != null) { 38 | //空对象 39 | Seckill seckill = schema.newMessage(); 40 | ProtostuffIOUtil.mergeFrom(bytes, seckill, schema); 41 | //seckill 被反序列化 42 | return seckill; 43 | } 44 | } finally { 45 | jedis.close(); 46 | } 47 | } catch (Exception e) { 48 | LOG.error(e.getMessage()); 49 | } 50 | return null; 51 | } 52 | 53 | /** 54 | * Seckill 对象传递到redis中 55 | * 56 | * @param seckill 57 | * @return 58 | */ 59 | public String putSeckill(Seckill seckill) { 60 | //set:Object(Seckill)->序列化->byte[] ->发送给redis 61 | try { 62 | Jedis jedis = jedisPool.getResource(); 63 | try { 64 | String key = "seckill:" + seckill.getSeckillId(); 65 | byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE)); 66 | //超时缓存 67 | int timeOut = 60 * 60; 68 | String result = jedis.setex(key.getBytes(), timeOut, bytes); 69 | return result; 70 | } finally { 71 | jedis.close(); 72 | } 73 | } catch (Exception e) { 74 | LOG.error(e.getMessage()); 75 | } 76 | return null; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/dto/Exposer.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.ToString; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 暴露秒杀地址DTO(dto:web层和service层传递数据用) 11 | * Created by wchb7 on 16-5-13. 12 | */ 13 | 14 | @Getter 15 | @Setter 16 | @ToString 17 | public class Exposer implements Serializable { 18 | 19 | private static final long serialVersionUID = 7602244494037452541L; 20 | 21 | /** 22 | * 秒杀是否开启 23 | */ 24 | private boolean exposed; 25 | 26 | private String md5; 27 | 28 | private long seckillId; 29 | 30 | /** 31 | * 系统时间(毫秒) 32 | */ 33 | private long now; 34 | 35 | private long start; 36 | 37 | private long end; 38 | 39 | public Exposer(boolean exposed, String md5, long seckillId) { 40 | this.exposed = exposed; 41 | this.md5 = md5; 42 | this.seckillId = seckillId; 43 | } 44 | 45 | public Exposer(boolean exposed, long seckillId, long now, long start, long end) { 46 | this.exposed = exposed; 47 | this.seckillId = seckillId; 48 | this.now = now; 49 | this.start = start; 50 | this.end = end; 51 | } 52 | 53 | public Exposer(boolean exposed, long seckillId) { 54 | this.exposed = exposed; 55 | this.seckillId = seckillId; 56 | } 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/dto/SeckillExecution.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.ToString; 6 | import org.junit.experimental.theories.suppliers.TestedOn; 7 | import org.seckill.entity.SuccessKilled; 8 | import org.seckill.enums.SeckillStatEnum; 9 | 10 | import java.io.Serializable; 11 | 12 | /** 13 | * 封装秒杀执行后的结果 14 | * Created by wchb7 on 16-5-13. 15 | */ 16 | @Getter 17 | @Setter 18 | @ToString 19 | public class SeckillExecution implements Serializable { 20 | 21 | private static final long serialVersionUID = 2160123709223365015L; 22 | 23 | private long seckillId; 24 | 25 | /** 26 | * 秒杀执行结果状态 27 | */ 28 | private int state; 29 | 30 | /** 31 | * 状态表示 32 | */ 33 | private String stateInfo; 34 | 35 | private SuccessKilled successKilled; 36 | 37 | public SeckillExecution(long seckillId, SeckillStatEnum statEnum) { 38 | this.seckillId = seckillId; 39 | this.state = statEnum.getState(); 40 | this.stateInfo = statEnum.getStateInfo(); 41 | } 42 | 43 | public SeckillExecution(long seckillId, SeckillStatEnum statEnum, String stateInfo) { 44 | this.seckillId = seckillId; 45 | this.state = statEnum.getState(); 46 | this.stateInfo = statEnum.getStateInfo(); 47 | this.stateInfo = stateInfo; 48 | } 49 | 50 | public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) { 51 | this.seckillId = seckillId; 52 | this.state = statEnum.getState(); 53 | this.stateInfo = statEnum.getStateInfo(); 54 | this.successKilled = successKilled; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/dto/SeckillResult.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.ToString; 6 | 7 | import java.io.Serializable; 8 | 9 | //DTO:完成WEB层到Service层的数据传递 10 | //所有的ajax请求的返回类型封装JSON结果 11 | @ToString 12 | @Getter 13 | @Setter 14 | public class SeckillResult implements Serializable { 15 | 16 | private static final long serialVersionUID = -3936895148526393338L; 17 | 18 | private boolean success; 19 | 20 | private T data; 21 | 22 | private String error; 23 | 24 | public SeckillResult(boolean success, String error) { 25 | this.success = success; 26 | this.error = error; 27 | } 28 | 29 | public SeckillResult(boolean success, T data) { 30 | this.success = success; 31 | this.data = data; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/entity/Seckill.java: -------------------------------------------------------------------------------- 1 | package org.seckill.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.ToString; 6 | import org.junit.experimental.theories.suppliers.TestedOn; 7 | 8 | import java.util.Date; 9 | 10 | @Setter 11 | @Getter 12 | @ToString 13 | public class Seckill { 14 | 15 | private long seckillId; 16 | 17 | private String name; 18 | 19 | private int number; 20 | 21 | private Date startTime; 22 | 23 | private Date endTime; 24 | 25 | private Date createTime; 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/entity/SuccessKilled.java: -------------------------------------------------------------------------------- 1 | package org.seckill.entity; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.ToString; 6 | 7 | import java.util.Date; 8 | 9 | @Getter 10 | @Setter 11 | @ToString 12 | public class SuccessKilled { 13 | 14 | /** 15 | * 一个秒杀seckill对应多个成功记录 16 | */ 17 | private Seckill seckill; 18 | 19 | private long seckillId; 20 | 21 | private long userPhone; 22 | 23 | private short state; 24 | 25 | private Date createTime; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/enums/SeckillStatEnum.java: -------------------------------------------------------------------------------- 1 | package org.seckill.enums; 2 | 3 | /** 4 | * Created by wchb7 on 16-5-14. 5 | */ 6 | public enum SeckillStatEnum { 7 | 8 | SUCCESS(1, "秒杀成功"), 9 | END(0, "秒杀结束"), 10 | REPEAT_KILL(-1, "重复秒杀"), 11 | INNER_ERROR(-2, "系统异常"), 12 | DATA_REWRITE(-3, "数据篡改"), 13 | NOT_LOGIN(-4, "未登陆"); 14 | 15 | private int state; 16 | 17 | private String stateInfo; 18 | 19 | SeckillStatEnum(int state, String stateInfo) { 20 | this.state = state; 21 | this.stateInfo = stateInfo; 22 | } 23 | 24 | public int getState() { 25 | return state; 26 | } 27 | 28 | public String getStateInfo() { 29 | return stateInfo; 30 | } 31 | 32 | public static SeckillStatEnum stateOf(int index) { 33 | for (SeckillStatEnum state : values()) { 34 | if (state.getState() == index) { 35 | return state; 36 | } 37 | } 38 | return null; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/exception/RepeatKillException.java: -------------------------------------------------------------------------------- 1 | package org.seckill.exception; 2 | 3 | /** 4 | * 重复秒杀异常(运行期异常) 5 | * RuntimeException 不需要try/catch 而且Spring 的声明式事务只接收RuntimeException回滚策略. 6 | * Created by wchb7 on 16-5-14. 7 | */ 8 | public class RepeatKillException extends SeckillException { 9 | 10 | public RepeatKillException(String message) { 11 | super(message); 12 | } 13 | 14 | public RepeatKillException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/exception/SeckillCloseException.java: -------------------------------------------------------------------------------- 1 | package org.seckill.exception; 2 | 3 | /** 4 | * Created by wchb7 on 16-5-14. 5 | */ 6 | public class SeckillCloseException extends SeckillException { 7 | 8 | public SeckillCloseException(String message) { 9 | super(message); 10 | } 11 | 12 | public SeckillCloseException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/exception/SeckillException.java: -------------------------------------------------------------------------------- 1 | package org.seckill.exception; 2 | 3 | /** 4 | * 秒杀相关业务异常 5 | * Created by wchb7 on 16-5-14. 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 | } 18 | -------------------------------------------------------------------------------- /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 | * Created by wchb7 on 16-5-13. 14 | * 业务接口:站在"使用者"的角度设计接口 15 | * 1.方法的定义的粒度.2.参数.3.返回类型(return /异常) 16 | */ 17 | public interface SeckillService { 18 | 19 | 20 | /** 21 | * 查询所有秒杀记录 22 | * 23 | * @return 24 | */ 25 | List getSeckillList(); 26 | 27 | 28 | /** 29 | * 查询单个秒杀记录 30 | * 31 | * @param seckillId 32 | * @return 33 | */ 34 | Seckill getById(Long seckillId); 35 | 36 | 37 | /** 38 | * 秒杀开启时输出秒杀接口地址 39 | * 否则输出系统时间和秒杀时间 40 | * 41 | * @param seckillId 42 | * @return 43 | */ 44 | Exposer exportSeckillUrl(Long seckillId); 45 | 46 | /** 47 | * 执行秒杀操作 48 | * 49 | * @param seckillId 50 | * @param userPhone 51 | * @param md5 52 | * @return 53 | */ 54 | SeckillExecution executeSeckill(Long seckillId, Long userPhone, String md5) throws SeckillException 55 | , RepeatKillException, SeckillCloseException; 56 | 57 | /** 58 | * 执行秒杀操作 by 存储过程 59 | * 60 | * @param seckillId 61 | * @param userPhone 62 | * @param md5 63 | * @return 64 | */ 65 | SeckillExecution executeSeckillProcedure(Long seckillId, Long userPhone, String md5); 66 | 67 | } 68 | -------------------------------------------------------------------------------- /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.apache.commons.lang3.StringUtils; 5 | import org.seckill.dao.SeckillDao; 6 | import org.seckill.dao.SuccessKilledDao; 7 | import org.seckill.dao.cache.RedisDao; 8 | import org.seckill.dto.Exposer; 9 | import org.seckill.dto.SeckillExecution; 10 | import org.seckill.entity.Seckill; 11 | import org.seckill.entity.SuccessKilled; 12 | import org.seckill.enums.SeckillStatEnum; 13 | import org.seckill.exception.RepeatKillException; 14 | import org.seckill.exception.SeckillCloseException; 15 | import org.seckill.exception.SeckillException; 16 | import org.seckill.service.SeckillService; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | import org.springframework.stereotype.Service; 21 | import org.springframework.transaction.annotation.Transactional; 22 | import org.springframework.util.DigestUtils; 23 | 24 | import java.util.Date; 25 | import java.util.HashMap; 26 | import java.util.List; 27 | import java.util.Map; 28 | 29 | 30 | @Service 31 | public class SeckillServiceImpl implements SeckillService { 32 | 33 | private static final Logger LOG = LoggerFactory.getLogger(SeckillServiceImpl.class); 34 | 35 | @Autowired 36 | private SeckillDao seckillDao; 37 | 38 | @Autowired 39 | private RedisDao redisDao; 40 | 41 | @Autowired 42 | private SuccessKilledDao successKilledDao; 43 | 44 | //md5盐值字符串,用于混淆md5 45 | private final String slat = "asdfasd2341242@#$@#$%$%%#@$%#@%^%^"; 46 | 47 | @Override 48 | public List getSeckillList() { 49 | return seckillDao.queryAll(0, 1000); 50 | } 51 | 52 | @Override 53 | public Seckill getById(Long seckillId) { 54 | return seckillDao.queryById(seckillId); 55 | } 56 | 57 | @Override 58 | public Exposer exportSeckillUrl(Long seckillId) { 59 | 60 | //优化点:缓存优化 超时的基础上维护一致性 61 | //1.访问redis 62 | Seckill seckill = redisDao.getSeckill(seckillId); 63 | 64 | if (seckill == null) { 65 | //2.访问数据库 66 | seckill = getById(seckillId); 67 | if (seckill == null) { 68 | return new Exposer(false, seckillId); 69 | } else { 70 | //3.放入redis 71 | redisDao.putSeckill(seckill); 72 | } 73 | } 74 | 75 | Date startTime = seckill.getStartTime(); 76 | Date endTime = seckill.getEndTime(); 77 | 78 | Date nowTime = new Date(); 79 | 80 | if (nowTime.getTime() > endTime.getTime() || nowTime.getTime() < startTime.getTime()) { 81 | return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime()); 82 | } 83 | 84 | //转化特定字符串的过程,不可逆 85 | String md5 = getMD5(seckillId); 86 | return new Exposer(true, md5, seckillId); 87 | } 88 | 89 | 90 | @Transactional 91 | /** 92 | * 使用注解控制事务的优点: 93 | * 1.开发团队达成一致约定,明确标注事务方法的编程风格. 94 | * 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部. 95 | * 3.不是所有的方法都需要事务.如一些查询的service.只有一条修改操作的service. 96 | */ 97 | @Override 98 | public SeckillExecution executeSeckill(Long seckillId, Long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException { 99 | 100 | if (StringUtils.isEmpty(md5) || !md5.equals(getMD5(seckillId))) { 101 | throw new SeckillException(SeckillStatEnum.DATA_REWRITE.getStateInfo()); 102 | } 103 | 104 | //执行秒杀逻辑:1.减库存.2.记录购买行为 105 | Date nowTime = new Date(); 106 | 107 | try { 108 | 109 | //记录购买行为 110 | int inserCount = successKilledDao.insertSuccessKilled(seckillId, userPhone); 111 | 112 | if (inserCount <= 0) { 113 | //重复秒杀 114 | throw new RepeatKillException(SeckillStatEnum.REPEAT_KILL.getStateInfo()); 115 | } else { 116 | //减库存 热点商品竞争 117 | int updateCount = seckillDao.reduceNumber(seckillId, nowTime); 118 | 119 | if (updateCount <= 0) { 120 | //rollback 121 | throw new SeckillCloseException(SeckillStatEnum.END.getStateInfo()); 122 | } else { 123 | //秒杀成功 commit 124 | SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); 125 | return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled); 126 | } 127 | 128 | } 129 | 130 | 131 | } catch (SeckillCloseException e1) { 132 | throw e1; 133 | } catch (RepeatKillException e2) { 134 | throw e2; 135 | } catch (Exception e) { 136 | LOG.error(e.getMessage()); 137 | //所有的编译期异常转化为运行期异常,spring的声明式事务做rollback 138 | throw new SeckillException("seckill inner error: " + e.getMessage()); 139 | } 140 | 141 | } 142 | 143 | @Override 144 | public SeckillExecution executeSeckillProcedure(Long seckillId, Long userPhone, String md5) { 145 | if (StringUtils.isEmpty(md5) || !md5.equals(getMD5(seckillId))) { 146 | throw new SeckillException(SeckillStatEnum.DATA_REWRITE.getStateInfo()); 147 | } 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 | try { 157 | seckillDao.killByProcedure(map); 158 | //获取result 159 | int result = MapUtils.getInteger(map, "result", -2); 160 | if (result == 1) { 161 | SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); 162 | return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, sk); 163 | } else { 164 | return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result)); 165 | } 166 | 167 | } catch (Exception e) { 168 | LOG.error(e.getMessage()); 169 | return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR); 170 | } 171 | 172 | } 173 | 174 | private String getMD5(long seckillId) { 175 | String base = seckillId + "/" + slat; 176 | String md5 = DigestUtils.md5DigestAsHex(base.getBytes()); 177 | LOG.info("_________________________________md5: " + md5); 178 | return md5; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/web/SeckillController.java: -------------------------------------------------------------------------------- 1 | package org.seckill.web; 2 | 3 | import org.seckill.dto.Exposer; 4 | import org.seckill.dto.SeckillExecution; 5 | import org.seckill.dto.SeckillResult; 6 | import org.seckill.entity.Seckill; 7 | import org.seckill.enums.SeckillStatEnum; 8 | import org.seckill.exception.RepeatKillException; 9 | import org.seckill.exception.SeckillCloseException; 10 | import org.seckill.service.SeckillService; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Controller; 15 | import org.springframework.ui.Model; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | import javax.servlet.ServletContext; 19 | import java.util.Date; 20 | import java.util.List; 21 | 22 | /** 23 | * Created by wchb7 on 16-5-23. 24 | */ 25 | 26 | @Controller 27 | @RequestMapping("/seckill")//url:/模块/资源/{id}/细分 28 | public class SeckillController { 29 | 30 | private static final Logger LOG = LoggerFactory.getLogger(SeckillController.class); 31 | 32 | @Autowired 33 | private SeckillService seckillService; 34 | 35 | @Autowired 36 | private ServletContext servletContext; 37 | 38 | @RequestMapping(value = "/list", method = RequestMethod.GET) 39 | public String list(Model model) { 40 | //list.jsp + model = ModelAndView 41 | //获取列表页 42 | 43 | List list = seckillService.getSeckillList(); 44 | model.addAttribute("list", list); 45 | 46 | return "list"; 47 | } 48 | 49 | @RequestMapping(value = "/{seckillId}/detail", method = RequestMethod.GET) 50 | public String detail(@PathVariable("seckillId") Long seckillId, Model model) { 51 | 52 | if (seckillId == null) { 53 | return "redirect:/seckill/list"; 54 | } 55 | 56 | Seckill seckill = seckillService.getById(seckillId); 57 | 58 | if (seckill == null) { 59 | return "forward:/seckill/list"; 60 | } 61 | 62 | model.addAttribute("seckill", seckill); 63 | 64 | return "detail"; 65 | 66 | } 67 | 68 | // ajax json 69 | @RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST, 70 | produces = {"application/json;charset=UTF-8"}) 71 | @ResponseBody 72 | public SeckillResult exposer(@PathVariable("seckillId") Long seckillId) { 73 | 74 | SeckillResult result; 75 | 76 | try { 77 | Exposer exposer = seckillService.exportSeckillUrl(seckillId); 78 | result = new SeckillResult(true, exposer); 79 | } catch (Exception e) { 80 | LOG.error(e.getMessage()); 81 | result = new SeckillResult(false, e.getMessage()); 82 | } 83 | 84 | return result; 85 | } 86 | 87 | // ajax json 88 | @RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST, 89 | produces = {"application/json;charset=UTF-8"}) 90 | @ResponseBody 91 | public SeckillResult execute(@PathVariable("seckillId") Long seckillId, @PathVariable("md5") String md5, 92 | @CookieValue(value = "killPhone", required = false) Long killPhone) { 93 | 94 | if (killPhone == null) { 95 | return new SeckillResult(false, SeckillStatEnum.NOT_LOGIN.getStateInfo()); 96 | } 97 | 98 | try { 99 | SeckillExecution execution = seckillService.executeSeckill(seckillId, killPhone, md5); 100 | //SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, killPhone, md5); 101 | return new SeckillResult(true, execution); 102 | } catch (RepeatKillException e) { 103 | SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL); 104 | return new SeckillResult(true, execution); 105 | 106 | } catch (SeckillCloseException e2) { 107 | SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END); 108 | return new SeckillResult(true, execution); 109 | 110 | } catch (Exception e) { 111 | LOG.error(e.getMessage()); 112 | SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR); 113 | return new SeckillResult(true, execution); 114 | } 115 | 116 | } 117 | 118 | @RequestMapping(value = "/time/now", method = RequestMethod.GET) 119 | @ResponseBody 120 | public SeckillResult time() { 121 | Date now = new Date(); 122 | return new SeckillResult(true, now.getTime()); 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/main/resources/mapper/SeckillDao.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | UPDATE seckill set number = number -1 12 | where seckill_id = #{seckillId} 13 | AND start_time #{killTime} 14 | AND end_time >= #{killTime} 15 | AND number >0; 16 | 17 | 18 | 19 | 24 | 25 | 26 | 32 | 33 | 34 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main/resources/mapper/SuccessKilledDao.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | INSERT ignore INTO success_killed(seckill_id,user_phone,state)VALUES (#{seckilledId},#{userPhone},1) 9 | 10 | 11 | 12 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/mybatis-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/spring/application-context.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | classpath:jdbc.properties 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-dao.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-service.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/sql/seckill.sql: -------------------------------------------------------------------------------- 1 | -- 秒杀存储过程 2 | 3 | DELIMITER $$ -- console ; 转换为 $$ 4 | -- 定义存储过程 5 | -- 参数: in 输入参数; out 输出参数 6 | -- row_count():返回上一条修改类型的sql(delete,update,insert)的影响行数 7 | -- row_count(): 0:未修改数据;>0 :表示修改的行数;<0:sql错误/未执行修改的sql 8 | CREATE PROCEDURE `test_mysql`.`execute_seckill` 9 | (in v_seckill_id bigint,in v_phone bigint,in v_kill_time TIMESTAMP ,out r_result int) 10 | 11 | BEGIN 12 | 13 | DECLARE insert_count int DEFAULT 0; 14 | START TRANSACTION ; 15 | INSERT ignore INTO success_killed(seckill_id,user_phone,create_time,state)VALUES(v_seckill_id,v_phone,v_kill_time,1); 16 | 17 | SELECT ROW_COUNT() INTO insert_count; 18 | IF(insert_count = 0)THEN 19 | ROLLBACK ; 20 | SET r_result=-1; 21 | ELSEIF(insert_count<0)THEN 22 | ROLLBACK ; 23 | SET r_result=-2; 24 | 25 | ELSE 26 | UPDATE seckill set number = number-1 WHERE seckill_id = v_seckill_id and end_time > v_kill_time and start_time 0; 28 | SELECT ROW_COUNT() INTO insert_count; 29 | IF(insert_count<=0)THEN 30 | ROLLBACK ; 31 | SET r_result = -2; 32 | ELSE 33 | COMMIT ; 34 | SET r_result = 1; 35 | END IF; 36 | END IF; 37 | END ; 38 | $$ 39 | 40 | -- 存储过程定义 41 | 42 | DELIMITER ; 43 | set @r_result=-3; 44 | -- 执行存储过程 45 | call execute_seckill(1004,15821112222,now(),@r_result); 46 | 47 | -- 获取结果 48 | SELECT @r_result; 49 | 50 | show create procedure execute_seckill\G 51 | 52 | -- 存储过程 53 | -- 存储过程优化的是事务行级锁持有的时间 54 | -- 不要过度依赖存储过程, 简单的逻辑可以应用存储过程 55 | -- QPS:一个秒杀单6000/qps -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/common/head.jsp: -------------------------------------------------------------------------------- 1 | <%-- 2 | JSP静态包含: <%@include file="common/head.jsp" %> 最终是一个servlet 3 | 4 | JSP动态包含:多个servlet 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" %> -------------------------------------------------------------------------------- /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 | 秒杀详情页 8 | <%@include file="common/head.jsp" %> 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |

${seckill.name}

17 |
18 | 19 |
20 |

21 | <%--显示time图标--%> 22 | 23 | <%--展示倒计时--%> 24 | 25 |

26 |
27 |
28 |
29 | 30 | <%--登录弹出层 输入电话--%> 31 | 63 | 64 | 65 | <%--jQery文件,务必在bootstrap.min.js之前引入--%> 66 | 67 | 68 | <%--使用CDN 获取公共js http://www.bootcdn.cn/--%> 69 | <%--jQuery Cookie操作插件--%> 70 | 71 | <%--jQuery countDown倒计时插件--%> 72 | 73 | 74 | 75 | 76 | 86 | 87 | -------------------------------------------------------------------------------- /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 | 秒杀列表页 8 | <%@include file="common/head.jsp" %> 9 | 10 | 11 | 12 |
13 |
14 |
15 |

秒杀列表

16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 42 | 45 | 48 | 49 | 50 | 51 | 52 |
名称库存开始时间结束时间创建时间详情页
${sk.name}${sk.number} 37 | 38 | 40 | 41 | 43 | 44 | 46 | Link 47 |
53 | 54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | seckill-dispatcher 10 | org.springframework.web.servlet.DispatcherServlet 11 | 15 | 16 | contextConfigLocation 17 | classpath:spring/spring-*.xml 18 | 19 | 20 | 21 | 22 | seckill-dispatcher 23 | 24 | / 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> 2 | 3 | 4 | JavaWeb高并发秒杀 5 | 6 | 7 | 8 | Hello Seckill!
9 | 10 | 秒杀列表页面 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/webapp/resource/script/seckill.js: -------------------------------------------------------------------------------- 1 | //存放主要交互逻辑的js代码 2 | // javascript 模块化(package.类.方法) 3 | 4 | var seckill = { 5 | 6 | //封装秒杀相关ajax的url 7 | URL: { 8 | now: function () { 9 | return '/seckill/time/now'; 10 | }, 11 | exposer: function (seckillId) { 12 | return '/seckill/' + seckillId + '/exposer'; 13 | }, 14 | execution: function (seckillId, md5) { 15 | return '/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 killPhone = $.cookie('killPhone'); 36 | //验证手机号 37 | if (!seckill.validatePhone(killPhone)) { 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('killPhone', 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 | $.get(seckill.URL.now(), {}, function (result) { 67 | if (result && result['success']) { 68 | var nowTime = result['data']; 69 | //时间判断 计时交互 70 | seckill.countDown(seckillId, nowTime, startTime, endTime); 71 | } else { 72 | console.log('result: ' + result); 73 | alert('result: ' + result); 74 | } 75 | }); 76 | } 77 | }, 78 | 79 | handlerSeckill: function (seckillId, node) { 80 | //获取秒杀地址,控制显示器,执行秒杀 81 | node.hide().html(''); 82 | 83 | $.post(seckill.URL.exposer(seckillId), {}, function (result) { 84 | //在回调函数种执行交互流程 85 | if (result && result['success']) { 86 | var exposer = result['data']; 87 | if (exposer['exposed']) { 88 | //开启秒杀 89 | //获取秒杀地址 90 | var md5 = exposer['md5']; 91 | var killUrl = seckill.URL.execution(seckillId, md5); 92 | console.log("killUrl: " + killUrl); 93 | //绑定一次点击事件 94 | $('#killBtn').one('click', function () { 95 | //执行秒杀请求 96 | //1.先禁用按钮 97 | $(this).addClass('disabled');//,<-$(this)===('#killBtn')-> 98 | //2.发送秒杀请求执行秒杀 99 | $.post(killUrl, {}, function (result) { 100 | if (result && result['success']) { 101 | var killResult = result['data']; 102 | var state = killResult['state']; 103 | var stateInfo = killResult['stateInfo']; 104 | //显示秒杀结果 105 | node.html('' + stateInfo + ''); 106 | } 107 | }); 108 | }); 109 | node.show(); 110 | } else { 111 | //未开启秒杀(浏览器计时偏差) 112 | var now = exposer['now']; 113 | var start = exposer['start']; 114 | var end = exposer['end']; 115 | seckill.countDown(seckillId, now, start, end); 116 | } 117 | } else { 118 | console.log('result: ' + result); 119 | } 120 | }); 121 | 122 | }, 123 | 124 | countDown: function (seckillId, nowTime, startTime, endTime) { 125 | console.log(seckillId + '_' + nowTime + '_' + startTime + '_' + endTime); 126 | var seckillBox = $('#seckill-box'); 127 | if (nowTime > endTime) { 128 | //秒杀结束 129 | seckillBox.html('秒杀结束!'); 130 | } else if (nowTime < startTime) { 131 | //秒杀未开始,计时事件绑定 132 | var killTime = new Date(startTime + 1000);//todo 防止时间偏移 133 | seckillBox.countdown(killTime, function (event) { 134 | //时间格式 135 | var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒 '); 136 | seckillBox.html(format); 137 | }).on('finish.countdown', function () { 138 | //时间完成后回调事件 139 | //获取秒杀地址,控制现实逻辑,执行秒杀 140 | console.log('______fininsh.countdown'); 141 | seckill.handlerSeckill(seckillId, seckillBox); 142 | }); 143 | } else { 144 | //秒杀开始 145 | seckill.handlerSeckill(seckillId, seckillBox); 146 | } 147 | } 148 | 149 | } -------------------------------------------------------------------------------- /src/test/java/org/seckill/Test.java: -------------------------------------------------------------------------------- 1 | package org.seckill; 2 | 3 | public class Test { 4 | 5 | @org.junit.Test 6 | public void test() { 7 | System.out.printf("haha"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/org/seckill/dao/SeckillDaoTest.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dao; 2 | 3 | import com.mchange.v2.c3p0.ComboPooledDataSource; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.seckill.entity.Seckill; 7 | import org.springframework.test.context.ContextConfiguration; 8 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 9 | 10 | import javax.annotation.Resource; 11 | import java.sql.Connection; 12 | import java.sql.DriverManager; 13 | import java.util.*; 14 | 15 | /** 16 | * 配置Spring和Junit整合,junit启动时加载springIOC容器 17 | * spring-test,junit 18 | */ 19 | 20 | @RunWith(SpringJUnit4ClassRunner.class) 21 | @ContextConfiguration("classpath:spring/spring-dao.xml") 22 | public class SeckillDaoTest { 23 | 24 | 25 | //注入Dao实现类依赖 26 | @Resource 27 | private SeckillDao seckillDao; 28 | 29 | @Test 30 | public void testQueryById() throws Exception { 31 | long id = 1000; 32 | Seckill seckill = seckillDao.queryById(id); 33 | System.out.println(seckill); 34 | } 35 | 36 | 37 | @Test 38 | public void testReduceNumber() throws Exception { 39 | // Java没有保存形参的记录:QueryAll(int offset,int limit)->QueryAll(arg0,arg1); 40 | // 因为java形参的问题,多个基本类型参数的时候需要用@Param("seckillId")注解区分开来 41 | List seckills = seckillDao.queryAll(0, 100); 42 | for (Seckill seckill : seckills) { 43 | System.out.println(seckill); 44 | } 45 | } 46 | 47 | 48 | @Test 49 | public void testQueryAll() throws Exception { 50 | Date killTime = new Date(); 51 | int updateCount = seckillDao.reduceNumber(1000L, killTime); 52 | System.out.println("updateCount: " + updateCount); 53 | } 54 | 55 | 56 | @Test 57 | public void testGetConnection() { 58 | 59 | String url = "jdbc:mysql://10.112.1.110:3306/test_mysql"; 60 | String driver = "com.mysql.jdbc.Driver"; 61 | String user = "root"; 62 | String passwd = "111111"; 63 | 64 | try { 65 | Class.forName(driver); 66 | } catch (Exception e) { 67 | System.out.println("Get Connection failed!!!"); 68 | } 69 | 70 | try { 71 | Connection con = DriverManager.getConnection(url, user, passwd); 72 | 73 | System.out.println("Get Connection Success!!!"); 74 | 75 | Properties properties = con.getClientInfo(); 76 | Iterator> it = properties.entrySet().iterator(); 77 | while (it.hasNext()) { 78 | Map.Entry entry = it.next(); 79 | System.out.println(entry.getKey() + "---------------" + entry.getValue()); 80 | } 81 | } catch (Exception e) { 82 | e.printStackTrace(); 83 | } 84 | } 85 | 86 | 87 | @Test 88 | public void testComboPooledDataSource() throws Exception { 89 | String url = "jdbc:mysql://10.112.1.110:3306/test_mysql"; 90 | String driver = "com.mysql.jdbc.Driver"; 91 | String user = "root"; 92 | String passwd = "111111"; 93 | 94 | ComboPooledDataSource cpds = new ComboPooledDataSource(); 95 | 96 | cpds.setDriverClass(driver); 97 | cpds.setJdbcUrl(url); 98 | cpds.setUser(user); 99 | cpds.setPassword(passwd); 100 | 101 | cpds.setMinPoolSize(5); 102 | cpds.setAcquireIncrement(5); 103 | cpds.setMaxPoolSize(30); 104 | cpds.setMaxIdleTime(60); 105 | 106 | Connection con = cpds.getConnection(); 107 | 108 | System.out.println("Get Connection Success!!! " + con); 109 | 110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /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 | @RunWith(SpringJUnit4ClassRunner.class) 12 | @ContextConfiguration("classpath:spring/spring-dao.xml") 13 | public class SuccessKilledDaoTest { 14 | 15 | @Resource 16 | private SuccessKilledDao successKilledDao; 17 | 18 | @Test 19 | public void testInsertSuccessKilled() throws Exception { 20 | long id = 1000L; 21 | long phone = 15811112222L; 22 | int insertCount = successKilledDao.insertSuccessKilled(id, phone); 23 | System.out.println("insertCount: " + insertCount); 24 | } 25 | 26 | @Test 27 | public void testQueryByIdWithSeckill() throws Exception { 28 | 29 | long id = 1000L; 30 | long phone = 15811112222L; 31 | SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(id, phone); 32 | System.out.println(successKilled); 33 | if (successKilled != null) { 34 | System.out.println(successKilled.getSeckill()); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/test/java/org/seckill/dao/cache/RedisDaoTest.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dao.cache; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.seckill.dao.SeckillDao; 6 | import org.seckill.entity.Seckill; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.test.context.ContextConfiguration; 9 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Created by wchb7 on 16-5-27. 15 | */ 16 | @RunWith(SpringJUnit4ClassRunner.class) 17 | @ContextConfiguration("classpath:spring/spring-dao.xml") 18 | public class RedisDaoTest { 19 | 20 | private long id = 1001; 21 | 22 | @Autowired 23 | private RedisDao redisDao; 24 | 25 | @Autowired 26 | private SeckillDao seckillDao; 27 | 28 | @Test 29 | public void testSeckill() throws Exception { 30 | //get and put 31 | Seckill seckill = redisDao.getSeckill(id); 32 | if (seckill == null) { 33 | seckill = seckillDao.queryById(id); 34 | if (seckill != null) { 35 | String result = redisDao.putSeckill(seckill); 36 | System.out.println(result); 37 | seckill = redisDao.getSeckill(id); 38 | System.out.println(seckill); 39 | } 40 | } 41 | } 42 | 43 | 44 | } -------------------------------------------------------------------------------- /src/test/java/org/seckill/service/SeckillServiceTest.java: -------------------------------------------------------------------------------- 1 | package org.seckill.service; 2 | 3 | import org.apache.commons.logging.Log; 4 | import org.apache.commons.logging.LogFactory; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.seckill.dto.Exposer; 8 | import org.seckill.dto.SeckillExecution; 9 | import org.seckill.entity.Seckill; 10 | import org.seckill.exception.RepeatKillException; 11 | import org.seckill.exception.SeckillCloseException; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.test.context.ContextConfiguration; 14 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 15 | 16 | @RunWith(SpringJUnit4ClassRunner.class) 17 | @ContextConfiguration({"classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml"}) 18 | public class SeckillServiceTest { 19 | 20 | private Log LOG = LogFactory.getLog(this.getClass()); 21 | 22 | @Autowired 23 | private SeckillService seckillService; 24 | 25 | @Test 26 | public void testGetById() throws Exception { 27 | long id = 1000; 28 | Seckill seckill = seckillService.getById(id); 29 | System.out.println(seckill); 30 | } 31 | 32 | @Test 33 | public void testGetSeckillList() throws Exception { 34 | System.out.println(seckillService.getSeckillList()); 35 | } 36 | 37 | @Test 38 | public void testSeckillLogic() { 39 | 40 | long id = 1000; 41 | Exposer exposer = seckillService.exportSeckillUrl(id); 42 | System.out.println(exposer); 43 | if (exposer.isExposed()) { 44 | 45 | long phone = 15821739222L; 46 | 47 | String md5 = exposer.getMd5(); 48 | 49 | try { 50 | SeckillExecution seckillExecution = seckillService.executeSeckill(id, phone, md5); 51 | System.out.println(seckillExecution); 52 | } catch (RepeatKillException e) { 53 | LOG.error(e.getMessage()); 54 | } catch (SeckillCloseException e) { 55 | LOG.error(e.getMessage()); 56 | } 57 | 58 | } else { 59 | LOG.warn(exposer.toString()); 60 | } 61 | 62 | } 63 | 64 | @Test 65 | public void testExportSeckillUrl() throws Exception { 66 | long id = 1001; 67 | Exposer exposer = seckillService.exportSeckillUrl(id); 68 | System.out.println(exposer); 69 | // Exposer{exposed=true, md5='9b8082b22ded08718a4255e9f482a80c', seckillId=1000, now=0, start=0, end=0} 70 | } 71 | 72 | @Test 73 | public void testExecuteSeckill() { 74 | long id = 1011; 75 | long phone = 15821739111L; 76 | 77 | String md5 = "9b8082b22ded08718a4255e9f482a80c"; 78 | 79 | try { 80 | 81 | SeckillExecution seckillExecution = seckillService.executeSeckill(id, phone, md5); 82 | 83 | System.out.println(seckillExecution); 84 | 85 | } catch (Exception e) { 86 | LOG.error(e.getMessage()); 87 | } 88 | } 89 | 90 | @Test 91 | public void testExecuteSeckillProcedure() { 92 | long seckillId = 1004l; 93 | long phone = 15811111122l; 94 | 95 | Exposer exposer = seckillService.exportSeckillUrl(seckillId); 96 | 97 | if (exposer.isExposed()) { 98 | String md5 = exposer.getMd5(); 99 | SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5); 100 | System.out.println(execution.getStateInfo()); 101 | } 102 | 103 | } 104 | } --------------------------------------------------------------------------------