三、高并发秒杀API之Service层设计与实现

         Dao层设计与实现=》接口设计+SQL编写

         实现了代码和SQL的分离,方便Review,dao层也叫做数据访问层,是对远程存储系统执行操作的过程,这些操作统一存放在Dao层。

         而通过Dao组成的 逻辑 则是有Service来完成。

 

1 秒杀Service接口设计

在src/main/java/or.seckill包下创建:

       service包:存放我们的Service接口和其实现类

       exception包:存放service层出现的异常例如重复秒杀商品异常、秒杀已关闭等异常

       dto包:作为传输层,dto和entity的区别在于:entity用于业务数据的封装,而dto用于完成web和service层的数据传递。


创建Service接口 SeckillService.java

/**
 * 业务接口:站在“使用者”的角度设计接口
 * 三个方面:方法定义粒度,参数,返回类型(return类型、异常要友好,不能乱返回)
 * @author Terence
 *
 */
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
     */
    void executeSeckill(long seckillId,long userPhone,String md5)
            throws SeckillException,RepeatKillException,SeckillCloseException; 
}

 

抛出的三个异常分别是秒杀业务相关异常、秒杀关闭异常、秒杀重复异常

秒杀业务相关异常SeckillException.java

/**
 * 秒杀业务相关异常
 * @author Terence
 *
 */
public class SeckillException extends RuntimeException {
      public SeckillException(String message) {
           super(message);
        }
 
        public SeckillException(String message, Throwable cause) {
           super(message, cause);
        }
}

 

秒杀关闭异常SeckillCloseException.java

/**
 * 秒杀关闭异常,当秒杀结束时用户还要进行秒杀就会出现这个异常
 *
 */
public class SeckillCloseException extends SeckillException{
    public SeckillCloseException(String message) {
        super(message);
    }
 
    public SeckillCloseException(String message, Throwable cause) {
        super(message, cause);
    }
}

 

秒杀重复异常RepeatKillException.java

/**
 * 重复秒杀异常,是一个运行期异常,不需要我们手动try catch
 * Mysql只支持运行期异常的回滚操作
 * @author Terence
 *
 */
public class RepeatKillException extends SeckillException {
 
    public RepeatKillException(String message) {
        super(message);
    }
 
    public RepeatKillException(String message, Throwable cause) {
        super(message, cause);
    }
} 


2 秒杀Service接口的实现

Service包下创建impl包:存放service包下接口的实现类。

SeckillServiceImpl.java内容如下:

public class SeckillServiceImpl implements SeckillService {
    private Logger logger=LoggerFactory.getLogger(this.getClass());
    private SeckillDao seckillDao;
    private SuccessKilledDao successKilledDao;
   
    //md5盐值字符串,用于混淆md5;
    private final String salt="jnqw&o4ut922v#y54vq34U#*mn4v";
 
    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(nowTime.getTime()<startTime.getTime()||nowTime.getTime()>endTime.getTime())
            return new Exposer(false,seckillId,nowTime.getTime(),startTime.getTime(),endTime.getTime());
        //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 SeckillExecutionexecuteSeckill(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 newSeckillExecution(seckillId,1,”秒杀成功” ,successKilled);

        其中,“1”和“秒杀成功”这两个字段表示的是一种操作执行的状态,用来输出给前端,通常用数据字段记录这些状态;

        在这里考虑用枚举将封存常量表示状态,实现数据字典;具体枚举语法请自行学习。


在org.seckill包下新建一个枚举包enums,创建枚举类SeckillStateEnum.java:

/**
 * 使用枚举表示常量数据字典
 * @author Terence
 *
 */
public enum SeckillStateEnum {
    SUCCESS(1,"秒杀成功"),
    END(0,"秒杀结束"),
    REPEAT_KILL(-1,"重复秒杀"),
    INNER_ERROR(-2,"系统异常"),
    DATA_REWRITE(-3,"数据篡改");
    private int state;
    private String stateInfo;
    SeckillStateEnum(int state, String info) {
        this.state = state;
        this.stateInfo = info;
    }
 
    public int getState() {
        return state;
    }
 
