专栏首页微信公众号:Java团长(二)Java高并发秒杀API之Service层

(二)Java高并发秒杀API之Service层

上一篇分享一个整合SSM框架的高并发和商品秒杀项目


首先在编写Service层代码前,我们应该首先要知道这一层到底时干什么的,这里摘取来自ITEYE一位博主的原话

Service层主要负责业务模块的逻辑应用设计。同样是首先设计接口,再设计其实现的类,接着再Spring的配置文件中配置其实现的关联。这样我们就可以在应用中调用Service接口来进行业务处理。Service层的业务实现,具体要调用到已定义的DAO层的接口,封装Service层的业务逻辑有利于通用的业务逻辑的独立性和重复利用性,程序显得非常简洁。

在项目中要降低耦合的话,分层是一种很好的概念,就是各层各司其职,尽量不做不相干的事,所以Service层的话顾名思义就是业务逻辑,处理程序中的一些业务逻辑,以及调用DAO层的代码,这里我们的DAo层就是连接数据库的那一层,调用关系可以这样表达: View(页面)>Controller(控制层)>Service(业务逻辑)>Dao(数据访问)>Database(数据库)

  • 首先还是接口的设计,设计Service秒杀商品的接口 SeckillService 首先在som.suny包下建立interfaces这个包,这个包里面存放Service相关的接口,然后建立SeckillService接口文件,代码如下:
public interface SeckillService {

  /**
   *  查询全部的秒杀记录.
   * @return 数据库中所有的秒杀记录
   */
  List<Seckill> getSeckillList();

  /**
   *   查询单个秒杀记录
   * @param seckillId   秒杀记录的ID
   * @return   根据ID查询出来的记录信息
   */
  Seckill getById(long seckillId);

  /**
   * 在秒杀开启时输出秒杀接口的地址,否则输出系统时间跟秒杀地址
   * @param seckillId  秒杀商品Id
   * @return  根据对应的状态返回对应的状态实体
   */
  Exposer exportSeckillUrl(long seckillId);

  /**
   * 执行秒杀操作,有可能是失败的,失败我们就抛出异常
   * @param seckillId  秒杀的商品ID
   * @param userPhone 手机号码
   * @param md5   md5加密值
   * @return   根据不同的结果返回不同的实体信息
   */
  SeckillExecution executeSeckill(long seckillId,long userPhone,String md5)throws SeckillException,RepeatKillException,SeckillCloseException;

建立后接口之后我们要写实现类了,在写实现类的时候我们肯定会碰到一个这样的问题,你要向前端返回json数据的话,你是返回什么样的数据好?直接返回一个数字状态码或者时文字?这样设计肯定是不好的,所以我们应该向前段返回一个实体信息json,里面包含了一系列的信息,无论是哪种状态都应该可以应对,既然是与数据库字段无关的类,那就不是PO了,所以我们建立一个DTO数据传输类,关于常见的几种对象我的解释如下:

  • PO: 也就是我们在为每一张数据库表写一个实体的类
  • VO, 对某个页面或者展现层所需要的数据,封装成一个实体类
  • BO, 就是业务对象,我也不是很了解
  • DTO, 跟VO的概念有点混淆,也是相当于页面需要的数据封装成一个实体类
  • POJO, 简单的无规则java对象

com.suny下建立dto包,然后建立Exposer类,这个类是秒杀时数据库那边处理的结果的对象

public class Exposer {
  /*是否开启秒杀 */
  private boolean exposed;
  /*  对秒杀地址进行加密措施  */
  private String md5;
  /* id为seckillId的商品秒杀地址   */
  private long seckillId;
  /* 系统当前的时间   */
  private LocalDateTime now;
  /* 秒杀开启的时间   */
  private LocalDateTime start;
  /*  秒杀结束的时间  */
  private LocalDateTime end;

  public Exposer() {
  }

  public Exposer(boolean exposed, String md5, long seckillId) {
      this.exposed = exposed;
      this.md5 = md5;
      this.seckillId = seckillId;
  }

  public Exposer(boolean exposed, long seckillId, LocalDateTime now, LocalDateTime start, LocalDateTime 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 LocalDateTime getNow() {
      return now;
  }

  public void setNow(LocalDateTime now) {
      this.now = now;
  }

  public LocalDateTime getStart() {
      return start;
  }

  public void setStart(LocalDateTime start) {
      this.start = start;
  }

  public LocalDateTime getEnd() {
      return end;
  }

  public void setEnd(LocalDateTime end) {
      this.end = end;
  }

  @Override
  public String toString() {
      return "Exposer{" +
              "秒杀状态=" + exposed +
              ", md5加密值='" + md5 + '\'' +
              ", 秒杀ID=" + seckillId +
              ", 当前时间=" + now +
              ", 开始时间=" + start +
              ", 结束=" + end +
              '}';
  }
}

然后我们给页面返回的数据应该是更加友好的封装数据,所以我们再在com.suny.dto包下再建立SeckillExecution用来封装给页面的结果:

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;
    }

