高并发系统

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.单元测试:junit4

1.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
{

/**
* 减库存
* @param seckillId
* @param killTime
* @return 如果影响行数>1,表示更新库存的记录行数
*/
int reduceNumber(long seckillId, Date killTime);

/**
* 根据id查询秒杀的商品信息
* @param seckillId
* @return
*/
Seckill queryById(long seckillId);

/**
* 根据偏移量查询秒杀商品列表
* @param off
* @param limit
* @return
*/
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 {

/**
* 插入购买明细,可过滤重复
* @param seckillId
* @param userPhone
* @return插入的行数
*/
int insertSuccessKilled(long seckillId,long userPhone);


/**
* 根据秒杀商品的id查询明细SuccessKilled对象(该对象携带了Seckill秒杀产品对象)
* @param seckillId
* @return
*/
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://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8
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
/**
* Created by codingBoy on 16/11/27.
* 配置spring和junit整合,这样junit在启动时就会加载spring容器
*/
@RunWith(SpringJUnit4ClassRunner.class)
//告诉junit spring的配置文件
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class SeckillDaoTest {

//注入Dao实现类依赖
@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)
//告诉junit spring的配置文件
@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 {

/**
* 查询全部的秒杀记录
* @return
*/
List<Seckill> getSeckillList();

/**
*查询单个秒杀记录
* @param seckillId
* @return
*/
Seckill getById(long seckillId);


//再往下,是我们最重要的行为的一些接口

/**
* 在秒杀开启时输出秒杀接口的地址,否则输出系统时间和秒杀时间
* @param seckillId
*/
Exposer exportSeckillUrl(long seckillId);


/**
* 执行秒杀操作,有可能失败,有可能成功,所以要抛出我们允许的异常
* @param seckillId
* @param userPhone
* @param md5
* @return
*/
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
/**
* Created by codingBoy on 16/11/27.
* 暴露秒杀地址(接口)DTO
*/
public class Exposer {

//是否开启秒杀
private boolean exposed;

//对秒杀地址加密措施
private String md5;

//id为seckillId的商品的秒杀地址
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
/**
* 封装执行秒杀后的结果:是否秒杀成功
* Created by codingBoy on 16/11/27.
*/
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
/**
* 重复秒杀异常,是一个运行期异常,不需要我们手动try catch
* Mysql只支持运行期异常的回滚操作
* Created by codingBoy on 16/11/27.
*/
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
/**
* 秒杀关闭异常,当秒杀结束时用户还要进行秒杀就会出现这个异常
* Created by codingBoy on 16/11/27.
*/
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
/**
* 秒杀相关的所有业务异常
* Created by codingBoy on 16/11/27.
*/
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());

//加入一个混淆字符串(秒杀接口)的salt,为了我避免用户猜出我们的md5值,值任意给,越复杂越好
private final String salt="shsdssljdd'l.";

//注入Service依赖
@Autowired //@Resource
private SeckillDao seckillDao;

@Autowired //@Resource
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());
}

//秒杀开启,返回秒杀商品的id、用给接口加密的md5
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
//@Component @Service @Dao @Controller
@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
/**
* 使用注解控制事务方法的优点:
* 1.开发团队达成一致约定,明确标注事务方法的编程风格
* 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部
* 3.不是所有的方法都需要事务,如只有一条修改操作、只读操作不要事务控制
*/
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)
//告诉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 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这样的方式提交。
1
/模块/资源/{标示}/集合1

三、SpringMVC

SpringMVC理论

运行流程如下

image-20200624090352357

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请求地质映射原理

image-20200624091145089

注解映射技巧

image-20200624091544482

请求方法细节处理

image-20200624091715790

image-20200624091922286

image-20200624092009114

配置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")//url:模块/资源/{}/细分
public class SeckillController
{
@Autowired
private SeckillService seckillService;

@RequestMapping(value = "/list",method = RequestMethod.GET)
public String list(Model model)
{
//list.jsp+mode=ModelAndView
//获取列表页
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";
}

//ajax ,json暴露秒杀接口的方法
@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
//将所有的ajax请求返回类型,全部封装成json数据
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进行开发即可

image-20200624105135131

image-20200624105047625·

image-20200624105004600

image-20200624105022654

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
//存放主要交互逻辑的js代码
// javascript 模块化(package.类.方法)

var seckill = {

//封装秒杀相关ajax的url
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;//直接判断对象会看对象是否为空,空就是undefine就是false; isNaN 非数字返回true
} else {
return false;
}
},