    public String getInfo() {
        return stateInfo;
    }
 
 
    public static  SeckillStateEnum stateOf(int index)
    {
        for (SeckillStateEnum state : values())
        {
            if (state.getState()==index)
            {
                return state;
            }
        }
        return null;
    }
}

 

修改秒杀操作的非业务类SeckillExcution.java里面涉及到的state和stateInfo参数的构造方法,将其替换为枚举类型:

public SeckillExecution(long seckillId,SeckillStateEnum stateEnum,
            SuccessKilled successKilled) {
        super();
        this.seckillId = seckillId;
        this.state = stateEnum.getState();
        this.stateInfo = stateEnum.getInfo();
        this.successKilled = successKilled;
    }
    public SeckillExecution(long seckillId,SeckillStateEnum stateEnum) {
        super();
        this.seckillId = seckillId;
        this.state = stateEnum.getState();
        this.stateInfo = stateEnum.getInfo();
    }

          并且,使用枚举类型修改Service接口实现类的返回语句:将returnnewSeckillExecution(seckillId,1,”秒杀成功” ,successKilled)修改为return new SeckillExecution (seckillId,SeckillStateEnum.SUCCESS,successKilled)表示一种操作执行状态。

          至此,Service接口实现类实现完成,接下来要将Service交付给Spring容器托管,主要是进行一些配置。

 

3 基于Spring的Service依赖管理

Sprign托管Service,实际就是通过Spring IOC管理依赖,主要通过依赖注入。

利用对象工程这些依赖进行依赖管理,给出一致的访问接口,通过applicationContext或者注解来拿到一个管理的实例。

                


那么,对于该项目有哪些依赖呢?

如图:

                       


那么,为什么使用Spring IOC呢?

第一,对象的创建统一托管

第二,规范的生命周期的管理

第三,灵活的依赖注入

第四,一致的对象注入

 

Spring-IOC注入方式和场景是怎么的呢?

                      

 

实现:

在spring包下创建一个文件spring-service.xml文件:用于扫描service类

<?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-3.0.xsd" >
         <!--扫描service包下素有使用注解的类型 -->
         <context:component-scan base-package="org.seckill.service"/>      
</beans>

然后采用注解的方式将Service接口的实现类添加到SpringIOC容器中对其进行管理:

/**
 * spring提供的注解
 * @component代表所有的组件统称Spring组件实例,如果知道具体的组件类型则使用@Service、@Dao/@Controller *
 *
 */
 
@Service  
public class SeckillServiceImpl implements SeckillService {
    private Logger logger=LoggerFactory.getLogger(this.getClass());
   
    @Autowired //主动获取实例注入
    private SeckillDao seckillDao;
    @Autowired
    private SuccessKilledDao successKilledDao;
   
    //md5盐值字符串,用于混淆md5;
    private final String salt="jnqw&o4ut922v#y54vq34U#*mn4v";
 
    public List<Seckill> getSeckillList() {
        return seckillDao.queryAll(0, 4);
    }
……

 

对Service类使用@Service注解表明这是一个Service类,注入SpringIOC容器被管理。

对要使用的到实例使用@Autowired声明,实现实例的获取,并自动注入。

 

接下来,我们来运用Spring的声明式事务对我们项目中的事务进行管理。

 

4 使用Spring声明式事物配置管理事物

                                 

                           事物管理流程:事物开启=》修改SQL1,修改SQL2,修改SQLn=》提交/回滚事务


现在使用第三方框架管理这个流程,使其摆脱事务编码,这个就叫做声明式事务,

方式一:早期的Spring管理事务是ProxyFactoryBean+XML的方式

方式二:后来添加了tx:advice+aop命名空间使得一次配置永久生效

方式三:使用注解@Transactional 来控制事务,也是推荐的一种方式。


为什么推荐使用注解控制事务的呢?

(1)开发团队达成一致约定,明确标注事务方法的编程风格

(2)保证事务方法的执行时间尽可能短,不要穿插其他网络操作(RPC/HTTP请求或者),或者剥离到事务方法外部。

(3)不是所有的方法都需要事务,如只有一条修改操作或只读操作不需要事务控制。 