    @Override
    public String toString() {
        return "SeckillExecution{" +
                "秒杀的商品ID=" + seckillId +
                ", 秒杀状态=" + state +
                ", 秒杀状态信息='" + stateInfo + '\'' +
                ", 秒杀的商品=" + successKilled +
                '}';
    }
}
定义秒杀中可能会出现的异常
  • 定义一个基础的异常,所有的子异常继承这个异常SeckillException
/**
*  秒杀基础异常
* Created by 孙
*/
public class SeckillException extends RuntimeException {
  public SeckillException(String message) {
      super(message);
  }

  public SeckillException(String message, Throwable cause) {
      super(message, cause);
  }
}

+ 首选可能会出现秒杀关闭后被秒杀情况,所以建立秒杀关闭异常`SeckillCloseException`,需要继承我们一开始写的基础异常

/**
 * 秒杀已经关闭异常,当秒杀结束就会出现这个异常
 * Created by 孙
 */
public class SeckillCloseException extends SeckillException{
    public SeckillCloseException(String message) {
        super(message);
    }

    public SeckillCloseException(String message, Throwable cause) {
        super(message, cause);
    }
}
  • 然后还有可能发生重复秒杀异常RepeatKillException
/**
* 重复秒杀异常,不需要我们手动去try catch
* Created by 孙
*/
public class RepeatKillException extends SeckillException{
 public RepeatKillException(String message) {
     super(message);
 }

 public RepeatKillException(String message, Throwable cause) {
     super(message, cause);
 }
}

实现Service接口

@Service
public class SeckillServiceImpl implements SeckillService {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    /* 加入一个盐值,用于混淆*/
    private final String salt = "thisIsASaltValue";

    @Autowired
    private SeckillMapper seckillMapper;
    @Autowired
    private SuccessKilledMapper successKilledMapper;


    /**
     * 查询全部的秒杀记录.
     *
     * @return 数据库中所有的秒杀记录
     */
    @Override
    public List<Seckill> getSeckillList() {
        return seckillMapper.queryAll(0, 4);
    }

    /**
     * 查询单个秒杀记录
     *
     * @param seckillId 秒杀记录的ID
     * @return 根据ID查询出来的记录信息
     */
    @Override
    public Seckill getById(long seckillId) {
        return seckillMapper.queryById(seckillId);
    }

    /**
     * 在秒杀开启时输出秒杀接口的地址,否则输出系统时间跟秒杀地址
     *
     * @param seckillId 秒杀商品Id
     * @return 根据对应的状态返回对应的状态实体
     */
    @Override
    public Exposer exportSeckillUrl(long seckillId) {
        // 根据秒杀的ID去查询是否存在这个商品
       /* Seckill seckill = seckillMapper.queryById(seckillId);
        if (seckill == null) {
            logger.warn("查询不到这个秒杀产品的记录");
            return new Exposer(false, seckillId);
        }*/
        Seckill seckill = redisDao.getSeckill(seckillId);
        if (seckill == null) {
            // 访问数据库读取数据
            seckill = seckillMapper.queryById(seckillId);
            if (seckill == null) {
                return new Exposer(false, seckillId);
            } else {
                // 放入redis
                redisDao.putSeckill(seckill);
            }
        }

        // 判断是否还没到秒杀时间或者是过了秒杀时间
        LocalDateTime startTime = seckill.getStartTime();
        LocalDateTime endTime = seckill.getEndTime();
        LocalDateTime nowTime = LocalDateTime.now();
        //   开始时间大于现在的时候说明没有开始秒杀活动    秒杀活动结束时间小于现在的时间说明秒杀已经结束了
       /* if (!nowTime.isAfter(startTime)) {
            logger.info("现在的时间不在开始时间后面,未开启秒杀");
            return new Exposer(false, seckillId, nowTime, startTime, endTime);
        }
        if (!nowTime.isBefore(endTime)) {
            logger.info("现在的时间不在结束的时间之前,可以进行秒杀");
            return new Exposer(false, seckillId, nowTime, startTime, endTime);
        }*/
        if (nowTime.isAfter(startTime) && nowTime.isBefore(endTime)) {
            //秒杀开启,返回秒杀商品的id,用给接口加密的md5
            String md5 = getMd5(seckillId);
            return new Exposer(true, md5, seckillId);
        }
        return new Exposer(false, seckillId, nowTime, startTime, endTime);


    }

