在上一篇博客完成了秒杀应用的DAO层的接口设计和SQL编写工作,将代码和SQL分离,那么DAO的拼接逻辑将在Service层完成。
传送门:高并发秒杀应用:DAO层设计
Service层的设计
首先我们需要新建一些包,用来存放与Service层相关的东西,为了能够结构清晰。创建一个service包,存放逻辑接口和其实现;创建一个dto包,这个包的作用类似entity包,他是用来存放service的传输数据的封装;再创建一个exception包,存放程序将会出现的异常类,比如重复秒杀,秒杀过期等。
业务接口应该站在"使用者"的角度设计接口,并且要遵循三个方面:①:方法定义粒度。②:参数:越简练越好。③返回类型:return 的类型友好/也可以抛出异常。
创建SeckillService接口
新建一个SeckillService接口,其中应实现的接口:
①.获取全部商品数据的getSeckillList接口。
②.通过Id获得相应商品信息的getById的接。
③.exportSeckillUrl接口,秒杀开启时,需要输出秒杀接口的地址,否则输出系统时间和秒杀时间。ps:目的 是为了防止用户拿到地址后根据浏览器等插件进行非法秒杀。
④.执行秒杀接口executeSeckill。
package org.seckill.service;
/**
* @author yangxin
* @version 1.00
* @time 2018/12/7 8:58
*/
public interface SeckillService {
/**
* 查询所有秒杀记录。
* @author yangxin
* @param
* @return
* @exception
* @date 2018/12/7 9:02
*/
List<SecKill> getSeckillList();
/**
* 根据Id查询一个秒杀接口。
* @author yangxin
* @param @seckillId:商品ID
* @return
* @exception
* @date 2018/12/7 9:04
*/
SecKill getById(long seckillId);
/**
* 秒杀开启时:输出秒杀接口的地址,否则输出系统时间和秒杀时间。
* 防止Url规则拼出秒杀地址,再根据浏览器插件,来秒杀。
* @author yangxin
* @return
* @exception
* @date 2018/12/7 9:05
*/
Exposer exportSeckillUrl(long seckillId);
/**
*执行秒杀操作。
* 使用Md5进行判断用户是否串改md5加密的地址来是否进行秒杀。
* @author yangxin
* @return 封装的秒杀结果的数据
* @date 2018/12/7 9:15
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillCloseException;
}
Ps:SeckillExecution 和Exposer 是封装传输层的dto数据。
package org.seckill.dto;
/**
* 暴露秒杀地址DTO,DTO表示数据传输层的应用。
* 这个虽然和业务没有多大关系,但是这是对业务逻辑返回值的一个封装。
* @author yangxin
* @version 1.00
* @time 2018/12/7 9:07
*/
public class Exposer {
private boolean exposed;
private String md5;
private long seckillId;
//系统时间。
private long now;
private long start;
private long end;
@Override
public String toString() {
return "Exposer{" +
"exposed=" + exposed +
", md5='" + md5 + '\'' +
", seckillId=" + seckillId +
", now=" + now +
", start=" + start +
", end=" + 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;
}
/*
相应的get/Set方法。
*/
}
SeckillExecution
package org.seckill.dto;
import org.seckill.entity.SuccessKill;
import org.seckill.enums.SeckillStateEnums;
/**
* 封装秒杀执行后的结果
* @author yangxin
* @version 1.00
* @time 2018/12/7 9:18
*/
public class SeckillExecution {
private long seckillId;
private int state;
private String stateInfo;
private SuccessKill successKill;
@Override
public String toString() {
return "SeckillExecution{" +
"seckillId=" + seckillId +
", state=" + state +
", stateInfo='" + stateInfo + '\'' +
", successKill=" + successKill +
'}';
}
public SeckillExecution(long seckillId, SeckillStateEnums seckillStateEnums,
SuccessKill successKill) {
this.seckillId = seckillId;
this.state = seckillStateEnums.getState();
this.stateInfo = seckillStateEnums.getStateInfo();
this.successKill = successKill;
}
public SeckillExecution(long seckillId,SeckillStateEnums seckillStateEnums) {
this.seckillId = seckillId;
this.state = seckillStateEnums.getState();
this.stateInfo = seckillStateEnums.getStateInfo();
}
/*
相应的get/Set方法。
*/}
executeSeckill抛出的自定义异常RepeatKillException、SeckillCloseException、SeckillExcep-
tion,其定义如下:
1 package org.seckill.exception;
2
3 /**
4 * 秒杀相关的异常
5 * @author yangxin
6 * @version 1.00
7 * @time 2018/12/7 9:29
8 */
9 public class SeckillException extends RuntimeException{
10 public SeckillException(String message) {
11 super(message);
12 }
13
14 public SeckillException(String message, Throwable cause) {
15 super(message, cause);
16 }
17 }
18
19
20 package org.seckill.exception;
21
22 /**
23 * 重复秒杀异常(运行期异常)。
24 * 异常:运行期异常、编译期异常。运行期异常不需要手动try/catch。
25 * spring 的声明式事务只接口运行期异常,如果是编译期异常他是不会做回滚的。
26 * @author yangxin
27 * @version 1.00
28 * @time 2018/12/7 9:23
29 */
30 public class RepeatKillException extends SeckillException{
31 public RepeatKillException(String message) {
32 super(message);
33 }
34
35 public RepeatKillException(String message, Throwable cause) {
36 super(message, cause);
37 }
38 }
39
40 package org.seckill.exception;
41
42 /**
43 *
44 * 秒杀关闭异常。
45 * @author yangxin
46 * @version 1.00
47 * @time 2018/12/7 9:27
48 */
49 public class SeckillCloseException extends SeckillException {
50
51 public SeckillCloseException(String message) {
52 super(message);
53 }
54
55 public SeckillCloseException(String message, Throwable cause) {
56 super(message, cause);
57 }
58 }
实现SeckillService接口创建SeckillServiceImpl类
使用Spring托管Service依赖;新建一个springService-config.xml的配置,添加注解配置。
<context:component-scan base-package="org.seckill.service"/>
我们就可以在相应的类中使用注解,减少实现这些对象的代码量,关于SpringIoc的注解的配置可以参考这篇博客。Spring 三:Spring Bean装配之注解形式
我们对SeckillServiceImpl使用@Service注解将他注入到spring容器中。我们实现这些类需要的成员变量:
实现getSeckillList和getById函数比较简单,因为在Dao层设计的时候就已经写好了,现在只需要调用就可以。
在exportSeckillUrl函数中,我们首先要根据id获得Seckill对象,来判断是否有这个商品,如果没有就返回Exposer(false,seckillId),表示非法。如果存在那么就需要判断现在是不是在秒杀时间中,如果是,那么返回state=true和md5
加密的地址,和商品id。如果没有就返回false。
关于MD5加密
整个逻辑实现
executeSeckill:
首先根据md5加密的地址判断地址是否合法,再根据现在的时间判断秒杀是不是已经关闭,如果能够能够成功秒杀,那么就进行相应的库存变化。
在这个函数中我们需要事务用spring管理,因为如果用户在某些操作不当造成的异常,导致数据提交,造成损失,我们所以需要配置声明事务。让他在发生异常的时候进行数据回滚,避免数据的操作错误,如果没有异常那么就进行数据提交,来保证其安全性。
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
<!--ref="dataSource"标红表示在当前的配置下面找不到这个dataSource这个配置,显然这个配置在springDao-config.xml中
进行配置的,所以在运行中会自动的合并这两个配置文件并且找到这个DataSource。
-->
</bean>
<!--配置基于注解的声明式事务
默认使用注解来管理事务驱动
-->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
executeSeckill函数
@Override @Transactional /* * 使用注解控制事务方法的优点。 * ①:开发团队达成一致的约定,明确标注事务方法的编程风格。 * ②:保证事务方法的执行时间尽可能的短。避免一些长时间的操作在这里面执行,如果有http/等相关的长时间操作,可以从里面剥离出来。 * ③:不是所有的方法都需要事务,如只有一条修改操作、查询等只读操作不需要事务操作。
* */
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillCloseException {
if(md5==null||!md5.equals(getSalt(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 close");
}else{
int insertCount=successKillDao.insertSuccessKilled(seckillId,userPhone);
if(insertCount<=0){
throw new RepeatKillException("seckill repeated");
}else{
SuccessKill successKill=successKillDao.queryByIdWithSecKill(seckillId,userPhone);
return new SeckillExecution(seckillId, SeckillStateEnums.SUCCESS,successKill);
}
}
}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());
}
整个实现类如下
1 package org.seckill.service.Impl;
2
3 /**
4 * @author yangxin
5 * @time 2018/12/7 9:38
6 */
7
8 @Service
9 public class SeckillServiceImpl implements SeckillService {
10
11 private Logger logger= LoggerFactory.getLogger(this.getClass());
12
13 @Autowired
14 private SeckillDao seckillDao;
15
16 @Autowired
17 private SuccessKillDao successKillDao;
18
19 //MD5的盐。
20 private final String salt="dsaiofpjsfs";
21
22 @Override
23 public List<SecKill> getSeckillList() {
24 return seckillDao.queryAll(0,4);
25 }
26
27 @Override
28 public SecKill getById(long seckillId) {
29 return seckillDao.queryById(seckillId);
30 }
31
32 @Override
33 public Exposer exportSeckillUrl(long seckillId) {
34 SecKill secKill=seckillDao.queryById(seckillId);
35 if(secKill==null){
36 return new Exposer(false,seckillId);
37 }
38 Date startTime=secKill.getStartTime();
39 Date endTime=secKill.getEndTime();
40 Date newTime=new Date();
41
42 if(newTime.getTime()<startTime.getTime()||
43 newTime.getTime()>endTime.getTime()){
44 return new Exposer(false,seckillId,newTime.getTime(),startTime.getTime(),endTime.getTime());
45 }
46 String md5=getSalt(seckillId);
47 return new Exposer(true,md5,seckillId);
48 }
49
50 private String getSalt(long seckillId){
51 String md5=seckillId+'/'+salt;
52 return DigestUtils.md5DigestAsHex(md5.getBytes());
53 }
54
55 @Override
56 @Transactional
57 /*
58 * 使用注解控制事务方法的优点。
59 * ①:开发团队达成一致的约定,明确标注事务方法的编程风格。
60 * ②:保证事务方法的执行时间尽可能的短。避免一些长时间的操作在这里面执行,如果有http/等相关的长时间操作,可以从里面剥离出来。
61 * ③:不是所有的方法都需要事务,如只有一条修改操作、查询等只读操作不需要事务操作。
62 * */
63 public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
64 throws SeckillException, RepeatKillException, SeckillCloseException {
65 if(md5==null||!md5.equals(getSalt(seckillId))){
66 throw new SeckillException("seckill data rewrite");
67 }
68 Date nowTime=new Date();
69 try{
70 int updateCount=seckillDao.reduceNumber(seckillId,nowTime);
71 if(updateCount<=0){
72 throw new SeckillCloseException("seckill is close");
73 }else{
74 int insertCount=successKillDao.insertSuccessKilled(seckillId,userPhone);
75
76 if(insertCount<=0){
77 throw new RepeatKillException("seckill repeated");
78 }else{
79 SuccessKill successKill=successKillDao.queryByIdWithSecKill(seckillId,userPhone);
80 return new SeckillExecution(seckillId, SeckillStateEnums.SUCCESS,successKill);
81 }
82 }
83 }catch (SeckillCloseException e1){//先判断是不是这类异常,是就输出这类异常的信息,定位准确
84 throw e1;
85 }
86 catch (RepeatKillException e2){//先判断是不是这类异常,是就输出这类异常的信息,定位准确
87 throw e2;
88 }
89 catch (Exception e){
90 logger.error(e.getMessage(),e);
91 //所有编译期异常 转化为运行期异常。
92 throw new SeckillException("seckill inner error:"+e.getMessage());
93 }
94
95
96
97 }
98 }
新建Service的测试类进行测试
1 package org.seckill.service.Impl;
2
3
4 import java.util.List;
5
6 import static org.junit.Assert.*;
7
8 /**
9 * @author yangxin
10 * @time 2018/12/7 13:57
11 */
12 @RunWith(SpringJUnit4ClassRunner.class)
13 @ContextConfiguration({"classpath:spring/springDao-config.xml",
14 "classpath:spring/springService-config.xml"})
15 public class SeckillServiceImplTest {
16
17 private final Logger logger= LoggerFactory.getLogger(this.getClass());
18
19 @Autowired
20 private SeckillService seckillService;
21
22 @Test
23 public void getSeckillList() {
24 List<SecKill> secKill=seckillService.getSeckillList();
25 logger.info("list={}",secKill);
26 }
27
28 @Test
29 public void getById() {
30 long id=1000;
31 SecKill secKill=seckillService.getById(id);
32 logger.info("secKill={}",secKill);
33 }
34
35 @Test
36 public void exportSeckillUrl() {
37 long id=1000;
38 Exposer exposer=seckillService.exportSeckillUrl(id);
39 logger.info("exposer={}",exposer);
40 //Exposer{exposed=true, md5='16050dc4a64b9b243d541d0ddf3b935f', seckillId=1000, now=0, start=0, end=0}
41 }
42
43 @Test
44 public void executeSeckill() {
45 long id=1000;
46 long phone=18555663589L;
47 String salt="16050dc4a64b9b243d541d0ddf3b935f";
48 try{
49 SeckillExecution seckillExecution=seckillService.executeSeckill(id,phone,salt);
50 logger.info("seckillExecution={}"+seckillExecution);
51 }catch (RepeatKillException e){
52 logger.error(e.getMessage(),e);
53 }
54 }
55 }