高并发秒杀应用:Service层设计(Spring的IOC与AOP的使用)

在上一篇博客完成了秒杀应用的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容器中。我们实现这些类需要的成员变量:

 

 

实现getSeckillListgetById函数比较简单,因为在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 }

 

 

posted @ 2018-12-07 15:28  轻抚丶两袖风尘  阅读(808)  评论(0编辑  收藏  举报