├── .idea ├── artifacts │ ├── seckill_war.xml │ └── seckill_war_exploded.xml ├── compiler.xml ├── dataSources.xml ├── dictionaries │ └── joryun.xml ├── encodings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── libraries │ ├── Maven__aopalliance_aopalliance_1_0.xml │ ├── Maven__c3p0_c3p0_0_9_1_2.xml │ ├── Maven__ch_qos_logback_logback_classic_1_1_1.xml │ ├── Maven__ch_qos_logback_logback_core_1_1_1.xml │ ├── Maven__com_alibaba_druid_0_2_26.xml │ ├── Maven__com_dyuproject_protostuff_protostuff_api_1_0_8.xml │ ├── Maven__com_dyuproject_protostuff_protostuff_collectionschema_1_0_8.xml │ ├── Maven__com_dyuproject_protostuff_protostuff_core_1_0_8.xml │ ├── Maven__com_dyuproject_protostuff_protostuff_runtime_1_0_8.xml │ ├── Maven__com_fasterxml_jackson_core_jackson_annotations_2_5_0.xml │ ├── Maven__com_fasterxml_jackson_core_jackson_core_2_5_4.xml │ ├── Maven__com_fasterxml_jackson_core_jackson_databind_2_5_4.xml │ ├── Maven__commons_logging_commons_logging_1_2.xml │ ├── Maven__javax_servlet_javax_servlet_api_3_1_0.xml │ ├── Maven__jstl_jstl_1_2.xml │ ├── Maven__junit_junit_4_11.xml │ ├── Maven__mysql_mysql_connector_java_5_1_35.xml │ ├── Maven__org_apache_commons_commons_pool2_2_3.xml │ ├── Maven__org_hamcrest_hamcrest_core_1_3.xml │ ├── Maven__org_mybatis_mybatis_3_3_0.xml │ ├── Maven__org_mybatis_mybatis_spring_1_2_3.xml │ ├── Maven__org_slf4j_slf4j_api_1_7_12.xml │ ├── Maven__org_springframework_spring_aop_4_1_7_RELEASE.xml │ ├── Maven__org_springframework_spring_beans_4_1_7_RELEASE.xml │ ├── Maven__org_springframework_spring_context_4_1_7_RELEASE.xml │ ├── Maven__org_springframework_spring_core_4_1_7_RELEASE.xml │ ├── Maven__org_springframework_spring_expression_4_1_7_RELEASE.xml │ ├── Maven__org_springframework_spring_jdbc_4_1_7_RELEASE.xml │ ├── Maven__org_springframework_spring_test_4_1_7_RELEASE.xml │ ├── Maven__org_springframework_spring_tx_4_1_7_RELEASE.xml │ ├── Maven__org_springframework_spring_web_4_1_7_RELEASE.xml │ ├── Maven__org_springframework_spring_webmvc_4_1_7_RELEASE.xml │ ├── Maven__redis_clients_jedis_2_7_3.xml │ └── Maven__taglibs_standard_1_1_2.xml ├── misc.xml ├── modules.xml └── sqldialects.xml ├── README.md ├── pom.xml ├── seckill.iml ├── sql └── seckill.sql └── src ├── main ├── java │ └── org │ │ └── seckill │ │ ├── controller │ │ └── SeckillController.java │ │ ├── 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 ├── resources │ ├── jdbc.properties │ ├── logback.xml │ ├── mapper │ │ ├── SeckillDao.xml │ │ └── SuccessKilledDao.xml │ ├── mybatis-config.xml │ └── spring │ │ ├── spring-dao.xml │ │ ├── spring-service.xml │ │ └── spring-web.xml ├── sql │ └── schema.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 ├── dao ├── SeckillDaoTest.java ├── SuccessKilledDaoTest.java └── cache │ └── RedisDaoTest.java └── service └── SeckillServiceTest.java /.idea/artifacts/seckill_war.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/target 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/seckill_war_exploded.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/target/seckill 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mysql 6 | true 7 | com.mysql.jdbc.Driver 8 | jdbc:mysql://localhost:3306/seckill 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/dictionaries/joryun.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | seckill 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__aopalliance_aopalliance_1_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__c3p0_c3p0_0_9_1_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__ch_qos_logback_logback_classic_1_1_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__ch_qos_logback_logback_core_1_1_1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_alibaba_druid_0_2_26.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_dyuproject_protostuff_protostuff_api_1_0_8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_dyuproject_protostuff_protostuff_collectionschema_1_0_8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_dyuproject_protostuff_protostuff_core_1_0_8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_dyuproject_protostuff_protostuff_runtime_1_0_8.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_annotations_2_5_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_core_2_5_4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__com_fasterxml_jackson_core_jackson_databind_2_5_4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__commons_logging_commons_logging_1_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__javax_servlet_javax_servlet_api_3_1_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__jstl_jstl_1_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__junit_junit_4_11.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__mysql_mysql_connector_java_5_1_35.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_apache_commons_commons_pool2_2_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_mybatis_mybatis_3_3_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_mybatis_mybatis_spring_1_2_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_12.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_springframework_spring_aop_4_1_7_RELEASE.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_springframework_spring_beans_4_1_7_RELEASE.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_springframework_spring_context_4_1_7_RELEASE.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_springframework_spring_core_4_1_7_RELEASE.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_springframework_spring_expression_4_1_7_RELEASE.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_springframework_spring_jdbc_4_1_7_RELEASE.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_springframework_spring_test_4_1_7_RELEASE.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_springframework_spring_tx_4_1_7_RELEASE.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_springframework_spring_web_4_1_7_RELEASE.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__org_springframework_spring_webmvc_4_1_7_RELEASE.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__redis_clients_jedis_2_7_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/libraries/Maven__taglibs_standard_1_1_2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Google Web Toolkit issues 60 | 61 | 62 | 63 | 64 | Android 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/sqldialects.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 高并发秒杀系统 2 | 开发环境:IDEA,Tomcat,MySQL,Redis 3 | 4 | 项目构建:Maven 5 | 6 | 软件环境:SSM(SpringMVC,Spring,MyBatis) 7 | 8 | 项目描述:一套以秒杀商品为目的而搭建制作的高并发系统。基本实现用户根据商家设定的库存量进行秒杀的过程。 9 | 10 | 技术描述:基于SpringMVC,Spring,MyBatis实现的高并发秒杀系统。代码设计风格基于RESTful,以c3p0作为连接池,Redis数据库为媒介实现高并发技术。其中,对于相关的DAO,Service操作,均添加了Junit单元测试实例。 11 | 12 | ### 开发文档 13 | 14 | #### 一、业务分析 15 | 16 | 1.秒杀系统业务流程 17 | 18 | ![](https://github.com/Joryun/MarkdownPhotos/blob/master/seckillPhotos/seckill-operation.png) 19 | 20 | 2.秒杀业务的核心:库存的处理 21 | 22 | 3.针对库存业务分析:事务(1>.减内存 2>.记录购买明细) 23 | 24 | ![](https://github.com/Joryun/MarkdownPhotos/blob/master/seckillPhotos/seckill-operation2.png) 25 | 26 | 4.记录秒杀成功信息 27 | 28 | (1)购买成功的对象 29 | 30 | (2)成功的时间/有效期 31 | 32 | (3)付款/发货信息 33 | 34 | #### 二、异常情况分析 35 | 36 | 1. 减库存没有记录购买明细 37 | 38 | 2. 记录明细但没有减库存 39 | 40 | 3. 出现超卖/少卖 41 | 42 | #### 三、难点分析 43 | 44 | 1. MySQL:事务 + 行级锁 45 | 46 | 2. 多用户秒杀 ——> Update库存数量 47 | 48 | #### 四、功能模块 49 | 50 | 1. 秒杀接口暴露(Exposer,封装的DTO) 51 | 52 | 2. 执行秒杀 53 | 54 | 3. 相关查询 55 | 56 | #### 五、开发流程 57 | 58 | 1. DAO设计编码 59 | 60 | 2. Service设计编码 61 | 62 | 3. Web设计编码(restful接口和前端交互等) 63 | 64 | 4. 高并发优化与分析 65 | 66 | -------------- 67 | 68 | (一)DAO设计编码 69 | 70 | Package: 71 | 72 | (1)org.seckill.dao 73 | 74 | (2)org.seckill.entity 75 | 76 | #### 1. 接口设计与SQL编写 77 | 78 | 注:Dao层不应夹杂着Service层的信息,Service层主要是对Dao层进行拼接,即为一系列逻辑!!! 79 | 80 | #### 2. 数据库设计与编码 81 | 82 | 两张表: 83 | 84 | (1) seckill //秒杀库存表 85 | 86 | (2) success_killed //秒杀成功明细表 87 | 88 | 89 | #### 3. DAO实体和接口编码 90 | 91 | (1) SeckillDao 92 | 93 | (2) SuccesskilledDao 94 | 95 | #### 4. Mybatis整合Spring 96 | 97 | (1) 编写mybatis-config.xml(全局配置) 98 | 99 | (2) 编写spring-dao.xml(配置dataSource,sqlSessionFactory等) 100 | 101 | #### 5. 完成Dao层集成测试(使用Junit4) 102 | 103 | 104 | (二)Service设计编码 105 | 106 | 107 | 108 | Package: 109 | 110 | (1)org.seckill.service 存放服务,即为一系列逻辑 111 | 112 | (2)org. seckill.exception 存放service接口所需要的异常,如重复秒杀,秒杀与关闭等 113 | 114 | (3)org. seckill.dto 数据传输层,与entity类似,存放一些表示数据的类型,web与service间的数据传递 115 | 116 | (4)org. seckill.enums 封装枚举类,表述常量字段-状态值(“秒杀成功”,“秒杀结束”等等) 117 | 118 | #### 1. 接口设计与实现 119 | 120 | (1)业务接口:站在“使用者”角度设计接口 121 | 122 | (2)三个方面:方法定义粒度,参数,返回类型(return 类型/异常) 123 | 124 | 125 | #### 2. 使用Spring托管Service依赖(Spring IOC) 126 | 127 | ![](https://github.com/Joryun/MarkdownPhotos/blob/master/seckillPhotos/service-2(1).png) 128 | 129 | (1) 业务对象依赖图 130 | 131 | ![](https://github.com/Joryun/MarkdownPhotos/blob/master/seckillPhotos/service-2(2).png) 132 | 133 | (2) 编写spring-service.xml 134 | 135 | (3) 扩展 136 | 137 | * Spring IOC 138 | 139 | (1) 为对象创建统一托管 140 | 141 | (2) 规范的生命周期管理 142 | 143 | (3) 灵活的依赖注入 144 | 145 | (4) 一致的获取对象 146 | 147 | * Spring IOC注入方式及场景 148 | 149 | (1) XML: 150 | 151 | 一.Bean实现类来自第三方类库,如DataSource等; 152 | 153 | 二.需要命名空间配置,如context,aop,mvc等 154 | 155 | (2) 注解:项目中自身开发使用的类,可直接在代码中使用注解,如:@Service,@Controller 156 | 157 | (3) Java配置类:需要通过代码控制对象创建逻辑的场景,如:自定义修改依赖类库 158 | 159 | 160 | #### 3. 配置并使用Spring声明式事务 161 | 162 | ProxyFactoryBean + XML ——> 早期使用方式(2.0) 163 | 164 | tx:advice + aop ——> 一次配置永久生效 165 | 166 | 注解@Transactional ——> 注解控制(推荐) 167 | 168 | * 什么时候回滚事务? 169 | 170 | 抛出的是运行期异常(RuntimeException) 171 | 避免使用不当的try...catch... 172 | 173 | * 使用注解控制事务方法的优点 174 | 175 | (1) 开发团队达成一致约定,明确标注事务方法的编程风格 176 | 177 | (2) 保证事务方法的执行时间尽可能短,不要穿插其它网络操作,RPC/HTTP请求或者剥离到事务方法外部 178 | 179 | (3) 不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制 180 | 181 | 182 | #### 4. 完成Service集成测试(使用Junit4) 183 | 184 | 185 | 186 | (三)Web设计编码 187 | 188 | 189 | #### 1. 前端交互逻辑 190 | 191 | (1) 秒杀系统前端页面流程 192 | 193 | ![](https://github.com/Joryun/MarkdownPhotos/blob/master/seckillPhotos/web-1(1).png) 194 | 195 | 196 | (2) 详情页流程逻辑 197 | 198 | ![](https://github.com/Joryun/MarkdownPhotos/blob/master/seckillPhotos/web-1(2).png) 199 | 200 | #### 2. 基于RestFul接口设计 201 | 202 | 秒杀API的URL设计 203 | 204 | * GET /seckill/list (秒杀列表) 205 | * GET /seckill/{id}/detail (详情页) 206 | * GET /seckill/time/now (系统时间) 207 | * POST /seckill/{id}/exposer (暴露秒杀) 208 | * POST /seckill/{id}/{md5}/execution (执行秒杀) 209 | 210 | #### 3. 整合SpringMVC框架及相关配置 211 | 212 | (1)编写web.xml 213 | 214 | (2)添加spring-web.xml 215 | 216 | #### 4. 实现秒杀相关的Restful接口 217 | 218 | (1)编写SeckillController 219 | 220 | (2)创建一个DTO类SeckillResult,封装所有ajax请求返回类型(json) 221 | 222 | 223 | #### 5. 基于Bootstrap框架开发页面 224 | 225 | (1) 抽取公共部分,开发common包下的内容 226 | 227 | ![](https://github.com/Joryun/MarkdownPhotos/blob/master/seckillPhotos/web-5(1).png) 228 | 229 | * head.jsp -> bootstrap中包含于head标签之内的内容,一般为css,编码设置及主题文件 230 | * tag.jsp -> 引入库文件,例如jstl库中的fmt等 231 | 232 | (2) 开发商品列表页list.jsp 233 | 234 | (3) 开发商品详情页detail.jsp 235 | 236 | #### 6. Cookie登录交互 237 | 238 | (1)编写js代码(基于模块化的js代码),完成登录验证(重点:验证手机号) 239 | 240 | (2)完成弹出层组件的逻辑设计 241 | 242 | 243 | #### 7. 计时交互 244 | 245 | (1) 判断时间(已开始,未开始,已结束) 246 | 247 | (2) 根据时间判断,对应前端组件显示不同内容 248 | 249 | #### 8. 秒杀交互 250 | 251 | (1)绑定按钮点击事件(one click),预防用户连续点击 252 | 253 | (2)考虑浏览器计时偏差 254 | 255 | (3)显示秒杀的结果 256 | 257 | 258 | 259 | (四)高并发优化与分析 260 | 261 | 262 | ###待续。。。 263 | 264 | 265 | 266 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 5 | 4.0.0 6 | org.seckill 7 | seckill 8 | war 9 | 1.0-SNAPSHOT 10 | seckill Maven Webapp 11 | http://maven.apache.org 12 | 13 | 14 | 15 | junit 16 | junit 17 | 4.11 18 | test 19 | 20 | 21 | 22 | 23 | org.slf4j 24 | slf4j-api 25 | 1.7.12 26 | 27 | 28 | ch.qos.logback 29 | logback-core 30 | 1.1.1 31 | 32 | 33 | 34 | ch.qos.logback 35 | logback-classic 36 | 1.1.1 37 | 38 | 39 | 40 | 41 | mysql 42 | mysql-connector-java 43 | 5.1.35 44 | runtime 45 | 46 | 47 | c3p0 48 | c3p0 49 | 0.9.1.2 50 | 51 | 52 | com.alibaba 53 | druid 54 | 0.2.26 55 | 56 | 57 | 58 | 59 | org.mybatis 60 | mybatis 61 | 3.3.0 62 | 63 | 64 | 65 | org.mybatis 66 | mybatis-spring 67 | 1.2.3 68 | 69 | 70 | 71 | 72 | taglibs 73 | standard 74 | 1.1.2 75 | 76 | 77 | jstl 78 | jstl 79 | 1.2 80 | 81 | 82 | com.fasterxml.jackson.core 83 | jackson-databind 84 | 2.5.4 85 | 86 | 87 | javax.servlet 88 | javax.servlet-api 89 | 3.1.0 90 | 91 | 92 | 93 | 94 | 95 | org.springframework 96 | spring-core 97 | 4.1.7.RELEASE 98 | 99 | 100 | org.springframework 101 | spring-beans 102 | 4.1.7.RELEASE 103 | 104 | 105 | org.springframework 106 | spring-context 107 | 4.1.7.RELEASE 108 | 109 | 110 | 111 | org.springframework 112 | spring-jdbc 113 | 4.1.7.RELEASE 114 | 115 | 116 | org.springframework 117 | spring-tx 118 | 4.1.7.RELEASE 119 | 120 | 121 | 122 | org.springframework 123 | spring-web 124 | 4.1.7.RELEASE 125 | 126 | 127 | org.springframework 128 | spring-webmvc 129 | 4.1.7.RELEASE 130 | 131 | 132 | 133 | org.springframework 134 | spring-test 135 | 4.1.7.RELEASE 136 | 137 | 138 | 139 | 140 | redis.clients 141 | jedis 142 | 2.7.3 143 | 144 | 145 | 146 | 147 | com.dyuproject.protostuff 148 | protostuff-core 149 | 1.0.8 150 | 151 | 152 | 153 | com.dyuproject.protostuff 154 | protostuff-runtime 155 | 1.0.8 156 | 157 | 158 | 159 | 160 | 161 | seckill 162 | 163 | 164 | -------------------------------------------------------------------------------- /seckill.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 | file://$MODULE_DIR$/src/main/resources/spring/spring-service.xml 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 | 74 | -------------------------------------------------------------------------------- /sql/seckill.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat MySQL Data Transfer 3 | 4 | Source Server : joryunMac 5 | Source Server Type : MySQL 6 | Source Server Version : 50718 7 | Source Host : localhost 8 | Source Database : seckill 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50718 12 | File Encoding : utf-8 13 | 14 | Date: 05/23/2017 17:21:40 PM 15 | */ 16 | 17 | SET NAMES utf8; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for `seckill` 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `seckill`; 24 | CREATE TABLE `seckill` ( 25 | `seckill_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品库存ID', 26 | `name` varchar(120) NOT NULL COMMENT '商品名称', 27 | `number` int(11) NOT NULL COMMENT '库存数量', 28 | `start_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '秒杀开始时间', 29 | `end_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '秒杀结束时间', 30 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', 31 | PRIMARY KEY (`seckill_id`), 32 | KEY `idx_start_time` (`start_time`), 33 | KEY `idx_end_time` (`end_time`), 34 | KEY `idx_create_time` (`create_time`) 35 | ) ENGINE=InnoDB AUTO_INCREMENT=1004 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表'; 36 | 37 | -- ---------------------------- 38 | -- Records of `seckill` 39 | -- ---------------------------- 40 | BEGIN; 41 | INSERT INTO `seckill` VALUES ('1000', '1000元秒杀iphone6', '98', '2017-05-07 19:55:10', '2017-05-08 00:00:00', '2017-03-21 13:48:11'), ('1001', '800元秒杀ipad', '197', '2017-05-07 00:15:06', '2017-05-08 00:00:00', '2017-03-21 13:48:11'), ('1002', '6600元秒杀mac book pro', '300', '2017-04-23 10:18:24', '2017-04-25 00:00:00', '2017-03-21 13:48:11'), ('1003', '7000元秒杀iMac', '400', '2017-04-23 10:18:26', '2017-04-25 00:00:00', '2017-03-21 13:48:11'); 42 | COMMIT; 43 | 44 | -- ---------------------------- 45 | -- Table structure for `success_killed` 46 | -- ---------------------------- 47 | DROP TABLE IF EXISTS `success_killed`; 48 | CREATE TABLE `success_killed` ( 49 | `seckill_id` bigint(20) NOT NULL COMMENT '秒杀商品ID', 50 | `user_phone` bigint(20) NOT NULL COMMENT '用户手机号', 51 | `state` tinyint(4) NOT NULL DEFAULT '-1' COMMENT '状态标识:-1:无效 0:成功 1:已付款 2:已发货', 52 | `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间', 53 | PRIMARY KEY (`seckill_id`,`user_phone`), 54 | KEY `idx_create_time` (`create_time`) 55 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表'; 56 | 57 | -- ---------------------------- 58 | -- Records of `success_killed` 59 | -- ---------------------------- 60 | BEGIN; 61 | INSERT INTO `success_killed` VALUES ('1000', '10086100861', '0', '2017-04-23 10:27:36'), ('1000', '10086100862', '0', '2017-04-23 10:31:00'), ('1000', '10086861000', '-1', '2017-05-23 17:21:07'), ('1001', '10086100861', '0', '2017-05-06 23:30:49'), ('1001', '10086100862', '0', '2017-04-23 10:48:32'), ('1001', '10086100863', '0', '2017-05-07 00:15:06'), ('1001', '10086861001', '0', '2017-05-23 17:21:20'); 62 | COMMIT; 63 | 64 | -- ---------------------------- 65 | -- Procedure structure for `execute_seckill` 66 | -- ---------------------------- 67 | DROP PROCEDURE IF EXISTS `execute_seckill`; 68 | delimiter ;; 69 | CREATE DEFINER=`root`@`localhost` PROCEDURE `execute_seckill`(IN v_seckill_id BIGINT, IN v_phone BIGINT, 70 | IN v_kill_time TIMESTAMP, OUT r_result INT) 71 | BEGIN 72 | DECLARE insert_count INT DEFAULT 0; 73 | START TRANSACTION ; 74 | 75 | INSERT IGNORE INTO success_killed 76 | (seckill_id, user_phone, create_time) 77 | VALUES (v_seckill_id, v_phone, v_kill_time); 78 | SELECT row_count INTO insert_count; 79 | 80 | IF (insert_count = 0) THEN 81 | ROLLBACK ; 82 | SET r_result = -1; 83 | ELSEIF (insert_count < 0) THEN 84 | ROLLBACK ; 85 | SET r_result = -2; 86 | ELSE 87 | UPDATE seckill 88 | SET number = number - 1 89 | WHERE seckill_id = v_seckill_id 90 | AND end_time > v_kill_time 91 | AND start_time < v_kill_time 92 | AND number > 0; 93 | SELECT row_count INTO insert_count; 94 | 95 | IF (insert_count = 0) THEN 96 | ROLLBACK ; 97 | SET r_result = 0; 98 | ELSEIF (insert_count < 0) THEN 99 | ROLLBACK ; 100 | SET r_result = -2; 101 | ELSE 102 | COMMIT ; 103 | SET r_result = 1; 104 | END IF ; 105 | 106 | END IF ; 107 | 108 | END 109 | ;; 110 | delimiter ; 111 | 112 | SET FOREIGN_KEY_CHECKS = 1; 113 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/controller/SeckillController.java: -------------------------------------------------------------------------------- 1 | package org.seckill.controller; 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 java.util.Date; 19 | import java.util.List; 20 | 21 | /** 22 | * Created by joryun on 2017/4/29. 23 | */ 24 | 25 | //@Controller 将Controller放入Spring容器之中 26 | @Controller 27 | @RequestMapping("/seckill") //url:/模块/资源/{id}/细分 eg: /seckill/list 28 | public class SeckillController { 29 | 30 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 31 | 32 | @Autowired 33 | private SeckillService seckillService; 34 | 35 | //获取列表页 36 | @RequestMapping(value = "/list", method = RequestMethod.GET) 37 | public String list(Model model) { 38 | 39 | List list = seckillService.getSeckillList(); 40 | model.addAttribute("list", list); 41 | 42 | //list.jsp + model = ModelAndView 43 | return "list"; 44 | } 45 | 46 | //秒杀详情页 47 | @RequestMapping(value = "/{seckillId}/detail", method = RequestMethod.GET) 48 | public String detail(@PathVariable("seckillId") Long seckillId, Model model) { 49 | 50 | if (seckillId == null) { 51 | return "redirect:/seckill/list"; 52 | } 53 | Seckill seckill = seckillService.getById(seckillId); 54 | if (seckill == null) { 55 | return "forward:/seckill/list"; 56 | } 57 | model.addAttribute("seckill", seckill); 58 | 59 | return "detail"; 60 | } 61 | 62 | //ajax json 63 | @RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST, 64 | produces = {"application/json;charset=UTF-8"}) 65 | @ResponseBody 66 | public SeckillResult exposer(@PathVariable("seckillId") Long seckillId) { 67 | 68 | SeckillResult result; 69 | 70 | try { 71 | Exposer exposer = seckillService.exportSeckillUrl(seckillId); 72 | result = new SeckillResult(true, exposer); 73 | 74 | } catch (Exception e) { 75 | logger.error(e.getMessage(), e); 76 | result = new SeckillResult(false, e.getMessage()); 77 | } 78 | 79 | return result; 80 | } 81 | 82 | //执行秒杀 83 | @RequestMapping(value = "/{seckillId}/{md5}/execution", 84 | method = RequestMethod.POST, 85 | produces = {"application/json;charset=UTF-8"}) 86 | @ResponseBody 87 | public SeckillResult execute(@PathVariable("seckillId") Long seckillId, 88 | @PathVariable("md5") String md5, 89 | @CookieValue(value = "killPhone", required = false) Long phone) { 90 | 91 | if (phone == null) { 92 | return new SeckillResult(false, "未注册"); 93 | } 94 | SeckillResult result; 95 | 96 | try { 97 | SeckillExecution execution = seckillService.executeSeckill(seckillId, phone, md5); 98 | return new SeckillResult(true, execution); 99 | 100 | } catch (RepeatKillException e1) { 101 | SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL); 102 | return new SeckillResult(true, execution); 103 | 104 | } catch (SeckillCloseException e2) { 105 | SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END); 106 | return new SeckillResult(true, execution); 107 | 108 | } catch (Exception e) { 109 | SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR); 110 | return new SeckillResult(true, execution); 111 | } 112 | 113 | } 114 | 115 | //获取系统时间 116 | @RequestMapping(value = "/time/now", method = RequestMethod.GET) 117 | @ResponseBody 118 | public SeckillResult time() { 119 | Date now = new Date(); 120 | return new SeckillResult(true, now.getTime()); 121 | } 122 | 123 | 124 | } 125 | -------------------------------------------------------------------------------- /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 | 9 | /** 10 | * Created by joryun on 2017/3/21. 11 | */ 12 | public interface SeckillDao { 13 | /** 14 | * 减库存 15 | * @param seckillId 16 | * @param killTime 17 | * @return 如果影响行数>1,表示更新的记录函数 18 | */ 19 | int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime); 20 | 21 | /** 22 | * 根据id查询秒杀对象 23 | * @param seckillId 24 | * @return 25 | */ 26 | Seckill queryById(long seckillId); 27 | 28 | /** 29 | * 根据偏移量查询秒杀商品列表 30 | * @param offset 31 | * @param limit 32 | * @return 33 | */ 34 | 35 | //注意:@Param表示给参数命名,若是不加,按照java没有保存形参的习性,传值至junit测试会报错 36 | //原因:对应的xml文件中sql语句要接受两个参数,若要正确传参,则需要给参数命名 37 | List queryAll(@Param("offset") int offset, @Param("limit") int limit); 38 | 39 | } 40 | -------------------------------------------------------------------------------- /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 joryun on 2017/3/21. 8 | */ 9 | public interface SuccessKilledDao { 10 | 11 | /** 12 | * 插入购买明细,可过滤重复(数据表success_killed使用联合主键) 13 | * 14 | * @param seckillId 15 | * @param userPhone 16 | * @return 插入行数 17 | */ 18 | int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone); 19 | 20 | /** 21 | * 根据id查询SuccessKilled并携带秒杀商品对象实体 22 | * 23 | * @param seckillId 24 | * @return 25 | */ 26 | SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone); 27 | } 28 | -------------------------------------------------------------------------------- /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.ProtobufIOUtil; 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 joryun on 2017/5/8. 14 | */ 15 | public class RedisDao { 16 | 17 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 18 | 19 | private final JedisPool jedisPool; 20 | 21 | public RedisDao(String ip, int port) { 22 | jedisPool = new JedisPool(ip, port); 23 | } 24 | 25 | private RuntimeSchema schema = RuntimeSchema.createFrom(Seckill.class); 26 | 27 | //从缓存得到seckill 28 | public Seckill getSeckill(long seckillId) { 29 | 30 | //redis操作逻辑 31 | try { 32 | 33 | Jedis jedis = jedisPool.getResource(); 34 | try { 35 | 36 | String key = "seckill:" + seckillId; 37 | 38 | //并没有实现内部序列化操作 39 | //get -> byte[] -> 反序列化 -> Object(Seckill) 40 | //采用自定义序列化 41 | //protostuff : pojo 42 | byte[] bytes = jedis.get(key.getBytes()); 43 | 44 | //从缓存获取到 45 | if (bytes != null) { 46 | //空对象 47 | Seckill seckill = schema.newMessage(); 48 | ProtobufIOUtil.mergeFrom(bytes, seckill, schema); 49 | return seckill; 50 | } 51 | 52 | } finally { 53 | jedis.close(); 54 | } 55 | 56 | } catch (Exception e) { 57 | logger.error(e.getMessage(), e); 58 | } 59 | 60 | return null; 61 | } 62 | 63 | 64 | //若缓存不存在,则存入缓存 65 | public String putSeckill(Seckill seckill) { 66 | 67 | //set Object(Seckill) -> 序列化 -> byte[] 68 | try { 69 | 70 | Jedis jedis = jedisPool.getResource(); 71 | 72 | try { 73 | 74 | String key = "seckill:" + seckill.getSeckillId(); 75 | //LinkedBuffer:缓存器 76 | byte[] bytes = ProtobufIOUtil.toByteArray(seckill, schema, 77 | LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE)); 78 | 79 | //超时缓存 80 | int timeout = 60 * 60; //1小时 81 | String result = jedis.setex(key.getBytes(), timeout, bytes); 82 | 83 | return result; 84 | 85 | } finally { 86 | jedis.close(); 87 | } 88 | 89 | } catch (Exception e) { 90 | logger.error(e.getMessage(), e); 91 | } 92 | 93 | return null; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/dto/Exposer.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dto; 2 | 3 | /** 4 | * Created by joryun on 2017/4/14. 5 | * 6 | * 暴露秒杀地址DTO 7 | */ 8 | public class Exposer { 9 | 10 | //是否开启秒杀 11 | private boolean exposed; 12 | 13 | //加密 14 | private String md5; 15 | 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 now, long start, long end) { 34 | this.exposed = exposed; 35 | this.now = now; 36 | this.start = start; 37 | this.end = end; 38 | } 39 | 40 | 41 | public Exposer(boolean exposed, long seckillId) { 42 | this.exposed = exposed; 43 | this.seckillId = seckillId; 44 | } 45 | 46 | public Exposer(boolean exposed, long seckillId, long now, long start, long end) { 47 | this.exposed = exposed; 48 | this.seckillId = seckillId; 49 | this.now = now; 50 | this.start = start; 51 | this.end = end; 52 | } 53 | 54 | public boolean isExposed() { 55 | return exposed; 56 | } 57 | 58 | public void setExposed(boolean exposed) { 59 | this.exposed = exposed; 60 | } 61 | 62 | public String getMd5() { 63 | return md5; 64 | } 65 | 66 | public void setMd5(String md5) { 67 | this.md5 = md5; 68 | } 69 | 70 | public long getSeckillId() { 71 | return seckillId; 72 | } 73 | 74 | public void setSeckillId(long seckillId) { 75 | this.seckillId = seckillId; 76 | } 77 | 78 | public long getNow() { 79 | return now; 80 | } 81 | 82 | public void setNow(long now) { 83 | this.now = now; 84 | } 85 | 86 | public long getStart() { 87 | return start; 88 | } 89 | 90 | public void setStart(long start) { 91 | this.start = start; 92 | } 93 | 94 | public long getEnd() { 95 | return end; 96 | } 97 | 98 | public void setEnd(long end) { 99 | this.end = end; 100 | } 101 | 102 | @Override 103 | public String toString() { 104 | return "Exposer{" + 105 | "exposed=" + exposed + 106 | ", md5='" + md5 + '\'' + 107 | ", seckillId=" + seckillId + 108 | ", now=" + now + 109 | ", start=" + start + 110 | ", end=" + end + 111 | '}'; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /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 | * Created by joryun on 2017/4/16. 8 | * 9 | * 封装秒杀执行后结果 10 | */ 11 | public class SeckillExecution { 12 | 13 | private long seckillId; 14 | 15 | //秒杀执行结果状态 16 | private int state; 17 | 18 | //状态表示 19 | private String stateInfo; 20 | 21 | //秒杀成功对象 22 | private SuccessKilled successKilled; 23 | 24 | public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) { 25 | this.seckillId = seckillId; 26 | this.state = statEnum.getState(); 27 | this.stateInfo = statEnum.getStateInfo(); 28 | this.successKilled = successKilled; 29 | } 30 | 31 | public SeckillExecution(long seckillId, SeckillStatEnum statEnum, String stateInfo) { 32 | this.seckillId = seckillId; 33 | this.state = statEnum.getState(); 34 | this.stateInfo = statEnum.getStateInfo(); 35 | } 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 long getSeckillId() { 44 | return seckillId; 45 | } 46 | 47 | public void setSeckillId(long seckillId) { 48 | this.seckillId = seckillId; 49 | } 50 | 51 | public int getState() { 52 | return state; 53 | } 54 | 55 | public void setState(int state) { 56 | this.state = state; 57 | } 58 | 59 | public String getStateInfo() { 60 | return stateInfo; 61 | } 62 | 63 | public void setStateInfo(String stateInfo) { 64 | this.stateInfo = stateInfo; 65 | } 66 | 67 | public SuccessKilled getSuccessKilled() { 68 | return successKilled; 69 | } 70 | 71 | public void setSuccessKilled(SuccessKilled successKilled) { 72 | this.successKilled = successKilled; 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | return "SeckillExecution{" + 78 | "seckillId=" + seckillId + 79 | ", state=" + state + 80 | ", stateInfo='" + stateInfo + '\'' + 81 | ", successKilled=" + successKilled + 82 | '}'; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/dto/SeckillResult.java: -------------------------------------------------------------------------------- 1 | package org.seckill.dto; 2 | 3 | /** 4 | * Created by joryun on 2017/4/29. 5 | */ 6 | 7 | //所有ajax请求返回类型,封装json结果 8 | public class SeckillResult { 9 | 10 | private boolean success; 11 | 12 | private T data; 13 | 14 | private String error; 15 | 16 | public SeckillResult(boolean success, T data) { 17 | this.success = success; 18 | this.data = data; 19 | } 20 | 21 | public SeckillResult(boolean success, String error) { 22 | this.success = success; 23 | this.error = error; 24 | } 25 | 26 | public boolean isSuccess() { 27 | return success; 28 | } 29 | 30 | public void setSuccess(boolean success) { 31 | this.success = success; 32 | } 33 | 34 | public T getData() { 35 | return data; 36 | } 37 | 38 | public void setData(T data) { 39 | this.data = data; 40 | } 41 | 42 | public String getError() { 43 | return error; 44 | } 45 | 46 | public void setError(String error) { 47 | this.error = error; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/entity/Seckill.java: -------------------------------------------------------------------------------- 1 | package org.seckill.entity; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * Created by joryun on 2017/3/21. 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 getEndTime() { 54 | return endTime; 55 | } 56 | 57 | public void setEndTime(Date endTime) { 58 | this.endTime = endTime; 59 | } 60 | 61 | public Date getCreateTime() { 62 | return createTime; 63 | } 64 | 65 | public void setCreateTime(Date createTime) { 66 | this.createTime = createTime; 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 joryun on 2017/3/21. 7 | */ 8 | public class SuccessKilled { 9 | private long seckillId; 10 | 11 | private long userPhone; 12 | 13 | private short state; 14 | 15 | private Date createTime; 16 | 17 | //多对一 18 | private Seckill seckill; 19 | 20 | public long getSeckillId() { 21 | return seckillId; 22 | } 23 | 24 | public void setSeckillId(long seckillId) { 25 | this.seckillId = seckillId; 26 | } 27 | 28 | public long getUserPhone() { 29 | return userPhone; 30 | } 31 | 32 | public void setUserPhone(long userPhone) { 33 | this.userPhone = userPhone; 34 | } 35 | 36 | public short getState() { 37 | return state; 38 | } 39 | 40 | public void setState(short state) { 41 | this.state = state; 42 | } 43 | 44 | public Date getCreateTime() { 45 | return createTime; 46 | } 47 | 48 | public void setCreateTime(Date createTime) { 49 | this.createTime = createTime; 50 | } 51 | 52 | public Seckill getSeckill() { 53 | return seckill; 54 | } 55 | 56 | public void setSeckill(Seckill seckill) { 57 | this.seckill = seckill; 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | return "SuccessKilled{" + 63 | "seckillId=" + seckillId + 64 | ", userPhone=" + userPhone + 65 | ", state=" + state + 66 | ", createTime=" + createTime + 67 | '}'; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/enums/SeckillStatEnum.java: -------------------------------------------------------------------------------- 1 | package org.seckill.enums; 2 | 3 | /** 4 | * Created by joryun on 2017/4/17. 5 | * 6 | * 数据字典:使用枚举表述常量数据字段 7 | */ 8 | public enum SeckillStatEnum { 9 | 10 | SUCCESS(1, "秒杀成功"), 11 | END(0, "秒杀结束"), 12 | REPEAT_KILL(-1, "重复秒杀"), 13 | INNER_ERROR(-2, "系统异常"), 14 | DATA_REWRITE(-3, "数据篡改"); 15 | 16 | private int state; 17 | 18 | private String stateInfo; 19 | 20 | SeckillStatEnum(int state, String stateInfo) { 21 | this.state = state; 22 | this.stateInfo = stateInfo; 23 | } 24 | 25 | public int getState() { 26 | return state; 27 | } 28 | 29 | public void setState(int state) { 30 | this.state = state; 31 | } 32 | 33 | public String getStateInfo() { 34 | return stateInfo; 35 | } 36 | 37 | public void setStateInfo(String stateInfo) { 38 | this.stateInfo = stateInfo; 39 | } 40 | 41 | public static SeckillStatEnum stateOf(int index){ 42 | for (SeckillStatEnum state : values()){ 43 | if(state.getState() == index){ 44 | return state; 45 | } 46 | } 47 | return null; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/exception/RepeatKillException.java: -------------------------------------------------------------------------------- 1 | package org.seckill.exception; 2 | 3 | /** 4 | * Created by joryun on 2017/4/16. 5 | * 6 | * 重复秒杀异常(运行期异常) 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 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/exception/SeckillCloseException.java: -------------------------------------------------------------------------------- 1 | package org.seckill.exception; 2 | 3 | /** 4 | * Created by joryun on 2017/4/16. 5 | * 6 | * 秒杀关闭异常 7 | */ 8 | public class SeckillCloseException extends SeckillException { 9 | 10 | public SeckillCloseException(String message) { 11 | super(message); 12 | } 13 | 14 | public SeckillCloseException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/exception/SeckillException.java: -------------------------------------------------------------------------------- 1 | package org.seckill.exception; 2 | 3 | /** 4 | * Created by joryun on 2017/4/16. 5 | * 6 | * 秒杀相关业务异常 7 | */ 8 | public class SeckillException extends RuntimeException { 9 | 10 | public SeckillException(String message) { 11 | super(message); 12 | } 13 | 14 | public SeckillException(String message, Throwable cause) { 15 | super(message, cause); 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 joryun on 2017/4/14. 14 | * 15 | * 业务接口:站在"使用者"角度设计接口 16 | * 17 | * 三个方面: 18 | * 1.方法定义粒度,方法定义的要非常清楚 19 | * 2.参数,要越简练越好 20 | * 3.返回类型(return 类型一定要友好/或者return异常,我们允许的异常) 21 | */ 22 | public interface SeckillService { 23 | 24 | /** 25 | * 查询全部的秒杀记录 26 | * @return 27 | */ 28 | List getSeckillList(); 29 | 30 | /** 31 | *查询单个秒杀记录 32 | * @param seckillId 33 | * @return 34 | */ 35 | Seckill getById(long seckillId); 36 | 37 | 38 | //再往下,是我们最重要的行为的一些接口 39 | 40 | /** 41 | * 在秒杀开启时输出秒杀接口的地址,否则输出系统时间和秒杀时间 42 | * @param seckillId 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) 55 | throws SeckillException,RepeatKillException,SeckillCloseException; 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/seckill/service/impl/SeckillServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.seckill.service.impl; 2 | 3 | import org.seckill.dao.SeckillDao; 4 | import org.seckill.dao.SuccessKilledDao; 5 | import org.seckill.dao.cache.RedisDao; 6 | import org.seckill.dto.Exposer; 7 | import org.seckill.dto.SeckillExecution; 8 | import org.seckill.entity.Seckill; 9 | import org.seckill.entity.SuccessKilled; 10 | import org.seckill.enums.SeckillStatEnum; 11 | import org.seckill.exception.RepeatKillException; 12 | import org.seckill.exception.SeckillCloseException; 13 | import org.seckill.exception.SeckillException; 14 | import org.seckill.service.SeckillService; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.stereotype.Service; 19 | import org.springframework.transaction.annotation.Transactional; 20 | import org.springframework.util.DigestUtils; 21 | 22 | import java.util.Date; 23 | import java.util.List; 24 | 25 | /** 26 | * Created by joryun on 2017/4/16. 27 | */ 28 | 29 | //@Component @Service @Dao @Controller 30 | @Service 31 | public class SeckillServiceImpl implements SeckillService { 32 | 33 | //日志对象 34 | private Logger logger = LoggerFactory.getLogger(this.getClass()); 35 | 36 | //加入一个混淆字符串(秒杀接口)的盐值,为避免用户猜出md5值,值任意给,越复杂越好 37 | private final String slat = "vnosdpowsmb%$^&*$^%&*sakvdSDHBHDNojn!!"; 38 | 39 | //注入Service依赖 40 | @Autowired //@Resource 41 | private SeckillDao seckillDao; 42 | 43 | @Autowired //@Resource 44 | private SuccessKilledDao successKilledDao; 45 | 46 | @Autowired 47 | private RedisDao redisDao; 48 | 49 | 50 | public List getSeckillList() { 51 | 52 | return seckillDao.queryAll(0, 4); 53 | } 54 | 55 | public Seckill getById(long seckillId) { 56 | 57 | return seckillDao.queryById(seckillId); 58 | } 59 | 60 | public Exposer exportSeckillUrl(long seckillId) { 61 | 62 | /** 63 | * 优化点:缓存优化:超时的基础上维护一致性 64 | */ 65 | 66 | //1:访问redis 67 | Seckill seckill = redisDao.getSeckill(seckillId); 68 | 69 | if (seckill == null) { 70 | //2:访问数据库 71 | seckill = seckillDao.queryById(seckillId); 72 | 73 | //查询不到秒杀产品记录 74 | if (seckill == null) { 75 | return new Exposer(false, seckillId); 76 | } else { 77 | //3:放入redis 78 | redisDao.putSeckill(seckill); 79 | } 80 | } 81 | 82 | 83 | Date startTime = seckill.getStartTime(); 84 | Date endTime = seckill.getEndTime(); 85 | Date nowTime = new Date(); 86 | 87 | //还未到秒杀时间,或者已经过秒杀时间 88 | if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) { 89 | return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime()); 90 | } 91 | 92 | //转换特定字符串的过程,不可逆 93 | String md5 = getMD5(seckillId); 94 | return new Exposer(true, md5, seckillId); 95 | } 96 | 97 | private String getMD5(long seckillId) { 98 | String base = seckillId + "/" + slat; 99 | String md5 = DigestUtils.md5DigestAsHex(base.getBytes()); 100 | return md5; 101 | } 102 | 103 | @Transactional 104 | /** 105 | * 使用注解控制事务方法的优点: 106 | * 1:开发团队达成一致约定,明确标注事务方法的编程风格 107 | * 2:保证事务方法的执行时间尽可能短,不要穿插其它网络操作,RPC/HTTP请求或者剥离到事务方法外部 108 | * 3:不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制 109 | */ 110 | public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) 111 | throws SeckillException, RepeatKillException, SeckillCloseException { 112 | 113 | if (md5 == null || !md5.equals(getMD5(seckillId))) { 114 | throw new SeckillException("seckill data rewrite"); 115 | 116 | } 117 | 118 | //执行秒杀逻辑:(1)减库存,(2)记录购买行为 119 | 120 | Date nowTime = new Date(); 121 | 122 | //try catch的原因,可能出现插入数据超时,或者数据库连接中断异常 123 | try { 124 | 125 | //记录购买行为 126 | int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone); 127 | //唯一:seckillId,userPhone 128 | if (insertCount <= 0) { 129 | //重复秒杀 130 | throw new RepeatKillException("seckill repeated"); 131 | } else { 132 | 133 | //减库存 134 | int updateCount = seckillDao.reduceNumber(seckillId, nowTime); 135 | if (updateCount <= 0) { 136 | //没有更新到记录,秒杀结束,rollback 137 | throw new SeckillCloseException("seckill is closed"); 138 | } else { 139 | 140 | //秒杀成功 141 | SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); 142 | 143 | return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled); 144 | } 145 | 146 | } 147 | 148 | 149 | //为了能报出精确的异常,因此先catch其它异常,再catch exception结束 150 | } catch (SeckillCloseException e1) { 151 | throw e1; 152 | } catch (RepeatKillException e2) { 153 | throw e2; 154 | } catch (Exception e) { 155 | logger.error(e.getMessage(), e); 156 | //所有编译异常转化为运行期异常 157 | throw new SeckillException("seckill inner error:" + e.getMessage()); 158 | } 159 | 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/resources/jdbc.properties: -------------------------------------------------------------------------------- 1 | jdbc.driver=com.mysql.jdbc.Driver 2 | 3 | jdbc.url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf8 4 | 5 | jdbc.username=root 6 | 7 | jdbc.password=666666 -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 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 | 16 | -------------------------------------------------------------------------------- /src/main/resources/mapper/SeckillDao.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 9 | 10 | 11 | UPDATE 12 | seckill 13 | SET 14 | number = number - 1 15 | WHERE seckill_id = #{seckillId} 16 | AND start_time #{killTime} 17 | AND end_time >= #{killTime} 18 | AND number > 0; 19 | 20 | 21 | 26 | 27 | 33 | 34 | -------------------------------------------------------------------------------- /src/main/resources/mapper/SuccessKilledDao.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | INSERT ignore INTO success_killed(seckill_id,user_phone,state) 10 | VALUES (#{seckillId}, #{userPhone}, 0) 11 | 12 | 13 | 32 | 33 | -------------------------------------------------------------------------------- /src/main/resources/mybatis-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-dao.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 31 | 32 | 33 | 34 | 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 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-service.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/main/resources/spring/spring-web.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/main/sql/schema.sql: -------------------------------------------------------------------------------- 1 | -- 秒杀执行存储过程 2 | DELIMITER $$ -- console ; 转换为 $$ 3 | -- 定义存储过程 4 | -- 参数:in 输入参数; out 输出参数 5 | -- row_count():返回上一条修改语句sql(delete, insert, update)的影响行数 6 | -- row_count: 0:未修改数据; >0:修改的行数; <0:sql错误或未执行修改sql 7 | 8 | CREATE PROCEDURE execute_seckill 9 | (IN v_seckill_id BIGINT, IN v_phone BIGINT, 10 | IN v_kill_time TIMESTAMP, OUT r_result INT) 11 | BEGIN 12 | DECLARE insert_count INT DEFAULT 0; 13 | START TRANSACTION ; 14 | 15 | INSERT IGNORE INTO success_killed 16 | (seckill_id, user_phone, create_time) 17 | VALUES (v_seckill_id, v_phone, v_kill_time); 18 | SELECT row_count INTO insert_count; 19 | 20 | IF (insert_count = 0) THEN 21 | ROLLBACK ; 22 | SET r_result = -1; 23 | ELSEIF (insert_count < 0) THEN 24 | ROLLBACK ; 25 | SET r_result = -2; 26 | ELSE 27 | UPDATE seckill 28 | SET number = number - 1 29 | WHERE seckill_id = v_seckill_id 30 | AND end_time > v_kill_time 31 | AND start_time < v_kill_time 32 | AND number > 0; 33 | SELECT row_count INTO insert_count; 34 | 35 | IF (insert_count = 0) THEN 36 | ROLLBACK ; 37 | SET r_result = 0; 38 | ELSEIF (insert_count < 0) THEN 39 | ROLLBACK ; 40 | SET r_result = -2; 41 | ELSE 42 | COMMIT ; 43 | SET r_result = 1; 44 | END IF ; 45 | 46 | END IF ; 47 | 48 | END; 49 | $$ 50 | -- 存储过程定义结束 51 | 52 | DELIMITER ; 53 | 54 | SET @r_result = -3; 55 | -- 执行存储过程 56 | CALL execute_seckill(1003, 10086100866, now(), @r_result); 57 | -- 获取结果 58 | SELECT @r_result; -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/common/head.jsp: -------------------------------------------------------------------------------- 1 | <%-- 采用bootstrap默认模板,head标签里包含的内容是公用部分,提取放置head页面里 --%> 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/jsp/common/tag.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 5 | <%@taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> 6 | <%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> -------------------------------------------------------------------------------- /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 |
14 |