    private String getMd5(long seckillId) {
        String base = seckillId + "/" + salt;
        return DigestUtils.md5DigestAsHex(base.getBytes());
    }

    /**
     * 执行秒杀操作,失败的,失败我们就抛出异常
     *
     * @param seckillId 秒杀的商品ID
     * @param userPhone 手机号码
     * @param md5       md5加密值
     * @return 根据不同的结果返回不同的实体信息
     */
    @Override
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException {
        if (md5 == null || !md5.equals(getMd5(seckillId))) {
            logger.error("秒杀数据被篡改");
            throw new SeckillException("seckill data rewrite");
        }
        // 执行秒杀业务逻辑
        LocalDateTime nowTIme = LocalDateTime.now();

        try {
            //执行减库存操作
            int reduceNumber = seckillMapper.reduceNumber(seckillId, nowTIme);
            if (reduceNumber <= 0) {
                logger.warn("没有更新数据库记录,说明秒杀结束");
                throw new SeckillCloseException("seckill is closed");
            } else {
                // 这里至少减少的数量不为0了,秒杀成功了就增加一个秒杀成功详细
                int insertCount = successKilledMapper.insertSuccessKilled(seckillId, userPhone);
                // 查看是否被重复插入,即用户是否重复秒杀
                if (insertCount <= 0) {
                    throw new RepeatKillException("seckill repeated");
                } else {
                    // 秒杀成功了,返回那条插入成功秒杀的信息
                    SuccessKilled successKilled = successKilledMapper.queryByIdWithSeckill(seckillId, userPhone);
//                    return new SeckillExecution(seckillId,1,"秒杀成功");
                      return new SeckillExecution(seckillId,1,"秒杀成功",successKilled);
                }
            }
        } catch (SeckillCloseException | RepeatKillException e1) {
            throw e1;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            // 把编译期异常转换为运行时异常
            throw new SeckillException("seckill inner error : " + e.getMessage());
        }
 }

在这里我们捕获了运行时异常,这样做的原因就是Spring的事物默认就是发生了RuntimeException才会回滚,可以检测出来的异常是不会导致事物的回滚的,这样的目的就是你明知道这里会发生异常,所以你一定要进行处理.如果只是为了让编译通过的话,那捕获异常也没多意思,所以这里要注意事物的回滚. 然后我们还发现这里存在硬编码的现象,就是返回各种字符常量,例如秒杀成功,秒杀失败等等,这些字符串时可以被重复使用的,而且这样维护起来也不方便,要到处去类里面寻找这样的字符串,所有我们使用枚举类来管理这样状态,在con.suny包下建立enum包,专门放置枚举类,然后再建立SeckillStatEnum枚举类:

/**
 * 常量枚举类
 * Created by 孙
 */
public enum SeckillStatEnum {
    SUCCESS(1, "秒杀成功"),
    END(0, "秒杀结束"),
    REPEAT_KILL(-1, "重复秒杀"),
    INNER_ERROR(-2, "系统异常"),
    DATE_REWRITE(-3, "数据篡改");

    private int state;
    private String info;

    SeckillStatEnum() {
    }

    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 statEnum : values()) {
            if (statEnum.getState() == index) {
                return statEnum;
            }
        }
        return null;
    }
}

既然把这些改成了枚举,那么在SeckillServiceImpl类中的executeSeckill方法中成功秒杀的返回值就应该修改为

return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);

改了这里以后会发现会报错,因为在实体类那边构造函数可不是这样的,然后修改SeckillExecution类的构造函数,把statestateInfo的值设置从构造函数里面的SeckillStatEnum中取出值来设置:

/*  秒杀成功返回的实体  */
    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();
    }

下一步肯定要注入Service了

首先在resources/spring下建立applicationContext-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" 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/context
        http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--配置自动扫描service包下的注解,在这里配置了自动扫描后,com.suny.service包下所有带有@Service注解的类都会被加入Spring容器中-->
    <context:component-scan base-package="com.suny.service"/>

    <!--配置事物,这里时使用基于注解的事物-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--注入数据库连接池-->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--开启基于注解的申明式事物-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>

在这里开启了基于注解的事物,常见的事物操作有以下几种方法

  • 在Spring早期版本中是使用ProxyFactoryBean+XMl方式来配置事物.
  • 在Spring配置文件使用tx:advice+aop命名空间,好处就是一次配置永久生效,你无须去关心中间出的问题,不过出错了你很难找出来在哪里出了问题
  • 注解@Transactional的方式,注解可以在方法定义,接口定义,类定义,public方法上,但是不能注解在private,final,static等方法上,因为Spring的事物管理默认是使用Cglib动态代理的:
    • private方法因为访问权限限制,无法被子类覆盖
    • final方法无法被子类覆盖
    • static时类级别的方法,无法被子类覆盖
    • protected方法可以被子类覆盖,因此可以被动态字节码增强
