基于SSM实现高并发秒杀API(Service层)

DAO编码后的思考

DAO层演变为接口设计和SQL编写
好处:源代码与SQL分离,方便Review,DAO拼接等逻辑在Service层去完成.

事务的优点和注意事项

  1. 开发团队达成一致约定,明确标注事务方法的编程风格
  2. 保证事务方法的执行时间尽可能短,不要穿插远程调用或网络请求的操作,需要把这些剥离到事务方法外
  3. 不是所有方法都要放到事务中,如只有一条的修改,只读操作不需要事务控制.

编码过程

修改之前代码

加入spring.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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">
<context:component-scan base-package="com.yuda">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--导入其他-->
<import resource="spring-dao.xml"/>
<import resource="spring-service.xml"/>
</beans>

spring-service.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?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:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--启动注解-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

DTO

数据传输层:用于对Entity进行封装,然后传输给其他层.

Exposer.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
@Data
public class Exposer {
//是否开启
private boolean exposed;
//加密的url
private String md5;
//ID
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 now, long start, long end) {
this.exposed = exposed;
this.now = now;
this.start = start;
this.end = end;
}
public Exposer(boolean exposed, long seckillId) {
this.exposed = exposed;
this.seckillId = seckillId;
}
}
SeckillExecution.java

秒杀执行后的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
public class SeckillExecution {
//成功失败
private long seckillId;
//状态码
private int state;
//状态
private String stateInfo;
//秒杀对象
private SuccessKilled successKilled;
public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) {
this.seckillId = seckillId;
this.state = statEnum.getState();
this.stateInfo = statEnum.getStateinfo();
this.successKilled = successKilled;
}
public SeckillExecution(long seckillId, int state, String stateInfo) {
this.seckillId = seckillId;
this.state = state;
this.stateInfo = stateInfo;
}
}

使用枚举来封装数据字典

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
public enum SeckillStatEnum {
SUCCESS(1, "成功"),
END(0, "秒杀结束"),
REPEAT_KILL(-1, "重复秒杀"),
INNER_ERROR(-2, "系统异常"),
DATA_REWRITE(-3, "数据篡改");

private int state;
private String stateinfo;

SeckillStatEnum(int state, String stateinfo) {
this.state = state;
this.stateinfo = stateinfo;
}

public int getState() {
return state;
}
public String getStateinfo() {
return stateinfo;
}
//通过状态码找到数据信息
public static SeckillStatEnum stateOf(int index) {
for (SeckillStatEnum statEnum : values()) {
if (statEnum.getState() == index) {
return statEnum;
}
}
return null;
}
}

异常类编写

  1. SeckillCloseException 秒杀关闭异常
  2. SeckillException 秒杀异常
  3. RepeatKillException 重复秒杀异常

只举一个例子,其他类似

1
2
3
4
5
public class RepeatKillException extends RuntimeException {
public RepeatKillException(String s) {
super(s);
}
}

Service层正式编写

接口

1
2
3
4
5
6
7
8
9
10
public interface SeckillService {
//列出了
List<Seckill> getSeckillList();
//通过id得到秒杀对象
Seckill getById(long seckillId);
//获得URL
Exposer exportSeckillUrl(long seckillId);
//执行秒杀
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5);
}

实现

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
package com.yuda.serivce.impl;

import com.yuda.dao.SeckillDao;
import com.yuda.dao.SuccessKilledDao;
import com.yuda.dto.Exposer;
import com.yuda.dto.SeckillExecution;
import com.yuda.entity.Seckill;
import com.yuda.entity.SuccessKilled;
import com.yuda.enums.SeckillStatEnum;
import com.yuda.exception.RepeatKillException;
import com.yuda.exception.SeckillCloseException;
import com.yuda.exception.SeckillException;
import com.yuda.serivce.SeckillService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;

import java.util.Date;
import java.util.List;

/**
* @auther yuda
* Create on 2017/11/8 0:25.
* Project_name : seckill
* Package_name : com.yuda.serivce.impl
* Description : TODO
*/
@Service
@Transactional //每个方法执行在事务中
public class SeckillServiceImpl implements SeckillService {

private final Logger loggger = LoggerFactory.getLogger(this.getClass());

@Autowired
private SeckillDao seckillDao;

@Autowired
private SuccessKilledDao successKilledDao;

//混淆md5,随便写的,还可以提取出来,方便定期更改.
private final String slat = "abcafafafa;jo;213;ofj;";

@Override
public List<Seckill> getSeckillList() {
return seckillDao.queryAll(0, 10);
}

@Override
public Seckill getById(long seckillId) {
return seckillDao.queryById(seckillId);
}

@Override
public Exposer exportSeckillUrl(long seckillId) {
Seckill seckill = seckillDao.queryById(seckillId);
if (seckill == null) {
return new Exposer(false, seckillId);
}
Date startTime = seckill.getStartTime();
Date endTime = seckill.getEndTime();
Date nowTime = new Date();
if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) {
return new Exposer(false, seckillId, nowTime.getTime(), endTime.getTime());
}
//Md5转换为字符串,不可逆
String md5 = getMD5(seckillId);
return new Exposer(true, md5, seckillId);
}

@Override
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) {
//判断md5
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.insertSucceccKilled(seckillId, userPhone);
if (insertCount <= 0) {
//重复秒杀
throw new RepeatKillException("seckill repeated");
} else {
//秒杀成功
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
}
}
} catch (SeckillCloseException ee) {
throw ee;
} catch (RepeatKillException eee) {
throw eee;
} catch (Exception e) {
loggger.error(e.getMessage(), e);
//所有编译期异常,转换为运行期异常
throw new SeckillException("seckill inner error" + e.getMessage());
}
}
private String getMD5(long seckillId) {
String base = seckillId + "/" + slat;
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
}