Java高并发 [TOC]
业务分析与DAO层 一、创建项目和依赖 1.maven命令创建web骨架项目
官网资源地址
1 2 3 logback配置:http://logback.qos.ch/manual/configuration.html spring配置:http://docs.spring.io/spring/docs/ mybatis配置:http://mybatis.github.io/mybatis-3/zh/index.html
1 mvn archetype:create -DgroupId=org.seckill -DartifactId=seckill -DarchetypeArtifactId=maven-archetype-webapp
项目依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 0. 单元测试:junit41. java日志:slf4j,log4j,logback,common-logging slf4j是规范/接口 其余是日志实现 dependency:slf4j-api,logback-core,logback-classic 2. 数据库 数据库驱动 mysql-connector-java 数据库连接池 c3p0 3. DAO框架:Mybatis依赖 dependency:mybatis,mybatis-spring 4. Servlet Web相关依赖 dependency:standard,jstl,jackson-databind,javax.servlet-api 5. spring依赖 spring 核心依赖:spring-core,spring-beans,spring-context spring DAO依赖:spirng-jdbc,spring-tx spring web依赖:spring-web,spring-webmvc spring test依赖:spring-test
二、数据库设计与编码 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 -- 创建数据库 CREATE DATABASE seckill; --使用数据库 USE seckill; --创建秒杀库存表 CREATE TABLE seckill ( seckill_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品库存表' , 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 = '秒杀库存表' ; --初始化数据 INSERT INTO seckill (name, number, start_time, end_time) VALUES(); --秒杀成功明细表 create table success_killed ( seckill_id BIGINT NOT NULL COMMENT '秒杀商品id' , user_phone BIGINT NOT NULL COMMENT '用户手机号' , state TINYINT NOT NULL 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 = '秒杀成功明细' ; --连接数据库控制台 mysql -uroot -p
三、DAO层实体与编码 创建对应表的实体类,在java包下创建cn.codingxiaxw.entity包,创建一个Seckill.java实体类,代码如下:
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 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 void setSeckillId (long seckillId) { this .seckillId = seckillId; } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getNumber () { return number; } public void setNumber (int number) { this .number = number; } public Date getStartTime () { return startTime; } public void setStartTime (Date startTime) { this .startTime = startTime; } public Date getEndTime () { return endTime; } public void setEndTime (Date endTime) { this .endTime = endTime; } public Date getCreateTime () { return createTime; } 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,代码如下:
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public class SuccessKilled { private long seckillId; private long userPhone; private short state; private Date createTime; private Seckill seckill; public long getSeckillId () { return seckillId; } public void setSeckillId (long seckillId) { this .seckillId = seckillId; } public long getUserPhone () { return userPhone; } public void setUserPhone (long userPhone) { this .userPhone = userPhone; } public short getState () { return state; } public void setState (short state) { this .state = state; } public Date getCreateTime () { return createTime; } public void setCreateTime (Date createTime) { this .createTime = createTime; } public Seckill getSeckill () { return seckill; } public void setSeckill (Seckill seckill) { this .seckill = seckill; } @Override public String toString () { return "SuccessKilled{" + "seckillId=" + seckillId + ", userPhone=" + userPhone + ", state=" + state + ", createTime=" + createTime + '}' ; }
然后针对实体创建出对应dao层的接口,在cn.codingxiaxw.dao包下创建Seckill.java:
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 public interface SeckillDao { int reduceNumber (long seckillId, Date killTime) ; Seckill queryById (long seckillId) ; List<Seckill> queryAll (int off,int limit) ; }
和SuccessKilled.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public interface SuccessKilledDao { int insertSuccessKilled (long seckillId,long userPhone) ; SuccessKilled queryByIdWithSeckill (long seckillId,long userPhone) ; }
四、基于MyBatis实现DAO理论 Mybatis 特点
1、参数自由提供
2、mybatis和hibernate最大的区别就是mybatis的sql完全由自己写,所以这就提供了一个非常健壮的灵活性,可以充分的发挥你的sql的技巧。
3、mybatis的sql写在哪?
1.第一个是写在xml的配置文件里
2.第二个是可以通过注解的形式写sql,java5.0之后提供的新特性。
4、一些简单的sql可以通过注解的形式去实现,但是遇到一些复杂的sql的时候注解来实现的话就会显的很繁琐。xml配置文件为我们提供很多标签,来完成复杂逻辑sql的拼接,可以很方便的去帮我们完成封装。
5、如何实现DAO接口?
1.第一种,mybatis提供了mapper的机制,通过这种机制自动的去实现DAO接口。也就是说DAO接口只需要实现接口,不需要去实现类。
2.第二种那mybatis通过API编程的方式实现DAO接口。mybatis同样也提供了很多的api,这点和其他的ORmapping,JDBC很像,我可以直接通过开启一个connection,创建一个statement,然后那拿到一个resultSet,这是jdbc的API。同样的mybatis也有同样的API去帮我们实现,但是在实际的开发中,我们一般都是通过mapper自动实现DAO接口,这样的话我们就可以只关注我们的sql如何编写,如何去设计我们的DAO接口,帮我们节省了很多需要维护的程序。
接下来基于MyBatis来实现我们之前设计的Dao层接口 。首先需要配置我们的MyBatis,在resources包下创建MyBatis全局配置文件mybatis-config.xml文件,并新建包mapper,在浏览器中输入http://mybatis.github.io/mybatis-3/zh/index.html打开MyBatis的官网文档,点击左边的”入门”栏框,找到mybatis全局配置文件,在这里有xml的一个规范,也就是它的一个xml约束,拷贝 :
1 2 3 4 5 <?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全局配置文件中,然后在全局配置文件中加入如下配置信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?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的getGeneratekeys获取自增主键值--> <setting name="useGeneratedKeys" value="true" /> <!--使用列别名替换列名 默认值为true select name as title (实体中的属性名是title) form table; 开启后mybatis会自动帮我们把表中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约束:
1 2 3 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
加入到两个mapper映射xml文件中,然后对照Dao层方法编写我们的映射文件内容如下:
SeckillDao.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 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="cn.codingxiaxw.dao.SeckillDao" > <!--目的:为dao接口方法提供sql语句配置 即针对dao接口中的方法编写我们的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 * FROM seckill WHERE seckill_id=#{seckillId} </select> <select id="queryAll" resultType="Seckill" > SELECT * FROM seckill ORDER BY create_time DESC limit #{offset},#{limit} </select> </mapper>
SuccessKilledDao.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 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="cn.codingxiaxw.dao.SuccessKilledDao" > <insert id="insertSuccessKilled" > <!--当出现主键冲突时(即重复秒杀时),会报错;不想让程序报错,加入ignore--> INSERT ignore INTO success_killed (seckill_id,user_phone,state) VALUES (#{seckillId},#{userPhone},0 ) </insert> <select id="queryByIdWithSeckill" resultType="SuccessKilled" > <!--根据seckillId查询SuccessKilled对象,并携带Seckill对象--> <!--如何告诉mybatis把结果映射到SuccessKill属性同时映射到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" , 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=#{userPhone} </select> </mapper>
五、MyBatis和Spring的整合 mybatis与Spring的整合目标:
1、更少的编码
1). 只需要接口,不需要实现(Mybatis 自动完成)
2、更少的配置
1). 别名(Mybatis可以扫描对应包,因此使用一些类的时候不需要使用包名+类名)
2). 配置扫描
3). dao的实现
3、足够的灵活性
1). 自己定制SQL语句
2). 自由传参
接下来我们开始MyBatis和Spring的整合,整合目标:1.更少的编码:只写接口,不写实现类。2.更少的配置:别名、配置扫描映射xml文件、dao实现。3.足够的灵活性:自由定制SQL语句、自由传结果集自动赋值。
在resources包下创建一个spring包,里面放置spring对Dao、Service、transaction的配置文件。在浏览器中输入http://docs.spring.io/spring/docs/进入到Spring的官网中下载其pdf官方文档,在其官方文档中找到它的xml的定义内容头部 :
1 2 3 4 5 6 <?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" 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约束,然后添加二者整合的配置,内容如下:
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 46 47 48 49 50 51 52 53 54 55 56 <?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" 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. 配置数据库相关参数--> <context:property-placeholder location="classpath:jdbc.properties" /> <!--2. 数据库连接池--> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" > <!--配置连接池属性--> <property name="driverClass" value="${driver}" /> <!-- 基本属性 url、user、password --> <property name="jdbcUrl" value="${url}" /> <property name="user" value="${username}" /> <property name="password" value="${password}" /> <!--c3p0私有属性--> <property name="maxPoolSize" value="30" /> <property name="minPoolSize" value="10" /> <!--关闭连接后不自动commit--> <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" > <!--往下才是mybatis和spring真正整合的配置--> <!--注入数据库连接池--> <property name="dataSource" ref="dataSource" /> <!--配置mybatis全局配置文件:mybatis-config.xml--> <property name="configLocation" value="classpath:mybatis-config.xml" /> <!--扫描entity包,使用别名,多个用;隔开--> <property name="typeAliasesPackage" value="cn.codingxiaxw.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="cn.codingxiaxw.dao" /> </bean> </beans>
需要我们在resources包下创建jdbc.properties用于配置数据库的连接信息,内容如下:
1 2 3 4 driver=com.mysql.jdbc.Driver url=jdbc:mysql: username=root password=xiaxunwu1996.
六、DAO单元测试 这样我们便完成了Dao层编码的开发,接下来就可以利用junit进行我们Dao层编码的测试了。首先测试SeckillDao.java,利用IDEA快捷键shift+command+T对SeckillDao.java进行测试,然后IDEA会自动在test包的java包下为我们生成对SeckillDao.java中所有方法的测试类SeckillDaoTest.java,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class SeckillDaoTest { @Test public void reduceNumber () throws Exception { } @Test public void queryById () throws Exception { } @Test public void queryAll () throws Exception { } }
然后便可以在这个测试类中对SeckillDao接口的所有方法进行测试了,先测试queryById()方法,在该方法中添加内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"classpath:spring/spring-dao.xml"}) public class SeckillDaoTest { @Resource private SeckillDao seckillDao; @Test public void queryById () throws Exception { long seckillId=1000 ; Seckill seckill=seckillDao.queryById(seckillId); System.out.println(seckill.getName()); System.out.println(seckill); } }
右键选择”debug queryById()”,测试台成功输入该id为1000的商品信息,证明Dao的该方法正确,然后测试queryAll()方法,在该方法中添加如下内容:
1 2 3 4 5 6 7 8 9 @Test public void queryAll () throws Exception { List<Seckill> seckills=seckillDao.queryAll(0 ,100 ); for (Seckill seckill : seckills) { System.out.println(seckill); } }
然后运行该方法,程序报错,报错信息如下:
1 Caused by: org.apache.ibatis.binding.BindingException: Parameter 'offset' not found. Available parameters are [1, 0, param1, param2
意思就是无法完成offset参数的绑定,这也是我们java编程语言的一个问题,也就是java没有保存行参的记录,java在运行的时候会把List<Seckill> queryAll(int offset,int limit);中的参数变成这样:queryAll(int arg0,int arg1),这样我们就没有办法去传递多个参数。需要在SeckillDao接口中修改方法:
1 List<Seckill> queryAll(@Param("offset") int offset,@Param("limit") int limit);
这样才能使我们的MyBatis识别offset和limit两个参数,将Dao层方法中的这两个参数与xml映射文件中sql语句的传入参数完成映射。然后重新测试,发现测试通过。然后测试reduceNumber()方法,在该方法中加入如下内容:
1 2 3 4 5 6 7 8 9 @Test public void reduceNumber () throws Exception { long seckillId=1000 ; Date date=new Date (); int updateCount=seckillDao.reduceNumber(seckillId,date); System.out.println(updateCount); }
运行该方法,报错:
1 Caused by: org.apache.ibatis.binding.BindingException: Parameter 'seckillId' not found. Available parameters are [1, 0, param1, param2]
发现依然是我们之前那个错误,更改SeckillDao接口的reduceNumber()方法:
1 int reduceNumber (@Param("seckillId") long seckillId, @Param("killTime") Date killTime) ;
然后重新运行,测试通过,可是我们查询数据库发现该库存表的商品数量没有减少,是因为我们当前时间没有达到秒杀商品要求的时间,所以不会成功秒杀。接下来进行SuccessKilledDao接口相关方法的测试,依旧使用IDEA快捷键shift+command+T快速生成其方法的相应测试类:
1 2 3 4 5 6 7 8 9 10 11 public class SuccessKilledDaoTest { @Test public void insertSuccessKilled() throws Exception { } @Test public void queryByIdWithSeckill() throws Exception { } }
依然要在SuccessKilledDao的方法中用@Param注解完成参数的绑定,首先完成insertSuccessKilled()的测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"classpath:spring/spring-dao.xml"}) public class SuccessKilledDaoTest { @Resource private SuccessKilledDao successKilledDao; @Test public void insertSuccessKilled () throws Exception { long seckillId=1000 ; long userPhone=13476191877L ; int insertCount=successKilledDao.insertSuccessKilled(seckillId,userPhone); System.out.println("insertCount=" +insertCount); } }
运行成功,测试台打印出insertCount=1的信息,即我们修改了表中的一条记录,这时查看秒杀成功明细表,发现该用户的信息已经被插入。然后再次运行该测试方法,程序没有报主键异常的错,是因为我们在编写我们的明细表的时候添加了一个联合主键的字段,它保证我们明细表中的seckillId和userPhone不能重复插入,另外在SuccessDao.xml中写的插入语句的ignore关键字也保证了这点。控制台输出0,表示没有对明细表做插入操作。然后进行queryByIdWithSeckill()方法的测试,需要在Dao层的方法中添加@Param注解:
1 SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId,@Param("userPhone") long userPhone);
然后进行该方法的测试:
1 2 3 4 5 6 7 8 9 10 @Test public void queryByIdWithSeckill () throws Exception { long seckillId=1000L ; long userPhone=13476191877L ; SuccessKilled successKilled=successKilledDao.queryByIdWithSeckill(seckillId,userPhone); System.out.println(successKilled); System.out.println(successKilled.getSeckill()); }
运行,成功查询出我们明细表中id为1000且手机号码为13476191877的用户信息,并将表中对应的信息映射到SuccessKilled对象和Seckill对象的属性中。
Service层 Dao就是数据访问的缩写,它只进行数据的访问操作,接下来我们便进行Service层代码的编写
一、Service接口设计与实现 在org.seckill包下创建一个service包用于存放我们的Service接口和其实现类,创建一个exception包用于存放service层出现的异常例如重复秒杀商品异常、秒杀已关闭等异常,一个dto包作为传输层,dto和entity的区别在于:entity用于业务数据的封装,而dto用于完成web和service层的数据传递。
接口设计
首先创建我们Service接口,里面的方法应该是按”使用者”(程序员)的角度去设计,SeckillService.java,代码如下:
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 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,用于封装秒杀的地址信息,各个属性的作用在代码中已给出注释,代码如下:
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 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 public class Exposer { private boolean exposed; private String md5; private long seckillId; private long now; private long start; private long end; public Exposer (boolean exposed, String md5, long seckillId) { this .exposed = exposed; this .md5 = md5; this .seckillId = seckillId; } public Exposer (boolean exposed, long seckillId,long now, long start, long end) { this .exposed = exposed; this .seckillId=seckillId; this .now = now; this .start = start; this .end = end; } public Exposer (boolean exposed, long seckillId) { this .exposed = exposed; this .seckillId = seckillId; } public boolean isExposed () { return exposed; } public void setExposed (boolean exposed) { this .exposed = exposed; } public String getMd5 () { return md5; } public void setMd5 (String md5) { this .md5 = md5; } public long getSeckillId () { return seckillId; } public void setSeckillId (long seckillId) { this .seckillId = seckillId; } public long getNow () { return now; } public void setNow (long now) { this .now = now; } public long getStart () { return start; } public void setStart (long start) { this .start = start; } public long getEnd () { return end; } public void setEnd (long end) { this .end = end; } }
和SeckillExecution.java,用于判断秒杀是否成功,成功就返回秒杀成功的所有信息(包括秒杀的商品id、秒杀成功状态、成功信息、用户明细),失败就抛出一个我们允许的异常(重复秒杀异常、秒杀结束异常),代码如下:
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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 public class SeckillExecution { private long seckillId; private int state; private String stateInfo; private SuccessKilled successKilled; public SeckillExecution (long seckillId, int state, String stateInfo, SuccessKilled successKilled) { this .seckillId = seckillId; this .state = state; this .stateInfo = stateInfo; this .successKilled = successKilled; } public SeckillExecution (long seckillId, int state, String stateInfo) { this .seckillId = seckillId; this .state = state; this .stateInfo = stateInfo; } public long getSeckillId () { return seckillId; } public void setSeckillId (long seckillId) { this .seckillId = seckillId; } public int getState () { return state; } public void setState (int state) { this .state = state; } public String getStateInfo () { return stateInfo; } public void setStateInfo (String stateInfo) { this .stateInfo = stateInfo; } public SuccessKilled getSuccessKilled () { return successKilled; } public void setSuccessKilled (SuccessKilled successKilled) { this .successKilled = successKilled; } }
然后需要创建我们在秒杀业务过程中允许的异常,重复秒杀异常RepeatKillException.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class RepeatKillException extends SeckillException { public RepeatKillException (String message) { super (message); } public RepeatKillException (String message, Throwable cause) { super (message, cause); } }
秒杀关闭异常SeckillCloseException.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class SeckillCloseException extends SeckillException { public SeckillCloseException (String message) { super (message); } public SeckillCloseException (String message, Throwable cause) { super (message, cause); } }
和一个异常包含与秒杀业务所有出现的异常SeckillException.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class SeckillException extends RuntimeException { public SeckillException (String message) { super (message); } public SeckillException (String message, Throwable cause) { super (message, cause); } }
到此,接口的工作便完成,接下来进行接口实现类的编码工作。
接口实现 在service包下创建impl包存放它的实现类,SeckillServiceImpl.java,内容如下:
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 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 99 public class SeckillServiceImpl implements SeckillService { private Logger logger= LoggerFactory.getLogger(this .getClass()); private final String salt="shsdssljdd'l." ; @Autowired private SeckillDao seckillDao; @Autowired private SuccessKilledDao successKilledDao; public List<Seckill> getSeckillList () { return seckillDao.queryAll(0 ,4 ); } public Seckill getById (long seckillId) { 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 (startTime.getTime()>nowTime.getTime() || endTime.getTime()<nowTime.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+"/" +salt; String md5= DigestUtils.md5DigestAsHex(base.getBytes()); return md5; } 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=successKilledDao.insertSuccessKilled(seckillId,userPhone); if (insertCount<=0 ) { throw new RepeatKillException ("seckill repeated" ); }else { SuccessKilled successKilled=successKilledDao.queryByIdWithSeckill(seckillId,userPhone); return new SeckillExecution (seckillId,1 ,"秒杀成功" ,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代码中硬编码这两个参数,所以我们应该考虑用枚举的方式将这些常量封装起来,在org.seckill包下新建一个枚举包enums,创建一个枚举类型SeckillStatEnum.java,内容如下:
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 public enum SeckillStatEnum { SUCCESS(1 ,"秒杀成功" ), END(0 ,"秒杀结束" ), REPEAT_KILL(-1 ,"重复秒杀" ), INNER_ERROR(-2 ,"系统异常" ), DATE_REWRITE(-3 ,"数据篡改" ); private int state; private String info; SeckillStatEnum(int state, String info) { this .state = state; this .info = info; } public int getState () { return state; } public String getInfo () { return info; } public static SeckillStatEnum stateOf (int index) { for (SeckillStatEnum state : values()) { if (state.getState()==index) { return state; } } return null ; } }
然后修改执行秒杀操作的非业务类SeckillExecution.java里面涉及到state和stateInfo参数的构造方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public SeckillExecution (long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) { this .seckillId = seckillId; this .state = statEnum.getState(); this .stateInfo = statEnum.getInfo(); this .successKilled = successKilled; } public SeckillExecution (long seckillId, SeckillStatEnum statEnum) { this .seckillId = seckillId; this .state = statEnum.getState(); this .stateInfo = statEnum.getInfo(); }
然后便可修改实现类方法中的返回语句为:return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS,successKilled);,保证了一些常用常量数据被封装在枚举类型里。
目前为止我们Service的实现全部完成,接下来要将Service交给Spring的容器托管,进行一些配置。
二、Spring托管Service实现类 Spring-IOC注入方式和场景
1、XML方式:主要用于配置第三方类库或需要命名空间配置
2、注解:自己编写的类,直接在代码中使用注解,如@Service,@Controller
3、Java配置类:需要通过代码控制对象创建逻辑的场景,如自定义修改依赖类库
在spring包下创建一个spring-service.xml文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 <?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" 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" > <!--扫描service包下所有使用注解的类型--> <context:component-scan base-package ="cn.codingxiaxw.service" /> </beans>
然后采用注解的方式将Service的实现类加入到Spring IOC容器中:
1 2 3 @Service public class SeckillServiceImpl implements SeckillService
下面我们来运用Spring的声明式事务对我们项目中的事务进行管理。
三、配置并使用spring声明式事务 声明式事务的使用方式:
1.早期使用的方式:ProxyFactoryBean+XMl.
2.tx:advice+aop命名空间,这种配置的好处就是一次配置永久生效。
3.注解@Transactional的方式。在实际开发中,建议使用第三种对我们的事务进行控制,优点见下面代码中的注释。
Spring在抛出运行期异常时会回滚事务,两点注意:
1 2 3 1 非运行期异常时要注意,防止出现部分成功部分失败的情况(所以自己封装异常时,在需要的地方要implements RuntimeException )。2 小心使用try -catch :被try -catch 块包裹起来的异常Spring也是感觉不到的
下面让我们来配置声明式事务,在spring-service.xml中添加对事务的配置:
1 2 3 4 5 6 7 8 9 10 <!--配置事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" > <!--注入数据库连接池--> <property name="dataSource" ref="dataSource" /> </bean> <!--配置基于注解的声明式事务 默认使用注解来管理事务行为--> <tx:annotation-driven transaction-manager="transactionManager" />
然后在Service实现类的方法中,在需要进行事务声明的方法上加上事务的注解:
1 2 3 4 5 6 7 8 9 10 @Transactional public SeckillExecution executeSeckill (long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {}
下面针对我们之前做的业务实现类来做集成测试。
四、完成Service集成测试 在SeckillService接口中使用IDEA快捷键shift+command+T,快速生成junit测试类。Service实现类中前面两个方法很好实现,获取列表或者列表中的一个商品的信息即可,测试如下:
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 @RunWith(SpringJUnit4ClassRunner.class) @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 getSeckillList () throws Exception { List<Seckill> seckills=seckillService.getSeckillList(); System.out.println(seckills); } @Test public void getById () throws Exception { long seckillId=1000 ; Seckill seckill=seckillService.getById(seckillId); System.out.println(seckill); } }
重点就是exportSeckillUrl()方法和executeSeckill()方法的测试,接下来我们进行exportSeckillUrl()方法的测试,如下:
1 2 3 4 5 6 7 8 @Test public void exportSeckillUrl () throws Exception { long seckillId=1000 ; Exposer exposer=seckillService.exportSeckillUrl(seckillId); System.out.println(exposer); }
控制台中输入如下信息:
1 Exposer{exposed=false, md5='null', seckillId=1000, now=1480322072410, start=1451577600000, end=1451664000000}
没有给我们返回id为1000的商品秒杀地址,是因为我们当前的时间并不在秒杀时间开启之内,所以该商品还没有开启。需要修改数据库中该商品秒杀活动的时间在我们测试时的当前时间之内,然后再进行该方法的测试,控制台中输出如下信息:
1 Exposer{exposed=true, md5='bf204e2683e7452aa7db1a50b5713bae', seckillId=1000, now=0, start=0, end=0}
可知开启了id为1000的商品的秒杀,并给我们输出了该商品的秒杀地址。而第四个方法的测试就需要传入该地址让用户得到才能判断该用户是否秒杀到该地址的商品,然后进行第四个方法的测试,如下:
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void executeSeckill () throws Exception { long seckillId=1000 ; long userPhone=13476191876L ; String md5="bf204e2683e7452aa7db1a50b5713bae" ; SeckillExecution seckillExecution=seckillService.executeSeckill(seckillId,userPhone,md5); System.out.println(seckillExecution); }
控制台输出信息:
1 SeckillExecution{seckillId=1000, state=1, stateInfo='秒杀成功', successKilled=SuccessKilled{seckillId=1000, userPhone=13476191876, state=0, createTime=Mon Nov 28 16:45:38 CST 2016}}
证明电话为13476191876的用户成功秒杀到了该商品,查看数据库,该用户秒杀商品的明细信息已经被插入明细表,说明我们的业务逻辑没有问题。但其实这样写测试方法还有点问题,此时再次执行该方法,控制台报错,因为用户重复秒杀了。我们应该在该测试方法中添加try catch,将程序允许的异常包起来而不去向上抛给junit,更改测试代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void executeSeckill () throws Exception { long seckillId=1000 ; long userPhone=13476191876L ; String md5="bf204e2683e7452aa7db1a50b5713bae" ; try { SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5); System.out.println(seckillExecution); }catch (RepeatKillException e) { e.printStackTrace(); }catch (SeckillCloseException e1) { e1.printStackTrace(); } }
这样再测试该方法,junit便不会再在控制台中报错,而是认为这是我们系统允许出现的异常。由上分析可知,第四个方法只有拿到了第三个方法暴露的秒杀商品的地址后才能进行测试,也就是说只有在第三个方法运行后才能运行测试第四个方法,而实际开发中我们不是这样的,需要将第三个测试方法和第四个方法合并到一个方法从而组成一个完整的逻辑流程:
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 @Test public void testSeckillLogic () throws Exception { long seckillId=1000 ; Exposer exposer=seckillService.exportSeckillUrl(seckillId); if (exposer.isExposed()) { System.out.println(exposer); long userPhone=13476191876L ; String md5=exposer.getMd5(); try { SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5); System.out.println(seckillExecution); }catch (RepeatKillException e) { e.printStackTrace(); }catch (SeckillCloseException e1) { e1.printStackTrace(); } }else { System.out.println(exposer); } }
运行该测试类,控制台成功输出信息,库存会减少,明细表也会增加内容。重复执行,控制台不会报错,只是会抛出一个允许的重复秒杀异常。
Web层 一、前端交互流程设计 对于一个系统,需要产品经理、前端工程师和后端工程师的参数,产品经理将用户的需求做成一个开发文档交给前端工程师和后端工程师,前端工程师为系统完成页面的开发,后端工程师为系统完成业务逻辑的开发。对于我们这个秒杀系统,它的前端交互流程设计如下:
1 2 3 4 5 6 7 列表页->详情页->login-no->登录操作->写入cookie-yes->展示逻辑 -yes->展示逻辑 获取标准系统时间->时间判断(开始时间/结束时间)-结束->秒杀结束 -未开始->倒计时\ -已开始------->秒杀地址->执行秒杀
这个流程图就告诉了我们详情页的流程逻辑,前端工程师根据这个流程图设计页面,而我们后端工程师根据这个流程图开发我们对应的代码。前端交互流程是系统开发中很重要的一部分,接下来进行Restful接口设计的学习
二、Restful接口设计 什么是Restful:
1、很早以前就已经出现的的一个设计理念,真正被发扬广大是通过Ruby on Rails这个框架,他的设计规范里边就非常友好的去把hb,最前端的内个url如何表示、如何提交形成了一个规范,那么这个规范那就是restful。
2、本质上是一个优雅的URL表述方式。
3、他的意义就是他是一种资源的状态或者是状态转移,比如说当我们要去查询一个列表页的时候,其实他就要看这个资源,列表页的状态,我们用get请求去表述;当我们要去查询详情页的时候,也是详情页这个资源的一个状态;当我们要删除一个秒杀或者修改一个相关的操作的时候就涉及到状态的转移,这时候我们需要用到put或者 post这样的方式提交。
三、SpringMVC SpringMVC理论 运行流程如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 1、首先用户会发送一个请求,所有的请求都会映射到DispatcherServlet(中央控制器,SpringMVC的核心),这个servlet会拦截所有的请求, 2、默认会用到DefaultAnnotation HandlerMapping,这个的作用是用来映射我们的URL,具体就是我们的内一个URL对应到我们的内个Handler。 3、映射完了之后那,会默认用到DefaultAnnotation HandlerAdapter,这个的目的那是做handler适配, 4、然后会衔接到我们的controller。如果其中用到intercept(拦截器)的话他也会把拦截器绑定到我们的流程当中。 5、最终他的产出就是 ModelAndView,view可以理解成jsp页面,同时他会交付到中央处理器DispatchServlet当中。 6、他会发现你应用的是一个InternalResource ViewResolver,这个就是默认的jsp的一个view。 7、他就会把我们的Model和jsp页面相结合,最终返回给我们的用户。 如果你输出的是json的话,把jsp换成json就可以了。
HTTP请求地质映射原理
注解映射技巧
请求方法细节处理
配置SpringMVC 首先在WEB-INF的web.xml中进行我们前端控制器DispatcherServlet的配置,如下:
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 <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0" metadata-complete="true" > <!--用maven创建的web-app需要修改servlet的版本为3.1 --> <!--配置DispatcherServlet--> <servlet> <servlet-name>seckill-dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 配置SpringMVC 需要配置的文件 spring-dao.xml,spring-service.xml,spring-web.xml Mybatis -> spring -> springMvc --> <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,内容如下:
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 <?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" > <!--配置spring mvc--> <!--1 ,开启springmvc注解模式 a.自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter b.默认提供一系列的功能:数据绑定,数字和日期的format@NumberFormat ,@DateTimeFormat c: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 name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> <!--4 :扫描web相关的bean--> <context:component-scan base-package ="cn.codingxiaxw.web" /> </beans>
这样我们便完成了Spring MVC的相关配置(即将Spring MVC框架整合到了我们的项目中),接下来就要基于Restful接口进行我们项目的Controller开发工作了。
三、Contreller开发 Controller中的每一个方法都对应我们系统中的一个资源URL,其设计应该遵循Restful接口的设计风格。在cn.codingxiaxw包下创建一个web包用于放web层Controller开发的代码,在该包下创建一个SeckillController.java,内容如下:
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 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 @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); return "list" ; } @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) { e.printStackTrace(); 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 ,"未注册" ); } SeckillResult<SeckillExecution> result; try { SeckillExecution execution = seckillService.executeSeckill(seckillId, phone, md5); return new SeckillResult <SeckillExecution>(true , execution); }catch (RepeatKillException e1) { SeckillExecution execution=new SeckillExecution (seckillId, SeckillStatEnum.REPEAT_KILL); return new SeckillResult <SeckillExecution>(false ,execution); }catch (SeckillCloseException e2) { SeckillExecution execution=new SeckillExecution (seckillId, SeckillStatEnum.END); return new SeckillResult <SeckillExecution>(false ,execution); } catch (Exception e) { SeckillExecution execution=new SeckillExecution (seckillId, SeckillStatEnum.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包中创建它,内容如下:
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 public class SeckillResult <T> { private boolean success; private T data; private String error; public SeckillResult (boolean success, T data) { this .success = success; this .data = data; } public SeckillResult (boolean success, String error) { this .success = success; this .error = error; } public boolean isSuccess () { return success; } public void setSuccess (boolean success) { this .success = success; } public T getData () { return data; } public void setData (T data) { this .data = data; } public String getError () { return error; } public void setError (String error) { this .error = error; } }
到此,Controller的开发任务完成,接下来进行我们的页面开发。
四、页面开发 新建Jsp包,使用Bootstrap进行开发即可
·
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 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 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 var seckill = { URL : { now : function ( ) { return '/seckill/time/now' ; }, exposer : function (seckillId ) { return '/seckill/' + seckillId + '/exposer' ; }, execution : function (seckillId, md5 ) { return '/seckill/' + seckillId + '/' + md5 + '/execution' ; } }, validatePhone : function (phone ) { if (phone && phone.length == 11 && !isNaN (phone)) { return true ; } else { return false ; } }, detail : { init : function (params ) { var userPhone = $.cookie ('userPhone' ); if (!seckill.validatePhone (userPhone)) { var killPhoneModal = $('#killPhoneModal' ); killPhoneModal.modal ({ show : true , backdrop : 'static' , keyboard : false }); $('#killPhoneBtn' ).click (function ( ) { var inputPhone = $('#killPhoneKey' ).val (); console .log ("inputPhone: " + inputPhone); if (seckill.validatePhone (inputPhone)) { $.cookie ('userPhone' , inputPhone, {expires : 7 , path : '/seckill' }); window .location .reload (); } else { $('#killPhoneMessage' ).hide ().html ('<label class="label label-danger">手机号错误!</label>' ).show (300 ); } }); } var startTime = params['startTime' ]; var endTime = params['endTime' ]; var seckillId = params['seckillId' ]; $.get (seckill.URL .now (), {}, function (result ) { if (result && result['success' ]) { var nowTime = result['data' ]; seckill.countDown (seckillId, nowTime, startTime, endTime); } else { console .log ('result: ' + result); alert ('result: ' + result); } }); } }, handlerSeckill : function (seckillId, node ) { node.hide ().html ('<button class="btn btn-primary btn-lg" id="killBtn">开始秒杀</button>' ); $.get (seckill.URL .exposer (seckillId), {}, function (result ) { if (result && result['success' ]) { var exposer = result['data' ]; if (exposer['exposed' ]) { var md5 = exposer['md5' ]; var killUrl = seckill.URL .execution (seckillId, md5); console .log ("killUrl: " + killUrl); $('#killBtn' ).one ('click' , function ( ) { $(this ).addClass ('disabled' ); $.post (killUrl, {}, function (result ) { if (result && result['success' ]) { var killResult = result['data' ]; var state = killResult['state' ]; var stateInfo = killResult['stateInfo' ]; node.html ('<span class="label label-success">' + stateInfo + '</span>' ); } }); }); node.show (); } else { var now = exposer['now' ]; var start = exposer['start' ]; var end = exposer['end' ]; seckill.countDown (seckillId, now, start, end); } } else { console .log ('result: ' + result); } }); }, countDown : function (seckillId, nowTime, startTime, endTime ) { console .log (seckillId + '_' + nowTime + '_' + startTime + '_' + endTime); var seckillBox = $('#seckill-box' ); if (nowTime > endTime) { seckillBox.html ('秒杀结束!' ); } else if (nowTime < startTime) { var killTime = new Date (startTime + 1000 ); seckillBox.countdown (killTime, function (event ) { var format = event.strftime ('秒杀倒计时: %D天 %H时 %M分 %S秒 ' ); seckillBox.html (format); }).on ('finish.countdown' , function ( ) { console .log ('______fininsh.countdown' ); seckill.handlerSeckill (seckillId, seckillBox); }); } else { seckill.handlerSeckill (seckillId, seckillBox); } } }
高并发优化 一、高并发优化分析 CDN
CDN只能缓存url固定的资源:
秒杀地址是变化的,无法用CDN进行缓存
大部分写操作和最核心的数据的请求,无法用CDN
不能在缓存里减库存,因为并发,会有不一致的问题
——>通过mysql的事务来保证数据的一致性;
难点:一瞬间大量用户参与热门商品竞争,MySQL一行数据会有大量竞争,有大量的updata减库存竞争
上图使用的方案的缺点:运维成本和稳定性、开发成本、幂等性(重复秒杀问题)、不适合新手
瓶颈分析
优化:
前端: 动静态数据做分离,减少请求与响应时间;按钮防重复,防止用户发送无效的重复请求,因为秒杀活动一般都会有购买数量的限制,敲的次数再多,最后还是要查看是否已购。影响了效率,可有前端代为处理并优化
后端:使用CDN换存重要的静态资源等;在后端对活动结束时间、商品选购时间、业务的相关逻辑要求都放在后端代码中,并调用缓存来进行暂存,已减少对DB的直接操作,提高效率
1,前端控制触发次数,比如限制控制按钮的触发
2,使用CDN和缓存机制达到动静分离
3,减少行级锁和GC的时间,将事物控制在mysql中进行,比如存储过程
二、redis后端缓存优化编码 后端缓存流程:先在pom文件中引入相关redis依赖,java一般通过jedis来访问redis,然后创建一个redisDao的类,写入jedispool,从jedispool中获取到jedis对象。主要是在写两个方法,一个是从redis中get对象,另一个是向jedis中存入对象,因为redis并没有实现自动序列化功能,所以实际put对象的时候是将数据库中取到的对象序列化成二级制数组,然后根据对象类的反射得到的scheme序列化对象并存到redis中。同样redis取出对象的时候取到的是一个二进制数组,需要根据scheme和一个空对象将二进制数组转换成相应的对象。
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 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 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 public class RedisDao { private final JedisPool jedisPool; public RedisDao (String ip, int port) { jedisPool = new JedisPool (ip, port); } private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class); public Seckill getSeckill (long seckillId) { return getSeckill(seckillId, null ); } public Seckill getSeckill (long seckillId, Jedis jedis) { boolean hasJedis = jedis != null ; try { if (!hasJedis) { jedis = jedisPool.getResource(); } try { String key = getSeckillRedisKey(seckillId); byte [] bytes = jedis.get(key.getBytes()); if (bytes != null ) { Seckill seckill = schema.newMessage(); ProtostuffIOUtil.mergeFrom(bytes, seckill, schema); return seckill; } } finally { if (!hasJedis) { jedis.close(); } } } catch (Exception e) { } return null ; } public Seckill getOrPutSeckill (long seckillId, Function<Long, Seckill> getDataFromDb) { String lockKey = "seckill:locks:getSeckill:" + seckillId; String lockRequestId = UUID.randomUUID().toString(); Jedis jedis = jedisPool.getResource(); try { while (true ) { Seckill seckill = getSeckill(seckillId, jedis); if (seckill != null ) { return seckill; } boolean getLock = JedisUtils.tryGetDistributedLock(jedis, lockKey, lockRequestId, 1000 ); if (getLock) { seckill = getDataFromDb.apply(seckillId); putSeckill(seckill, jedis); return seckill; } try { Thread.sleep(100 ); } catch (InterruptedException ignored) { } } } catch (Exception ignored) { } finally { JedisUtils.releaseDistributedLock(jedis, lockKey, lockRequestId); jedis.close(); } return null ; } private String getSeckillRedisKey (long seckillId) { return "seckill:" + seckillId; } public String putSeckill (Seckill seckill) { return putSeckill(seckill, null ); } public String putSeckill (Seckill seckill, Jedis jedis) { boolean hasJedis = jedis != null ; try { if (!hasJedis) { jedis = jedisPool.getResource(); } try { String key = getSeckillRedisKey(seckill.getSeckillId()); byte [] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE)); int timeout = 60 * 60 ; String result = jedis.setex(key.getBytes(), timeout, bytes); return result; } finally { if (!hasJedis) { jedis.close(); } } } catch (Exception e) { } return null ; } }
三、并发优化
insert和update代码调换顺序的原因:insert不涉及到行级锁的竞争,所以可以放在前面执行其实降低了一半的行级锁持有时间,因为行级锁的开始时间是从update语句开始的。同时不用担心insert会先插入大量数据,因为insert和update是控制在一个事务当中的,只有update成功后insert才会真正的插入一条数据。
利用存储过程将执行秒杀的一条事务逻辑放到mysql服务端去执行,减少了客户端和服务端之间的延迟和gc时间,客户端只需要传入参数执行存储过程并根据得到的返回结果做相应的逻辑处理。存储过程比较适合于简单的逻辑。
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 46 47 48 49 50 51 52 DELIMITER $$ CREATE PROCEDURE 'seckill' .'execute_seckill' (in v_seckill bigint , in v_phone bigint , in v_kill_time timestamp , out r_result int ) BEGIN DECLARE insert_count int DEFAULT 0 ; START TRANSANCTION; insert ignore into success_killed (seckill_id,user_phone,create_time) values (v_seckill_id,v_phone,v_kill_time); select row_count() into insert_count; IF (insert_count = 0 ) THEN ROLLBACK ; set r_result = -1 ; ELSEIF(insert_count < 0 ) THEN ROLLBACK ; set r_result = -2 ; ELSE update seckill set number = number-1 where seckill_id = v_seckill_id and end_time > v_kill_time and start_time < v_kill_time and number > 0 ; select row_count() into insert_count; IF (insert_count = 0 ) THEN ROLLBACK ; SET r_result = 0 ; ELSEIF (insert_count < 0 ) THEN ROLLBACK ; set r_result = -2 ; ELSE COMMIT ; set r_result = 1 ; END IF; END IF; END $$ DELIMITER ; set @r_result = -3 ;call execute_seckill(1003 ,13567098891 ,now(),@r_result )select @r_result ;
在Java中调用事务接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Date killTime = new Date ();Map<String,Object> map = new HashMap <String, Object>(); map.put("seckillId" ,seckillId); map.put("phone" ,userPhone); map.put("killTime" ,killTime); map.put("result" ,null ); seckillDao.killByProcedure(map); try { seckillDao.killByProcedure(map); int result = MapUtils.getInteger(map,"result" ,-2 ); if (result == 1 ){ SuccesssKilled sk = successkilledDao. queryByIdWithSeckill(seckillId,userPhone); return new SeckillExecution (seckillId, SeckillStatEnum.SUCCESS, sk); }else { return new SeckillExecution (seckillId, SeckillStatEnum.stateOf(result)); } }catch (Exception e){ logger.error(e.getMessage(),e); return new SeckillExecution (seckillId, SeckillStatEnum.INNER_ERROR); }
四、系统部署架构 秒杀系统架构:
1、CDN缓存
2、智能DNS解析:Nginx
3、逻辑集群:jetty
4、分库分表:Mycat
5、统计分析:生成报表
一部分流量已经被cdn缓存锁拦截 不过秒杀的操作,秒杀的地址获取这样的请求不方便放入cdn中,所以访问到我们的服务器 我们的服务器会通过我们的dns查找到我们的地址 一般找到的是nginx地址,nginx一般部署到不同的机房,比如电信,移动,联通 这样的话智能的dns会根据用户的请求ip地址来智能的dns解析来请求最近的Nginx服务器 nginx还会给我们的服务器做负载均衡