maven+eclipse+SSM框架
来源:互联网 发布:js防水涂料怎么用 编辑:程序博客网 时间:2024/06/06 14:16
完成这个秒杀系统,需要完成四个模块的代码编写,分别是:
1.Java高并发秒杀APi之业务分析与DAO层代码编写。
2.Java高并发秒杀APi之Service层代码编写。
3.Java高并发秒杀APi之Web层代码编写。
其实完成这三个模块就可以完成我们的秒杀系统了,但对于我们的秒杀系统中一件秒杀商品,在秒杀的时候肯定会有成千上万的用户参与进来,通过上述三个模块完成的系统无法解决这么多用户的高并发操作,所以我们还需要第四个模块:
4.Java高并发秒杀APi之高并发优化。
首先我们先看一下效果图
列表页
详情页
1.相关技术介绍
mysql:1.这里我们采用手写代码创建相关表,掌握这种能力对我们以后的二次上线会有很大的帮助;2.sql技巧; 3.事务和行级锁的理解和一些应用;4.为了保证每个用户对某个商品只能秒杀一次,在表中设置联合组件(确保唯一性)。
--数据库初始化脚本 --创建数据库create database seckill;--使用数据库use seckill;--创建秒杀库存表CREATE TABLE seckill ( `seckill_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品库存ID', `name` VARCHAR (120) NOT NULL COMMENT '商品名称', `number` INT NOT NULL COMMENT '库存数量', `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `start_time` TIMESTAMP NOT NULL COMMENT '秒杀开启时间', `end_time` TIMESTAMP NOT NULL COMMENT '秒杀结束时间', PRIMARY KEY (seckill_id), KEY idx_start_time (start_time), KEY idx_end_time (end_time), KEY idx_create_time (create_time)) ENGINE = INNODB AUTO_INCREMENT = 1000 DEFAULT CHARSET = utf8 COMMENT = '秒杀库存表' ;ENGINE=InnoDB:目的是支持事务--show create table seckill /*展示创建表语句及注释*/--初始化数据insert into seckill(name,number,start_time,end_time)values ('1000元秒杀iPhone6',100,'2015-11-01 00:00:00','2015-11-02 00:00:00'), ('500元秒杀iPad2',200,'2015-11-01 00:00:00','2015-11-02 00:00:00'), ('300元秒杀小米4',300,'2015-11-01 00:00:00','2015-11-02 00:00:00'), ('200元秒杀红米note',400,'2015-11-01 00:00:00','2015-11-02 00:00:00');--秒杀成功明细表--用户登录认证相关的信息create table success_killed('seckill_id'bigint NOT NULL COMMENT '秒杀商品id','user_phone'bigint NOT NULL COMMENT '用户手机号','state' tinyint NOT NULL DEFAULT -1 COMMENT '状态表示:-1无效;0成功;1已付款;2 已发货','create_time' timestamp NOT NULL COMMENT '创建时间',PRIMARY KEY(seckill_id,user_phone),**/*联合主键*/**key idx_create_time(create_time))ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';--为什么手写DDL--记录每次上线的DDL修改--上线v1.1ALTER TABLE seckillDEOP INDEX idx_create_time,ADD index idex_c_s(start_time,create_time);--上线v1.2--ddl(Data Definition Language)数据库模式定义语言
MyBatis:1.DAO层的设计与开发;2.MyBatis的合理使用,使用Mapper动态代理的方式进行数据库的访问。3.MyBatis和Spring框架的整合。
Spring:1.Spring IOC帮我们整合Service以及Service所有的依赖。2.声明式事务。
Spring MVC :1.Restful接口设计和使用。Restful现在更多的被应用在一些互联网公司Web层接口的应用上。2.框架运作流程。3.Spring Controller的使用技巧。
2.Java高并发秒杀APi之业务分析与DAO层代码编写
2.1用Maven创建我们的项目seckill
在命令行中输入如下命令:
创建完成后,用eclipse打开
这是之后补全完整的项目
打开WEB-INF下的web.xml,它默认为我们创建servlet版本为2.3,需要修改它的根标签为:
<?xml version="1.0" encoding="UTF-8"?><web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" metadata-complete="true" version="3.0"></web-app>
然后打开pom.xml,在里面添加我们需要的第三方jar包的坐标配置信息,如SSM框架、数据库、日志,如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.seckill</groupId> <artifactId>seckill</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>seckill Maven Webapp</name> <url>http://maven.apache.org</url> <dependencies> <dependency> <!-- 使用junit4 --> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <!-- 补全项目依赖 --> <!-- 1:日志 Java日志:slf4j,log4j,logback,common-logging slf4j 是规范/接口 日志实现:log4j,logback,common-logging 使用:slf4j+logback --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.12</version> </dependency> <dependency> <groupId>ch.gos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.2.3</version> </dependency> <!-- 实现slf4j接口并整合 --> <dependency> <groupId>ch.gos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <!-- 2:数据库相关依赖 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.35</version> <scope>runtime</scope> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <!-- Dao框架:MyBatis依赖 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.3.0</version> </dependency> <!-- mybatis自身实现的spring整合依赖 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.2.3</version> </dependency> <!-- Servlet web相关依赖 --> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.5.4</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> <!-- spring依赖 --> <!-- 1)spring 核心依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>4.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.1.3.RELEASE</version> </dependency> <!-- 2)spring dao依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>4.1.3.RELEASE</version> </dependency> <!-- 3)spring web相关依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.1.3.RELEASE</version> </dependency> <!-- 4)spring test相关依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.1.3.RELEASE</version> </dependency> </dependencies> <build> <finalName>seckill</finalName> </build></project>
到此,我们项目的初始化工作完成。
2.2秒杀系统业务分析
秒杀系统业务流程如下:
由图可以发现,整个系统其实是针对库存做的系统。用户成功秒杀商品,对于我们系统的操作就是:1.减库存。2.记录用户的购买明细。下面看看我们用户对库存的业务分析:
记录用户的秒杀成功信息,我们需要记录:1.谁购买成功了。2.购买成功的时间/有效期。3.付款/发货信息。这些数据组成了用户的秒杀成功信息,也就是用户的购买行为。
为什么我们的系统需要事务?看如下这些故障:1.若是用户成功秒杀商品我们记录了其购买明细却没有减库存。导致商品的超卖。2.减了库存却没有记录用户的购买明细。导致商品的少卖。对于上述两个故障,若是没有事务的支持,损失最大的无疑是我们的用户和商家。在MySQL中,它内置的事务机制,可以准确的帮我们完成减库存和记录用户购买明细的过程。
MySQL实现秒杀的难点分析:当用户A秒杀id为10的商品时,此时MySQL需要进行的操作是:1.开启事务。2.更新商品的库存信息。3.添加用户的购买明细,包括用户秒杀的商品id以及唯一标识用户身份的信息如电话号码等。4.提交事务。若此时有另一个用户B也在秒杀这件id为10的商品,他就需要等待,等待到用户A成功秒杀到这件商品然后MySQL成功的提交了事务他才能拿到这个id为10的商品的锁从而进行秒杀,而同一时间是不可能只有用户B在等待,肯定是有很多很多的用户都在等待拿到这个行级锁。秒杀的难点就在这里,如何高效的处理这些竞争?如何高效的完成事务?在后面第4个模块如何进行高并发的优化为大家讲解。
我们只是实现秒杀的一些功能:1.秒杀接口的暴露。2.执行秒杀的操作。3.相关查询,比如说列表查询,详情页查询。我们实现这三个功能即可。接下来进行具体的编码工作,首先是Dao层的编码。
2.3Dao层设计开发
首先创建数据库,在上面已经给出
然后创建对应表的实体类,在java包下创建org.seckill.entity包,创建一个Seckill.java实体类,代码如下:
package org.seckill.entity;import java.util.Date;public class Seckill { private long seckillId; private String name; private int number; private Date startTime; private Date endTime; private Date createTime; public long getSeckillId() { return seckillId; } public String getName() { return name; } public int getNumber() { return number; } public Date getStartTime() { return startTime; } public Date getEndTime() { return endTime; } public Date getCreateTime() { return createTime; } public void setSeckillId(long seckillId) { this.seckillId = seckillId; } public void setName(String name) { this.name = name; } public void setNumber(int number) { this.number = number; } public void setStartTime(Date startTime) { this.startTime = startTime; } public void setEndTime(Date endTime) { this.endTime = endTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } @Override public String toString() { return "Seckill [seckillId=" + seckillId + ", name=" + name + ", number=" + number + ", startTime=" + startTime + ", endTime=" + endTime + ", createTime=" + createTime + "]"; }}
和一个SuccessKilled.java,代码如下:
package org.seckill.entity;import java.util.Date;public class SuccessKilled { private long seckillId; private long userPhone; private short state; private Date createTime; private Seckill seckill; public Seckill getSeckill() { return seckill; } public void setSeckill(Seckill seckill) { this.seckill = seckill; } public long getSeckillId() { return seckillId; } public long getUserPhone() { return userPhone; } public short getState() { return state; } public Date getCreateTime() { return createTime; } public void setSeckillId(long seckillId) { this.seckillId = seckillId; } public void setUserPhone(long userPhone) { this.userPhone = userPhone; } public void setState(short state) { this.state = state; } public void setCreateTime(Date createTime) { this.createTime = createTime; } @Override public String toString() { return "SuccessKilled [seckillId=" + seckillId + ", userPhone=" + userPhone + ", state=" + state + ", createTime=" + createTime + "]"; }}
然后针对实体创建出对应dao层的接口,在org.seckill.dao包下创建SeckillDao.java:
package org.seckill.dao;import java.util.Date;import java.util.List;import org.apache.ibatis.annotations.Param;import org.junit.runners.Parameterized.Parameter;import org.seckill.entity.Seckill;public interface SeckillDao { /* * 减库存 * @return 如果影响行数>1,表示更新的记录行数,若为0则减库存失败 * * */ int reduceNumber(@Param("seckillId")long seckillId,@Param("killTime")Date killTime);//@Param是为了同时传入两个或两个以上参数时,mapper访问数据库中参数混淆 /* * 根据ID查询秒杀对象 * */ Seckill queryById(long seckillId); /* * 根据偏移量查询秒杀商品列表 * */ List<Seckill> queryAll(@Param("offset")int offset,@Param("limit")int limit);}
和SuccessKillDao.java接口
package org.seckill.dao;import org.apache.ibatis.annotations.Param;import org.seckill.entity.SuccessKilled;public interface SuccessKillDao { /* * 插入购买明细,可过滤重复 * @return 插入的行数,返回0则表示插入失败 * */ int insertSuccessKilled(@Param("seckillId")long seckillId,@Param("userPhone")long userPhone,@Param("state")long state); /* * 根据id查询SuccessKilled并携带秒杀产品对象实体 * */ SuccessKilled queryByIdWithSeckill(@Param("seckillId")long seckilled,@Param("phone")long phone);}
接下来基于MyBatis来实现我们之前设计的Dao层接口。首先需要配置我们的MyBatis,在resources包下创建MyBatis全局配置文件mybatis-config.xml文件,
在浏览器中输入http://mybatis.github.io/mybatis-3/zh/index.html打开MyBatis的官网文档,点击左边的”入门”栏框,找到mybatis全局配置文件,在这里有xml的一个规范,也就是它的一个xml约束,拷贝:
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
到我们的项目mybatis全局配置文件(mybatis-config.xml)中,然后在全局配置文件中加入如下配置信息:
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!-- 配置全局属性 --> <settings> <!-- 使用jdbc的getGeneratedKeys获取数据库自增主键值 --> <setting name="useGeneratedKeys" value="true"/> <!-- 使用别列名替换列名 默认:true select name as title from table name是数据库列名,title是实体类属性名,会帮我们自动识别 --> <setting name="useColumnLabel" value="true"/> <!-- 开启驼峰命名转换:Table(create_time)->Entity(createTime) --> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> </configuration>
配置文件创建好后我们需要关注的是Dao接口该如何实现,mybatis为我们提供了mapper动态代理开发的方式为我们自动实现Dao的接口。在mapper包下创建对应Dao接口的xml映射文件,里面用于编写我们操作数据库的sql语句,SeckillDao.xml和SuccessKilledDao.xml。既然又是一个xml文件,我们肯定需要它的dtd文件,在官方文档中,点击左侧”XML配置”,在它的一些事例中,找到它的xml约束:
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.seckill.dao.SeckillDao" >
加入到两个mapper映射xml文件中,然后对照Dao层方法编写我们的映射文件内容如下:
SeckillDao.xml
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.seckill.dao.SeckillDao" > <!-- 目的:为DAO接口方法提供sql语句配置 --> <!-- 具体的sql --> <update id="reduceNumber"> update seckill set number=number-1 where seckill_id=#{seckillId} and start_time<![CDATA[<=]]>#{killTime} and end_time>=#{killTime} and number>0; </update> <select id="queryById" resultType="Seckill" parameterType="long"> select seckill_id,name,number,start_time,end_time,create_time from seckill where seckill_id=#{seckillId} </select> <select id="queryAll" resultType="Seckill"> select seckill_id,name,number,start_time,end_time,create_time from seckill order by create_time desc limit #{offset},#{limit} </select> </mapper>
SuccessKilledDao.xml:
<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.seckill.dao.SuccessKillDao"> <insert id="insertSuccessKilled"> <!-- sql技巧,主键冲突,不会报错而是返回0 --> insert ignore into success_killed(seckill_id,user_phone,state) values (#{seckillId},#{userPhone},0) </insert> <select id="queryByIdWithSeckill" resultType="SuccessKilled"> <!-- 根据id查询SuccessKilled并携带Seckill实体 --> <!-- 如果告诉MyBatis把结果映射到SuccessKilled同时映射seckill属性 --> <!-- 可以自由控制SQL --> SELECT sk.seckill_id, sk.user_phone, sk.create_time, sk.state, s.seckill_id "seckill.seckill_id", s.name "seckill.name", s.number "seckill.number", s.start_time "seckill.start_time", s.end_time "seckill.end_time", s.create_time "seckill.create_time" FROM success_killed sk INNER JOIN seckill s ON sk.seckill_id=s.seckill_id WHERE sk.seckill_id=#{seckillId} AND sk.user_phone=#{phone} </select> </mapper>
接下来我们开始MyBatis和Spring的整合,整合目标:1.更少的编码:只写接口,不写实现类。2.更少的配置:别名、配置扫描映射xml文件、dao实现。3.足够的灵活性:自由定制SQL语句、自由传结果集自动赋值。
在resources包下创建一个spring包,里面放置spring对Dao、Service、transaction的配置文件。在浏览器中输入http://docs.spring.io/spring/docs/进入到Spring的官网中下载其pdf官方文档,在其官方文档中找到它的xml的定义内容头部:
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
在spring包下创建一个spring配置dao层对象的配置文件spring-dao.xml,加入上述dtd约束,然后添加二者整合的配置,内容如下:
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 配置整合mybatis过程 --> <!-- 1.配置数据库相关参数 properties的属性:${url}--> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 2.数据库连接池 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <!-- 配置连接池属性 --> <property name="driverClass" value="${driver}"/> <property name="jdbcUrl" value="${url}"/> <property name="user" value="${jdbc.username}"/> <property name="password" value="${password}"/> <!-- c3p0连接池的私有属性 --> <property name="maxPoolSize" value="30"/> <property name="minPoolSize" value="10"/> <property name="autoCommitOnClose" value="false"/> <property name="checkoutTimeout" value="1000"/> <property name="acquireRetryAttempts" value="2"/> </bean> <!-- 约定大于配置 --> <!-- 3:配置sqlSessionFactory对象 --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!-- 注入数据库连接池 --> <property name="dataSource" ref="dataSource"/> <!-- 配置Mybatis全局配置文件:mybatis-config.xml --> <property name="configLocation" value="classpath:mybatis-config.xml"/> <!-- 扫描entity包 使用别名 --> <property name="typeAliasesPackage" value="org.seckill.entity"/> <!-- 扫描sql配置文件:mapper需要的xml文件 --> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <!-- 4.配置扫描Dao接口包,动态实现Dao接口,注入到Spring容器中 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 注入sqlSessionFactory --> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <!-- 给出需要扫描的Dao包 --> <property name="basePackage" value="org.seckill.dao"/> </bean> </beans>
需要我们在resources包下创建jdbc.properties用于配置数据库的连接信息,内容如下:
driver=com.mysql.jdbc.Driverurl=jdbc:mysql://127.0.0.1:3306/seckill?useUnicode=true&characterEncoding=utf8jdbc.username=rootpassword=397066
这样我们便完成了Dao层编码的开发,接下来就可以利用junit进行我们Dao层编码的测试了。首先测试SeckillDao.java,
我们生成对SeckillDao.java中所有方法的测试类SeckillDaoTest.java,内容如下:
package org.sekill.dao;import org.junit.Test;import org.junit.runner.RunWith;import org.seckill.dao.SeckillDao;import org.seckill.entity.Seckill;import java.util.Date;import java.util.List;import javax.annotation.*;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;/* * 配置spring和junit4整合,junit启动时加载springIOC容器 * */@RunWith(SpringJUnit4ClassRunner.class)// 告诉junit spring配置文件@ContextConfiguration({ "classpath:spring/spring-dao.xml" })public class SeckillDaotest { // 注入Dao实现类依赖 @Resource private SeckillDao seckillDao; @Test public void testReduceNumber() throws Exception { Date killTime = new Date(); int updateCount = seckillDao.reduceNumber(1000L, killTime); System.out.println("updateCount=" + updateCount); } @Test public void testQueryById() throws Exception { long id = 1000; Seckill seckill = seckillDao.queryById(id); System.out.println(seckill.getName()); } /* * java没有保存形参的记录 List<Seckill> queryAll(int offset,int limit)--> * List<Seckill> queryAll(arg0,arg1) */ @Test public void testQueryAll() throws Exception { List<Seckill> seckills = seckillDao.queryAll(0, 100); for (Seckill seckill : seckills) { System.out.println(seckill); } }}
对SuccessKillDaotest.java测试
package org.sekill.dao;import org.junit.Test;import org.junit.runner.RunWith;import org.seckill.dao.SeckillDao;import org.seckill.dao.SuccessKillDao;import org.seckill.entity.Seckill;import org.seckill.entity.SuccessKilled;import java.util.Date;import java.util.List;import javax.annotation.*;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;/* * 配置spring和junit4整合,junit启动时加载springIOC容器 * */@RunWith(SpringJUnit4ClassRunner.class)//告诉junit spring配置文件@ContextConfiguration({"classpath:spring/spring-dao.xml"})public class SuccessKillDaotest { //注入Dao实现类依赖 @Resource private SuccessKillDao successKillDao; @Test public void testinsertSuccessKilled() throws Exception{ long phonenumber=15858190140L; int insert=successKillDao.insertSuccessKilled(1002,phonenumber,0); System.out.println("insert"+insert); } @Test public void testqueryByIdWithSeckill() throws Exception{ long phonenumber=15858190140L; SuccessKilled successKilled = successKillDao.queryByIdWithSeckill(1001,phonenumber); System.out.println(successKilled); System.out.println(successKilled.getSeckill()); }}
到此,我们成功完成了Dao层开发及测试,接下来我们将进行Service层的开发工作
3.Java高并发秒杀APi之业务分析与Service层代码编写
开始Service层的编码之前,我们首先需要进行Dao层编码之后的思考:在Dao层我们只完成了针对表的相关操作包括写了接口方法和映射文件中的sql语句,并没有编写逻辑的代码,例如对多个Dao层方法的拼接,当我们用户成功秒杀商品时我们需要进行商品的减库存操作(调用SeckillDao接口)和增加用户明细(调用SuccessKilledDao接口),这些逻辑我们都需要在Service层完成。这也是一些初学者容易出现的错误,他们喜欢在Dao层进行逻辑的编写,其实Dao就是数据访问的缩写,它只进行数据的访问操作,接下来我们便进行Service层代码的编写。
1.秒杀Service接口设计
在org.seckill包下创建一个service包用于存放我们的Service接口和其实现类,创建一个exception包用于存放service层出现的异常例如重复秒杀商品异常、秒杀已关闭等异常,一个dto包作为传输层,dto和entity的区别在于:entity用于业务数据的封装,而dto用于完成web和service层的数据传递。
首先创建我们Service接口,里面的方法应该是按”使用者”的角度去设计,SeckillService.java,代码如下:
package org.seckill.service;import java.util.List;import org.seckill.dto.Exposer;import org.seckill.dto.SeckillExecution;import org.seckill.entity.Seckill;import org.seckill.exception.RepeatKillException;import org.seckill.exception.SeckillCloseException;import org.seckill.exception.SeckillException;/* * 业务接口:站在“使用者”角度设计接口 * 三个方面:方法定义粒度,参数,返回类型(return 类型/异常) * * */public interface SeckillService { /* * 查询所有秒杀记录 * */ List<Seckill> getSeckillList(); /* * 查询单个秒杀记录 * */ Seckill getById(long seckillId); /* * 秒杀开启是输出秒杀接口地址 * 否则输出系统时间和秒杀时间 * */ Exposer exportSeckillUrl(long seckillId); /* * * 执行秒杀操作 * */ SeckillExecution executeSeckill(long seckillId,long userPhone,String md5) throws SeckillException,RepeatKillException,SeckillCloseException;}
该接口中前面两个方法返回的都是跟我们业务相关的对象,而后两个方法返回的对象与业务不相关,这两个对象我们用于封装service和web层传递的数据,方法的作用我们已在注释中给出。相应在的dto包中创建Exposer.java,用于封装秒杀的地址信息,各个属性的作用在代码中已给出注释,代码如下:
package org.seckill.dto;/* * 暴露秒杀地址DTO * */public class Exposer { //是否开启秒杀 private boolean exposed; //一种加密措施 private String md5; //id private long seckillId; //系统当前时间 private long now; //开启秒杀时间 private long start; //结束秒杀时间 private long end; public Exposer(boolean exposed, String md5, long seckillId) { super(); this.exposed = exposed; this.md5 = md5; this.seckillId = seckillId; } public Exposer(boolean exposed, long seckillId,long now, long start, long end) { super(); this.exposed = exposed; this.seckillId=seckillId; this.now = now; this.start = start; this.end = end; } public Exposer(boolean exposed, long seckillId) { super(); this.exposed = exposed; this.seckillId = seckillId; } public boolean isExposed() { return exposed; } public String getMd5() { return md5; } public long getSeckillId() { return seckillId; } public long getNow() { return now; } public long getStart() { return start; } public long getEnd() { return end; } public void setExposed(boolean exposed) { this.exposed = exposed; } public void setMd5(String md5) { this.md5 = md5; } public void setSeckillId(long seckillId) { this.seckillId = seckillId; } public void setNow(long now) { this.now = now; } public void setStart(long start) { this.start = start; } public void setEnd(long end) { this.end = end; } @Override public String toString() { return "Exposer [exposed=" + exposed + ", md5=" + md5 + ", seckillId=" + seckillId + ", now=" + now + ", start=" + start + ", end=" + end + "]"; }}
和SeckillExecution.java,用于判断秒杀是否成功,成功就返回秒杀成功的所有信息(包括秒杀的商品id、秒杀成功状态、成功信息、用户明细),失败就抛出一个我们允许的异常(重复秒杀异常、秒杀结束异常),代码如下:
package org.seckill.dto;import org.seckill.entity.SuccessKilled;import org.seckill.enums.SeckillStaEnum;/* * 封装秒杀执行后的的结果 * */public class SeckillExecution { private long seckillId; // 秒杀执行结果状态 private int state; // 状态表示 private String stateInfo; // 秒杀成功对象 private SuccessKilled successKilled; public SeckillExecution(long seckillId, SeckillStaEnum seckillstaEnum, SuccessKilled successKilled) { super(); this.seckillId = seckillId; this.state = seckillstaEnum.getState(); this.stateInfo = seckillstaEnum.getStateInfo(); this.successKilled = successKilled; } public SeckillExecution(long seckillId, SeckillStaEnum seckillstaEnum) { super(); this.seckillId = seckillId; this.state = seckillstaEnum.getState(); this.stateInfo = seckillstaEnum.getStateInfo(); } public long getSeckillId() { return seckillId; } public int getState() { return state; } public String getStateInfo() { return stateInfo; } public SuccessKilled getSuccessKilled() { return successKilled; } public void setSeckillId(long seckillId) { this.seckillId = seckillId; } public void setState(int state) { this.state = state; } public void setStateInfo(String stateInfo) { this.stateInfo = stateInfo; } public void setSuccessKilled(SuccessKilled successKilled) { this.successKilled = successKilled; }}
然后需要创建我们在秒杀业务过程中允许的异常,重复秒杀异常RepeatKillException.java:
package org.seckill.exception;/* * 重复秒杀异常 * */public class RepeatKillException extends SeckillException{ public RepeatKillException(String message){ super(message); } public RepeatKillException(String message,Throwable cause){ super(message,cause); }}
秒杀关闭异常SeckillCloseException.java:
package org.seckill.exception;/* * 秒杀关闭异常 * */public class SeckillCloseException extends SeckillException{ public SeckillCloseException(String message){ super(message); } public SeckillCloseException(String message,Throwable cause){ super(message,cause); }}
和一个异常包含与秒杀业务所有出现的异常SeckillException.java:
package org.seckill.exception;public class SeckillException extends RuntimeException{ public SeckillException(String message){ super(message); } public SeckillException(String message,Throwable cause){ super(message,cause); }}
2.秒杀Service接口的实现
在service包下创建impl包存放它的实现类,SeckillServiceImpl.java,内容如下:
package org.seckill.service;import java.util.Date;import java.util.List;import org.seckill.dao.SeckillDao;import org.seckill.dao.SuccessKillDao;import org.seckill.dto.Exposer;import org.seckill.dto.SeckillExecution;import org.seckill.entity.Seckill;import org.seckill.entity.SuccessKilled;import org.seckill.enums.SeckillStaEnum;import org.seckill.exception.RepeatKillException;import org.seckill.exception.SeckillCloseException;import org.seckill.exception.SeckillException;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Transactional;import org.springframework.util.DigestUtils;/* * @Component @Seervice @Dao @Controller * */@Servicepublic class SeckillServiceImp implements SeckillService { private Logger logger = LoggerFactory.getLogger(this.getClass()); //注入Service依赖 @Autowired @Resource @Inject @Autowired private SeckillDao seckillDao; @Autowired private SuccessKillDao successKillDao; // md5盐值字符串,用于混淆MD5 private final String slat = "hdfinwefn92r8u2%$039ru*23rfn"; public List<Seckill> getSeckillList() { // TODO Auto-generated method stub return seckillDao.queryAll(0, 4); } public Seckill getById(long seckillId) { // TODO Auto-generated method stub return seckillDao.queryById(seckillId); } public Exposer exportSeckillUrl(long seckillId) { Seckill seckill = seckillDao.queryById(seckillId); if (seckill == null) { return new Exposer(false, seckillId); } Date startTime = seckill.getStartTime(); Date endTime = seckill.getEndTime(); // 系统当前时间 Date nowTime = new Date(); if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) { return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime()); } String md5 = getMD5(seckillId); return new Exposer(true, md5, seckillId); } private String getMD5(long seckillId) { String base = seckillId + "/" + slat; String md5 = DigestUtils.md5DigestAsHex(base.getBytes()); return md5; } @Transactional /* * 使用注解控制事务方法的优点: * 1.开发团队达成一致约定,明确标注事务方法的编程风格 * 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部 * 3.不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制 * */ public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException { if (md5 == null || !md5.equals(getMD5(seckillId))) { throw new SeckillException("seckill data rewrite"); } // 执行秒杀逻辑:减库存+记录购买行为 Date nowTime = new Date(); try { // 减库存 int updateCount = seckillDao.reduceNumber(seckillId, nowTime); if (updateCount <= 0) { // 没有更新到记录,秒杀结束 throw new SeckillCloseException("seckill is closed"); } else { // 记录购买行为 int insertCount = successKillDao.insertSuccessKilled(seckillId, userPhone, 0); // 唯一:seckillId,userPhone if (insertCount <= 0) { throw new RepeatKillException("seckill repeated"); } else { // 秒杀成功 SuccessKilled successKilled = successKillDao.queryByIdWithSeckill(seckillId, userPhone); return new SeckillExecution(seckillId, SeckillStaEnum.SUCCESS, successKilled); } } } catch (SeckillCloseException e1) { throw e1; } catch (RepeatKillException e2) { throw e2; } catch (Exception e) { logger.error(e.getMessage(), e); // 所有编译期异常 转化为运行期异常 throw new SeckillException("seckill inner error:" + e.getMessage()); } }}
对上述代码进行分析一下,在return new SeckillExecution(seckillId,1,”秒杀成功”,successKilled);代码中,我们返回的state和stateInfo参数信息应该是输出给前端的,但是我们不想在我们的return代码中硬编码这两个参数,所以我们应该考虑用枚举的方式将这些常量封装起来,在cn.codingxiaxw包下新建一个枚举包enums,创建一个枚举类型SeckillStatEnum.java,内容如下:
package org.seckill.enums;/* * 使用枚举表述常量数据字段 * */public enum SeckillStaEnum { SUCCESS(1, "秒杀成功"), END(0, "秒杀结束"), REPEAT_KILL(-1, "重复秒杀"), INNER_ERROR(-2, "系统异常"), DATA_REWRITE(-3, "数据篡改"); private int state; private String stateInfo; private SeckillStaEnum(int state, String stateInfo) { this.state = state; this.stateInfo = stateInfo; } public int getState() { return state; } public String getStateInfo() { return stateInfo; } public void setState(int state) { this.state = state; } public void setStateInfo(String stateInfo) { this.stateInfo = stateInfo; } public static SeckillStaEnum stateOf(int index) { for (SeckillStaEnum state : values()) { if (state.getState() == index) { return state; } } return null; }}
目前为止我们Service的实现全部完成,接下来要将Service交给Spring的容器托管,进行一些配置。
3.使用Spring托管Service依赖配置
在spring包下创建一个spring-service.xml文件,内容如下:
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 扫描service包下所有使用注解的类型 --> <context:component-scan base-package="org.seckill.service"/> <!-- 配置事务管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 注入数据库连接池 --> <property name="dataSource" ref="dataSource"/> </bean> <!-- 配置基于注解的声明式事务 默认使用注解来管理事务行为 --> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>
4.使用集成测试Service逻辑
package org.sekill.service;import java.util.List;import org.junit.Test;import org.junit.runner.RunWith;import org.seckill.dto.Exposer;import org.seckill.dto.SeckillExecution;import org.seckill.entity.Seckill;import org.seckill.exception.RepeatKillException;import org.seckill.exception.SeckillCloseException;import org.seckill.service.SeckillService;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;@RunWith(SpringJUnit4ClassRunner.class)//告诉junit spring配置文件@ContextConfiguration({"classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml"})public class SeckillServiceTest { private final Logger logger=LoggerFactory.getLogger(this.getClass()); @Autowired private SeckillService seckillService; @Test public void testGetSeckillList() throws Exception{ List<Seckill> list=seckillService.getSeckillList(); logger.info("list={}",list); } @Test public void testGetById() throws Exception{ long id=1000; Seckill seckill=seckillService.getById(id); logger.info("seckill={}",seckill); } @Test public void testExportSeckillUrl() throws Exception{ long id=1000; Exposer exposer=seckillService.exportSeckillUrl(id); logger.info("exposer={}",exposer); /* * exposer=Exposer [exposed=true, * md5=1a677398002f880414087d54be8c4ad5, * seckillId=1000, now=0, start=0, end=0] */ } @Test public void testSeckillLogic() throws Exception{ long id=1000; Exposer exposer=seckillService.exportSeckillUrl(id); if(exposer.isExposed()){ logger.info("exposer={}",exposer); long phone=15858190145L; String md5=exposer.getMd5(); try { SeckillExecution execution=seckillService.executeSeckill(id, phone, md5); logger.info("result={}",execution); } catch(RepeatKillException e){ logger.error(e.getMessage()); }catch (SeckillCloseException e) { logger.error(e.getMessage()); } }else{ //秒杀未开启 logger.warn("exposer={}",exposer); } }}
运行该测试类,控制台成功输出信息,库存会减少,明细表也会增加内容。重复执行,控制台不会报错,只是会抛出一个允许的重复秒杀异常。
目前为止,Dao层和Service层的集成测试我们都已经完成,接下来进行Web层的开发编码工作
3.Java高并发秒杀系统API(三)之Web层开发
1.前端交互流程设计
对于一个系统,需要产品经理、前端工程师和后端工程师的参数,产品经理将用户的需求做成一个开发文档交给前端工程师和后端工程师,前端工程师为系统完成页面的开发,后端工程师为系统完成业务逻辑的开发。对于我们这个秒杀系统,它的前端交互流程设计如下图:
这个流程图就告诉了我们详情页的流程逻辑,前端工程师根据这个流程图设计页面,而我们后端工程师根据这个流程图开发我们对应的代码。前端交互流程是系统开发中很重要的一部分,接下来进行Restful接口设计的学习。
2.Restful接口设计学习
什么是Restful?它就是一种优雅的URI表述方式,用来设计我们资源的访问URL。通过这个URL的设计,我们就可以很自然的感知到这个URL代表的是哪种业务场景或者什么样的数据或资源。基于Restful设计的URL,对于我们接口的使用者、前端、web系统或者搜索引擎甚至是我们的用户,都是非常友好的。关于Restful的了解大家去网上一搜一大把,我这里就不再做介绍了。下面看看我们这个秒杀系统的URL设计:
接下来基于上述资源接口来开始我们对Spring MVC框架的使用。
3.整合配置Spring MVC框架
首先在WEB-INF的web.xml中进行我们前端控制器DispatcherServlet的配置,如下:
<?xml version="1.0" encoding="UTF-8"?><web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" metadata-complete="true" version="3.0"> <servlet> <servlet-name>seckill-dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-*.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>seckill-dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping></web-app>
然后在spring容器中进行web层相关bean(即Controller)的配置,在spring包下创建一个spring-web.xml,内容如下:
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd "> <!-- 配置SpringMVC --> <!-- 1:开启SpringMVC注解模式 --> <!-- 简化配置: (1)自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter (2)提供一系列:数据绑定,数字和日期的format @NumberFormat,@DateTimeFormat xml,json默认读写支持 --> <mvc:annotation-driven/> <!-- 2:静态资源默认servlet配置 (1):加入对静态资源的处理:js,gif,png (2):准许使用“/”做整体映射--> <mvc:default-servlet-handler/> <!-- 3:配置jsp 显示ViewResolver --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"></property> <property name="prefix" value="/WEB-INF/jsp/"></property> <property name="suffix" value=".jsp"></property> </bean> <!-- 4:扫描web相关的bean --> <context:component-scan base-package="org.seckill.web"></context:component-scan> </beans>
这样我们便完成了Spring MVC的相关配置(即将Spring MVC框架整合到了我们的项目中),接下来就要基于Restful接口进行我们项目的Controller开发工作了。
4.Controller开发
Controller中的每一个方法都对应我们系统中的一个资源URL,其设计应该遵循Restful接口的设计风格。在org.seckill包下创建一个web包用于放web层Controller开发的代码,在该包下创建一个SeckillController.java,内容如下:
package org.seckill.web;import java.util.Date;import java.util.List;import org.seckill.dto.Exposer;import org.seckill.dto.SeckillExecution;import org.seckill.dto.SeckillResult;import org.seckill.entity.Seckill;import org.seckill.enums.SeckillStaEnum;import org.seckill.exception.RepeatKillException;import org.seckill.exception.SeckillCloseException;import org.seckill.exception.SeckillException;import org.seckill.service.SeckillService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.CookieValue;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.ResponseBody;@Controller //@Service @Component@RequestMapping("/seckill") public class SeckillController { @Autowired private SeckillService seckillService; @RequestMapping(value="/list",method=RequestMethod.GET) public String list(Model model){ //获取列表页 List<Seckill> list=seckillService.getSeckillList(); model.addAttribute("list",list); //list.jsp +model=ModelAndView return "list";//WEB-INF/jsp/"list".jsp } @RequestMapping(value="/{seckillId}/detail",method=RequestMethod.GET) public String detail(@PathVariable("seckillId") Long seckillId,Model model){ if (seckillId == null) { return "redirect:/seckill/list"; } Seckill seckill = seckillService.getById(seckillId); if (seckill == null) { return "forward:/seckill/list"; } model.addAttribute("seckill", seckill); return "detail"; } @RequestMapping(value="/{seckillId}/exposer", method=RequestMethod.POST, produces={"application/json;charset=UTF-8"}) @ResponseBody public SeckillResult<Exposer> exposer(Long seckillId){ SeckillResult<Exposer> result; try { Exposer exposer=seckillService.exportSeckillUrl(seckillId); result=new SeckillResult<Exposer>(true, exposer); } catch (Exception e) { result=new SeckillResult<Exposer>(false, e.getMessage()); } return result; } @RequestMapping(value="/{seckillId}/{md5}/execution", method=RequestMethod.POST, produces={"application/json;charset=UTF-8"}) @ResponseBody public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId, @PathVariable("md5") String md5, @CookieValue(value="killPhone",required=false)Long phone){ if(phone==null){ return new SeckillResult<SeckillExecution>(false, "未註冊"); } try { SeckillExecution execution=seckillService.executeSeckill(seckillId, phone, md5); return new SeckillResult<SeckillExecution>(true, execution); }catch(RepeatKillException e){ SeckillExecution execution=new SeckillExecution(seckillId, SeckillStaEnum.REPEAT_KILL); return new SeckillResult<SeckillExecution>(false, execution); } catch (SeckillCloseException e) { SeckillExecution execution=new SeckillExecution(seckillId, SeckillStaEnum.END); return new SeckillResult<SeckillExecution>(false, execution); }catch(Exception e){ SeckillExecution execution=new SeckillExecution(seckillId, SeckillStaEnum.INNER_ERROR); return new SeckillResult<SeckillExecution>(false, execution); } } @RequestMapping(value="/time/now",method=RequestMethod.GET) public SeckillResult<Long> time(){ Date now=new Date(); return new SeckillResult<Long>(true, now.getTime()); }}
Controller开发中的方法完全是对照Service接口方法进行开发的,第一个方法用于访问我们商品的列表页,第二个方法访问商品的详情页,第三个方法用于返回一个json数据,数据中封装了我们商品的秒杀地址,第四个方法用于封装用户是否秒杀成功的信息,第五个方法用于返回系统当前时间。代码中涉及到一个将返回秒杀商品地址封装为json数据的一个Vo类,即SeckillResult.java,在dto包中创建它,内容如下:
package org.seckill.dto;//所有ajax請求放回類型,封裝json結果public class SeckillResult<T> { private boolean success; private T data; private String error; public SeckillResult(boolean success, T data) { super(); this.success = success; this.data = data; } public SeckillResult(boolean success, String error) { super(); this.success = success; this.error = error; } public boolean isSuccess() { return success; } public T getData() { return data; } public String getError() { return error; } public void setSuccess(boolean success) { this.success = success; } public void setData(T data) { this.data = data; } public void setError(String error) { this.error = error; }}
到此,Controller的开发任务完成,接下来进行我们的页面开发。
5.页面开发
页面由前端工程师完成
然后运行Tomcat服务器,在浏览器中输入http://localhost:8080/seckill/list,即可访问我们的秒杀列表页面:
点击相应商品后面的详情页链接即可查看该商品是否开启秒杀、秒杀该商品等活动。到此,web层的开发也结束,我们的系统开发便告一段落。但往往这样一个秒杀系统,往往是会有成千上万的人进行参与,我们目前的系统是抗不起多少高并发操作的,所以后面我们会对本系统进行高并发的优化。
- maven+eclipse+SSM框架
- eclipse+maven+ssm框架
- eclipse + maven搭建SSM框架
- eclipse + maven搭建SSM框架
- eclipse + maven搭建SSM框架
- eclipse maven ssm框架搭建
- 【SSM】Eclipse使用Maven创建Web项目+整合SSM框架
- 【SSM】Eclipse使用Maven创建Web项目+整合SSM框架
- 【SSM】Eclipse使用Maven创建Web项目+整合SSM框架
- 【SSM】Eclipse使用Maven创建Web项目+整合SSM框架
- 【SSM】Eclipse使用Maven创建Web项目+整合SSM框架
- JAVA Eclipse使用Maven构建web项目详解(SSM框架)
- Eclipse使用Maven创建Web项目+整合SSM框架
- ssm框架学习---基于eclipse中maven的ssm框架的搭建几个问题汇总
- Maven搭建SSM框架
- maven+SSM框架整合
- Maven集成SSM框架
- Maven搭建SSM框架
- poj2007 Scrambled Polygon【极角排序】
- Android性能优化之UncaughtExceptionHandler定制自己的错误日志系统
- 面试感悟----一名3年工作经验的程序员应该具备的技能
- 数组中使用strcpy、memcpy复制字符串,末尾添加‘\0’
- Python异步通信模块asynchat
- maven+eclipse+SSM框架
- 集合相等问题
- SQL Server中建立外键的方法
- DotNet:Socket Server 异步套接字服务端实现
- 自动化测试之python--selenium的环境准备
- Drools入门学习
- 内核态 Socket TCP编程
- TCP(1)---三次握手及四次挥手
- C++实验四项目六