          事务方法嵌套,是声明式事务独有的概念,主要体现在其传播行为上。

什么时候回滚事务?

抛出运行期异常(RuntimeException)可以回滚,如果抛出非运行期异常(部分成功,部分失败),则不会回滚,所以抛异常的时候一定要小心不当的try-catch;

 

实现:

首先,在spring-service.xml中添加对事务的配置,具体说明见注释:

<!--配置事务管理器 -->  
         <bean id="transactionManager"class="org.springframework.datasource.DataSourceTransaction">
            <!-- 注入数据库连接池  在spring-dao.xml中已经配置,此处引用即可-->
            <property name="dataSource"ref="dataSource"/>
         </bean>
         <!-- 配置基于注解的声明式事务 , 默认使用注解来管理事务行为-->
         <tx:annotation-driven transaction-manager="transactionManager"/>

 

然后,在Service接口实现类的方法中,在需要事务声明的方法上加上事务的注解:

/**
     * 使用注解控制事务方法的优点:
     * 1:开发团队达成一致约定,明确标注事务方法的编程风格
     * 2:保证事务方法的执行时间尽可能短,不要穿插其他网络操作(RPC/HTTP请求或者),或者剥离到事务方法外部。
     * 3:不是所有的方法都需要事务,如只有一条修改操作或只读操作不需要事务控制。
     */
    @Transactional
     //秒杀是否成功,成功:减库存,增加明细;失败:抛出异常,事务回滚
    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");//秒杀数据被重写了
        }
……
}

 

完成了声明式事务控制,接下来要对Service业务层做集成测试了。

 

5 Service层集成测试

集成测试Dao层和Service层。

在test.org.seckill包下添加Service包,创建SeckillServiceTest.java

 

@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 getSeckillListTest() throws Exception {
        List<Seckill> list=seckillService.getSeckillList();
        logger.info("list={}",list);
        for(Seckill sk:list)
        {
        System.out.println(sk);
        }    
    }
 
    @Test
    public void getByIdTest() throws Exception { 
        long seckillId=1000;
        Seckill seckill=seckillService.getById(seckillId);
        logger.info("seckill={}",seckill);
        System.out.println(seckill);
    }
   
    @Test //完整的逻辑代码集成测试
    public void testExportSeckillLogic() throws Exception{
    long id=1001;
    Exposerexposer=seckillService.exportSeckillUrl(id);
     logger.info("exposer={}",exposer);
    //判断秒杀是否开启,如果开启则保留地址和盐值md5开始秒杀;
    if (exposer.isExposed())
        {
         //秒杀开启,则记录账户开始秒杀
         System.out.println(exposer);
 
            long userPhone=13476191876L;
            String md5=exposer.getMd5();
 
            try { //再次执行的时候会出现异常,要在集成测试中try-catch掉抛给Junit的异常。
 
                SeckillExecution seckillExecution = seckillService.executeSeckill(id, userPhone, md5);
                System.out.println(seckillExecution);
            }catch (RepeatKillException e)
            {
                e.printStackTrace();
            }catch (SeckillCloseException e1)
            {
                e1.printStackTrace();
            }
        }else {
            //秒杀未开启
            System.out.println(exposer);
        }
    }
   
    @Test //单独的执行测试
    public void testExecuteSeckill() throws Exception{
    long id=1001;
    long phone=1501936156052L;
    Stringmd5="1da8af7e7ad6829f9eb2e6f18cb45225";
   
    try {
         SeckillExecutionexecution=seckillService.executeSeckill(id, phone, md5);
        logger.info("result={}",execution);
        System.out.println(execution);
        }catch (RepeatKillException e)
        {
            e.printStackTrace();
        }catch (SeckillCloseException e1)
        {
            e1.printStackTrace();
        }
    }
}

 

        单元测试getSeckillListTest()和getByIdTest()方法,可以查询出秒杀的商品列表和秒杀单一商品详情。

        执行testExportSeckillLogic()进行逻辑上的集成测试:首先判断秒杀状态,如果在秒杀时间内则生成秒杀连接并返回exposer【 Exposerexposer=seckillService.exportSeckillUrl(id);】,继续利用生成的URL信息执行秒杀【SeckillExecutionexecution=seckillService.executeSeckill(id, phone, md5);】,返回一个秒杀结果,输出秒杀结果即可。

        重复秒杀会抛出异常,不可重复秒杀,为了不使其报错,这里try-catch掉即可。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值