//详情页秒杀逻辑
detail: {
//详情页初始化
init: function (params) {
//手机验证和登录,计时交互
//规划我们的交互流程
//在cookie中查找手机号
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(7天过期)
$.cookie('userPhone', inputPhone, {expires: 7, path: '/seckill'});
//验证通过  刷新页面
window.location.reload();
} else {
//todo 错误文案信息抽取到前端字典里
$('#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 () {
//执行秒杀请求
//1.先禁用按钮
$(this).addClass('disabled');//,<-$(this)===('#killBtn')->
//2.发送秒杀请求执行秒杀
$.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);//todo 防止时间偏移
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

http://img1.sycdn.imooc.com/5de0cb5200018d5108040516.jpg

CDN只能缓存url固定的资源:

  • 秒杀地址是变化的,无法用CDN进行缓存
  • 大部分写操作和最核心的数据的请求,无法用CDN

不能在缓存里减库存,因为并发,会有不一致的问题

——>通过mysql的事务来保证数据的一致性;

难点:一瞬间大量用户参与热门商品竞争,MySQL一行数据会有大量竞争,有大量的updata减库存竞争

image-20200628101808334

上图使用的方案的缺点:运维成本和稳定性、开发成本、幂等性(重复秒杀问题)、不适合新手

瓶颈分析

image-20200628102330027

优化:

前端: 动静态数据做分离,减少请求与响应时间;按钮防重复,防止用户发送无效的重复请求,因为秒杀活动一般都会有购买数量的限制,敲的次数再多,最后还是要查看是否已购。影响了效率,可有前端代为处理并优化

后端:使用CDN换存重要的静态资源等;在后端对活动结束时间、商品选购时间、业务的相关逻辑要求都放在后端代码中,并调用缓存来进行暂存,已减少对DB的直接操作,提高效率

image-20200702095239456

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
/**
* Created by codingBoy on 17/2/17.
*/
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);
}

/**
* 从redis获取信息
*
* @param seckillId id
* @return 如果不存在,则返回null
*/
public Seckill getSeckill(long seckillId, Jedis jedis) {
boolean hasJedis = jedis != null;
//redis操作逻辑
try {
if (!hasJedis) {
jedis = jedisPool.getResource();
}
try {
String key = getSeckillRedisKey(seckillId);
//并没有实现哪部序列化操作
//采用自定义序列化
//protostuff: pojo.
byte[] bytes = jedis.get(key.getBytes());
//缓存重获取到
if (bytes != null) {
Seckill seckill = schema.newMessage();
ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
//seckill被反序列化
return seckill;
}
} finally {
if (!hasJedis) {
jedis.close();
}
}
} catch (Exception e) {

}
return null;
}

/**
* 从缓存获取,如果没有,则从数据库获取
* 会用到分布式锁
*
* @param seckillId id
* @param getDataFromDb 从数据库获取的方法
* @return 返回商品信息
*/
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) {
// 获取到锁,从数据库拿数据, 然后存redis
seckill = getDataFromDb.apply(seckillId);
putSeckill(seckill, jedis);
return seckill;
}

// 获取不到锁,睡一下,等会再出发。sleep的时间需要斟酌,主要看业务处理速度
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
}
} catch (Exception ignored) {
} finally {
// 无论如何,最后要去解锁
JedisUtils.releaseDistributedLock(jedis, lockKey, lockRequestId);
jedis.close();
}
return null;
}

/**
* 根据id获取redis的key
*
* @param seckillId 商品id
* @return redis的key
*/
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));
//超时缓存,1小时
int timeout = 60 * 60;
String result = jedis.setex(key.getBytes(), timeout, bytes);

return result;
} finally {
if (!hasJedis) {
jedis.close();
}
}
} catch (Exception e) {

}

return null;
}
}

三、并发优化

image-20200702103739378

image-20200702103807647

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 $$
--console ; 转换为 $$ 作为结束执行语句的标志
-- 定义存储过程
-- 参数:in 输入参数;out 输出参数
-- row_count();返回上一条修改类型sql(delete,insert,update)的影响行数
-- row_count:return 0:未修改数据 >0:表示修改的行数 <0:sql错误/未执行修改sql
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);
//执行存储过程,result被复制
try{
seckillDao.killByProcedure(map);
//获取result
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还会给我们的服务器做负载均衡

文章作者: ZJH
文章链接: http://example.com/2020/10/17/java/高并发系统/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Zany's Blog