不能被Spring AOP事物增强的方法

序号

动态代理策略

不能被事物增强的方法

1

基于接口的动态代理

出了public以外的所有方法,并且 public static 的方法也不能被增强

2

基于Cglib的动态代理

private,static,final的方法

然后你要在Service类上添加注解@Service,不用在接口上添加注解:

@Service
public class SeckillServiceImpl implements SeckillService

既然已经开启了基于注解的事物,那我们就去需要被事物的方法上加个注解@Transactional吧:

@Transactional
    @Override
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException

Service层的测试

写测试类,我这里的测试类名为SeckillServiceImplTest:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/applicationContext-dao.xml", "classpath:spring/applicationContext-service.xml"})
public class SeckillServiceImplTest {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private SeckillService seckillService;

    @Test
    public void getSeckillList() throws Exception {
        List<Seckill> seckillList = seckillService.getSeckillList();
        logger.info(seckillList.toString());
        System.out.println(seckillList.toString());
    }

    @Test
    public void getById() throws Exception {
        long seckillId = 1000;
        Seckill byId = seckillService.getById(seckillId);
        System.out.println(byId.toString());
    }

    @Test
    public void exportSeckillUrl() throws Exception {
        long seckillId = 1000;
        Exposer exposer = seckillService.exportSeckillUrl(seckillId);
        System.out.println(exposer.toString());
    }

    @Test
    public void executeSeckill() throws Exception {
        long seckillId = 1000;
        Exposer exposer = seckillService.exportSeckillUrl(seckillId);
        if (exposer.isExposed()) {
            long userPhone = 12222222222L;
            String md5 = "bf204e2683e7452aa7db1a50b5713bae";
            try {
                SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5);
                System.out.println(seckillExecution.toString());
            } catch (SeckillCloseException | RepeatKillException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("秒杀未开启");
        }
    }

    @Test
    public void executeSeckillProcedureTest() {
        long seckillId = 1001;
        long phone = 1368011101;
        Exposer exposer = seckillService.exportSeckillUrl(seckillId);
        if (exposer.isExposed()) {
            String md5 = exposer.getMd5();
            SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5);
            System.out.println(execution.getStateInfo());
        }
    }


}

测试的话如果每个方法测试都通过就说明通过,如果报错了话就仔细看下哪一步错了检查下

本文分享自微信公众号 - Java团长(javatuanzhang)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-12-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java 几种常用 JSON 库性能比较

    本篇通过JMH来测试一下Java中几种常见的JSON解析库的性能。每次都在网上看到别人说什么某某库性能是如何如何的好,碾压其他的库。但是百闻不如一见,只有自己亲...

    Java团长
  • Spring Boot 自动配置的“魔法”是如何实现的?

    Spring Boot是Spring旗下众多的子项目之一,其理念是约定优于配置,它通过实现了自动配置(大多数用户平时习惯设置的配置作为默认配置)的功能来为用户快...

    Java团长
  • Java经典设计模式之五大创建型模式(附实例和详解)

    (1)创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

    Java团长
  • 设计模式之责任链模式

    在现实生活中,有很多请求并不是一个人说了就算的,例如面试时的工资,低于1万的薪水可能技术经理就可以决定了,但是1万~1万5的薪水可能技术经理就没这个权利批准,可...

    用户1205080
  • java中的Checked Exceptions和Unchecked Exceptions

    在java世界中有两种异常,一种Checked Exceptions ,另一种叫Unchecked Exceptions. 1) Checked Except...

    ImportSource
  • Springboot异常处理只会@ControllerAdvice+@ExceptionHandler?还远远不够!

    当系统出现异常时候,或404,或500,默认返回的错误页面通常非常简陋,用户也看不懂,这时候我们想通过一些手段,提示用户访问的资源不存在,或者请稍后再试。

    java思维导图
  • WebFlux拨云见日之前端整合,悟了吗? 顶

            从spring5中加入webflux的消息现世已经有一段时间了,也发现了一些朋友的公司在使用webfux,但是大多数都是用作为服务器之间的一些通...

    kinbug [进阶者]
  • Thinkphp5框架异常处理操作实例分析

    本文实例讲述了Thinkphp5框架异常处理操作。分享给大家供大家参考,具体如下:

    砸漏
  • 通过一个模拟程序让你明白WCF大致的执行流程

    在《通过一个模拟程序让你明白ASP.NET MVC是如何运行的》一文中我通过一个普通的ASP.NET Web程序模拟了ASP.NET MVC的执行流程,现在我们...

    蒋金楠
  • 1:基本概念

    六月的雨

扫码关注云+社区

领取腾讯云代金券