${seckill.name}

15 |
16 | 17 |
18 |

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

24 |
25 |
26 |
27 | 28 | <%-- 由于关注点在秒杀,而直接引入登陆插件,一般判断应传回后台进行 --%> 29 | <%--登录弹出层 输入电话--%> 30 | 63 | 64 | 65 | <%-- 提示:编写script语句时,勿用"/>"结尾,不然语句之下的js浏览器不加载 --%> 66 | 67 | <%--jQery文件,务必在bootstrap.min.js之前引入--%> 68 | 69 | 70 | <%--使用CDN 获取公共js http://www.bootcdn.cn/--%> 71 | <%--jQuery Cookie操作插件--%> 72 | 73 | <%--jQuery countDown倒计时插件--%> 74 | 75 | 76 | 77 | 78 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /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 | 34 | 35 | 36 | 37 | 38 | 41 | 44 | 47 | 48 | 49 | 50 | 51 |
名称库存开始时间结束时间创建时间详情页
${sk.name}${sk.number} 39 | 40 | 42 | 43 | 45 | 46 | 详情
52 |
53 |
54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | seckill-dispatcher 11 | org.springframework.web.servlet.DispatcherServlet 12 | 13 | 18 | 19 | contextConfigLocation 20 | classpath:spring/spring-*.xml 21 | 22 | 23 | 24 | 25 | 26 | seckill-dispatcher 27 | 28 | 29 | / 30 | 31 | 32 | 33 | DruidWebStatFilter 34 | com.alibaba.druid.support.http.WebStatFilter 35 | 36 | exclusions 37 | *.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/* 38 | 39 | 40 | 41 | 42 | DruidWebStatFilter 43 | /* 44 | 45 | 46 | 47 | DruidStatView 48 | com.alibaba.druid.support.http.StatViewServlet 49 | 50 | 51 | 52 | DruidStatView 53 | /druid/* 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/main/webapp/index.jsp: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Hello World!

4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/webapp/resource/script/seckill.js: -------------------------------------------------------------------------------- 1 | // 存放主要交互逻辑js代码 2 | // javascript 模块化 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 | detail: { 21 | 22 | //详情页初始化 23 | init: function (params) { 24 | //手机验证和登录,计时交互 25 | //规划交互流程 26 | //在cookie中查找手机号 27 | var killPhone = $.cookie('killPhone'); //若无登录,killPhone为空 28 | 29 | //验证手机号 30 | if (!seckill.validatePhone(killPhone)) { 31 | //绑定手机 32 | //控制输出 33 | var killPhoneModal = $('#killPhoneModal'); //获取电话弹出层组件 34 | killPhoneModal.modal({ 35 | show: true, //显示弹出层 36 | backdrop: 'static', //禁止位置关闭 37 | keyboard: false //关闭键盘事件 38 | }); 39 | 40 | //button事件 41 | $('#killPhoneBtn').click(function () { 42 | var inputPhone = $('#killPhoneKey').val(); 43 | console.log("inputPhone: " + inputPhone); //TODO 44 | 45 | if (seckill.validatePhone(inputPhone)) { 46 | 47 | //电话写入cookie(7天过期),path只在"/seckill"路径之下方有效 48 | $.cookie('killPhone', inputPhone, {expires: 7, path: '/seckill'}); 49 | //验证通过 刷新页面 50 | window.location.reload(); 51 | } else { 52 | // 53 | //todo 错误文案信息抽取到前端字典里 54 | //先hide,再填充内容,最后再show一下 55 | $('#killPhoneMessage').hide().html('').show(300); 56 | } 57 | }); 58 | } 59 | 60 | //已经登录 61 | //计时交互 62 | var startTime = params['startTime']; 63 | var endTime = params['endTime']; 64 | var seckillId = params['seckillId']; 65 | 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 | 80 | //验证手机号 81 | validatePhone: function (phone) { 82 | 83 | //直接判断对象会看对象是否为空,空就是undefine就是false; isNaN 非数字返回true 84 | if (phone && phone.length == 11 && !isNaN(phone)) { 85 | return true; 86 | } else { 87 | return false; 88 | } 89 | }, 90 | 91 | //时间判断事件 92 | countDown: function (seckillId, nowTime, startTime, endTime) { 93 | 94 | //获取前端页面展示倒计时的组件 95 | var seckillBox = $('#seckill-box'); 96 | 97 | if (nowTime > endTime) { 98 | //秒杀结束 99 | seckillBox.html('秒杀结束!'); 100 | } else if (nowTime < startTime) { 101 | 102 | //秒杀未开始,计时事件绑定 103 | var killTime = new Date(startTime + 1000);//todo 防止时间偏移 104 | seckillBox.countdown(killTime, function (event) { 105 | //时间格式 106 | var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒 '); 107 | seckillBox.html(format); 108 | }).on('finish.countdown', function () { 109 | 110 | //时间完成后回调事件 111 | //获取秒杀地址,控制显示逻辑,执行秒杀 112 | console.log('______fininsh.countdown'); 113 | seckill.handlerSeckill(seckillId, seckillBox); 114 | 115 | }); 116 | 117 | } else { 118 | //秒杀开始 119 | seckill.handlerSeckill(seckillId, seckillBox); 120 | } 121 | 122 | }, 123 | 124 | //处理秒杀事件 125 | handlerSeckill: function (seckillId, node) { 126 | //处理秒杀逻辑 127 | //获取秒杀地址,控制显示逻辑,执行秒杀 128 | node.hide().html(''); 129 | $.post(seckill.URL.exposer(seckillId), {}, function (result) { 130 | 131 | //在回调函数种执行交互流程 132 | if (result && result['success']) { 133 | var exposer = result['data']; 134 | 135 | if (exposer['exposed']) { 136 | //开启秒杀 137 | //获取秒杀地址 138 | var md5 = exposer['md5']; 139 | var killUrl = seckill.URL.execution(seckillId, md5); 140 | console.log("killUrl: " + killUrl); 141 | 142 | //绑定一次点击事件 143 | //预防用户连续点击,给服务器造成不必要的压力 144 | $('#killBtn').one('click', function () { 145 | //执行秒杀请求 146 | //1.先禁用按钮 147 | $(this).addClass('disabled');//,<-$(this)===('#killBtn')-> 148 | //2.发送秒杀请求执行秒杀 149 | $.post(killUrl, {}, function (result) { 150 | if (result && result['success']) { 151 | var killResult = result['data']; 152 | var state = killResult['state']; 153 | var stateInfo = killResult['stateInfo']; 154 | //显示秒杀结果 155 | node.html('' + stateInfo + ''); 156 | } 157 | }); 158 | }); 159 | node.show(); 160 | 161 | } else { 162 | //未开启秒杀(浏览器计时偏差) 163 | var now = exposer['now']; 164 | var start = exposer['start']; 165 | var end = exposer['end']; 166 | seckill.countDown(seckillId, now, start, end); 167 | } 168 | 169 | } else { 170 | console.log('result: ' + result); 171 | } 172 | }); 173 | 174 | } 175 | 176 | 177 | } -------------------------------------------------------------------------------- /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 | import java.util.Date; 11 | import java.util.List; 12 | 13 | /** 14 | * Created by joryun on 2017/4/12. 15 | * 16 | * 配置spring和junit整合,junit启动时加载springIOC容器 17 | * spring-test,junit 18 | */ 19 | 20 | @RunWith(SpringJUnit4ClassRunner.class) 21 | //告知junit spring配置文件 22 | @ContextConfiguration({"classpath:spring/spring-dao.xml"}) 23 | public class SeckillDaoTest { 24 | 25 | //注入Dao实现类依赖 26 | @Resource 27 | private SeckillDao seckillDao; 28 | 29 | @Test 30 | public void testQueryById() throws Exception { 31 | long id = 1000; 32 | Seckill seckill = seckillDao.queryById(id); 33 | System.out.println(seckill.getName()); 34 | System.out.println(seckill); 35 | } 36 | 37 | @Test 38 | public void testQueryAll() throws Exception { 39 | 40 | /** 41 | * junit测试不通过原因: 42 | * java没有保存形参的记录:queryAll(int offet, int limit) -> queryAll(arg0, arg1) 43 | */ 44 | 45 | List seckills = seckillDao.queryAll(0, 100); 46 | for (Seckill seckill : seckills){ 47 | System.out.println(seckill); 48 | } 49 | } 50 | 51 | @Test 52 | public void testReduceNumber() throws Exception { 53 | Date killTime = new Date(); 54 | int updateCount = seckillDao.reduceNumber(1000L, killTime); 55 | System.out.println("updateCount="+ updateCount); 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 | /** 12 | * Created by joryun on 2017/4/14. 13 | */ 14 | 15 | @RunWith(SpringJUnit4ClassRunner.class) 16 | @ContextConfiguration({"classpath:spring/spring-dao.xml"}) 17 | public class SuccessKilledDaoTest { 18 | 19 | @Resource 20 | private SuccessKilledDao successKilledDao; 21 | 22 | //单元测试,连续两次插入时,第二次返回insertCount=0,由于SuccessKilledDao对应的xml文件设置了联合主键 23 | @Test 24 | public void testInsertSuccessKilled() throws Exception { 25 | 26 | long id = 1001L; 27 | long phone = 15521075723L; 28 | int insertCount = successKilledDao.insertSuccessKilled(id, phone); 29 | System.out.println("insertCount="+ insertCount); 30 | } 31 | 32 | @Test 33 | public void testQueryByIdWithSeckill() throws Exception { 34 | 35 | long id = 1001L; 36 | long phone = 15521075723L; 37 | SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(id, phone); 38 | 39 | System.out.println(successKilled); 40 | System.out.println(successKilled.getSeckill()); 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /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 | /** 12 | * Created by joryun on 2017/5/8. 13 | */ 14 | 15 | @RunWith(SpringJUnit4ClassRunner.class) 16 | @ContextConfiguration({"classpath:spring/spring-dao.xml"}) 17 | public class RedisDaoTest { 18 | 19 | private long id = 1001; 20 | 21 | @Autowired 22 | private RedisDao redisDao; 23 | 24 | @Autowired 25 | private SeckillDao seckillDao; 26 | 27 | @Test 28 | public void testSeckill() throws Exception { 29 | 30 | //test : get and put 31 | Seckill seckill = redisDao.getSeckill(id); 32 | // System.out.println(seckill); 33 | 34 | if (seckill == null) { 35 | seckill = seckillDao.queryById(id); 36 | 37 | if (seckill != null){ 38 | String result = redisDao.putSeckill(seckill); 39 | System.out.println(result); 40 | 41 | seckill = redisDao.getSeckill(id); 42 | System.out.println(seckill); 43 | } 44 | } 45 | } 46 | 47 | 48 | } -------------------------------------------------------------------------------- /src/test/java/org/seckill/service/SeckillServiceTest.java: -------------------------------------------------------------------------------- 1 | package org.seckill.service; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.seckill.dto.Exposer; 6 | import org.seckill.dto.SeckillExecution; 7 | import org.seckill.entity.Seckill; 8 | import org.seckill.exception.RepeatKillException; 9 | import org.seckill.exception.SeckillCloseException; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 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 | import java.util.List; 17 | 18 | 19 | /** 20 | * Created by joryun on 2017/4/18. 21 | */ 22 | 23 | @RunWith(SpringJUnit4ClassRunner.class) 24 | //告诉junit spring的配置文件 25 | @ContextConfiguration({"classpath:spring/spring-dao.xml", 26 | "classpath:spring/spring-service.xml"}) 27 | 28 | public class SeckillServiceTest { 29 | 30 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 31 | 32 | @Autowired 33 | private SeckillService seckillService; 34 | 35 | @Test 36 | public void getSeckillList() throws Exception { 37 | List list = seckillService.getSeckillList(); 38 | logger.info("list={}", list); 39 | // System.out.println(list); 40 | 41 | } 42 | 43 | @Test 44 | public void getById() throws Exception { 45 | long id = 1000L; 46 | Seckill seckill = seckillService.getById(id); 47 | logger.info("seckill={}", seckill); 48 | } 49 | 50 | /* 51 | 注意:集成测试覆盖的完备性: 52 | (1)商品未开启或者已结束,测试 53 | (2)商品秒杀时间已开启,测试 54 | (3)商品重复秒杀,测试 55 | 56 | 将两个单元测试块合并到一起测试: 57 | 首先展示接口秒杀地址,当开启时执行秒杀,未开启则报出warn。且try catch包裹后允许重复执行!!! 58 | */ 59 | @Test 60 | public void exportSeckillLogic() throws Exception { 61 | long id = 1001L; 62 | Exposer exposer = seckillService.exportSeckillUrl(id); 63 | 64 | if (exposer.isExposed()){ 65 | logger.info("exposer={}", exposer); 66 | 67 | // long id = 1000L; 68 | long phone = 10086100862L; 69 | // String md5 = "784bb0a7963b3bf3cfc2fb3fdede630e"; 70 | String md5 = exposer.getMd5(); 71 | 72 | try { 73 | 74 | SeckillExecution execution = seckillService.executeSeckill(id, phone, md5); 75 | logger.info("result={}", execution); 76 | 77 | } catch (RepeatKillException e){ 78 | logger.error(e.getMessage()); 79 | 80 | } catch (SeckillCloseException e){ 81 | logger.error(e.getMessage()); 82 | } 83 | } else { 84 | //秒杀未开启 85 | logger.warn("exposer={}", exposer); 86 | } 87 | 88 | 89 | } 90 | 91 | 92 | // @Test 93 | // public void executeSeckill() throws Exception { 94 | // 95 | // /* 96 | // 重复执行异常: 97 | // org.seckill.exception.RepeatKillException: seckill repeated 98 | // */ 99 | 100 | // /* 101 | // result=SeckillExecution{ 102 | // seckillId=1000, 103 | // state=1, 104 | // stateInfo='秒杀成功', 105 | // successKilled=SuccessKilled{ 106 | // seckillId=1000, 107 | // userPhone=10086100862, 108 | // state=0, 109 | // createTime=Sun Apr 23 10:31:00 CST 2017 110 | // } 111 | // } 112 | // */ 113 | // } 114 | 115 | } --------------------------------------------------------------------------------